我的学习笔记

土猛的员外

AI大模型已经对我产生了巨大影响

五一小长假回来这一周,虽只有三天,对我来说却是翻天覆地的!

我回到了研发中心

研发中心是我2017年来力石的第一站,当时担任研发部经理,那会儿就相当于现在的研发总监。2019年初离开研发中心,后面辗转到大数据产品中心、BOD中心和运营中心。四年后,又回到了研发中心,虽然,现在的力石和那会儿比已经很大,此研发中心非彼研发中心!

因为AI浪潮而回来

这次回到研发中心最大的原因ChatGPT带来的这波AI浪潮,AI太热了,热到身边几乎无人不知,且有“妖魔化”的趋势,言必大模型。对于大模型来势汹汹,至少在目前的能力范围内,我们还算应对有度。相比于国内大多数人是在看热闹,我们已经做了几件事:

  • 3月份已经搭建了大模型,并在公司内部进行了内测,也邀请了为数不多的客户进行了测试,效果还算不错;
  • 目前团队已经可以自主训练大模型,包括ChatGLM-6B、LLaMa系列和GPTNeox(如Dolly2)等,且开源了一个30亿参数的Lora版本大模型训练Project;
  • 结合大模型已经出来一两个应用Demo,比如根据给定的文件资料来提问题,答案会限定在给定资料范围内。

因为以上这些原因,加上公司对“小知”这个产品一直以来的期望,所以老板极力主张我放掉手上的“活”,全力去带小知这个产品。

关于销售工作的感悟

最近一年不到,除了直属团队的管理之外,我做的最认真的工作是销售。

其实我不是销售小白,之前自己两次创业,做的就是2B(软件业务)、2C(卖真皮女包)的销售,但是这一年做的更多是2G的销售。那是真的不一样啊,与人相处,还要处理各种复杂的人际关系,这一点和前面的销售面临的难题是完全不一样的。

从去年6月份的良渚项目开始,六个项目只丢了一个,成绩上来说还算可以。通过这一年,我也更加认识了自己:

  • 作为销售,我是愚笨的,这点从几个客户委婉地对话中我能清晰地感觉到。乐观的角度来看是我比较真诚,不会工于心计;
  • 作为一个“资深”老研发,一个懂产品的人,做销售真的很有优势。其中有两个项目,我自己完成了商务销售、售前文档编写的工作,还在后面一大段时间兼任了项目经理。如果我在和客户的相处中再“八面玲珑”一点,那应该是了不得的;
  • 但是现在停下来仔细想,问自己,我发现自己是不喜欢项目类型的销售工作的。我更喜欢的是产品,是一种可扩展、可复制,边际成本更低的方式去服务客户;
  • 我以前非常排斥所谓的“方法论”,因为我觉得事物是发展变化的,方法论代表的是迂腐。但是这一年销售工作中遇到的棘手麻烦,都有一些方法论帮我度过。这些方法论大多是一些思维模型,可以用draw.io把图画出来,就能推导出整个问题的突破口在哪里,也就发现了“解题钥匙”。

接下来有时间,我会再好好总结一下这一年的经验收获,对我自己肯定是很有帮助的。

接下来的工作

这周把手上的项目都交接掉了,但是近一个月内应该还会帮着过渡一段时间。接下来会更加潜心在小知这个产品上,期待着小知能翻天覆地,守得云开见月明!

新领导让我多关注关注PMF的事情,也就是寻找和调整产品-市场的结合点。这件事两年前老板也和我说过,说明PMF在他们眼里是我的一个标签了,这时候不自觉地想到老板的一句话——公司不缺你这么一个销售,你去做销售不划算!

嗯,那就好好搞产品吧。

就像插秧一样,退步方为向前!

最后感谢老华这一年的互相扶助和照顾,你差点把我培养成3T(IT、DT、OT)人才了,哈哈哈。






关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

AI编程语言--Mojo

为什么要发明Mojo?

当我们开始创建Modular(公司名)时,并没有打算构建一种新的编程语言。但是,当我们致力于统一全球ML/AI基础设施的平台时,我们意识到整个技术栈上的编程过于复杂。此外,我们手工编写了大量的MLIR(一种用于编译器和工具链的领域特定语言,旨在提高代码生成的可重用性和灵活性)代码,并且还不太遂人愿。

我们想要的是一种创新和可扩展的编程模型,可以针对加速器和其他在机器学习中普遍存在的异构系统。这意味着需要具有强大的编译期元编程自适应编译技术集成、整个编译流程中缓存等功能,但这些功能在现有语言上都不支持。

虽然加速器很重要,但最常见而被忽视的“加速器”之一是主机CPU。今天,CPU拥有许多类似张量核心加速块和其他AI加速单元,但它们也作为专用加速器无法处理(例如数据加载、预处理和后处理,以及与外部系统集成)操作时候使用。因此,显而易见我们不能使用一种仅适用于特定处理器的“加速语言”来提升AI。

应用人工智能系统需要解决所有这些问题,我们认为没有理由不能只用一种语言来完成。于是,Mojo诞生了。

我们决定将Mojo的创新放在编译器内部,也会对当前和新兴加速器进行优化和支持。但在语言层面,并不需要进行大刀阔斧地创新。因此,我们选择采用Python生态系统,因为它被广泛使用、受到AI生态系统的喜爱,并且非常好用!

Mojo 是 Python 家族的一员

Mojo语言有着崇高的目标——我们希望与Python生态系统完全兼容,同时也需要可预测的底层(语言的)性能和底层(系统级)控制,并且需要能够将代码子集部署到加速器上。我们还不希望出现生态系统分裂的情况——我们希望人们随着时间推移发现我们的工作是有用的,并且不想再次发生像Python 2 => Python 3迁移这样的事情。

这些都是非常重要的目标!

幸运的是,虽然Mojo是一个全新的代码库,但在概念上并没有从零开始。采用Python大大简化了我们的设计工作,因为大多数语法已经被指定了。相反,我们可以把精力集中在构建编译模型和设计特定系统编程功能上。此外,我们还受益于其他语言(例如Clang、Rust、Swift、Julia、Zig、Nim等)所做出来巨大贡献,并利用MLIR编译器生态系统。同时,由于对Swift编程语言有经验,在将庞大Objective-C社区迁移到新语言方面也获得了很多好处。

此外,我们认为Mojo正确长期目标应该提供Python超集(即与现有程序兼容),并立即采用CPython以实现长尾生态系统。对于Python程序员来说,我们期望Mojo会让你有亲切感,并它还提供开发系统级代码的能力,使您能够做到Python无法胜任那些本来需要使用C和C++来做的事情。我们不参与“静态更好”或者“动态更好”的观点——我们认为在正确使用他们,两者都很好,并且语言应该使程序员能够做出决策。

Mojo和Python有多兼容?

Mojo已经支持许多Python的核心功能,包括async/await、错误处理、可变参数等等,但是…它仍然处于早期阶段,并且缺少许多功能 ——所以今天它们并不是非常兼容。 Mojo甚至还没有支持类(Class)!

尽管如此,我们还是要谈一下两个非常显著但却不同的兼容性方面的经验: “Clang”编译器的作用对象是LLVM中的C、C++和Objective-C(以及CUDA、OpenCL等),其主要目标是成为GCC、MSVC和其他现有编译器的“兼容替代品”。虽然和我们今天的话题很难进行直接比较,但Clang问题的复杂性似乎比实现Python兼容替代品大一个数量级。所以这件事情的完美解决可以给大家带来信心,相信我们能够正确地为Python社区做出贡献。

另一个例子就是Swift编程语言,它采用了Objective-C运行时和语言生态系统,并逐步将数百万程序员(以及大量代码)转移到完全不同的编程语言上。通过Swift, 我们学会了如何“运行时兼容”,并与遗留运行时合作。在Python和Mojo方面,在CPython运行时下直接协作,并具有与CPython类和对象集成类似的支持,而无需编译代码。这将使我们能够与现有代码的大量生态系统进行交流,同时提供渐进式迁移方法,其中为迁移付出的增量工作将产生增量收益。

总体而言,我们相信兼容性、设计持续警惕和逐步实现完全兼容性的北极星会在时间上帮助我们达到目标。

与Python的有意区别

兼容性和可迁移性是成功的关键,但我们也希望Mojo成为一种一流的语言,并且不能因为无法引入新关键字或添加几个语法规则而受到限制。因此,我们对兼容性的处理方式有两个方面:

  1. 我们利用CPython来运行所有现有的Python3代码,“开箱即用”,不需要修改并使用其运行时,在整个生态系统中实现完全兼容。但以这种方式运行代码将得不到使用Mojo的任何好处,这个生态系统存在和可用将快速加速Mojo的启动,并利用Python已经非常适合高级编程这一事实。

  2. 我们将提供一个机械化迁移工具,为那些想要将Python代码转移到Mojo上提供很好的兼容性。例如,Mojo提供了反引号功能,可以使用任何关键字作为标识符,从而为使用这些关键字作为标识符或关键字参数的代码提供了一个简单机械化迁移路径。迁移到Mojo上后,则可以利用先进的系统编程特性。

总之,这使得Mojo能够在大多数基于CPython环境下良好地集成,并且让程序员能够逐步地(每次模块或文件)将代码转移到Mojo上。苹果公司执行Objective-C到Swift迁移时也采用了这种方法。Swift代码能够子类化和利用Objective-C类,并且程序员可以逐步地在他们的应用程序中采用Swift。Swift还支持构建对于Objective-C程序员有用的API,我们期望Mojo也是实现CPython API的好方式。

构建Mojo和迁移支持需要一些时间,但我们相信这将使我们集中精力,避免分心。我们也认为与CPython的关系可以从两个方向建立——如果CPython团队最终在Mojo而不是C中重新实现解释器,那不是很酷吗?

关于动机

Mojo的目标是将创新的编程模型带到加速器和其他在机器学习中普遍存在的异构系统中。话虽如此,最重要和普遍的“加速器”之一实际上是主机CPU。这些CPU正在获得许多类似张量核心的加速块和其他专用AI加速单元,但它们也重要地作为“后备”,以支持加速器不支持的操作。这包括数据加载、预处理、后处理以及与使用C++编写的外部系统集成等任务。

因此,我们意识到无法构建一个仅针对问题狭窄子集(例如仅适用于张量)的有限制语言。我们需要支持全面通用编程范畴。同时,我们没有看到需要在语法或社区方面进行创新,因此决定采用并完善Python生态系统。

为什么是Python?

Python是机器学习领域和其他许多领域的主导力量。它易于学习,被重要的程序员群体(例如数据科学家)所知晓,拥有一个惊人的社区、大量有价值的包以及各种良好的工具。通过其动态编程特性,Python支持开发美观而富有表现力的API,这使得像TensorFlow和PyTorch这样的机器学习框架将Python作为前端来使用他们在C++中实现高性能运行时。

对于Modular来说,Python是我们API使用端技术栈中不可动摇的一部分——这是由我们客户决定的。考虑到我们技术栈中其他所有内容都可以协商处理,因此“Python优先”方法似乎很合理。

更加主观地说,我们认为Python是一种优美语言——用简单和可组合的抽象来设计,避免了不必要的、多余的、缩进式的标点符号,并且构建了强大的(动态的)元编程特性,这些特性是扩展到我们需要的模块化的一个途径。我们希望那些处于Python生态系统中的人们看到我们新方向时会将其视为推动Python迈向下一个级别——完善它——而不是试图与之竞争。

Python的问题

Python存在着众所周知的问题,最明显的是低级性能差和CPython实现决策(如GIL)。虽然有许多积极的项目正在进行中以改善这些挑战,但Python带来的问题更深层次地影响了人工智能领域。在本文中,我们不会谈论那些技术限制,而是将重点放在这些限制对2023年产生的影响上。

请注意,在本节中提到的Python都指CPython实现。稍后我们将讨论其他实现。

(Python和C/C++)两个世界的问题

由于各种原因,Python并不适合系统编程。幸运的是,Python在作为粘合层方面有着惊人的优势,并且与C和C++之类的低级绑定允许使用具有更好性能特征的C、C++和许多其他语言构建库。这就是使得像numpy、TensorFlow和PyTorch以及生态系统中大量其他库成为可能的原因。

然而,虽然这种方法是构建高性能Python库的有效方式,但它也带来了一些代价:构建这些混合库非常复杂,需要对cpython内部有低级别理解,并需要掌握C/C++/…编程知识(从根本上破坏了最初使用Python的目标之一),使得难以发展大型框架,并且(在ML情况下)将世界推向比“急切模式”系统更差基础可用性的“图形化”编程模型。TensorFlow就是一个例子,但PyTorch 2中大部分工作都集中在发现图形以启用更积极地编译方法。

除了两个世界问题本质上涉及到系统复杂性外,在生态系统中所有其他事物都变得更加复杂。调试器通常无法跨越Python和C代码进行步进调试,而那些可以的调试器也没有被广泛接受。对于软件包生态系统来说,处理C/C++代码而不是单个世界是一种痛苦。像PyTorch这样具有重要C++投入的项目正在有意识地尝试将其代码库中更多的内容移植到Python中,因为他们知道这样做会提高可用性。

三个世界的问题以及N个时间的问题

两个世界的问题在Python生态系统中普遍存在,但对于机器学习框架的开发人员来说情况更加糟糕。人工智能得到了广泛的加速,而这些加速器使用定制编程语言如CUDA。虽然CUDA是C++开发的,但它有自己特殊的问题和限制,并且没有像调试器或分析工具一样一致性强大的工具。此外,它实际上被锁定在单个硬件制造商!

人工智能领域在硬件方面拥有令人难以置信数量的创新成果,因此复杂性正在失控地螺旋上升。现在已经有许多尝试为加速器构建受限编程系统(OpenCL、Sycl、OneAPI等)。这种复杂度爆炸趋势还在继续增长,并且这些系统都没有解决困扰行业并使其受损最严重的基本工具和生态系统碎片化问题。

移动端和服务器部署

Python 生态系统面临的另一个挑战是部署。这包括希望仔细控制依赖项的人,一些人更喜欢能够部署密封编译的a.out文件,多线程和性能也非常重要。我们希望看到 Python 生态系统在这些领域迈出步伐。

相关工作:改进Python的其他方法

有许多方法可以改进Python,包括让Python变得更快和替换GIL的工作,看起来像Python但是它们是其子集的语言以及与Python集成但不是第一类语言的嵌入式DSL。虽然我们无法列出所有努力,但我们可以谈论这些领域中的一些挑战以及为什么它们不适合Modular使用。

改进CPython和JIT编译Python

最近,人们在改善CPython性能和其他实现问题方面投入了大量精力,这对社区来说产生了巨大的成果。这项工作非常棒,因为它逐步改进了当前的CPython实现。Python 3.11通过内部改进提供了比Python 3.10快10-60%的改进,并且Python 3.12旨在通过跟踪优化器更进一步地提高性能。许多其他项目正在尝试驯服GIL,并且像PyPy(以及许多其他项目)使用JIT编译和跟踪方法加速Python。

这些都是伟大的努力,但对于将统一语言放到加速器上并不有帮助。如今,许多加速器仅支持非常有限的动态特性或者以可怕的性能支持动态特性。此外,系统程序员不仅寻求“性能”,他们通常还想要很多关于计算发生方式“可预测和控制”的内容。

虽然我们欣赏这些方法,并认为它们对社区具有价值和吸引力,但遗憾的是它们不能满足我们的需求。我们希望消除在Python库中使用C或C++所需,在某些情况下完全不接受动态特性,因此这些方法并没有帮助。

Python子集和其他类似Python的语言

有许多尝试构建“可部署”Python的方法,其中一个例子是PyTorch项目中的TorchScript。这些方法很有用,因为它们通常提供低依赖性的部署解决方案,并且有时具有高性能。由于它们使用类似Python语法,所以比起新颖的语言更容易学习。

另一方面,这些语言并没有得到广泛采用-因为它们是子集,通常不与Python生态系统互操作,在工具(例如调试器)方面也不太好,并且经常单方面更改Python中不便利的行为,从而破坏兼容性并使生态系统分裂。例如,其中许多会将简单整数的行为更改为包装而非生成与Python兼容的数学结果。

这些方法存在挑战之处在于他们试图解决 Python 的弱点, 但却不能像 Python 强项那样出色地完成任务. 最好情况下, 这些可以提供 C 和 C++ 的新选择 - 但如果不能解决 Python 的动态使用场景,则无法解决“两个世界问题”。此方法导致了碎片化和不兼容性, 并使迁移变得困难甚至不可能 - 回想一下从 Python 2 到 Python 3 的迁移是多么具有挑战性。

Python中的嵌入式DSL

另一种常见的方法是在Python中构建嵌入式DSL,通常使用Python装饰器安装。有许多这样的例子,例如TensorFlow中的@tf.function装饰器、OpenAI Triton编程模型中的@triton.jit等。这些系统的一个主要优点是它们与所有Python生态系统工具兼容,并且可以本地集成到Python逻辑中,允许嵌入式小语言与动态用例下Python强大功能共存。

不幸的是,这些系统提供的嵌入式小语言经常具有令人惊讶的限制,在调试器和其他工作流工具方面集成效果不佳,并且不支持我们为统一异构计算并编写大规模内核和系统所寻求的本地语言集成水平。我们希望通过简化事物并使其更加一致来推进整个系统可用性。嵌入式DSL是快速启动演示程序的便捷方式,但我们愿意付出额外努力和工作来为我们自己设定目标。

一览Mojo编程风格

您可以像使用Python一样从终端运行Mojo程序。因此,如果您有一个名为hello.mojo(或hello.(火)——是的,文件扩展名可以是这个表情符号!)的文件,请键入mojo hello.mojo:
这个(火)就像:

^_^因为这个表情打不出来。

1
2
3
4
5
6
7
8
9
10
11
$ cat hello.(火)
def main():
print("hello world")
for x in range(9, 0, -3):
print(x)
$ mojo hello.(火)
hello world
9
6
3
$

你可以使用表情符号或.mojo后缀。

如果您对深入了解 Mojo 的内部实现细节感兴趣,可以查看标准库中的类型、笔记本中的示例代码、博客和其他样例代码。

考虑到我们的兼容性目标以及Python在高级应用和动态API方面的优势,我们不必花费太多时间来解释语言中这些部分是如何工作的。另一方面,Python对系统编程的支持主要委托给C语言,并且我们希望提供一个在该领域表现出色的单一系统。因此,本节将详细介绍每个主要组件和功能,并说明如何使用它们并附有示例。

letvar声明

在Mojo中的def内部,您可以为一个名称分配一个值,并且它会隐式地创建一个函数作用域变量,就像Python一样。这提供了一种非常动态和低仪式感的编写代码的方式,但由于以下两个原因而具有挑战性:

  1. 系统程序员经常希望声明某个值是不可变的,以实现类型安全性和性能。
  2. 他们可能希望在赋值时如果错误拼写了变量名,则会出错。

为支持此功能,Mojo提供了作用域运行时值声明:let是不可变的,var是可变的。这些值使用词法作用域并支持名称遮蔽:

1
2
3
4
5
6
7
def your_function(a, b):
let c = a
c = b # error: c is immutable

if c != b:
var c = b
stuff()

letvar声明支持类型说明符以及模式,还支持延迟初始化:

1
2
3
4
5
6
7
8
9
10
def your_function():
let x: Int8 = 42
let y: Int64 = 17

let z: Int8
if x != 0:
z = 1
else:
z = foo()
use(z)

Note that let and var are completely opt-in when in def declarations. You can still use implicitly declared values as with Python, and they get function scope as usual.

请注意,在def声明中,letvar是完全可选的。您仍然可以像Python一样使用隐式声明的值,并且它们会像往常一样获得函数作用域。

struct类型(译者:这是Rust的概念)

Mojo基于MLIR和LLVM,这两个编译器和代码生成系统在许多编程语言中都得到了广泛应用。这使我们能够更好地控制数据组织、直接访问数据字段以及其他提高性能的方式。现代系统编程语言的一个重要特征是,在不损失性能的情况下,在复杂低级操作之上构建高级别且安全的抽象。在Mojo中,这由struct类型提供。

在Mojo中,struct类似于Python class:它们都支持方法、字段、运算符重载、元编程装饰器等功能。它们之间的区别如下:

  1. Python类是动态的:它们允许动态分派、monkey-patching(或“swizzling”)以及在运行时动态绑定实例属性。

  2. Mojo structs是静态的:它们在编译时绑定(您不能在运行时添加方法)。Structs允许您通过牺牲灵活性来换取性能,并且易于使用而又安全。

以下是一个简单定义struct的示例:

Here’s a simple definition of a struct:

1
2
3
4
5
6
7
8
9
10
struct MyPair:
var first: Int
var second: Int
def __init__(self&, first: Int, second: Int):
self.first = first
self.second = second
def __lt__(self, rhs: MyPair) -> Bool:
return self.first < rhs.first or
(self.first == rhs.first and
self.second < rhs.second)

从语法上讲,与 Python 类相比,结构体中的所有实例属性都必须使用 var let 声明显式声明。

好了,详细的Mojo语法手册可以看这里






关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

SvelteKit中文4-高级概念

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念

高级路由

Rest参数

如果路由段的数量未知,您可以使用rest语法——例如,您可以像这样实现GitHub的文件查看器…

1
/[org]/[repo]/tree/[branch]/[...file]

…在这种情况下,对/sveltejs/kit/tree/master/documentation/docs/04-advanced-routing.md的请求将导致以下参数可用于页面:

1
2
3
4
5
6
{
org: 'sveltejs',
repo: 'kit',
branch: 'master',
file: 'documentation/docs/04-advanced-routing.md'
}

src/routes/a/[...rest]/z/+page.svelte 将匹配/a/z(即根本没有参数)以及/a/b/z/a/b/c/z 等等。请确保检查 rest 参数的值是否有效,例如使用一个匹配器。

404 pages

剩余参数还允许您呈现自定义的404页面。鉴于这些路由…

1
2
3
4
5
6
7
src/routes/
├ marx-brothers/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte

…如果您访问/marx-brothers/karl,则不会呈现marx-brothers/+error.svelte文件,因为没有匹配的路由。 如果要呈现嵌套的错误页面,则应创建与任何/marx-brothers/*请求匹配的路由,并从中返回404:

1
2
3
4
5
6
7
8
src/routes/
├ marx-brothers/
| ├ [...path]/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte

src/routes/marx-brothers/[…path]/+page.js

1
2
3
4
5
6
7
import { error } from '@sveltejs/kit';


/** @type {import('./$types').PageLoad} */
export function load(event) {
throw error(404, 'Not Found');
}

If you don’t handle 404 cases, they will appear in handleError

可选参数

[lang]/home 这样的路由包含一个名为lang的参数,该参数是必需的。有时将这些参数设置为可选项会更加有益,因此在此示例中,home en/home 都指向同一页。您可以通过将参数放入另一对方括号中来实现:[[lang]]/home

请注意,可选路由参数不能跟在 rest 参数后面([...rest]/[[optional]]),因为参数匹配是“贪婪”的,并且可选参数始终不会被使用。

Matching

src/routes/archive/[page] 这样的路由会匹配 /archive/3,但它也会匹配 /archive/potato。我们不希望这种情况发生。您可以通过向 params 目录添加一个匹配器(该匹配器接受参数字符串("3" "potato")并在其有效时返回 true)来确保路由参数格式正确…

src/params/integer.js

1
2
3
4
/** @type {import('@sveltejs/kit').ParamMatcher} */
export function match(param) {
return /^\d+$/.test(param);
}

…并扩充您的路线:

1
2
src/routes/archive/[page]
src/routes/archive/[page=integer]

如果路径名不匹配,SvelteKit 将尝试匹配其他路由(使用下面指定的排序顺序),最终返回 404。

params 目录中的每个模块都对应一个匹配器,除了 *.test.js*.spec.js 文件可以用于单元测试您的匹配器。

Matchers run both on the server and in the browser.

排序

一个路径可能匹配多个路由。例如,以下每个路由都将匹配 /foo-abc:

1
2
3
4
5
src/routes/[...catchall]/+page.svelte
src/routes/[[a=x]]/+page.svelte
src/routes/[b]/+page.svelte
src/routes/foo-[c]/+page.svelte
src/routes/foo-abc/+page.svelte

SvelteKit 需要知道正在请求的路由。为此,它根据以下规则对其进行排序…

  • 更具体的路由优先级更高(例如,没有参数的路由比一个动态参数的路由更具体,依此类推)
  • 带有匹配器([name=type])的参数优先级高于没有匹配器([name])的参数
  • [[optional]] [...rest] 参数除非它们是路由最后一部分,否则将被忽略。换句话说,对于排序目的而言,x/[[y]]/z x/z 等效处理。
  • 平局按字母顺序解决。

由此导致了这种顺序,意味着/foo-abc将调用src/routes/foo-abc/+page.svelte,而/foo-def将调用src/routes/foo-[c]/+page.svelte,而不是更少具体的路由:

1
2
3
4
5
src/routes/foo-abc/+page.svelte
src/routes/foo-[c]/+page.svelte
src/routes/[[a=x]]/+page.svelte
src/routes/[b]/+page.svelte
src/routes/[...catchall]/+page.svelte

编码

一些字符不能在文件系统中使用——Linux和Mac上的 /,Windows上的 \ / : * ? " < > |# %字符在URL中具有特殊含义,而[ ] ( )字符对于SvelteKit也具有特殊含义,因此这些字符不能直接用作您路由的一部分。

要在路由中使用这些字符,可以使用十六进制转义序列,其格式为[x+nn]其中nn是十六进制字符代码:

  • \[x+5c]
  • /[x+2f]
  • :[x+3a]
  • *[x+2a]
  • ?[x+3f]
  • "[x+22]
  • <[x+3c]
  • >[x+3e]
  • |[x+7c]
  • #[x+23]
  • %[x+25]
  • [[x+5b]
  • ][x+5d]
  • ([x+28]
  • )[x+29]

例如,要创建一个 /smileys/:-) 路由,您需要创建一个 src/routes/smileys/[x+3a]-[x+29]/+page.svelte 文件。

您可以使用 JavaScript 确定字符的十六进制代码:

1
':'.charCodeAt(0).toString(16); // '3a', hence '[x+3a]'

您也可以使用Unicode转义序列。通常情况下,您不需要这样做,因为您可以直接使用未编码的字符,但是如果由于某种原因无法在文件名中包含表情符号等,则可以使用转义字符。换句话说,以下两者是等效的:

1
2
src/routes/[u+d83e][u+dd2a]/+page.svelte
src/routes/../+page.svelte

Unicode转义序列的格式是[u+nnnn],其中nnnn是介于000010ffff之间的有效值。(与JavaScript字符串转义不同,不需要使用代理对来表示ffff以上的代码点。)要了解有关Unicode编码的更多信息,请参阅Programming with Unicode。

由于TypeScript无法处理以.字符开头的目录,因此在创建例如.well-known路由时编码这些字符可能会很有用:src/routes/[x+2e]well-known/...

高级布局

默认情况下,“布局层次结构”是“路由层次结构”的镜像。在某些情况下,这可能不是你想要的。

(group)

也许你有一些路由是“应用程序”路由,应该有一个布局(例如/dashboard/item),而其他的则是“营销”路由,应该有不同的布局(/blog/testimonials)。我们可以使用括号将这些路由分组到一个目录中 - 与普通目录不同,(app)(marketing)不会影响它们内部路径名的URL:

1
2
3
4
5
6
7
8
9
10
11
src/routes/
│ (app)/
│ ├ dashboard/
│ ├ item/
│ └ +layout.svelte
│ (marketing)/
│ ├ about/
│ ├ testimonials/
│ └ +layout.svelte
├ admin/
└ +layout.svelte

您也可以直接将 +page 放置在 (group)中,例如如果/应该是一个(app)(marketing) 页面。

突破布局

根布局适用于应用程序的每个页面 - 如果省略,则默认为<slot />。如果您希望某些页面具有与其余部分不同的布局层次结构,则可以将整个应用程序放在一个或多个组中,除了不应继承公共布局的路由之外。

在上面的示例中,/admin路由既不继承(app)也不继承(marketing)布局。

+page@

页面可以按路由基础打破当前的布局层次结构。假设我们在前面示例中的(app)组内有一个/item/[id]/embed路由:

1
2
3
4
5
6
7
8
9
10
src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page.svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
└ +layout.svelte

通常情况下,这将继承根布局、(app)布局、layout布局和[id]布局。我们可以通过在段名称后面添加@来重置到其中一个布局,或者对于根布局,使用空字符串。在此示例中,我们可以从以下选项中选择:

  • +page@[id].svelte - inherits from src/routes/(app)/item/[id]/+layout.svelte
  • +page@item.svelte - inherits from src/routes/(app)/item/+layout.svelte
  • +page@(app).svelte - inherits from src/routes/(app)/+layout.svelte
  • +page@.svelte - inherits from src/routes/+layout.svelte
1
2
3
4
5
6
7
8
9
10
src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page@(app).svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
└ +layout.svelte

+layout@

像页面一样,布局本身也可以使用相同的技术打破其父布局层次结构。例如,+layout@.svelte 组件将为所有子路由重置层次结构。

1
2
3
4
5
6
7
8
9
10
11
src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page.svelte // uses (app)/item/[id]/+layout.svelte
│ │ │ ├ +layout.svelte // inherits from (app)/item/+layout@.svelte
│ │ │ └ +page.svelte // uses (app)/item/+layout@.svelte
│ │ └ +layout@.svelte // inherits from root layout, skipping (app)/+layout.svelte
│ └ +layout.svelte
└ +layout.svelte

何时使用布局组

并非所有的用例都适合布局分组,也不应该感到必须使用它们。可能是因为您的用例会导致复杂的(group)嵌套,或者您不想为单个异常值引入(group)。完全可以使用其他手段,如组合(可重用load函数或Svelte组件)或if语句来实现所需功能。以下示例显示了一个布局,它将倒回到根布局并重用其他布局也可以使用的组件和函数:

src/routes/nested/route/+layout@.svelte

1
2
3
4
5
6
7
8
<script>
import ReusableLayout from '$lib/ReusableLayout.svelte';
export let data;
</script>

<ReusableLayout {data}>
<slot />
</ReusableLayout>

src/routes/nested/route/+layout.js

1
2
3
4
5
6
7
8
import { reusableLoad } from '$lib/reusable-load-function';


/** @type {import('./$types').PageLoad} */
export function load(event) {
// Add additional logic here, if needed
return reusableLoad(event);
}

Hooks

‘Hooks’ 是您声明的应用程序范围函数,SvelteKit 将在特定事件发生时调用它们,从而使您对框架的行为具有细粒度控制。

有两个 hooks 文件,都是可选的:

  • src/hooks.server.js - 应用程序服务器端钩子
  • src/hooks.client.js - 应用程序客户端钩子

这些模块中的代码将在应用程序启动时运行,因此它们非常适合初始化数据库客户端等操作。

您可以使用 config.kit.files.hooks 配置这些文件的位置。

Server hooks

以下钩子可以添加到 src/hooks.server.js 文件中:

handle

每当 SvelteKit 服务器接收到请求时,此函数都会运行 - 无论是在应用程序运行期间还是在预渲染期间 - 并确定响应。它接收一个表示请求的event对象和一个名为 resolve 的函数,该函数呈现路由并生成Response。这使您可以修改响应标头或正文,或完全绕过 SvelteKit(例如,用于编程实现路由)。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
if (event.url.pathname.startsWith('/custom')) {
return new Response('custom response');
}


const response = await resolve(event);
return response;
}

对于静态资源的请求,包括已经预渲染的页面,SvelteKit 不会进行处理。

如果未实现,则默认为 ({ event, resolve }) => resolve(event)。要向请求添加自定义数据,该数据将传递给 +server.js 和服务器加载函数中的处理程序,请填充以下所示的event.locals对象。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
event.locals.user = await getUserInformation(event.cookies.get('sessionid'));


const response = await resolve(event);
response.headers.set('x-custom-header', 'potato');


return response;
}

你可以定义多个处理函数,并使用序列助手函数执行它们。

resolve 还支持第二个可选参数,该参数可以更好地控制响应的呈现方式。该参数是一个对象,可以具有以下字段:

  • transformPageChunk(opts:{html:string,done:boolean}):MaybePromise <string | undefined> ——将自定义转换应用于HTML。如果done为true,则它是最终块。不能保证块是格式良好的HTML(例如,它们可能包括元素的开放标记但不包括其关闭标记),但它们总是在合理的边界处分割,例如%sveltekit.head%或layout/page组件。
  • filterSerializedResponseHeaders(name:string,value:string):boolean ——确定在使用fetch加载资源时哪些标题应包含在序列化响应中。默认情况下,不会包含任何内容。
  • preload(input: {type:'js'|'css'|'font'|'asset',path:string}): boolean - 确定要添加到<head>标签以预加载的文件。构建代码块时找到每个文件时都会调用该方法-因此如果您例如在+page.svelteimport “./styles.css”,则访问该页面时将使用解析后的路径调用preload 该CSS文件。请注意,在dev模式下不会调用预加载程序,因为它取决于构建时发生的分析。预加载可以通过更早地下载资产来提高性能,但如果下载过多而不必要,则也可能会损害性能。默认情况下,jscss文件将被预加载程序预加载所有当前未预先加载资产文件 ,但我们可能稍后根据反馈添加此功能。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('old', 'new'),
filterSerializedResponseHeaders: (name) => name.startsWith('x-'),
preload: ({ type, path }) => type === 'js' || path.includes('/important/')
});


return response;
}

请注意,resolve(...) 永远不会抛出错误,它将始终返回一个带有适当状态代码的 Promise<Response>。如果在 handle 过程中发生其他地方抛出错误,则被视为致命错误,并且 SvelteKit 将响应一个 JSON 表示形式的错误或回退错误页面(可以通过 src/error.html 自定义),具体取决于 Accept 标头。您可以在此处阅读更多关于错误处理的信息。

handleFetch

该功能允许您修改(或替换)在服务器上运行的loadaction函数内发生的fetch请求(或预渲染期间)。

例如,当用户执行客户端导航到相应页面时,您的load函数可能会向公共URLhttps://api.yourapp.com发出请求,但在SSR期间直接访问API可能更有意义(绕过任何代理和负载均衡器)。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ request, fetch }) {
if (request.url.startsWith('https://api.yourapp.com/')) {
// clone the original request, but change the URL
request = new Request(
request.url.replace('https://api.yourapp.com/', 'http://localhost:9999/'),
request
);
}


return fetch(request);
}

资质证明

对于同源请求,SvelteKit的fetch实现将转发cookieauthorization 头,除非将credentials 选项设置为“omit”

对于跨域请求,如果请求URL属于应用程序的子域名,则会包括cookie。例如,如果您的应用程序位于my-domain.com上,并且您的API位于api.my-domain.com上,则cookie将包含在请求中。

如果您的应用程序和API位于兄弟子域名上 - 例如www.my-domain.comapi.my-domain.com - 那么属于共同父域(如my-domain.com)的cookie不会被包括在内,因为SvelteKit无法知道该cookie所属的域。在这些情况下,您需要使用handleFetch手动包含cookie:

src/hooks.server.js

1
2
3
4
5
6
7
8
9
/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ event, request, fetch }) {
if (request.url.startsWith('https://api.my-domain.com/')) {
request.headers.set('cookie', event.request.headers.get('cookie'));
}


return fetch(request);
}

Shared hooks

以下内容可以添加到 src/hooks.server.jssrc/hooks.client.js

handleError

如果在加载或渲染过程中出现意外错误,将调用此函数并传递 errorevent。这样可以实现两个目的:

  • 你可以记录错误
  • 也可以生成一个定制的、安全的错误表示形式,以供用户查看,省略敏感信息如消息和堆栈跟踪。返回值成为 $page.error 的值。如果是 404 错误(你可以通过 event.route.id 为空来检测),则默认为 { message: 'Not Found' };对于其他所有情况,默认为 { message: 'Internal Error' }。要使其类型安全,请声明一个App.Error接口来自定义期望的形状(必须包括 message:string,以保证合理的回退行为)。

以下代码展示了将错误形状定义为 { message: string; errorId: string },并从 handleError 函数中相应地返回它的示例:

src/app.d.ts

1
2
3
4
5
6
7
8
9
10
11
declare global {
namespace App {
interface Error {
message: string;
errorId: string;
}
}
}


export {};

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as Sentry from '@sentry/node';
import crypto from 'crypto';


Sentry.init({/*...*/})


/** @type {import('@sveltejs/kit').HandleServerError} */
export async function handleError({ error, event }) {
const errorId = crypto.randomUUID();
// example integration with https://sentry.io/
Sentry.captureException(error, { event, errorId });


return {
message: 'Whoops!',
errorId
};
}

src/hooks.client.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import * as Sentry from '@sentry/svelte';


Sentry.init({/*...*/})


/** @type {import('@sveltejs/kit').HandleClientError} */
export async function handleError({ error, event }) {
const errorId = crypto.randomUUID();
// example integration with https://sentry.io/
Sentry.captureException(error, { event, errorId });


return {
message: 'Whoops!',
errorId
};
}

src/hooks.client.js 中,handleError 的类型是 HandleClientError 而不是 HandleServerError,并且eventNavigationEvent 而不是 RequestEvent

此函数不适用于预期错误(那些使用从 @sveltejs/kit 导入的 error 函数抛出的错误)。

在开发过程中,如果由于 Svelte 代码中的语法错误而导致错误,则传入的错误会附加一个frame属性,以突出显示错误位置。

确保 handleError 永远不会抛出错误。

Errors

错误是软件开发中不可避免的事实。SvelteKit 根据错误发生的位置、错误类型以及传入请求的性质,采用不同的处理方式。

Error objects

SvelteKit区分预期和非预期错误,两者默认都表示为简单的{ message: string }对象。

您可以添加其他属性,例如code或跟踪id,如下面的示例所示。(在使用TypeScript时,这需要您重新定义Error类型,如类型安全性中所述)。

Expected errors

预期错误是使用从 @sveltejs/kit 导入的错误助手创建的错误:

src/routes/blog/[slug]/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { error } from '@sveltejs/kit';
import * as db from '$lib/server/database';


/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
const post = await db.getPost(params.slug);


if (!post) {
throw error(404, {
message: 'Not found'
});
}


return { post };
}

这告诉 SvelteKit 将响应状态码设置为 404 并渲染一个 +error.svelte 组件,其中$page.error是作为 error(...) 的第二个参数提供的对象。

src/routes/+error.svelte

1
2
3
4
5
<script>
import { page } from '$app/stores';
</script>

<h1>{$page.error.message}</h1>

如果需要,您可以向错误对象添加额外的属性…

1
2
3
4
throw error(404, {
message: 'Not found',
code: 'NOT_FOUND'
});

否则,为了方便起见,您可以将字符串作为第二个参数传递:

1
2
throw error(404, { message: 'Not found' });
throw error(404, 'Not found');

Unexpected errors

意外错误”是指在处理请求时发生的任何其他异常。由于这些可能包含敏感信息,因此不会向用户公开意外错误消息和堆栈跟踪。

默认情况下,意外错误将被打印到控制台上(或者在生产环境中,记录到服务器日志中),而暴露给用户的错误具有通用形式:

1
{ "message": "Internal Error" }

意外错误将通过handleError钩子进行处理,您可以在其中添加自己的错误处理 - 例如,将错误发送到报告服务或返回自定义错误对象。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as Sentry from '@sentry/node';


Sentry.init({/*...*/})


/** @type {import('@sveltejs/kit').HandleServerError} */
export function handleError({ error, event }) {
// example integration with https://sentry.io/
Sentry.captureException(error, { event });


return {
message: 'Whoops!',
code: error?.code ?? 'UNKNOWN'
};
}

Make sure that handleError never throws an error

Responses

如果在 handle 函数或 +server.js 请求处理程序内部发生错误,SvelteKit 将根据请求的 Accept 标头响应回退错误页面或错误对象的 JSON 表示。

您可以通过添加src/error.html文件来自定义回退错误页面:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>%sveltekit.error.message%</title>
</head>
<body>
<h1>My custom error page</h1>
<p>Status: %sveltekit.status%</p>
<p>Message: %sveltekit.error.message%</p>
</body>
</html>

SvelteKit 将使用它们对应的值替换 %sveltekit.status% %sveltekit.error.message%

如果错误发生在渲染页面时的load函数内,SvelteKit 将在最接近错误位置的+error.svelte组件中呈现。如果错误发生在 +layout(.server).js中的加载函数中,则树中最接近该布局上方(而不是旁边)的 +error.svelte 文件是最接近的错误边界。

例外情况是当错误发生在根 +layout.js 或者 +layout.server.js 内部时,因为根布局通常包含了+error.svelte组件。这种情况下,SvelteKit 使用备用错误页面。

Type safety

如果您正在使用TypeScript并且需要自定义错误的形状,可以通过在应用程序中声明一个App.Error接口来实现(按照惯例,在src/app.d.ts中,尽管它可以存在于TypeScript“看到”的任何位置):

src/app.d.ts

1
2
3
4
5
6
7
8
9
10
declare global {
namespace App {
interface Error {
code: string;
id: string;
}
}
}

export {};

这个接口总是包含一个message: string属性。

Link options

在 SvelteKit 中,使用<a>元素(而不是特定于框架的 <Link> 组件)来在应用程序的路由之间导航。如果用户单击链接,其 href 属性“属于”应用程序(而不是指向外部站点的链接),则 SvelteKit 将通过导入其代码并调用任何需要获取数据的load函数来导航到新页面。

您可以使用 data-sveltekit-* 属性自定义链接行为。这些属性可以应用于<a>本身或父元素。

这些选项也适用于method="GET" <form> 元素。

data-sveltekit-preload-data

在浏览器注册用户点击链接之前,我们可以检测到他们在桌面上悬停鼠标或触发了touchstartmousedown事件。在这两种情况下,我们可以猜测即将出现一个click事件。

SvelteKit 可以利用这些信息提前导入代码和获取页面数据,这可以为我们节省几百毫秒的时间——这是让用户界面感觉卡顿和流畅之间的差异。

我们可以使用 data-sveltekit-preload-data 属性来控制此行为,该属性有两个值:

  • "hover" 的意思是,如果鼠标停留在链接上,预加载将开始。在移动设备上,touchstart 事件触发时即开始预加载。
  • “tap”的意思是,只要注册了touchstartmousedown事件,就会立即开始预加载。

默认的项目模板在src/app.html文件中的 <body> 元素上应用了 data-sveltekit-preload-data="hover" 属性,这意味着默认情况下每个链接都会在鼠标悬停时预加载:

1
2
3
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>

有时,在用户悬停在链接上时调用load可能是不可取的,因为它很可能会导致误报(点击并不一定跟随悬停),或者因为数据正在快速更新,延迟可能意味着过期。

在这些情况下,您可以指定“tap”值,这将导致SvelteKit仅在用户轻触或单击链接时调用load

1
2
3
<a data-sveltekit-preload-data="tap" href="/stonks">
Get current stonk values
</a>

你也可以通过$app/navigation编程调用 preloadData 方法。

如果用户选择了减少数据使用,即navigator.connection.saveDatatrue,则数据将永远不会预加载。

data-sveltekit-preload-code

即使在您不想为链接预加载数据的情况下,预加载代码也可能是有益的。 data-sveltekit-preload-code 属性与 data-sveltekit-preload-data 类似,但它可以取四个值之一,按“渴望程度”递减:

  • "eager" 表示链接将立即预加载
  • “viewport” 表示链接将在进入视口时预加载
  • “hover” - 与上述相同,只是仅预加载代码
  • “tap” - 与上述相同,只是仅预加载代码

请注意,viewporteager 只适用于紧随导航后立即出现在 DOM 中的链接 - 如果稍后添加链接(例如在{ #if } 块中),则只有在hovertap时才会预加载。这是为了避免由于过度观察 DOM 的更改而导致的性能问题。

由于预加载代码是预加载数据的先决条件,因此只有在它指定比任何已存在的 data-sveltekit-preload-data 属性更急切的值时,该属性才会生效。

data-sveltekit-preload-data 一样,如果用户选择了减少数据使用量,则会忽略此属性。

data-sveltekit-reload

有时候,我们需要告诉 SvelteKit 不要处理一个链接,而是让浏览器来处理它。在链接中添加 data-sveltekit-reload 属性即可

1
<a data-sveltekit-reload href="/path">Path</a>

当链接被点击时,会导致整个页面的跳转。

带有 rel="external" 属性的链接将接受相同的处理。此外,在预渲染期间它们将被忽略。

data-sveltekit-replacestate

有时候,您不希望导航在浏览器的会话历史中创建一个新条目。将 data-sveltekit-replacestate 属性添加到链接中…

1
<a data-sveltekit-replacestate href="/path">Path</a>

当链接被点击时,将替换当前的history记录条目而不是使用pushState创建一个新的条目。

data-sveltekit-keepfocus

有时候你不希望在导航后重置焦点。例如,可能你有一个搜索表单,在用户输入时提交,而你想保持文本输入框的焦点。可以给它添加一个data-sveltekit-keepfocus属性

1
2
3
<form data-sveltekit-keepfocus>
<input type="text" name="query">
</form>

将导致当前焦点元素在导航后保持焦点。一般来说,避免在链接上使用此属性,因为聚焦的元素将是<a> 标签(而不是先前聚焦的元素),屏幕阅读器和其他辅助技术用户通常期望在导航后移动焦点。您还应仅在导航后仍存在的元素上使用此属性。如果该元素不存在,则用户的重点将丢失,这会给辅助技术用户带来困惑。

data-sveltekit-noscroll

当导航到内部链接时,SvelteKit会镜像浏览器的默认导航行为:它将更改滚动位置为0,0,以便用户位于页面的左上角(除非链接包括#hash,在这种情况下它将滚动到具有匹配ID的元素)。

在某些情况下,您可能希望禁用此行为。向链接添加data-sveltekit-noscroll属性…

1
<a href="path" data-sveltekit-noscroll>Path</a>

…点击链接后将阻止滚动。

禁用选项

要禁用已启用的元素中的任何选项,请使用“off”值:

1
2
3
4
5
6
7
8
9
10
11
<div data-sveltekit-preload-data>
<!-- these links will be preloaded --> <a href="/a">a</a>
<a href="/b">b</a>
<a href="/c">c</a>

<div data-sveltekit-preload-data="off">
<!-- these links will NOT be preloaded --> <a href="/d">d</a>
<a href="/e">e</a>
<a href="/f">f</a>
</div>
</div>

要有条件地将属性应用于元素,请执行以下操作:

1
<div data-sveltekit-reload={shouldReload ? '' : 'off'}>

这是有效的,因为在HTML中,<element attribute>等同于<element attribute = "">

Service workers

服务工作者充当代理服务器,处理应用程序内的网络请求。这使得您的应用程序可以离线工作,但即使您不需要离线支持(或者由于正在构建的应用程序类型而无法实现),使用服务工作者通常也值得加快导航速度,通过预缓存已构建的 JS 和 CSS。

在 SvelteKit 中,如果您有一个src/service-worker.js文件(或 src/service-worker.jssrc/service-worker/index.js 等),它将被捆绑并自动注册。如果需要,可以更改服务工作者的位置。

如果需要使用自己的逻辑注册服务工作者或使用其他解决方案,则可以禁用自动注册。默认注册类似于以下内容:

1
2
3
4
5
if ('serviceWorker' in navigator) {
addEventListener('load', function () {
navigator.serviceWorker.register('./path/to/service-worker.js');
});
}

深入service worker

在 service worker 中,您可以访问 $service-worker 模块,该模块为您提供所有静态资源、构建文件和预渲染页面的路径。还提供了一个应用程序版本字符串,您可以使用它来创建唯一的缓存名称以及部署的base路径。如果您的 Vite 配置指定了 define(用于全局变量替换),则这也将应用于服务工作者以及服务器/客户端构建。

以下示例会急切地缓存已构建的应用程序和static文件,并在发生所有其他请求时缓存它们。这将使每个页面在访问后都能离线工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/// <reference types="@sveltejs/kit" />
import { build, files, version } from '$service-worker';


// Create a unique cache name for this deployment
const CACHE = `cache-${version}`;


const ASSETS = [
...build, // the app itself
...files // everything in `static`
];


self.addEventListener('install', (event) => {
// Create a new cache and add all files to it
async function addFilesToCache() {
const cache = await caches.open(CACHE);
await cache.addAll(ASSETS);
}


event.waitUntil(addFilesToCache());
});


self.addEventListener('activate', (event) => {
// Remove previous cached data from disk
async function deleteOldCaches() {
for (const key of await caches.keys()) {
if (key !== CACHE) await caches.delete(key);
}
}


event.waitUntil(deleteOldCaches());
});


self.addEventListener('fetch', (event) => {
// ignore POST requests etc
if (event.request.method !== 'GET') return;


async function respond() {
const url = new URL(event.request.url);
const cache = await caches.open(CACHE);


// `build`/`files` can always be served from the cache
if (ASSETS.includes(url.pathname)) {
return cache.match(url.pathname);
}


// for everything else, try the network first, but
// fall back to the cache if we're offline
try {
const response = await fetch(event.request);


if (response.status === 200) {
cache.put(event.request, response.clone());
}


return response;
} catch {
return cache.match(event.request);
}
}


event.respondWith(respond());
});

缓存时要小心!在某些情况下,过期的数据可能比离线不可用的数据更糟糕。由于浏览器会在缓存太满时清空缓存,因此您还应该小心缓存大型资产(如视频文件)。

发布期间

服务工作者在生产环境中进行捆绑,但在开发过程中不会进行捆绑。因此,只有支持服务工作者模块的浏览器才能在开发期间使用它们。如果您手动注册服务工作者,则需要在开发过程中传递 { type: 'module' } 选项:

1
2
3
4
5
6
import { dev } from '$app/environment';


navigator.serviceWorker.register('/service-worker.js', {
type: dev ? 'module' : 'classic'
});

在开发过程中,buildprerendered是空数组。

类型安全

为服务工作者设置适当的类型需要一些手动设置。在您的 service-worker.js 文件中,在文件顶部添加以下内容:

1
2
3
4
5
6
7
/// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />


const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self));

这将禁用像HTMLElement这样的DOM类型,因为它们在服务工作者中不可用,并实例化正确的全局变量。将self重新分配给sw允许您在过程中进行类型转换(有几种方法可以做到这一点,但最简单的方法需要没有额外文件)。在文件的其余部分使用sw而不是self。对SvelteKit类型的引用确保$service-worker导入具有适当的类型定义。

其他解决方案

SvelteKit的服务工作者实现是故意低级的。如果您需要更全面但也更有见解的解决方案,我们建议查看像Vite PWA插件这样使用Workbox的解决方案。对于关于服务工作者的更一般信息,我们建议参考MDN Web文档。

服务端优先modules

像一个好朋友一样,SvelteKit 会保守你的秘密。当在同一个代码库中编写后端和前端时,很容易意外地将敏感数据导入到前端代码中(例如包含 API 密钥的环境变量)。SvelteKit 提供了一种完全防止这种情况发生的方法:仅限服务器模块。

私有化环境变量

$env/static/private$env/dynamic/private 模块只能被导入到仅在服务器上运行的模块中,例如hooks.server.js+page.server.js。这些模块在模块部分有介绍。

属于你自己的modules

你可以通过两种方式将自己的模块设置为仅服务器可用:

  • 将文件名添加.server,例如secrets.server.js
  • 将它们放置在$lib/server中,例如$lib/server/secrets.js

如何工作

无论是直接还是间接地导入仅限于服务器的代码,只要您有面向公众的代码…

$lib/server/secrets.js

1
export const atlantisCoordinates = [/* redacted */];

src/routes/utils.js

1
2
3
4
export { atlantisCoordinates } from '$lib/server/secrets.js';


export const add = (a, b) => a + b;

src/routes/+page.svelte

1
2
3
<script>
import { add } from './utils.js';
</script>

…SvelteKit will error:

1
2
3
4
Cannot import $lib/server/secrets.js into public-facing code:
- src/routes/+page.svelte
- src/routes/utils.js
- $lib/server/secrets.js

尽管公共代码 - src/routes/+page.svelte - 仅使用 add 导出而不是 secret atlantisCoordinates 导出,但秘密代码可能最终会出现在浏览器下载的 JavaScript 中,因此导入链被认为是不安全的。

该功能还适用于动态导入,甚至包括像 await import(./${foo}.js) 这样的插值导入,只有一个小问题:在开发过程中,如果公共代码和仅服务器模块之间存在两个或多个动态导入,则非法导入将无法在第一次加载代码时检测到。

像Vitest这样的单元测试框架不区分仅服务器和公共界面代码。因此,在运行测试时,当process.env.TEST === 'true'时,非法导入检测被禁用。

Asset handling(资产处理)

缓存和内联

Vite将自动处理导入的资源以提高性能。哈希值将添加到文件名中,以便可以缓存,并且小于assetsInlineLimit的资源将被内联。

1
2
3
4
5
<script>
import logo from '$lib/assets/logo.png';
</script>

<img alt="The project logo" src={logo} />

如果您更喜欢在标记中直接引用资产,则可以使用预处理器,例如svelte-preprocess-import-assets。

对于通过CSS url()函数包含的资产,您可能会发现vitePreprocess很有用。

转换

您可能希望将图像转换为输出压缩图像格式,例如.webp.avif,具有不同设备大小的响应式图像,或者去除EXIF数据以保护隐私。对于静态包含的图像,您可以使用Vite插件(如vite-imagetools)。您还可以考虑使用CDN,在基于Accept HTTP标头和查询字符串参数提供适当转换后的图像。

快照

短暂的DOM状态,例如侧边栏的滚动位置、<input>元素的内容等,在从一个页面导航到另一个页面时会被丢弃。

例如,如果用户填写了表单但在提交之前点击了链接,然后点击浏览器的返回按钮,则他们填写的值将丢失。在需要保留该输入的情况下,可以对DOM状态进行快照,并在用户导航回来时恢复它。

要做到这一点,请从+page.svelte+layout.svelte中导出具有capturerestore 方法的快照对象:

+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
let comment = '';
/** @type {import('./$types').Snapshot<string>} */ export const snapshot = {
capture: () => comment,
restore: (value) => comment = value
};
</script>

<form method="POST">
<label for="comment">Comment</label>
<textarea id="comment" bind:value={comment} />
<button>Post comment</button>
</form>

当您离开此页面时, capture 功能会在页面更新之前立即调用,并将返回值与浏览器历史堆栈中的当前条目关联。如果您导航回来,则 restore 函数会在页面更新后立即使用存储的值进行调用。

数据必须可序列化为JSON,以便可以将其持久化到sessionStorage中。这允许在重新加载页面或用户从不同站点导航回来时恢复状态。

避免从capture返回非常大的对象 - 一旦被捕获,对象将在会话期间保留在内存中,在极端情况下可能太大而无法持久化到sessionStorage

打包

你可以使用SvelteKit构建应用程序和组件库,使用@sveltejs/package包(npm create svelte有一个选项可为您设置此内容)。

当您创建应用程序时,src/routes的内容是面向公众的东西;src/lib包含您的应用程序内部库。

组件库具有与SvelteKit应用程序完全相同的结构,只是src/lib是面向公众的部分,并且您的根package.json用于发布该软件包。 src/routes可能是文档或演示站点,以配合该库使用,也可能仅是在开发过程中使用的沙箱。

@sveltejs/package 运行 svelte-package 命令将获取 src/lib 目录的内容并生成一个 dist 目录(可以进行配置),其中包含以下内容:

  • src/lib目录下的所有文件。Svelte组件将被预处理,TypeScript文件将被转译为JavaScript。
  • 生成用于Svelte、JavaScript和TypeScript文件的类型定义(d.js文件)。您需要安装typescript >= 4.0.0才能使用此功能。类型定义放置在其实现旁边,手写的d.js文件会原样复制过来。您可以禁用生成,但我们强烈建议不要这样做——使用您库的人可能会使用TypeScript,并且他们需要这些类型定义文件。

@sveltejs/package 版本1 生成了一个 package.json 文件。现在不再这样做,它将使用您项目中的 package.json,并验证其是否正确。如果您仍在使用版本1,请参阅此 PR 获取迁移说明。

package.json的解剖结构

由于您现在正在为公共使用构建库,因此您的package.json文件的内容将变得更加重要。通过它,您可以配置包的入口点、发布到npm上的文件以及库所依赖的依赖项。让我们逐个浏览最重要的字段。

name

这是您的软件包名称。其他人可以使用该名称安装它,并在 https://npmjs.com/package/<name> 上可见。

1
2
3
{
"name": "your-library"
}

license

每个软件包都应该有一个许可证字段,以便人们知道如何使用它。一种非常流行的、在分发和重用方面非常自由且没有保修的许可证是MIT。

1
2
3
{
"license": "MIT"
}

在这里阅读更多相关信息。请注意,您的软件包中还应该包含一个许可证文件。

files

这会告诉npm要打包和上传到npm的哪些文件。它应该包含你的输出文件夹(默认为dist)。你的package.jsonREADMELICENSE将始终被包括在内,因此您不需要指定它们。

1
2
3
{
"files": ["dist"]
}

为了排除不必要的文件(例如单元测试或仅从src/routes等导入的模块),您可以将它们添加到.npmignore文件中。这将导致更小、安装更快的软件包。

exports

exports字段包含了该软件包的入口点。如果您通过npm create svelte@latest设置一个新的库项目,它将被设置为单个导出项,即软件包根目录:

1
2
3
4
5
6
7
8
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
}
}
}

这告诉打包工具和工具链,你的软件包只有一个入口点,即根目录,并且所有内容都应该通过它导入,就像这样:

1
import { Something } from 'your-library';

typesvelte键是导出条件。它们告诉工具在查找your-library导入时要导入哪个文件:

  • TypeScript看到types条件并查找类型定义文件。如果您没有发布类型定义,则省略此条件。
  • Svelte感知工具看到svelte条件,并知道这是一个Svelte组件库。如果您发布的库不导出任何Svelte组件,也可以在非Svelte项目中使用(例如,一个Svelte存储库),则可以将此条件替换为default

以前的 @sveltejs/package 版本还添加了 package.json 导出。这不再是模板的一部分,因为所有工具现在都可以处理没有显式导出 package.json 的情况。

您可以根据自己的喜好调整导出并提供更多入口。例如,如果您想直接公开 src/lib/Foo.svelte 组件而不是重新导出组件的 src/lib/index.js 文件,则可以创建以下导出映射…

1
2
3
4
5
6
7
8
{
"exports": {
"./Foo.svelte": {
"types": "./dist/Foo.svelte.d.ts",
"svelte": "./dist/Foo.svelte"
}
}
}

..你的库的使用者可以像这样导入组件:

1
import Foo from 'your-library/Foo.svelte';

请注意,如果您提供类型定义,则执行此操作需要额外的注意。在此处阅读有关警告的更多信息。

通常情况下,导出映射的每个键都是用户从您的包中导入某些内容所必须使用的路径,而值则是将被导入的文件路径或包含这些文件路径的导出条件映射。

svelte

这是一个遗留字段,使工具能够识别Svelte组件库。在使用svelte导出条件时不再需要它,但为了向后兼容旧的工具而保留它是很好的,这些旧工具还不知道导出条件。它应该指向你的根入口点。

1
2
3
{
"svelte": "./dist/index.js"
}

TypeScript

即使您不使用TypeScript,也应该为库提供类型定义,以便那些使用TypeScript的人在使用您的库时获得正确的智能提示。@sveltejs/package可以帮助您自动生成大部分类型定义。默认情况下,在打包库时,JavaScript、TypeScript和Svelte文件都会自动生成类型定义。您需要确保导出映射中的types条件指向正确的文件即可。通过npm create svelte@latest初始化一个库项目时,默认已经为根导出设置好了这一点。

但是如果你有其他东西而不是根导出——例如提供your-library/foo导入——你需要额外注意提供类型定义。不幸的是,默认情况下,TypeScript将无法解析像{ "./foo": { "types": "./dist/foo.d.ts", ... }}这样的导出类型条件。相反,它将在库根目录(即your-library/foo.d.ts而不是your-library/dist/foo.d.ts)中搜索foo.d.ts文件。要解决此问题,您有两个选择:

第一种选择是要求使用您的库的人将tsconfig.json(或jsconfig.json)中moduleResolution选项设置为bundler(从TypeScript 5开始可用,在未来最佳且推荐选项),node16nodenext 。这会让TypeScript实际查看exports map并正确解析类型。

第二种选择是利用TypeScript中名为typesVersions特性来连接各种类型定义。这是package.json内部字段之一,用于检查不同TypeScript版本的不同类型定义,并且还包含路径映射功能。我们利用该路径映射功能来实现我们想要的效果。对于上述foo导出,相应的typesVersions如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"exports": {
"./foo": {
"types": "./dist/foo.d.ts",
"svelte": "./dist/foo.js"
}
},
"typesVersions": {
">4.0": {
"foo": ["./dist/foo.d.ts"]
}
}
}

>4.0 告诉 TypeScript 如果使用的 TypeScript 版本大于 4(在实践中应该始终为真),则检查内部映射。内部映射告诉 TypeScript,your-library/foo 的类型定义可以在./dist/foo.d.ts中找到,这基本上复制了导出条件。你还可以使用 * 通配符来一次性地使许多类型定义可用而不必重复自己。请注意,如果选择 typesVersions,则必须通过它声明所有类型导入,包括根导入(其定义为 "index":[..])。

最佳实践

除非您打算仅供其他 SvelteKit 项目使用,否则应避免在包中使用类似 $app 的 SvelteKit 特定模块。例如,您可以使用 import { BROWSER } from 'esm-env'(请参阅 esm-env 文档),而不是使用 import { browser } from '$app/environment'。您还可以通过传递诸如当前 URL 或导航操作之类的 prop 来代替直接依赖于 $app/stores$app/navigation 等等。以这种更通用的方式编写应用程序也将使设置测试工具、UI 演示等变得更加容易。

确保通过 svelte.config.js(而不是vite.config.js tsconfig.json)添加别名,以便它们由 svelte-package 处理。

您应该仔细考虑对包所做的更改是否为错误修复、新功能或破坏性更改,并相应地更新软件包版本。请注意,如果从现有库中删除 exports 中的任何路径或其中任何export条件,则应将其视为破坏性更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"exports": {
".": {
"types": "./dist/index.d.ts",
// changing `svelte` to `default` is a breaking change:
"svelte": "./dist/index.js"
"default": "./dist/index.js"
},
// removing this is a breaking change:
"./foo": {
"types": "./dist/foo.d.ts",
"svelte": "./dist/foo.js",
"default": "./dist/foo.js"
},
// adding this is ok:
"./bar": {
"types": "./dist/bar.d.ts",
"svelte": "./dist/bar.js",
"default": "./dist/bar.js"
}
}
}

选项

svelte-package接受以下选项:

  • -w/--watch — 监听src/lib中的文件变化并重新构建包
  • -i/--input — 输入目录,其中包含所有包文件。默认为src/lib
  • -o/--o — 输出目录,处理后的文件将写入其中。您的package.json导出应指向该目录内的文件,并且files数组应包括该文件夹。默认为dist
  • -t/--types — 是否创建类型定义(d.js 文件)。我们强烈建议这样做,因为它有助于生态系统库质量提升。默认值为true

发布

发布生成的软件包:

1
npm publish

注意事项

所有相关文件导入都需要完全指定,遵循Node的ESM算法。这意味着对于像src/lib/something/index.js这样的文件,您必须包括带有扩展名的文件名:

1
2
import { something } from './something';
import { something } from './something/index.js';

如果您正在使用TypeScript,则需要以相同的方式导入.ts文件,但是要使用.js文件结尾而不是.ts文件结尾。(这是TypeScript设计决策之外我们无法控制的。)在tsconfig.jsonjsconfig.json中设置“moduleResolution”:“NodeNext”将有助于解决此问题。

除了Svelte文件(预处理)和TypeScript文件(转换为JavaScript),所有其他文件都会按原样复制。

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念



关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

SvelteKit中文3-编译发布

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念

构建 SvelteKit 应用程序分为两个阶段,这两个阶段都在运行 vite build 时发生(通常通过 npm run build 运行)。

首先,Vite 创建了一个优化的生产构建版本,包括您的服务器代码、浏览器代码和服务工作线程(如果有)。如果适当的话,在此阶段执行预渲染。

其次,适配器接受此生产构建并针对您的目标环境进行调整 - 更多信息请参见以下页面。

构建期间

SvelteKit将在构建期间加载您的+page/layout(.server).js文件(以及它们导入的所有文件)进行分析。任何不应在此阶段执行的代码都必须检查从$app/environment构建为false

1
2
3
4
5
6
7
8
9
10
import { building } from '$app/environment';
import { setupMyDatabase } from '$lib/server/database';

if (!building) {
setupMyDatabase();
}

export function load() {
// ...
}

预览你的应用程序

构建完成后,您可以通过vite预览(通过npm run preview)在本地查看生产版本。请注意,这将在Node中运行应用程序,因此不是您部署的应用程序的完美复制 - 适配器特定的调整(如平台对象)不适用于预览。

适配器

在部署 SvelteKit 应用程序之前,您需要为部署目标进行适应。适配器是小型插件,它们将构建的应用程序作为输入,并生成用于部署的输出。

针对各种平台存在官方适配器 - 这些在以下页面中有所记录:

Additional community-provided adapters exist for other platforms.

Using adapters

Your adapter is specified in svelte.config.js:

svelte.config.js

1
2
3
4
5
6
7
8
9
10
11
12
import adapter from 'svelte-adapter-foo';

/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
// adapter options go here
})
}
};

export default config;

Platform特定上下文

一些适配器可能可以访问有关请求的其他信息。例如,Cloudflare Workers 可以访问包含 KV 命名空间等内容的 env 对象。这可以作为platform属性传递给钩子和服务器路由中使用的 RequestEvent,请参阅每个适配器的文档以了解更多信息。

零配置发布

当你使用 npm create svelte@latest 创建一个新的 SvelteKit 项目时,默认会安装 adapter-auto。这个适配器会在部署时自动安装和使用支持环境的正确适配器:

建议在确定目标环境后,将适当的适配器安装到您的 devDependencies 中,因为这样会将适配器添加到锁定文件中,并略微提高 CI 的安装时间。

环境特定配置

要添加配置选项,例如在 adapter-verceladapter-netlify 中添加 { edge: true },您必须安装底层适配器 - adapter-auto 不接受任何选项。

增加社区版adapters

您可以通过编辑adapters.js并打开拉取请求来添加对其他适配器的零配置支持。

Node servers

使用 adapter-node 生成独立的 Node 服务器。

用法

使用 npm i -D @sveltejs/adapter-node 进行安装,然后将适配器添加到您的svelte.config.js文件中:

svelte.config.js

1
2
3
4
5
6
7
import adapter from '@sveltejs/adapter-node';

export default {
kit: {
adapter: adapter()
}
};

Deploying

首先,使用npm run build构建您的应用程序。这将在适配器选项中指定的输出目录(默认为build)中创建生产服务器。

要运行应用程序,您需要输出目录、项目的package.json文件以及node_modules中的生产依赖项。可以通过复制package.jsonpackage-lock.json并运行npm ci --omit dev来生成生产依赖项(如果您的应用程序没有任何依赖项,则可以跳过此步骤)。然后,您可以使用以下命令启动应用程序:

1
node build

开发依赖将使用Rollup打包到您的应用程序中。要控制给定软件包是否捆绑或外部化,请分别将其放置在package.json文件中的devDependenciesdependencies中。

环境变量

在开发和预览中,SvelteKit 将从您的 .env 文件(或 .env.local.env.[mode],由 Vite 确定)读取环境变量。

在生产中,.env 文件不会自动加载。要这样做,请在项目中安装 dotenv

1
npm install dotenv

…在运行构建的应用程序之前调用它:

1
2
node build
node -r dotenv/config build

PORT and HOST

默认情况下,服务器将使用端口3000在0.0.0.0上接受连接。这些可以通过PORTHOST环境变量进行自定义:

1
HOST=127.0.0.1 PORT=4000 node build

ORIGIN, PROTOCOL_HEADER and HOST_HEADER

HTTP 无法为 SvelteKit 提供一种可靠的方式来知道当前请求的 URL。告诉 SvelteKit 应用程序正在哪里提供服务的最简单方法是设置 ORIGIN 环境变量:

1
2
3
4
ORIGIN=https://my.site node build

# or e.g. for local previewing and testing
ORIGIN=http://localhost:3000 node build

通过这样做,对/stuff路径的请求将正确地解析为 https://my.site/stuff。或者,您可以指定头文件来告诉 SvelteKit 请求协议和主机,从而它可以构建起源 URL:

1
PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build

x-forwarded-protox-forwarded-host是事实上的标准头,如果您使用反向代理(例如负载均衡器和CDN),则会转发原始协议和主机。只有在服务器位于可信赖的反向代理后面时,才应设置这些变量;否则,客户端可能会伪造这些头部信息。

如果adapter-node无法正确确定您的部署URL,则在使用表单操作时可能会遇到此错误:

跨站点POST表单提交被禁止

ADDRESS_HEADER and XFF_DEPTH

传递给钩子和端点的RequestEvent对象包括一个event.getClientAddress()函数,该函数返回客户端的IP地址。默认情况下,这是连接的remoteAddress。如果您的服务器在一个或多个代理后面(例如负载均衡器),则此值将包含最内部代理的IP地址而不是客户端的IP地址,因此我们需要指定ADDRESS_HEADER来读取地址:

1
ADDRESS_HEADER=True-Client-IP node build

标题很容易被欺骗。就像PROTOCOL_HEADERHOST_HEADER一样,设置这些之前应该知道自己在做什么。

如果 ADDRESS_HEADER X-Forwarded-For,则头部值将包含一个逗号分隔的 IP 地址列表。XFF_DEPTH 环境变量应指定在您的服务器前面有多少个受信任的代理。例如,如果有三个受信任的代理,第三个代理将转发原始连接和前两个代理的地址:

1
<client address>, <proxy 1 address>, <proxy 2 address>

有些指南会告诉你阅读最左边的地址,但这样会让你容易受到欺骗攻击:

1
<spoofed address>, <client address>, <proxy 1 address>, <proxy 2 address>

我们相反地从右边读取,考虑到受信任的代理数量。在这种情况下,我们将使用XFF_DEPTH=3。

如果您需要阅读最左边的地址(并且不关心欺骗),例如提供地理定位服务,其中IP地址真实性比可信度更重要,则可以通过检查应用程序中的x-forwarded-for标头来实现。

BODY_SIZE_LIMIT

最大请求体大小(以字节为单位),包括流式传输。默认值为512kb。如果您需要更高级的功能,可以使用0来禁用此选项,并在处理程序中实现自定义检查。

Options

适配器可以配置各种选项:

svelte.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import adapter from '@sveltejs/adapter-node';

export default {
kit: {
adapter: adapter({
// default options are shown
out: 'build',
precompress: false,
envPrefix: '',
polyfill: true
})
}
};

out

构建服务器的目录。默认为 build —— 即在创建后本地启动node build服务器。

precompress

启用使用gzip和brotli对资源和预渲染页面进行预压缩。默认值为false

envPrefix

如果您需要更改用于配置部署的环境变量的名称(例如,为了与您无法控制的环境变量解决冲突),则可以指定前缀:

1
2
3
4
5
envPrefix: 'MY_CUSTOM_';
MY_CUSTOM_HOST=127.0.0.1 \
MY_CUSTOM_PORT=4000 \
MY_CUSTOM_ORIGIN=https://my.site \
node build

polyfill

控制构建是否会为缺失的模块加载 polyfills。默认值为 true,仅在使用 Node 18.11 或更高版本时禁用。

Custom server

适配器会在您的构建目录中创建两个文件 ——index.jshandler.js。运行 index.js,例如 node build(如果您使用默认的构建目录),将在配置的端口上启动服务器。

或者,您可以导入 handler.js 文件,该文件导出了适用于 Express、Connect 或 Polka(甚至只是内置的 http.createServer)的处理程序,并设置自己的服务器:

my-server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { handler } from './build/handler.js';
import express from 'express';

const app = express();

// add a route that lives separately from the SvelteKit app
app.get('/healthcheck', (req, res) => {
res.end('ok');
});

// let SvelteKit handle everything else, including serving prerendered pages and static assets
app.use(handler);

app.listen(3000, () => {
console.log('listening on port 3000');
});

Troubleshooting

在服务器退出之前有清理的钩子吗?

SvelteKit 中没有内置此功能,因为这样的清理钩子高度依赖于您所在的执行环境。对于 Node,您可以使用其内置的 process.on(..) 来实现一个回调,在服务器退出之前运行:

1
2
3
4
5
6
7
function shutdownGracefully() {
// anything you need to clean up manually goes in here
db.shutdown();
}

process.on('SIGINT', shutdownGracefully);
process.on('SIGTERM', shutdownGracefully);

Static site generation

要将SvelteKit用作静态站点生成器(SSG),请使用adapter-static

这将把您的整个网站预渲染为一组静态文件。如果您只想预渲染一些页面并动态服务器呈现其他页面,则需要与prerender选项一起使用不同的适配器。

用法

使用npm i -D @sveltejs/adapter-static进行安装,然后将适配器添加到您的 svelte.config.js 文件中:

svelte.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import adapter from '@sveltejs/adapter-static';


export default {
kit: {
adapter: adapter({
// default options are shown. On some platforms
// these options are set automatically — see below
pages: 'build',
assets: 'build',
fallback: null,
precompress: false,
strict: true
})
}
};

…and add the prerender option to your root layout:

…并将prerender 选项添加到您的根布局中:

src/routes/+layout.js

1
2
// This can be false if you're using a fallback (i.e. SPA mode)
export const prerender = true;

You must ensure SvelteKit’s trailingSlash option is set appropriately for your environment. If your host does not render /a.html upon receiving a request for /a then you will need to set trailingSlash: 'always' to create /a/index.html instead.

Zero-config support

一些平台具有零配置支持(未来将会有更多):

在这些平台上,您应该省略适配器选项,以便adapter-static 可以提供最佳配置:

svelte.config.js

1
2
3
4
5
6
export default {
kit: {
adapter: adapter({...})
adapter: adapter()
}
};

Options

pages

要写入预渲染页面的目录。默认为 build

assets(资产)

将静态资源(static目录下的内容以及SvelteKit生成的客户端JS和CSS)写入的目录。通常情况下,这应该与pages相同,并且默认为pages的值,但在极少数情况下,您可能需要将页面和资产输出到不同位置。

fallback(回退)

指定单页应用程序模式的回退页面,例如 index.html200.html404.html

precompress(预压缩)

如果true, 使用Brotli和Gzip预压缩文件。这将生成.br.gz文件。

strict

默认情况下,adapter-static 会检查您的应用程序的所有页面和端点(如果有)是否都已经预渲染,或者您已经设置了fallback。此检查存在是为了防止您意外发布一款无法访问其部分内容的应用程序,因为它们不包含在最终输出中。如果您知道这是可以接受的(例如当某个页面仅在特定条件下存在时),则可以将strict设置为 false 来关闭此检查。

GitHub Pages

在为 GitHub Pages 构建时,请确保更新 paths.base 以匹配您的仓库名称,因为该站点将从 https://your-username.github.io/your-repo-name 提供而不是从根目录提供。

您需要在static文件夹中放置一个空的 .nojekyll 文件来防止 GitHub 提供的 Jekyll 管理您的站点。

GitHub Pages 的配置可能如下所示:

svelte.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import adapter from '@sveltejs/adapter-static';


const dev = process.argv.includes('dev');


/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
paths: {
base: dev ? '' : process.env.BASE_PATH,
}
}
};

当您进行更改时,您可以使用GitHub Actions自动将站点部署到GitHub Pages。以下是一个示例工作流程:

.github/workflows/deploy.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
name: Deploy to GitHub Pages


on:
push:
branches: 'main'


jobs:
build_site:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3


# If you're using pnpm, add this step then change the commands and cache key below to use `pnpm`
# - name: Install pnpm
# uses: pnpm/action-setup@v2
# with:
# version: 8


- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: npm


- name: Install dependencies
run: npm install


- name: build
env:
BASE_PATH: '/your-repo-name'
run: |
npm run build
touch build/.nojekyll


- name: Upload Artifacts
uses: actions/upload-pages-artifact@v1
with:
# this should match the `pages` option in your adapter-static options
path: 'build/'


deploy:
needs: build_site
runs-on: ubuntu-latest


permissions:
pages: write
id-token: write


environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v1

单页面apps

您可以通过在根布局中禁用 SSR,将使用任何适配器的 SvelteKit 应用程序转换为完全客户端渲染的单页应用程序(SPA):

src/routes/+layout.js

1
export const ssr = false;

在大多数情况下,这是不推荐的:它会损害SEO,倾向于降低感知性能,并且如果JavaScript失败或被禁用(比你想象的更常见),则使您的应用程序无法访问用户。

如果您没有任何服务器端逻辑(即+page.server.js+layout.server.js+server.js文件),则可以使用adapter-static通过添加回退页面来创建您的SPA。

Usage

使用 npm i -D @sveltejs/adapter-static 进行安装,然后在svelte.config.js中添加适配器和以下选项:

svelte.config.js

1
2
3
4
5
6
7
8
9
10
import adapter from '@sveltejs/adapter-static';


export default {
kit: {
adapter: adapter({
fallback: '200.html' // may differ from host to host
})
}
};

fallback页面是由SvelteKit从您的页面模板(例如app.html)创建的HTML页面,它加载您的应用程序并导航到正确的路由。例如Surge,一个静态Web主机,允许您添加一个200.html文件来处理不对应于静态资源或预渲染页面的任何请求。

在一些主机上可能是index.html或完全不同的其他内容 - 请查阅平台文档。

Apache

要在Apache上运行SPA,您应该添加一个static/.htaccess文件来将请求路由到回退页面:

1
2
3
4
5
6
7
8
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^200\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /200.html [L]
</IfModule>

渲染单个页面

如果您想要某些页面进行预渲染,您可以重新启用 SSR 并在应用程序的这些部分中同时使用prerender

src/routes/my-prerendered-page/+page.js

1
2
export const prerender = true;
export const ssr = true;

Cloudflare Pages

要部署到 Cloudflare Pages,请使用 adapter-cloudflare

当您使用 adapter-auto 时,此适配器将默认安装,但建议将其添加到您的项目中,以便自动为 event.platform 进行类型设置。

Comparisons

  • adapter-cloudflare - 支持所有SvelteKit功能;适用于Cloudflare Pages构建
  • adapter-cloudflare-workers - 支持所有SvelteKit功能;适用于Cloudflare Workers构建
  • adapter-static - 仅生成客户端静态资源;与Cloudflare Pages兼容

除非您有使用 adapter-cloudflare-workers 的特定原因,否则建议您改用此适配器。两个适配器具有等效的功能,但 Cloudflare Pages 提供了诸如 GitHub 集成、自动构建和部署、预览部署、即时回滚等功能。

Usage

使用 npm i -D @sveltejs/adapter-cloudflare 进行安装,然后将适配器添加到您的 svelte.config.js 文件中:

svelte.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import adapter from '@sveltejs/adapter-cloudflare';


export default {
kit: {
adapter: adapter({
// See below for an explanation of these options
routes: {
include: ['/*'],
exclude: ['<all>']
}
})
}
};

Options

routes 选项允许您自定义适配器-Cloudflare生成的_routes.json文件。

  • include 定义将调用函数的路由,并默认为['/*']

  • exclude 定义不会调用函数的路由 - 这是一种更快、更便宜的方式来提供应用程序的静态资源。此数组可以包含以下特殊值:

    • 包含您应用程序构建工件(Vite 生成的文件)

    • 包含您静态目录中的内容

    • 包含预渲染页面列表

    • (默认)包括以上所有内容

您最多可以有100个includeexclude规则组合。通常情况下,您可以省略routes选项,但如果(例如)您的 <prerendered> 路径超过该限制,则可能会发现手动创建一个排除列表(其中包括 '/articles/*' 而不是自动生成 ['/articles/foo', '/articles/bar', '/articles/baz', ...])很有帮助。

Deployment(发布)

请按照 Cloudflare Pages 的入门指南开始操作。

在配置项目设置时,您必须使用以下设置:

  • 框架预设 - 无
  • 构建命令 - npm run buildvite build
  • 构建输出目录 - .svelte-kit/cloudflare
  • 环境变量
    • NODE_VERSION: 16

您需要在“production”和“preview”环境中都添加NODE_VERSION环境变量。您可以在项目设置期间或稍后在Pages项目设置中添加此变量。SvelteKit要求使用Node 16.14或更高版本,因此您应该将NODE_VERSION值设为16

环境变量

env对象包含KV/DO命名空间等信息,通过platform属性传递给SvelteKit,同时还有contextcaches,这意味着您可以在钩子和端点中访问它:

1
2
3
export async function POST({ request, platform }) {
const x = platform.env.YOUR_DURABLE_OBJECT_NAMESPACE.idFromName('x');
}

要使这些类型在您的应用程序中可用,请在src/app.d.ts中引用它们:

src/app.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
declare global {
namespace App {
interface Platform {
env?: {
YOUR_KV_NAMESPACE: KVNamespace;
YOUR_DURABLE_OBJECT_NAMESPACE: DurableObjectNamespace;
};
}
}
}

export {};

platform.env 仅在生产构建中可用。请使用 wrangler 在本地进行测试。

Notes

位于项目根目录下的/functions文件夹中的函数不会被包含在部署中,该部署编译为单个_worker.js文件。应将函数实现为SvelteKit应用程序中的服务器端点。

特定于Cloudflare Pages的_headers_redirects文件可通过将它们放入/static文件夹来用于静态资产响应(如图像)。

但是,它们对由SvelteKit动态呈现的响应没有影响,这些响应应从服务器端点或使用handle钩子返回自定义标头或重定向响应。

Troubleshooting

访问文件系统

在Serverless/Edge环境中,您无法通过fs.readFileSync等方法访问文件系统。如果您需要以这种方式访问文件,请在预渲染期间构建应用程序时执行此操作。例如,如果您有一个博客并且不想通过CMS管理内容,则需要对内容进行预渲染(或从获取内容的端点进行预渲染),并在每次添加新内容时重新部署博客。

Cloudflare Workers

要部署到Cloudflare Workers,请使用adapter-cloudflare-workers

除非您有特定的原因使用此适配器,否则我们建议使用adapter-cloudflare

Requires Wrangler v2.

Usage

使用 npm i -D @sveltejs/adapter-cloudflare-workers 进行安装,然后将适配器添加到您的 svelte.config.js 文件中:

svelte.config.js

1
2
3
4
5
6
7
8
import adapter from '@sveltejs/adapter-cloudflare-workers';


export default {
kit: {
adapter: adapter()
}
};

Basic Configuration

该适配器期望在项目根目录中找到一个名为 wrangler.toml 的文件。它应该长这样:

wrangler.toml

1
2
3
4
5
6
7
8
9
10
name = "<your-service-name>"
account_id = "<your-account-id>"

main = "./.cloudflare/worker.js"
site.bucket = "./.cloudflare/public"

build.command = "npm run build"

compatibility_date = "2021-11-12"
workers_dev = true

<your-service-name> 可以是任何名称。<your-account-id>可以在登录 Cloudflare 仪表板后从 URL 的末尾获取:

1
https://dash.cloudflare.com/<your-account-id>

您应该将 .cloudflare 目录(或者您为 main site.bucket 指定的任何目录)添加到您的 .gitignore 文件中。

如果您还没有安装wrangler并登录,您需要先进行安装:

1
2
npm i -g wrangler
wrangler login

然后你可以build应用,并发布它:

1
wrangler publish

Custom config(自定义配置)

如果您想使用除wrangler.toml以外的配置文件,可以按照以下方式操作:

svelte.config.js

1
2
3
4
5
6
7
8
import adapter from '@sveltejs/adapter-cloudflare-workers';


export default {
kit: {
adapter: adapter({ config: '<your-wrangler-name>.toml' })
}
};

Environment variables(环境变量)

env对象包含KV/DO命名空间等信息,通过platform属性传递给SvelteKit,同时还有contextcaches,这意味着您可以在钩子和端点中访问它:

1
2
3
export async function POST({ request, platform }) {
const x = platform.env.YOUR_DURABLE_OBJECT_NAMESPACE.idFromName('x');
}

要使这些类型在您的应用程序中可用,请在src/app.d.ts中引用它们:

src/app.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
declare global {
namespace App {
interface Platform {
env?: {
YOUR_KV_NAMESPACE: KVNamespace;
YOUR_DURABLE_OBJECT_NAMESPACE: DurableObjectNamespace;
};
}
}
}

export {};

platform.env 仅在生产构建中可用。请使用 wrangler 在本地进行测试。

Troubleshooting

Worker size limits

在部署到工作线程时,SvelteKit 生成的服务器会被捆绑成一个单独的文件。如果经过缩小后超出大小限制,则 Wrangler 将无法发布您的工作线程。通常情况下不太可能达到此限制,但某些大型库可能会导致这种情况发生。在这种情况下,您可以尝试通过仅在客户端上导入此类库来减小工作线程的大小。有关更多信息,请参见 FAQ。

Accessing the file system

在Serverless/Edge环境中,您无法通过fs.readFileSync等方法访问文件系统。如果您需要以这种方式访问文件,请在预渲染期间构建应用程序时执行此操作。例如,如果您有一个博客并且不想通过CMS管理内容,则需要对内容进行预渲染(或从获取内容的端点进行预渲染),并在每次添加新内容时重新部署博客。

Netlify

要部署到 Netlify,请使用 adapter-netlify

当您使用 adapter-auto 时,此适配器将默认安装,但将其添加到项目中允许您指定特定于 Netlify 的选项。

Usage

使用npm i -D @sveltejs/adapter-netlify进行安装,然后将适配器添加到您的 svelte.config.js 文件中:

svelte.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import adapter from '@sveltejs/adapter-netlify';


export default {
kit: {
// default options are shown
adapter: adapter({
// if true, will create a Netlify Edge Function rather
// than using standard Node-based functions
edge: false,


// if true, will split your app into multiple functions
// instead of creating a single one for the entire app.
// if `edge` is true, this option cannot be used
split: false
})
}
};

然后,请确保在项目根目录中有一个netlify.toml文件。这将根据build.publish设置确定静态资产的写入位置,如此示例配置所示:

1
2
3
[build]
command = "npm run build"
publish = "build"

如果 netlify.toml 文件或 build.publish 值缺失,则默认值为 "build"。请注意,如果您已在 Netlify UI 中将发布目录设置为其他内容,则还需要在 netlify.toml 中设置它,或者使用默认值 "build"

Node version

新项目将默认使用 Node 16。但是,如果您正在升级一段时间前创建的项目,则可能会停留在旧版本上。请参阅 Netlify 文档以了解手动指定 Node 16 或更新版本的详细信息。

Netlify边缘函数(beta)

SvelteKit 支持 Netlify Edge Functions 的 beta 版本。如果您将选项 edge: true 传递给adapter函数,则服务器端渲染将在基于 Deno 的边缘函数中发生,该函数部署在靠近站点访问者的位置。如果设置为 false(默认值),则网站将部署到基于标准 Node 的 Netlify 函数上。

svelte.config.js

1
2
3
4
5
6
7
8
9
10
11
12
import adapter from '@sveltejs/adapter-netlify';


export default {
kit: {
adapter: adapter({
// will create a Netlify Edge Function using Deno-based
// rather than using standard Node-based functions
edge: true
})
}
};

Netlify的替代方案,以实现SvelteKit的功能

您可以使用 SvelteKit 直接提供的功能构建您的应用程序,而无需依赖于任何 Netlify 功能。使用这些功能的 SvelteKit 版本将允许它们在开发模式下使用,在集成测试中进行测试,并且如果您决定切换到其他适配器,则可以与其一起工作。但是,在某些情况下,您可能会发现使用这些功能的 Netlify 版本很有益处。一个例子是,如果您正在将已经托管在 Netlify 上的应用迁移到 SvelteKit。

重定向规则

在编译过程中,重定向规则会自动添加到您的 _redirects 文件中。(如果尚不存在,则将创建该文件。)这意味着:

netlify.toml 中的 [[redirects]] 永远不会匹配,因为 _redirects 具有更高的优先级。因此,请始终将规则放在 _redirects 文件中。
_redirects 不应具有任何自定义“捕获所有”规则,例如 /* /foobar/:splat。否则,由于 Netlify 仅处理第一个匹配规则,自动添加的规则永远不会被应用。

Netlify Forms

  1. 按照此处描述创建您的Netlify HTML表单,例如作为/routes/contact/+page.svelte。(不要忘记添加隐藏的form-name输入元素!)
  2. Netlify的构建机器人在部署时解析您的HTML文件,这意味着您的表单必须预渲染为HTML。 您可以将export const prerender = true添加到contact.svelte中以仅预渲染该页面,或者设置kit.prerender.force:true选项以预渲染所有页面。
  3. 如果您的Netlify表单具有自定义成功消息(如<form netlify ... action="/success">),则请确保相应的/routes/success/+page.svelte存在并已经预渲染。

Netlify Functions

通过这个适配器,SvelteKit 端点可以作为 Netlify 函数托管。Netlify 函数处理程序具有额外的上下文信息,包括 Netlify Identity 信息。您可以通过 hooks 和+page.server +layout.server 端点内的event.platform.context字段访问此上下文。当适配器配置中 edge 属性为 false 时,它们是无服务器函数;当其为 true 时,则是边缘函数。

+page.server.js

1
2
3
4
export const load = async (event) => {
const context = event.platform.context;
console.log(context); // shows up in your functions log in the Netlify app
};

此外,您可以通过创建一个目录并将配置添加到 netlify.toml 文件中来添加自己的 Netlify 函数。例如:

1
2
3
4
5
6
[build]
command = "npm run build"
publish = "build"

[functions]
directory = "functions"

Troubleshooting

访问文件系统

在Serverless/Edge环境中,您无法通过fs.readFileSync等方法访问文件系统。如果您需要以这种方式访问文件,请在预渲染期间构建应用程序时执行此操作。例如,如果您有一个博客并且不想通过CMS管理内容,则需要对内容进行预渲染(或从获取内容的端点进行预渲染),并在每次添加新内容时重新部署博客。

Vercel

要部署到 Vercel,请使用 adapter-vercel

当您使用 adapter-auto 时,此适配器将默认安装,但将其添加到项目中允许您指定特定于 Vercel 的选项。

Usage

使用 npm i -D @sveltejs/adapter-vercel 进行安装,然后将适配器添加到您的 svelte.config.js 文件中:

svelte.config.js

1
2
3
4
5
6
7
8
9
10
import adapter from '@sveltejs/adapter-vercel';


export default {
kit: {
adapter: adapter({
// see the 'Deployment configuration' section below
})
}
};

Deployment configuration

要控制您的路由如何作为函数部署到 Vercel,您可以通过上面显示的选项或在 +server.js+page(.server).js +layout(.server).js 文件中使用 export const config 指定部署配置。

例如,您可以将应用程序的某些部分部署为 Edge Functions…

about/+page.js

1
2
3
4
/** @type {import('@sveltejs/adapter-vercel').Config} */
export const config = {
runtime: 'edge'
};

…以及其他作为无服务器函数(请注意,通过在布局中指定config,它适用于所有子页面):

admin/+layout.js

1
2
3
4
/** @type {import('@sveltejs/adapter-vercel').Config} */
export const config = {
runtime: 'nodejs18.x'
};

以下选项适用于所有函数:

  • runtime: 'edge''nodejs16.x' 'nodejs18.x'。默认情况下,适配器将根据您在 Vercel 仪表板上配置的 Node 版本选择'nodejs16.x' 'nodejs18.x'
  • regions: 边缘网络区域的数组(默认为 ["iad1"] 用于无服务器函数),或者如果运行时是 edge,则为'all'(其默认值)。请注意,仅企业版计划支持多个无服务功能区域
  • split: 如果设置为 true,则会导致路由部署为单独的函数。如果在适配器级别上将 split 设置为 true,则所有路由都将作为单独的函数部署。

此外,以下选项适用于边缘功能:

  • envVarsInUse:应该在边缘功能内可访问的环境变量数组
  • external:esbuild 在捆绑函数时应视为外部依赖项的数组。这只应该用于排除不会在 Node 外运行的可选依赖项

以下选项适用于无服务器功能:

  • memory:函数可用内存量。默认值为1024 Mb,并且可以减少到128 Mb或以64Mb增量增加到Pro或Enterprise帐户中最高3008 Mb。
  • maxDuration:函数执行时间限制。Hobby 帐户默认值为10秒,Pro 帐户60秒,Enterprise 帐户900秒。
  • isr:配置增量静态再生,如下所述

如果您的函数需要访问特定区域中的数据,则建议将它们部署在相同的区域(或接近该区域)以获得最佳性能。

增量静态再生

Vercel 支持增量静态再生 (ISR),它提供了预渲染内容的性能和成本优势,同时具备动态渲染内容的灵活性。

要将 ISR 添加到路由中,请在您的config对象中包含 isr 属性:

blog/[slug]/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { BYPASS_TOKEN } from '$env/static/private';


export const config = {
isr: {
// Expiration time (in seconds) before the cached asset will be re-generated by invoking the Serverless Function.
// Setting the value to `false` means it will never expire.
expiration: 60,


// Random token that can be provided in the URL to bypass the cached version of the asset, by requesting the asset
// with a __prerender_bypass=<token> cookie.
//
// Making a `GET` or `HEAD` request with `x-prerender-revalidate: <token>` will force the asset to be re-validated.
bypassToken: BYPASS_TOKEN,


// List of valid query parameters. Other parameters (such as utm tracking codes) will be ignored,
// ensuring that they do not result in content being regenerated unnecessarily
allowQuery: ['search']
}
};

expiration(过期)属性是必需的;其他所有属性都是可选的。

环境变量

Vercel 提供了一组特定于部署的环境变量。与其他环境变量一样,这些变量可以从 $env/static/private$env/dynamic/private 访问(有时候会有所不同——稍后再说),并且无法从其公共对应项中访问。要从客户端访问其中一个变量:

+layout.server.js

1
2
3
4
5
6
7
8
9
import { VERCEL_COMMIT_REF } from '$env/static/private';


/** @type {import('./$types').LayoutServerLoad} */
export function load() {
return {
deploymentGitBranch: VERCEL_COMMIT_REF
};
}

+layout.svelte

1
2
3
4
5
<script>
/** @type {import('./$types').LayoutServerData} */ export let data;
</script>

<p>This staging environment was deployed from {data.deploymentGitBranch}.</p>

由于在 Vercel 上构建时,所有这些变量在构建时间和运行时间之间都没有改变,因此我们建议使用 $env/static/private —— 它将静态地替换这些变量,从而实现像死代码消除等优化 —— 而不是 $env/dynamic/private。如果您正在使用 edge: true 进行部署,则必须使用 $env/static/private 或填充 envVarsInUse 配置。

Notes

Vercel函数

如果您在项目根目录的 api 目录中包含了 Vercel 函数,则任何对/api/*的请求都不会由 SvelteKit 处理。您应该将这些实现为 SvelteKit 应用程序中的 API 路由,除非您需要使用非 JavaScript 语言,在这种情况下,您需要确保在 SvelteKit 应用程序中没有任何 /api/* 路由。

Node version

在某个日期之前创建的项目将默认使用 Node 14,而 SvelteKit 需要 Node 16 或更高版本。您可以在项目设置中更改 Node 版本。

Troubleshooting

文件访问系统

在Serverless/Edge环境中,您无法通过fs.readFileSync等方法访问文件系统。如果您需要以这种方式访问文件,请在预渲染期间构建应用程序时执行此操作。例如,如果您有一个博客并且不想通过CMS管理内容,则需要对内容进行预渲染(或从获取内容的端点进行预渲染),并在每次添加新内容时重新部署博客。

编写适配器

如果您的首选环境尚未存在适配器,您可以自己构建。我们建议查看与您类似的平台适配器的源代码,并将其复制为起点。

适配器包必须实现以下 API,以创建一个 Adapter

1
2
3
4
5
6
7
8
9
10
11
12
13
/** @param {AdapterSpecificOptions} options */
export default function (options) {
/** @type {import('@sveltejs/kit').Adapter} */
const adapter = {
name: 'adapter-package-name',
async adapt(builder) {
// adapter implementation
}
};


return adapter;
}

Adapter 及其参数的类型可以在 types/index.d.ts 中找到。

adapt方法中,适配器应该完成以下几件事情:

  • 清空构建目录

  • 使用 builder.writeClientbuilder.writeServer builder.writePrerendered 编写 SvelteKit 输出

  • 输出代码:

    • ${builder.getServerDirectory()}/index.js 导入 Server
    • 使用 builder.generateManifest({ relativePath }) 生成的清单实例化应用程序
    • 监听来自平台的请求,如果需要将其转换为标准请求,调用server.respond(request, { getClientAddress })函数生成响应并进行响应
    • 通过传递给 server.respond platform 选项向 SvelteKit 公开任何特定于平台的信息
    • 在必要时全局模拟 fetch 在目标平台上工作。SvelteKit 为可以使用 node-fetch 的平台提供了 @sveltejs/kit/install-fetch 助手
  • 将输出打包以避免在目标平台上需要安装依赖项(如果必要的话)

  • 将用户的静态文件和生成的JS / CSS放置在目标平台的正确位置

如有可能,我们建议将适配器输出放在 build/ 目录下,并将任何中间输出放置在 .svelte-kit/[adapter-name] 下。

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念



关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

SvelteKit中文2-核心概念

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念

Routing

SvelteKit 的核心是基于文件系统的路由器。您应用程序的路由(即用户可以访问的 URL 路径)由代码库中的目录定义:

  • src/routes 是根路径
  • src/routes/about 创建了一个 /about 路径
  • src/routes/blog/[slug] 创建了一个带有参数 slug 的路径,该参数可用于在用户请求像 /blog/hello-world 这样的页面时动态加载数据。

您可以通过编辑项目配置将src/routes更改为不同的目录。

每个路由目录包含一个或多个路由文件,可以通过它们的+前缀进行识别。

+page

+page.svelte

+page.svelte 组件定义了你的应用程序中的一个页面。默认情况下,页面在服务器端(SSR)上进行初始请求渲染,并在浏览器端(CSR)上进行后续导航渲染。

src/routes/+page.svelte

1
2
<h1>Hello and welcome to my site!</h1>
<a href="/about">About my site</a>

src/routes/about/+page.svelte

1
2
3
<h1>About this site</h1>
<p>TODO...</p>
<a href="/">Home</a>

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>

<h1>{data.title}</h1>
<div>{@html data.content}</div>

请注意,SvelteKit 使用<a>元素在路由之间导航,而不是特定于框架的<Link>组件。

+page.js

通常情况下,在页面渲染之前需要加载一些数据。为此,我们添加一个 +page.js 模块,该模块导出一个 load 函数:

src/routes/blog/[slug]/+page.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { error } from '@sveltejs/kit';

/** @type {import('./$types').PageLoad} */

export function load({ params }) {

if (params.slug === 'hello-world') {

return {

title: 'Hello world!',

content: 'Welcome to our blog. Lorem ipsum dolor sit amet...'

};

}


throw error(404, 'Not found');

}

此功能与 +page.svelte 并行运行,这意味着它在服务器端渲染期间和客户端导航期间都在浏览器中运行。有关 API 的完整详细信息,请参见 load

除了 load 之外,+page.js 还可以导出配置页面行为的值:

  • export const prerender = truefalse 'auto'
  • export const ssr = truefalse
  • export const csr = truefalse

您可以在页面选项中找到更多有关此类内容的信息。

+page.server.js

如果您的load函数只能在服务器上运行 - 例如,如果它需要从数据库获取数据或者您需要访问私有环境变量(如API密钥)- 那么您可以将+page.js重命名为+page.server.js,并将PageLoad类型更改为PageServerLoad
src/routes/blog/[slug]/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { error } from '@sveltejs/kit';


/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
const post = await getPostFromDatabase(params.slug);


if (post) {
return post;
}


throw error(404, 'Not found');
}

在客户端导航期间,SvelteKit 将从服务器加载此数据,这意味着返回的值必须可序列化使用 devalue。有关 API 的完整详细信息,请参见 load

+page.js 相似,+page.server.js 可以导出页面选项 - prerenderssrcsr

+page.server.js 文件还可以导出 actions。如果 load 允许您从服务器读取数据,则 actions 允许您使用 <form> 元素将数据写入服务器。要了解如何使用它们,请参见表单操作部分。

+error

如果在load过程中出现错误,SvelteKit 将呈现默认的错误页面。您可以通过添加 +error.svelte 文件来针对每个路由自定义此错误页面:

src/routes/blog/[slug]/+error.svelte

1
2
3
4
5
<script>
import { page } from '$app/stores';
</script>

<h1>{$page.status}: {$page.error.message}</h1>

SvelteKit会“向上遍历树”,寻找最近的错误边界 - 如果上面的文件不存在,它将尝试src/routes/blog/+error.sveltesrc/routes/+error.svelte,在呈现默认错误页面之前。如果失败(或者如果错误是从位于根+error“上方”的根+layout的load函数中抛出的),SvelteKit将退出并呈现静态回退错误页面,您可以通过创建一个src/error.html文件来自定义该页面。

如果在+layout(.server).js中发生错误,则树中最接近该布局(而不是旁边)的+error.svelte文件是最接近的错误边界。

如果找不到路由(404),则使用src/routes/+error.svelte(或默认错误页面,如果该文件不存在)。

当在handle函数或+server.js请求处理程序内发生错误时,不会使用+error.svelte

+layout

到目前为止,我们已经将页面视为完全独立的组件 - 在导航时,现有的+page.svelte组件将被销毁,并且一个新的组件将取代它。

但在许多应用程序中,有些元素应该在每个页面上可见,例如顶级导航或页脚。我们可以将它们放入布局中,而不是在每个+page.svelte中重复它们。

+layout.svelte

要创建适用于每个页面的布局,请创建一个名为src/routes/+layout.svelte的文件。默认布局(如果您没有自己带来)如下所示…

1
<slot></slot>

…但我们可以添加任何标记、样式和行为。唯一的要求是组件包括一个用于页面内容的。例如,让我们添加一个导航栏:

src/routes/+layout.svelte

1
2
3
4
5
6
7
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/settings">Settings</a>
</nav>

<slot></slot>

如果我们为 //about and /settings…创建页面

src/routes/+page.svelte

1
<h1>Home</h1>

src/routes/about/+page.svelte

1
<h1>About</h1>

src/routes/settings/+page.svelte

1
<h1>Settings</h1>

…导航栏将始终可见,单击三个页面之间只会导致h1被替换。

布局可以嵌套。假设我们不仅有单个/settings页面,而是有像/settings/profile/settings/notifications这样的嵌套页面,并且具有共享子菜单(例如,请参见github.com/settings的实际示例)。

我们可以创建一个仅适用于/settings以下页面的布局(同时继承具有顶级导航的根布局):

src/routes/settings/+layout.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
/** @type {import('./$types').LayoutData} */ export let data;
</script>

<h1>Settings</h1>

<div class="submenu">
{#each data.sections as section}
<a href="/settings/{section.slug}">{section.title}</a>
{/each}
</div>

<slot></slot>

默认情况下,每个布局都会继承其上面的布局。有时这并不是您想要的 - 在这种情况下,高级布局可以帮助您。

+layout.js

就像 +page.svelte +page.js 加载数据一样,你的+layout.svelte组件可以从+layout.js中的 load 函数获取数据。
src/routes/settings/+layout.js

1
2
3
4
5
6
7
8
9
/** @type {import('./$types').LayoutLoad} */
export function load() {
return {
sections: [
{ slug: 'profile', title: 'Profile' },
{ slug: 'notifications', title: 'Notifications' }
]
};
}

如果一个名为 +layout.js 的文件导出页面选项 - prerenderssrcsr,它们将作为子页面的默认值。

从布局的load函数返回的数据也可用于所有其子页面:

src/routes/settings/profile/+page.svelte

1
2
3
4
5
<script>
/** @type {import('./$types').PageData} */ export let data;

console.log(data.sections); // [{ slug: 'profile', title: 'Profile' }, ...]
</script>

通常情况下,在页面之间导航时,布局数据不会改变。SvelteKit 将在必要时智能重新运行load函数。

+layout.server.js

将您的布局load函数移动到 +layout.server.js 并将 LayoutLoad 类型更改为 LayoutServerLoad,即可在服务器上运行它。

+layout.js 一样,+layout.server.js 可以导出页面选项 - prerenderssrcsr

+server

除了页面外,您还可以使用+server.js文件(有时称为“API路由”或“端点”)定义路由,从而完全控制响应。您的+server.js文件导出与HTTP动词相对应的函数,如GETPOSTPATCHPUTDELETEOPTIONS,这些函数接受RequestEvent参数并返回一个Response对象。

例如,我们可以创建一个带有GET处理程序的/api/random-number路由:

src/routes/api/random-number/+server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { error } from '@sveltejs/kit';


/** @type {import('./$types').RequestHandler} */
export function GET({ url }) {
const min = Number(url.searchParams.get('min') ?? '0');
const max = Number(url.searchParams.get('max') ?? '1');


const d = max - min;


if (isNaN(d) || d < 0) {
throw error(400, 'min and max must be numbers, and min must be less than max');
}


const random = min + Math.random() * d;


return new Response(String(random));
}

Response 的第一个参数可以是 ReadableStream,这使得流式传输大量数据或创建服务器发送事件成为可能(除非部署到像 AWS Lambda 这样缓冲响应的平台)。

您可以使用 @sveltejs/kit 中的 errorredirect json 方法来方便地处理错误(但不一定要这样做)。

如果抛出错误(无论是 throw error(...) 还是意外错误),响应将是该错误的 JSON 表示形式或回退错误页面 —— 可以通过 src/error.html 自定义 —— 具体取决于 Accept 标头。在这种情况下,+error.svelte 组件将不会被渲染。您可以在此处阅读有关错误处理的更多信息。

创建 OPTIONS 处理程序时,请注意 Vite 将注入Access-Control-Allow-OriginAccess-Control-Allow-Methods标头 - 这些标头在生产环境中将不存在,除非您添加它们。

Receiving data

通过导出POST/PUT/PATCH/DELETE/OPTIONS处理程序,可以使用+server.js文件创建完整的API:

src/routes/add/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
let a = 0;
let b = 0;
let total = 0;

async function add() {
const response = await fetch('/api/add', {
method: 'POST',
body: JSON.stringify({ a, b }),
headers: {
'content-type': 'application/json'
}
});

total = await response.json();
}
</script>

<input type="number" bind:value={a}> +
<input type="number" bind:value={b}> =
{total}

<button on:click={add}>Calculate</button>

src/routes/api/add/+server.js

1
2
3
4
5
6
7
8
import { json } from '@sveltejs/kit';


/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
const { a, b } = await request.json();
return json(a + b);
}

一般来说,表单操作是从浏览器向服务器提交数据的更好方式。

Content negotiation

+server.js文件可以放置在与+page文件相同的目录中,从而允许相同的路由成为页面或API端点。为了确定哪个是哪个,SvelteKit应用以下规则:

  • PUT/PATCH/DELETE/OPTIONS请求始终由+server.js处理,因为它们不适用于页面
  • 如果接受标头优先考虑text/html(换句话说,这是浏览器页面请求),则GET/POST请求被视为页面请求;否则它们将由+server.js处理

$types

在上面的示例中,我们一直从 $types.d.ts 文件中导入类型。如果您使用 TypeScript(或带有 JSDoc 类型注释的 JavaScript)来处理根文件,则 SvelteKit 会在隐藏目录中为您创建此文件,以便在处理根文件时提供类型安全性。

例如,将 export let data 注释为 [PageData](对于 +layout.svelte 文件则是 LayoutData),告诉 TypeScript 数据的类型是从 load 返回的任何内容:

src/routes/blog/[slug]/+page.svelte

1
2
3
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>

反过来,使用PageLoadPageServerLoadLayoutLoadLayoutServerLoad(分别用于+page.js+page.server.js+layout.js+layout.server.js)为load函数进行注释可以确保参数和返回值的正确类型。

如果您正在使用VS Code或任何支持语言服务器协议和TypeScript插件的IDE,则可以完全省略这些类型! Svelte的IDE工具将为您插入正确的类型,因此您无需自己编写即可获得类型检查。它还与我们的命令行工具svelte-check一起使用。

您可以在我们关于省略$types的博客文章中了解更多信息。

Other files

路由目录中的任何其他文件都将被 SvelteKit 忽略。这意味着您可以将组件和实用程序模块与需要它们的路由放在一起。

如果多个路由需要使用组件和模块,则最好将它们放在 $lib 中。

Loading data

在渲染 +page.svelte 组件(以及其包含的 +layout.svelte 组件)之前,我们通常需要获取一些数据。这是通过定义load函数来完成的。

Page data

一个 +page.svelte 文件可以有一个兄弟文件 +page.js,该文件导出一个load函数,其返回值可通过 data 属性在页面中使用:

src/routes/blog/[slug]/+page.js

1
2
3
4
5
6
7
8
9
/** @type {import('./$types').PageLoad} */
export function load({ params }) {
return {
post: {
title: `Title for ${params.slug} goes here`,
content: `Content for ${params.slug} goes here`
}
};
}

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>

由于生成的 $types 模块,我们获得了完整的类型安全。

+page.js 文件中,一个load函数会在服务器和浏览器上都运行。如果您的load函数应该始终在服务器上运行(例如因为它使用私有环境变量或访问数据库),那么它将放置在 +page.server.js 中。

更实际版本的博客文章load函数只在服务器上运行并从数据库中提取数据,可能看起来像这样:

src/routes/blog/[slug]/+page.server.js

1
2
3
4
5
6
7
8
9
import * as db from '$lib/server/database';


/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return {
post: await db.getPost(params.slug)
};
}

请注意,类型已从PageLoad更改为PageServerLoad,因为服务器负载函数可以访问其他参数。要了解何时使用+page.js和何时使用+page.server.js,请参见Universal vs server。

Layout data

您的 +layout.svelte 文件也可以通过 +layout.js+layout.server.js 加载数据。

src/routes/blog/[slug]/+layout.server.js

1
2
3
4
5
6
7
8
9
import * as db from '$lib/server/database';


/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
return {
posts: await db.getPostSummaries()
};
}

src/routes/blog/[slug]/+layout.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
/** @type {import('./$types').LayoutData} */ export let data;
</script>

<main>
<!-- +page.svelte is rendered in this <slot> --> <slot />
</main>

<aside>
<h2>More posts</h2>
<ul>
{#each data.posts as post}
<li>
<a href="/blog/{post.slug}">
{post.title}
</a>
</li>
{/each}
</ul>
</aside>

从布局load函数返回的数据可供子+layout.svelte组件和+page.svelte组件以及其所属的布局使用。

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import { page } from '$app/stores';

/** @type {import('./$types').PageData} */
export let data;

// we can access `data.posts` because it's returned from
// the parent layout `load` function
$: index = data.posts.findIndex(post => post.slug === $page.params.slug);
$: next = data.posts[index - 1];
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>

{#if next}
<p>Next post: <a href="/blog/{next.slug}">{next.title}</a></p>
{/if}

如果多个load函数返回具有相同键的数据,则最后一个“获胜”——布局load返回{ a: 1,b: 2 }和页面加载返回{ b: 3,c: 4 }的结果将是{ a: 1,b:3,c:4}

$page.data

+page.svelte 组件以及它上面的每个 +layout.svelte 组件都可以访问自己的数据和所有父级数据。

在某些情况下,我们可能需要相反的操作 - 父布局可能需要访问页面数据或子布局中的数据。例如,根布局可能想要访问从 +page.js+page.server.js 返回的 title 属性。这可以通过 $page.data 来实现:

src/routes/+layout.svelte

1
2
3
4
5
6
7
<script>
import { page } from '$app/stores';
</script>

<svelte:head>
<title>{$page.data.title}</title>
</svelte:head>

$page.data的类型信息由App.PageData提供。

通用vs服务器

As we’ve seen, there are two types of load function:

  • +page.js and +layout.js files export universal load functions that run both on the server and in the browser
  • +page.server.js and +layout.server.js files export server load functions that only run server-side

Conceptually, they’re the same thing, but there are some important differences to be aware of.

正如我们所看到的,有两种类型的load函数:

  • +page.js 和 +layout.js 文件导出通用的加载函数,在服务器和浏览器上都可以运行

  • +page.server.js 和 +layout.server.js 文件导出仅在服务器端运行的服务器加载函数

从概念上讲,它们是相同的东西,但需要注意一些重要区别。

什么时候运行哪个加载函数?

服务器load函数始终在服务器上运行。

默认情况下,通用的load函数在 SSR 期间首次访问页面时在服务器上运行。然后,在水合作用期间再次运行,并重复使用来自获取请求的任何响应。所有后续调用通用load函数都发生在浏览器中。您可以通过页面选项自定义此行为。如果禁用了服务器端渲染,则会获得 SPA,并且通用load功能始终在客户端上运行。

除非您预渲染页面,否则将在运行时调用load函数- 在这种情况下,它将在构建时间调用。

Input

通用和服务器load函数都可以访问描述请求(paramsroute url)以及各种函数(fetchsetHeadersparent depends)的属性。这些在以下章节中进行了描述。

服务器load函数使用 ServerLoadEvent 调用,该事件从 RequestEvent 继承 clientAddresscookieslocalsplatformrequest 属性。

通用load函数使用 LoadEvent 调用,该事件具有 data 属性。如果您在 +page.js+page.server.js(或 +layout.js+layout.server.js)中都有load函数,则服务器load函数的返回值是通用load函数参数的 data 属性。

Output

一个通用的load函数可以返回一个包含任何值的对象,包括自定义类和组件构造函数等。

服务器load函数必须返回可使用devalue序列化的数据 - 任何可以表示为JSON的内容以及BigIntDateMapSetRegExp之类的内容,或者是重复/循环引用 - 以便它可以通过网络传输。您的数据可能包括promises,在这种情况下,它将被流式传输到浏览器。

在什么时间用哪个函数

服务器load函数在需要直接从数据库或文件系统访问数据,或需要使用私有环境变量时非常方便。

通用的load函数在需要从外部API去fetch数据且不需要私人凭据时非常有用,因为SvelteKit可以直接从API获取数据而无需通过您的服务器。当您需要返回无法序列化的内容(例如Svelte组件构造函数)时,它们也很有用。

在极少数情况下,您可能需要同时使用两者 - 例如,您可能需要返回一个自定义类的实例,并将其初始化为来自服务器的数据。

Using URL data

通常,load函数在某种程度上取决于URL。为此,load函数提供了urlrouteparams参数。

url

一个 URL 实例,包含诸如 originhostnamepathnamesearchParams(其中包含解析后的查询字符串作为 URLSearchParams 对象)等属性。由于服务器上不可用,因此在load期间无法访问 url.hash

在某些环境中,这是在服务器端渲染期间从请求头中派生出来的。例如,如果您正在使用adapter-node,则可能需要配置适配器以使URL正确。

route

包含当前路由目录的名称,相对于 src/routes:

src/routes/a/[b]/[…c]/+page.js

1
2
3
4
/** @type {import('./$types').PageLoad} */
export function load({ route }) {
console.log(route.id); // '/a/[b]/[...c]'
}

params

params是从url.pathnameroute.id派生出来的。

假设route.id/a/[b]/[...c]url.pathname/a/x/y/z,则params对象如下所示:

1
2
3
4
{
"b": "x",
"c": "y/z"
}

进行获取请求

从外部API或+server.js 处理程序获取数据,您可以使用提供的fetch函数,它与native fetch web API具有相同的行为,并具有一些附加功能:

  • 它可用于在服务器上进行凭证请求,因为它继承了页面请求的cookieauthorization标头
  • 它可以在服务器上进行相对请求(通常,在服务器上下文中使用时,fetch需要带有源URL)
  • 内部请求(例如+server.js路由)在运行时直接进入处理程序函数,而无需HTTP调用开销
  • 在服务器端渲染期间,响应将被捕获并通过钩入 Response 对象的 textjson 方法内联到呈现的 HTML 中。请注意,除非通过 filterSerializedResponseHeaders 显式包含,否则不会序列化标头。然后,在水合期间,响应将从 HTML 中读取,确保一致性并防止额外的网络请求 - 如果您在使用浏览器fetch而不是 load fetch 时在浏览器控制台中收到警告,则原因就在于此。

src/routes/items/[id]/+page.js

1
2
3
4
5
6
7
8
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params }) {
const res = await fetch(`/api/items/${params.id}`);
const item = await res.json();


return { item };
}

只有当目标主机与 SvelteKit 应用程序相同或是其更具体的子域时,才会传递 Cookie。

Cookies and headers

服务器load函数可以获取和设置cookies

src/routes/+layout.server.js

1
2
3
4
5
6
7
8
9
10
11
12
import * as db from '$lib/server/database';


/** @type {import('./$types').LayoutServerLoad} */
export async function load({ cookies }) {
const sessionid = cookies.get('sessionid');


return {
user: await db.getUser(sessionid)
};
}

在设置 cookie 时,请注意path属性。默认情况下,cookie 的path是当前的路径名。例如,在页面 admin/user 上设置 cookie,则默认情况下该 cookie 只能在admin页面中使用。在大多数情况下,您可能希望将路径设置为 ‘/‘,以使 cookie 在整个应用程序中可用。

服务器和通用load函数都可以访问setHeaders函数,该函数在服务器上运行时可以为响应设置标头。(在浏览器中运行时,setHeaders没有效果。) 如果您想要页面被缓存,则这非常有用:

src/routes/products/+page.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, setHeaders }) {
const url = `https://cms.example.com/products.json`;
const response = await fetch(url);


// cache the page for the same length of time
// as the underlying data
setHeaders({
age: response.headers.get('age'),
'cache-control': response.headers.get('cache-control')
});


return response.json();
}

多次设置相同的标题(即使在不同的load函数中)是错误的 - 您只能设置给定标题一次。您不能使用setHeaders添加set-cookie标头 - 请改用cookies.set(name,value,options)

Using parent data

有时候,一个load函数需要访问其父级load函数的数据是很有用的,这可以通过使用 await parent() 来实现:

src/routes/+layout.js

1
2
3
4
/** @type {import('./$types').LayoutLoad} */
export function load() {
return { a: 1 };
}

src/routes/abc/+layout.js

1
2
3
4
5
/** @type {import('./$types').LayoutLoad} */
export async function load({ parent }) {
const { a } = await parent();
return { b: a + 1 };
}

src/routes/abc/+page.js

1
2
3
4
5
/** @type {import('./$types').PageLoad} */
export async function load({ parent }) {
const { a, b } = await parent();
return { c: a + b };
}

src/routes/abc/+page.svelte

1
2
3
4
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>
<!-- renders `1 + 2 = 3` --><p>{data.a} + {data.b} = {data.c}</p>

请注意,在 +page.js 中的load函数接收来自布局load函数的合并数据,而不仅仅是直接父级的数据。

+page.server.js+layout.server.js 中,parent 返回来自父级 +layout.server.js 文件的数据。

+page.js或者 +layout.js 中,它将返回来自父级+layout.js文件的数据。然而,缺少+layout.js被视为一个 ({data}) => data 函数,这意味着它也会返回未被+layout.js文件“遮蔽”的父级+layout.server.js文件中的数据

使用await parent()时要注意不要引入瀑布式渲染。例如,在此处 getData(params) 不依赖于调用parent()的结果,因此我们应该先调用它以避免延迟渲染。

+page.js

1
2
3
4
5
6
7
8
9
10
11
/** @type {import('./$types').PageLoad} */
export async function load({ params, parent }) {
const parentData = await parent();
const data = await getData(params);
const parentData = await parent();

return {
...data
meta: { ...parentData.meta, ...data.meta }
};
}

Errors

如果在load过程中出现错误,则会呈现最近的 +error.svelte。对于预期的错误,请使用 @sveltejs/kit 中的 error 帮助程序来指定 HTTP 状态代码和可选消息:

src/routes/admin/+layout.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { error } from '@sveltejs/kit';


/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
throw error(401, 'not logged in');
}


if (!locals.user.isAdmin) {
throw error(403, 'not an admin');
}
}

如果出现意外错误,SvelteKit将调用 handleError 并将其视为 500 内部错误。

Redirects

要重定向用户,请使用@sveltejs/kitredirect 助手,指定应将其重定向到的位置以及3xx状态代码。

src/routes/user/+layout.server.js

1
2
3
4
5
6
7
8
9
import { redirect } from '@sveltejs/kit';


/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
throw redirect(307, '/login');
}
}

确保您没有捕获被抛出的重定向,否则将阻止 SvelteKit 处理它。

在浏览器中,您还可以使用$app.navigationgoto函数,在load函数之外以编程方式导航。

使用 Promises 进行流式传输

返回对象的顶层承诺将被等待,这使得返回多个承诺而不创建瀑布变得容易。在使用服务器load时,嵌套的承诺将随着它们的解决而流式传输到浏览器中。如果您有缓慢、非必要数据,则此功能很有用,因为您可以在所有数据可用之前开始呈现页面:

src/routes/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @type {import('./$types').PageServerLoad} */
export function load() {
return {
one: Promise.resolve(1),
two: Promise.resolve(2),
streamed: {
three: new Promise((fulfil) => {
setTimeout(() => {
fulfil(3)
}, 1000);
})
}
};
}

这对于创建骨架加载状态非常有用,例如:

src/routes/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>

<p>
one: {data.one}
</p>
<p>
two: {data.two}
</p>
<p>
three:
{#await data.streamed.three}
Loading...
{:then value}
{value}
{:catch error}
{error.message}
{/await}
</p>

在不支持流式传输的平台上,比如 AWS Lambda,响应将被缓冲。这意味着页面只有在所有承诺都解决后才会渲染出来。

只有在启用 JavaScript 时,流数据才能正常工作。如果页面是服务器渲染的,则应避免从通用load函数返回嵌套的 promises,因为这些不会被流式传输 - 相反,在浏览器中重新运行该函数时,promise 将被重新创建。

并行加载

在渲染(或导航到)页面时,SvelteKit 会同时运行所有load函数,避免请求的瀑布流。在客户端导航期间,调用多个服务器load函数的结果被分组为单个响应。一旦所有load函数都返回了,页面就会被呈现出来。

重新运行加载函数

SvelteKit会跟踪每个load函数的依赖关系,以避免在导航期间不必要地重新运行它。

例如,给定这样一对load函数…

src/routes/blog/[slug]/+page.server.js

1
2
3
4
5
6
7
8
9
import * as db from '$lib/server/database';


/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return {
post: await db.getPost(params.slug)
};
}

src/routes/blog/[slug]/+layout.server.js

1
2
3
4
5
6
7
8
9
import * as db from '$lib/server/database';


/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
return {
posts: await db.getPostSummaries()
};
}

…在 +page.server.js 中,如果我们从/blog/trying-the-raw-meat-diet导航到 /blog/i-regret-my-choicesparams.slug 发生了变化,那么它将重新运行。而在 +layout.server.js 中不会重新运行,因为数据仍然有效。换句话说,我们不会第二次调用 db.getPostSummaries()

调用 await parent() load 函数也会重新运行,如果父级 load 函数被重新运行的话。

依赖跟踪在 load 函数返回后不适用 - 例如,在嵌套的 promise 中访问 params.x 不会导致函数在 params.x 更改时重新运行。(别担心,在开发中如果你意外这样做了会收到警告)。相反,在您的 load 函数主体中访问参数。

Manual invalidation

您还可以使用invalidate(url)重新运行适用于当前页面的加载函数,该函数将重新运行所有依赖于url的load函数,并且可以使用invalidateAll()重新运行每个加载函数。

如果一个load函数调用fetch(url)depends(url),则它依赖于url。请注意,url可以是以[a-z]::开头的自定义标识符。

src/routes/random-number/+page.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, depends }) {
// load reruns when `invalidate('https://api.example.com/random-number')` is called...
const response = await fetch('https://api.example.com/random-number');


// ...or when `invalidate('app:random')` is called
depends('app:random');


return {
number: await response.json()
};
}

src/routes/random-number/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
import { invalidate, invalidateAll } from '$app/navigation';
/** @type {import('./$types').PageData} */ export let data;

function rerunLoadFunction() {
// any of these will cause the `load` function to re-run invalidate('app:random');
invalidate('https://api.example.com/random-number');
invalidate(url => url.href.includes('random-number'));
invalidateAll();
}
</script>

<p>random number: {data.number}</p>
<button on:click={rerunLoadFunction}>Update random number</button>

总结一下,以下情况会导致负载函数重新运行:

  • 它引用了 params 的某个属性,其值已更改
  • 它引用了 url 的某个属性(例如 url.pathnameurl.search),其值已更改
  • 它调用 await parent() 并且父级负载函数重新运行
  • 通过 fetch depends 声明对特定 URL 的依赖关系,并使用invalidate(url)标记该 URL 无效时
  • 所有活动的加载函数都被强制重新运行以使用 invalidateAll()

paramsurl可能会因为 <a href=".."> 链接点击、<form> 交互、goto 调用或redirect而发生变化。

请注意,重新运行load函数将更新相应的 +layout.svelte +page.svelte 中的数据 prop;它不会导致组件被重新创建。因此,内部状态得到保留。如果这不是您想要的结果,则可以在 afterNavigate 回调中重置所需内容,并/或在 {#key ...} 块中包装组件。

Form actions

+page.server.js文件可以导出操作,允许您使用<form>元素将数据POST到服务器。

在使用<form>时,客户端JavaScript是可选的,但您可以轻松地通过JavaScript逐步增强表单交互以提供最佳用户体验。

Default actions

在最简单的情况下,一个页面声明了一个默认操作:

src/routes/login/+page.server.js

1
2
3
4
5
6
/** @type {import('./$types').Actions} */
export const actions = {
default: async (event) => {
// TODO log the user in
}
};

要从/login页面调用此操作,只需添加一个<form>即可,无需JavaScript:

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
<form method="POST">
<label>
Email
<input name="email" type="email">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
</form>

如果有人点击按钮,浏览器将通过POST请求将表单数据发送到运行默认操作的服务器。

Actions总是使用POST请求,因为GET请求不应该具有副作用。

我们还可以通过添加action属性并指向页面来从其他页面调用该操作(例如,如果根布局中的导航中有登录小部件):

src/routes/+layout.svelte

1
2
<form method="POST" action="/login">
<!-- content --></form>

Named actions

一个页面可以拥有多个命名动作,而不是只有一个default动作:

src/routes/login/+page.server.js

1
2
3
4
5
6
7
8
9
10
/** @type {import('./$types').Actions} */
export const actions = {
default: async (event) => {
login: async (event) => {
// TODO log the user in
},
register: async (event) => {
// TODO register the user
}
};

要调用命名操作,请添加一个查询参数,名称前缀为 / 字符:

src/routes/login/+page.svelte

1
<form method="POST" action="?/register">

src/routes/+layout.svelte

1
<form method="POST" action="/login?/register">

除了 action 属性外,我们还可以在按钮上使用 formaction 属性将相同的表单数据 POST 到与父 <form> 不同的操作:

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
<form method="POST">
<form method="POST" action="?/login">
<label>
Email
<input name="email" type="email">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
<button formaction="?/register">Register</button>
</form>

我们不能在命名操作旁边设置默认操作,因为如果您向未重定向的命名操作POST,则查询参数将保留在URL中,这意味着下一个默认的POST将通过之前的命名操作进行。

解剖action

每个操作都会接收到一个 RequestEvent 对象,允许您使用request.formData()读取数据。在处理请求后(例如通过设置 cookie 登录用户),该操作可以响应数据,这些数据将通过相应页面的form属性和 $page.form 应用程序范围内直到下一次更新可用。

src/routes/login/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** @type {import('./$types').PageServerLoad} */
export async function load({ cookies }) {
const user = await db.getUserFromSession(cookies.get('sessionid'));
return { user };
}

/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');

const user = await db.getUser(email);
cookies.set('sessionid', await db.createSession(user));

return { success: true };
},
register: async (event) => {
// TODO register the user
}
};

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
<script>
/** @type {import('./$types').PageData} */
export let data;
/** @type {import('./$types').ActionData} */
export let form;
</script>

{#if form?.success}
<!-- this message is ephemeral; it exists because the page was rendered in
response to a form submission. it will vanish if the user reloads -->
<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}

Validation errors

如果由于无效数据而无法处理请求,您可以将验证错误(以及先前提交的表单值)返回给用户,以便他们可以再次尝试。fail函数允许您返回HTTP状态码(通常是400或422,在验证错误的情况下),以及数据。状态代码可通过$page.status获得,表单数据可通过form获得:

src/routes/login/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { fail } from '@sveltejs/kit';

/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');

if (!email) {
return fail(400, { email, missing: true });
}

const user = await db.getUser(email);

if (!user || user.password !== hash(password)) {
return fail(400, { email, incorrect: true });
}

cookies.set('sessionid', await db.createSession(user));

return { success: true };
},
register: async (event) => {
// TODO register the user
}
};

请注意,作为一项预防措施,我们仅将电子邮件返回到页面 —— 而不是密码。

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form method="POST" action="?/login">
{#if form?.missing}<p class="error">The email field is required</p>{/if}
{#if form?.incorrect}<p class="error">Invalid credentials!</p>{/if}
<label>
Email
<input name="email" type="email">
<input name="email" type="email" value={form?.email ?? ''}>
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
<button formaction="?/register">Register</button>
</form>

返回的数据必须可序列化为JSON。除此之外,结构完全由您决定。例如,如果页面上有多个表单,则可以使用id属性或类似方法来区分返回的表单数据所属于哪个<form>

Redirects

重定向(和错误)的工作方式与load时完全相同:

src/routes/login/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { fail, redirect } from '@sveltejs/kit';

/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ cookies, request, url }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');

const user = await db.getUser(email);
if (!user) {
return fail(400, { email, missing: true });
}

if (user.password !== hash(password)) {
return fail(400, { email, incorrect: true });
}

cookies.set('sessionid', await db.createSession(user));

if (url.searchParams.has('redirectTo')) {
throw redirect(303, url.searchParams.get('redirectTo'));
}

return { success: true };
},
register: async (event) => {
// TODO register the user
}
};

Loading data

当一个操作运行后,页面将被重新渲染(除非发生重定向或意外错误),并且该操作的返回值可用作form属性提供给页面。这意味着您的页面load函数将在操作完成后运行。

请注意,handle 在调用动作之前运行,并且不会在load函数之前重新运行。这意味着如果例如您使用 handle 基于 cookie 来填充 event.locals,则必须在设置或删除 cookie 时更新 event.locals:

src/hooks.server.js

1
2
3
4
5
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
event.locals.user = await getUser(event.cookies.get('sessionid'));
return resolve(event);
}

src/routes/account/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @type {import('./$types').PageServerLoad} */
export function load(event) {
return {
user: event.locals.user
};
}

/** @type {import('./$types').Actions} */
export const actions = {
logout: async (event) => {
event.cookies.delete('sessionid');
event.locals.user = null;
}
};

Progressive enhancement(渐进增强)

在前面的章节中,我们构建了一个可以在没有客户端JavaScript的情况下工作的/login操作——看不到任何fetch。这很棒,但是当JavaScript可用时,我们可以逐步增强我们的表单交互以提供更好的用户体验。

use:enhance

逐步增强表单的最简单方法是添加 use:enhance 操作:

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
<script>
import { enhance } from '$app/forms';

/** @type {import('./$types').ActionData} */
export let form;
</script>

<form method="POST" use:enhance>

是的,增强操作和<form action>都被称为“action”,有点令人困惑。这些文档充满了行动。抱歉。

没有参数时,use:enhance将模拟浏览器本机行为,只是没有完整的页面重新加载。它将:

  • 在成功或无效响应时更新form属性 $page.form$page.status,但仅当操作在您提交的页面上时才更新。例如,如果您的表单看起来像 <form action="/somewhere/else" ..>,则不会更新form $page。这是因为在本机表单提交情况下,您将被重定向到操作所在的页面。如果要无论如何更新它们,请使用 applyAction
  • 在成功响应中重置 <form> 元素并使所有数据失效,并使用 invalidateAll
  • 对于重定向响应调用 goto
  • 如果发生错误,则呈现最近的 +error 边界。
  • 将焦点重置为适当的元素。

要自定义行为,您可以提供一个 SubmitFunction,在表单提交之前立即运行,并(可选)返回一个回调函数,该回调函数将与 ActionResult一起运行。请注意,如果您返回了一个回调函数,则不会触发上述默认行为。要恢复它,请调用 update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form
method="POST"
use:enhance={({ form, data, action, cancel, submitter }) => {
// `form` is the `<form>` element
// `data` is its `FormData` object
// `action` is the URL to which the form is posted
// `cancel()` will prevent the submission
// `submitter` is the `HTMLElement` that caused the form to be submitted

return async ({ result, update }) => {
// `result` is an `ActionResult` object
// `update` is a function which triggers the logic that would be triggered if this callback wasn't set
};
}}
>

你可以使用这些函数来显示和隐藏加载界面等。

applyAction

如果您提供自己的回调函数,您可能需要重现默认的 use:enhance 行为的一部分,例如显示最近的 +error 边界。大多数情况下,调用传递给回调函数的 update 就足够了。如果您需要更多定制化,则可以使用 applyAction

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
import { enhance, applyAction } from '$app/forms';

/** @type {import('./$types').ActionData} */
export let form;
</script>

<form
method="POST"
use:enhance={({ form, data, action, cancel }) => {
// `form` is the `<form>` element
// `data` is its `FormData` object
// `action` is the URL to which the form is posted
// `cancel()` will prevent the submission

return async ({ result }) => {
// `result` is an `ActionResult` object
if (result.type === 'error') {
await applyAction(result);
}
};
}}
>

applyAction(result) 的行为取决于 result.type

  • success, failure — 将 $page.status 设置为 result.status,并将表单和 $page.form 更新为 result.data(与从 enhance 提交的位置无关,不同于 update
  • redirect — 调用 goto(result.location)
  • error — 使用 result.error 渲染最近的 +error 边界

在所有情况下,焦点都会被重置。

自定义事件监听器

我们也可以自己实现渐进增强,而不使用 use:enhance,在 <form> 上使用普通的事件监听器:

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
import { invalidateAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';
/** @type {import('./$types').ActionData} */ export let form;
/** @type {any} */ let error;

async function handleSubmit(event) {
const data = new FormData(this);

const response = await fetch(this.action, {
method: 'POST',
body: data
});
/** @type {import('@sveltejs/kit').ActionResult} */ const result = deserialize(await response.text());

if (result.type === 'success') {
// re-run all `load` functions, following the successful update await invalidateAll();
}

applyAction(result);
}
</script>

<form method="POST" on:submit|preventDefault={handleSubmit}>
<!-- content --></form>

请注意,在使用 $app/forms 中相应的方法进一步处理响应之前,您需要对其进行反序列化。JSON.parse() 不足以支持表单操作(如load函数)返回 Date BigInt 对象。

如果您在 +page.server.js 旁边还有一个 +server.js,则默认情况下 fetch 请求将被路由到该位置。要将 POST 请求发送到 +page.server.js 中的操作,请使用自定义 x-sveltekit-action 标头:

1
2
3
4
5
6
7
const response = await fetch(this.action, {
method: 'POST',
body: data,
headers: {
'x-sveltekit-action': 'true'
}
});

可选方案

表单操作是向服务器发送数据的首选方式,因为它们可以逐步增强,但您也可以使用 ·+server.js· 文件来公开(例如)JSON API。以下是这种交互可能看起来像的方式:

send-message/+page.svelte

1
2
3
4
5
6
7
8
9
<script>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>

<button on:click={rerun}>Rerun CI</button>

api/ci/+server.js

1
2
3
4
/** @type {import('./$types').RequestHandler} */
export function POST() {
// do something
}

GET vs POST

正如我们所见,要调用表单操作,必须使用 method="POST"

有些表单不需要将数据 POST 到服务器 - 比如搜索输入框。对于这些表单,您可以使用 method="GET"(或者等效地根本不指定方法),SvelteKit 将像 <a> 元素一样处理它们,使用客户端路由器而不是完整的页面导航:

1
2
3
4
5
6
<form action="/search">
<label>
Search
<input name="q">
</label>
</form>

提交此表单将导航到/search?q=...并调用您的加载函数,但不会触发任何操作。与<a>元素一样,您可以在 <form> 上设置 data-sveltekit-reloaddata-sveltekit-replacestatedata-sveltekit-keepfocus data-sveltekit-noscroll 属性来控制路由器的行为。

Page options

默认情况下,SvelteKit 会在服务器上首先渲染(或预渲染)任何组件,并将其作为 HTML 发送到客户端。然后它会再次在浏览器中呈现该组件,以使其交互,在一个称为水合的过程中。因此,您需要确保组件可以在两个地方运行。然后 SvelteKit 将初始化一个路由器来接管随后的导航。

您可以通过从 +page.js +page.server.js 导出选项来逐页控制这些内容,也可以使用共享的 +layout.js+layout.server.js 来控制一组页面。要定义整个应用程序的选项,请从根布局导出它们。子布局和页面覆盖父布局设置的值,因此例如您可以启用整个应用程序的预渲染功能,然后禁用需要动态呈现的页面。

您可以混合和匹配这些选项在应用程序不同区域内使用。例如你可以对营销页面进行最大速度预渲染、对动态页面进行服务器端呈现以提高 SEO 和可访问性,并通过仅在客户端上呈现将管理部分转换成 SPA 。这使得 SvelteKit 非常灵活多变。

预渲染

你的应用程序中至少有一些路由可以表示为在构建时生成的简单HTML文件。这些路由可以进行预渲染。

+page.js/+page.server.js/+server.js

1
export const prerender = true;

或者,您可以在根目录下的 +layout.js +layout.server.js 中设置 export const prerender = true,并预渲染除明确标记为不可预渲染的页面之外的所有内容:

+page.js/+page.server.js/+server.js

1
export const prerender = false;

具有 prerender = true 的路由将从用于动态 SSR 的清单中排除,使您的服务器(或无服务器/边缘函数)更小。在某些情况下,您可能希望预渲染一个路由,但也要将其包含在清单中(例如,在像 /blog/[slug] 这样的路由上,您希望预渲染最近/流行的内容但是对长尾进行服务器呈现)- 对于这些情况,有第三个选项,“auto”:

+page.js/+page.server.js/+server.js

1
export const prerender = 'auto';

如果您的整个应用程序适合预渲染,您可以使用adapter-static,它将输出适用于任何静态 Web 服务器的文件。

预渲染器将从您的应用程序根目录开始,并为任何可预渲染页面或+server.js路由生成文件。每个页面都会被扫描,以查找指向其他候选预渲染页面的<a>元素 - 因此,通常不需要指定应访问哪些页面。如果确实需要指定预渲染器应访问哪些页面,则可以在prerender配置中使用entries选项进行设置。

在预渲染期间,从$app/environment导入的building值将为true

预渲染服务器路由

与其他页面选项不同,预渲染也适用于 +server.js 文件。这些文件不受布局的影响,但如果有任何从它们获取数据的页面,则会继承默认值。例如,如果一个 +page.js 包含此load函数…

+page.js

1
2
3
4
5
6
7
export const prerender = true;

/** @type {import('./$types').PageLoad} */
export async function load({ fetch }) {
const res = await fetch('/my-server-route.json');
return await res.json();
}

….那么如果 src/routes/my-server-route.json/+server.js 不包含自己的 export const prerender = false,则将视为可预渲染。

什么时候不做预渲染

基本规则是:对于一个可以预渲染的页面,任何两个直接访问它的用户必须从服务器获取相同的内容。

并非所有页面都适合预渲染。任何预渲染的内容都将被所有用户看到。当然,您可以在预渲染页面中的 ·onMount· 中获取个性化数据,但这可能会导致较差的用户体验,因为它将涉及空白初始内容或加载指示器。

请注意,您仍然可以预渲染基于页面参数加载数据的页面,例如 src/routes/blog/[slug]/+page.svelte 路由。

在预渲染期间访问 url.searchParams 是被禁止的。如果需要使用它,请确保只在浏览器中这样做(例如在 onMount 中)。

具有操作的页面无法进行预渲染,因为服务器必须能够处理操作 POST 请求。

预渲染和服务器端渲染

如果将 ssr 选项设置为 false,则每个请求都会导致相同的空 HTML shell。由于这会导致不必要的工作,SvelteKit 默认预渲染任何找到的页面,其中prerender没有明确设置为 false

路由冲突

由于预渲染会写入文件系统,因此不可能有两个端点导致目录和文件具有相同的名称。例如,src/routes/foo/+server.js src/routes/foo/bar/+server.js 将尝试创建 foo foo/bar,这是不可能的。

因此,建议您始终包括文件扩展名 - src/routes/foo.json/+server.jssrc/routes/foo/bar.json/+server.js将导致foo.jsonfoo/bar.json文件和谐地并存。

对于页面,我们通过编写foo/index.html而不是foo来避免这个问题。

故障排除

如果你遇到了像“以下路由被标记为可预渲染,但未进行预渲染”这样的错误,那是因为相关的路由(或者如果它是一个页面,则是其父布局)具有 export const prerender = true ,但该页面实际上没有被预渲染,因为它没有被预渲染爬虫访问到。

由于这些路由无法动态服务器端呈现,在人们尝试访问相关路由时会导致错误。解决方法有两种:

  • 确保 SvelteKit 可以通过从 config.kit.prerender.entries 中跟随链接找到该路线。如果不能通过爬行其他入口点找到带参数的动态路径(即包含[parameters]的页面),请将其添加到此选项中;否则,它们不会被预渲染,因为 SvelteKit 不知道参数应该具有什么值。未标记为可预渲染的页面将被忽略,并且它们与其他页面之间的链接也不会被爬行。
  • export const prerender = true 更改为 export const prerender ='auto' 。使用'auto'的路线可以进行动态服务器端呈现。

ssr

通常情况下,SvelteKit 首先在服务器上呈现您的页面,并将该 HTML 发送到客户端进行水合作用。如果您将 ssr 设置为 false,则会呈现一个空的“外壳”页面。如果您的页面无法在服务器上呈现(例如,因为使用了仅限于浏览器的全局变量,如 document),则这很有用,但在大多数情况下不建议这样做(请参见附录)。

+page.js

1
export const ssr = false;

如果你在根 +layout.js 中添加 export const ssr = false,那么整个应用程序将只在客户端渲染 —— 这基本上意味着你把应用程序变成了单页应用。

csr

通常情况下,SvelteKit会将服务器渲染的HTML转换为交互式客户端渲染(CSR)页面。有些页面根本不需要JavaScript - 许多博客文章和“关于”页面属于这一类别。在这些情况下,您可以禁用CSR:

+page.js

1
export const csr = false;

如果 ssr and csr 都设置为 false, 那么不会有任何内容被预渲染!!!

trailingSlash

默认情况下,SvelteKit 会从 URL 中删除尾随斜杠 —— 如果您访问 /about/,它将响应重定向到 /about。您可以使用 trailingSlash 选项更改此行为,该选项可以是 'never'(默认值)、'always' 'ignore'

与其他页面选项一样,您可以从 +layout.js+layout.server.js 导出此值,并将其应用于所有子页面。您还可以从 +server.js 文件中导出配置。

src/routes/+layout.js

1
export const trailingSlash = 'always';

此选项也会影响预渲染。如果 trailingSlashalways,则像/about这样的路由将生成一个 about/index.html 文件,否则它将创建 about.html,反映静态 Web 服务器约定。

忽略尾部斜杠不是推荐的做法——相对路径的语义在两种情况下有所不同(从/x./y/y,但从/x/./y/x/y),而且/x/x/被视为不同的URL,这对SEO有害。

配置

通过适配器的概念,SvelteKit 可以在各种平台上运行。每个平台可能都有特定的配置来进一步调整部署 - 例如,在 Vercel 上,您可以选择将应用程序的某些部分部署在边缘,而将其他部分部署在无服务器环境中。

config 是一个具有键值对的对象,在顶层。除此之外,具体形状取决于您使用的适配器。每个适配器都应该提供 Config 接口进行导入以实现类型安全性。请查阅您所使用适配器的文档获取更多信息。

src/routes/+page.js

1
2
3
4
/** @type {import('some-adapter').Config} */
export const config = {
runtime: 'edge'
};

config对象在顶层合并(但不是更深的级别)。这意味着,如果您只想覆盖上面+layout.js中的某些值,则无需在+page.js中重复所有值。例如,此布局配置…

src/routes/+layout.js

1
2
3
4
5
6
7
export const config = {
runtime: 'edge',
regions: 'all',
foo: {
bar: true
}
}

…被此页面配置覆盖…

src/routes/+page.js

1
2
3
4
5
6
export const config = {
regions: ['us1', 'us2'],
foo: {
baz: true
}
}

这导致该页面的配置值为 { runtime: 'edge', regions: ['us1', 'us2'], foo: { baz: true } } for that page.

状态管理

如果你习惯于构建仅客户端应用程序,那么在跨服务器和客户端的应用程序中进行状态管理可能会让人感到害怕。本节提供了一些避免常见陷阱的提示。

避免在服务器上使用共享状态

浏览器是有状态的——随着用户与应用程序的交互,状态存储在内存中。另一方面,服务器是无状态的——响应内容完全由请求内容确定。

从概念上讲,就是这样。但实际上,服务器通常具有长寿命并且被多个用户共享。因此,在共享变量中不要存储数据非常重要。例如,请考虑以下代码:

+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let user;

/** @type {import('./$types').PageServerLoad} */
export function load() {
return { user };
}

/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {
const data = await request.formData();

// NEVER DO THIS!
user = {
name: data.get('name'),
embarrassingSecret: data.get('secret')
};
}
}

user变量是由连接到此服务器的所有人共享的。如果爱丽丝提交了一个令人尴尬的秘密,而鲍勃在她之后访问了该页面,那么鲍勃将知道爱丽丝的秘密。此外,当爱丽丝在同一天晚些时候返回网站时,服务器可能已经重新启动,导致她的数据丢失。

相反地,您应该使用 cookie 对用户进行身份验证,并将数据持久化到数据库中。

负载无副作用

出于同样的原因,你的load函数应该是纯函数——没有副作用(除了偶尔使用 console.log(...))。例如,你可能会想在load函数中写入一个 store,以便在组件中使用该 store 值:

+page.js

1
2
3
4
5
6
7
8
9
import { user } from '$lib/user';

/** @type {import('./$types').PageLoad} */
export async function load({ fetch }) {
const response = await fetch('/api/user');

// NEVER DO THIS!
user.set(await response.json());
}

与前面的例子一样,这将一个用户的信息放在所有用户共享的位置。相反,只需返回数据即可…

+page.js

1
2
3
4
5
6
7
export async function load({ fetch }) {
const response = await fetch('/api/user');

return {
user: await response.json()
};
}

…并将其传递给需要它的组件,或使用 $page.data

如果您没有使用 SSR,则不会意外地将一个用户的数据暴露给另一个用户。但是,您仍应避免在load函数中产生副作用-这样您的应用程序就更容易理解了。

使用带有上下文的存储

你可能会想知道,如果我们不能使用自己的存储库,我们如何能够使用 $page.data 和其他应用商店。答案是服务器上的应用商店使用 Svelte 的上下文 API - 存储库附加到组件树中,并通过setContext进行订阅时检索 getContext。我们可以对自己的存储库执行相同的操作:

src/routes/+layout.svelte

1
2
3
4
5
6
7
8
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
/** @type {import('./$types').LayoutData} */ export let data;
// Create a store and update it when necessary... const user = writable();
$: user.set(data.user);
// ...and add it to the context for child components to access setContext('user', user);
</script>

src/routes/user/+page.svelte

1
2
3
4
5
6
<script>
import { getContext } from 'svelte';
// Retrieve user store from context const user = getContext('user');
</script>

<p>Welcome {$user.name}</p>

如果您不使用 SSR(并且可以保证将来不需要使用SSR),那么您可以安全地在共享模块中保留状态,而无需使用上下文API。

组件状态被保留

当您在应用程序中导航时,SvelteKit 会重复使用现有的布局和页面组件。例如,如果您有这样一个路由…

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
/** @type {import('./$types').PageData} */
export let data;
// THIS CODE IS BUGGY!
const wordCount = data.content.split(' ').length;
const estimatedReadingTime = wordCount / 250;
</script>

<header>
<h1>{data.title}</h1>
<p>Reading time: {Math.round(estimatedReadingTime)} minutes</p>
</header>

<div>{@html data.content}</div>

…然后从 /blog/my-short-post 导航到 /blog/my-long-post 不会导致组件被销毁和重新创建。数据属性(以及data.titledata.content)将发生更改,但由于代码没有重新运行,estimatedReadingTime 将不会被重新计算。

相反,我们需要使该值具有响应性:

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
7
<script>
/** @type {import('./$types').PageData} */
export let data;

$: wordCount = data.content.split(' ').length;
$: estimatedReadingTime = wordCount / 250;
</script>

像这样重复使用组件意味着侧边栏滚动状态等内容得以保留,而且您可以轻松地在不同值之间进行动画处理。但是,如果您确实需要在导航时完全销毁和重新挂载组件,则可以使用此模式:

1
2
3
{#key $page.url.pathname}
<BlogPost title={data.title} content={data.title} />
{/key}

在URL中存储状态

如果您有应该在重新加载后保留并/或影响SSR的状态,例如表格上的过滤器或排序规则,则URL搜索参数(如?sort=price&order=ascending)是放置它们的好地方。您可以将它们放在<a href="..."><form action="...">属性中,也可以通过goto('?key=value')以编程方式设置它们。可以通过url参数在load函数内部访问它们,在组件内部通过$page.url.searchParams访问它们。

在快照中存储短暂状态

一些 UI 状态,例如“手风琴是否打开?”,是可丢弃的——如果用户导航离开或刷新页面,则状态丢失并不重要。在某些情况下,如果用户导航到另一页然后返回,您确实希望数据保持不变,但将状态存储在 URL 或数据库中会过度复杂化。为此,SvelteKit 提供了快照功能,让您将组件状态与历史记录条目关联起来。

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念



关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

SvelteKit中文1-开始

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念

介绍

如果您是Svelte或SvelteKit的新手,我们建议您查看交互式教程。

如果遇到困难,请在Discord聊天室寻求帮助。

什么是SvelteKit?

SvelteKit是一个使用Svelte快速开发强大、高性能Web应用程序的框架。如果你来自React,SvelteKit类似于Next。如果你来自Vue,SvelteKit类似于Nuxt。

什么是Svelte?

简而言之,Svelte 是一种编写用户界面组件的方式——例如导航栏、评论区或联系表单——这些组件在用户浏览器中可见并与其交互。Svelte 编译器将您的组件转换为 JavaScript 代码,可以运行以呈现页面的 HTML 和样式化页面的 CSS。您不需要了解 Svelte 就能理解本指南的其余部分,但它会有所帮助。如果您想了解更多,请查看 Svelte 教程。

SvelteKit 在 Svelte 的基础上提供了什么?

Svelte 渲染 UI 组件。您可以使用 Svelte 组合这些组件并渲染整个页面,但编写完整应用程序需要的不仅是 Svelte。

SvelteKit 提供了基本功能,如路由器 - 即在单击链接时更新 UI,并提供服务器端渲染(SSR)。但除此之外,使用所有现代最佳实践构建应用程序非常复杂。这些实践包括构建优化,以便只加载所需的最小代码;离线支持;在用户启动导航之前预加载页面;可配置的呈现方式,允许您在服务器端进行 SSR、在浏览器客户端进行客户端呈现或者通过预渲染来构建时呈现应用程序的不同部分等等。SvelteKit 为您处理所有繁琐的事情,使您可以专注于创造性工作。

它利用带有 Svelte 插件的 Vite 进行模块热替换(HMR),从而立即反映浏览器中代码更改,并提供快速且功能丰富的开发体验。

创建一个Project

开始构建 SvelteKit 应用程序的最简单方法是运行 npm create:

1
2
3
4
npm create svelte@latest my-app
cd my-app
npm install
npm run dev

第一个命令将在my-app目录中搭建一个新项目,并询问您是否想设置一些基本工具,例如TypeScript。有关设置其他工具的指针,请参阅常见问题解答。随后的命令将安装其依赖项并在localhost:5173上启动服务器。

有两个基本概念:

  • 你的应用程序中每个页面都是一个 Svelte 组件
  • 通过将文件添加到项目的 src/routes 目录来创建页面。这些页面将被服务器渲染,以便用户首次访问您的应用程序时尽可能快地加载,然后客户端应用程序接管。

尝试编辑文件以了解所有内容如何工作。

编译器安装

我们建议使用带有Svelte扩展的Visual Studio Code(也称为VS Code),但是还支持许多其他编辑器。

项目结构

一个典型的SvelteKit项目的目录结构看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
my-project/
├ src/
│ ├ lib/
│ │ ├ server/
│ │ │ └ [your server-only lib files]
│ │ └ [your lib files]
│ ├ params/
│ │ └ [your param matchers]
│ ├ routes/
│ │ └ [your routes]
│ ├ app.html
│ ├ error.html
│ ├ hooks.client.js
│ └ hooks.server.js
├ static/
│ └ [your static assets]
├ tests/
│ └ [your tests]
├ package.json
├ svelte.config.js
├ tsconfig.json
└ vite.config.js

如果您在运行npm create svelte@latest时选择了这些选项,您还会找到常见的文件,例如.gitignore.npmrc(以及.prettierrc.eslintrc.cjs等)

项目文件

src

src目录包含了你的项目主要内容。除了src/routessrc/app.html之外,其他都是可选的。

  • lib 包含您的库代码(实用程序和组件),可以通过 $lib 别名导入,或使用 svelte-package 打包以进行分发
    • server 包含仅限服务器的库代码。可以使用 $lib/server 别名导入。SvelteKit 将防止您在客户端代码中导入这些内
  • params 包含应用程序所需的任何参数匹配器
  • routes 包含应用程序的路由。您还可以将其他仅在单个路由中使用的组件放置在此处
  • app.html 是页面模板 - 一个 HTML 文档,其中包含以下占位符:
    • %sveltekit.head% - 应用程序需要的 <link><script> 元素,以及任何<svelte:head>内容
    • %sveltekit.body% - 渲染页面的标记。这应该位于 <div> 或其他元素内部,而不是直接位于 <body> 内部,以避免浏览器扩展注入然后被水合过程销毁引起错误。如果不是这种情况,则 SvelteKit 在开发时会警告您。
    • %sveltekit.assets% - 如果指定了 paths.assets,则为 paths.assets 的路径;否则为相对路径到 paths.base
    • %sveltekit.nonce% - 如果使用,则为手动包括链接和脚本提供 CSP nonce
    • %sveltekit.env.[NAME]%- 这将在渲染时替换为 [NAME] 环境变量,它必须以 publicPrefix(通常为 PUBLIC_)开头。如果不匹配,则会回退到 ‘’。
  • error.html 是在其他所有内容失败时呈现的页面。它可以包含以下占位符:
    • %sveltekit.status% - HTTP 状态
    • %sveltekit.error.message% - 错误消息
  • hooks.client.js 包含您的客户端钩子
  • hooks.server.js 包含您的服务器钩子
  • service-worker.js 包含您的服务工作者

(项目是否包含.js.ts文件取决于您创建项目时选择使用TypeScript。您可以在本页面底部的切换中在文档之间切换JavaScript和TypeScript。)

如果您在设置项目时添加了Vitest,则单元测试将位于src目录中,并带有.test.js扩展名。

static

任何应该原样提供的静态资源,例如robots.txtfavicon.png,请放在此处。

tests

如果您在设置项目时添加了 Playwright 进行浏览器测试,则测试将存储在此目录中。

package.json

你的 package.json 文件必须包含 @sveltejs/kitsveltevite 作为 devDependencies

当你使用 npm create svelte@latest 创建项目时,你会注意到 package.json 包括 "type": "module"。这意味着 .js 文件被解释为具有 importexport 关键字的本地 JavaScript 模块。传统的 CommonJS 文件需要一个 .cjs 的文件扩展名。

svelte.config.js

该文件包含您的 Svelte 和 SvelteKit 配置。

tsconfig.json

该文件(或 jsconfig.json,如果您更喜欢对.js文件进行类型检查而不是 .ts 文件)配置了 TypeScript,如果您在 npm create svelte@latest 期间添加了类型检查。由于 SvelteKit 需要特定的配置方式,因此它会生成自己的 .svelte-kit/tsconfig.json 文件,并且您自己的配置extends

vite.config.js

一个 SvelteKit 项目实际上只是一个使用了 @sveltejs/kit/vite 插件以及其他 Vite 配置的 Vite 项目。

其他文件

.svelte-kit

在开发和构建项目时,SvelteKit 会在 .svelte-kit 目录中生成文件(可以配置为 outDir)。您可以忽略其内容,并随时删除它们(下次进行 devbuild 时将重新生成它们)。

Web标准

在整个文档中,您会看到对 SvelteKit 构建在其之上的标准 Web API 的引用。我们不是重复造轮子,而是使用平台,这意味着您现有的 Web 开发技能也适用于 SvelteKit。相反地,学习 SvelteKit 将帮助您成为其他领域更好的 Web 开发人员。

这些 API 在所有现代浏览器和许多非浏览器环境(如 Cloudflare Workers、Deno 和 Vercel Edge Functions)中都可用。在开发过程中,在基于 Node 的环境(包括 AWS Lambda)的适配器中,在必要时通过 polyfills 提供支持(目前是这样 - Node 正在快速添加对更多 Web 标准的支持)。

特别是,您将熟悉以下内容:

Fetch APIs

SvelteKit 使用 fetch 从网络获取数据。它在钩子、服务器路由以及浏览器中都可用。

load函数、服务器钩子和 API 路由中,有一种特殊版本的 fetch 可以直接在服务器端渲染期间调用端点,而无需进行 HTTP 调用,并保留凭据。(要在 load 之外的服务器端代码中进行带凭据的 fetch,请显式传递 cookie 和/或authorization。)它还允许您发出相对请求,而服务器端 fetch 通常需要完全限定的 URL。

除了 fetch 本身之外,Fetch API 还包括以下接口:

Request

Request的实例可以在钩子和服务器路由中作为event.request访问。它包含有用的方法,如request.json()request.formData(),用于获取发布到端点的数据。

Response

await fetch(...) +server.js 文件中的处理程序返回 Response 的实例。从根本上说,SvelteKit 应用程序是将Request转换为Response的机器。

Headers

Headers接口允许您读取传入的request.headers并设置传出的response.headers:

src/routes/what-is-my-user-agent/+server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { json } from '@sveltejs/kit';


/** @type {import('./$types').RequestHandler} */
export function GET(event) {
// log all headers
console.log(...event.request.headers);


return json({
// retrieve a specific header
userAgent: event.request.headers.get('user-agent')
});
}

FormData

处理HTML本地表单提交时,您将使用FormData对象。

src/routes/hello/+server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { json } from '@sveltejs/kit';


/** @type {import('./$types').RequestHandler} */
export async function POST(event) {
const body = await event.request.formData();


// log all fields
console.log([...body]);


return json({
// get a specific field's value
name: body.get('name') ?? 'world'
});
}

Stream APIs

大多数情况下,您的端点将返回完整数据,就像上面的userAgent示例一样。有时候,您可能需要返回一个太大而无法一次性放入内存或以块传递的响应,在这种情况下平台提供了流——ReadableStream、WritableStream和TransformStream。

URL APIs

URL由URL接口表示,其中包括有用的属性,如originpathname(在浏览器中还有hash)。此接口出现在各种地方 - 钩子和服务器路由中的event.url,页面中的$page.url,在beforeNavigateafterNavigate中的fromto等。

URLSearchParams

无论在哪里遇到URL,您都可以通过url.searchParams访问查询参数,它是URLSearchParams的一个实例:

1
const foo = url.searchParams.get('foo');

Web Crypto

Web Crypto API 可通过全局变量 crypto 使用。它在内容安全策略标头内部使用,但您也可以将其用于生成 UUID 等其他操作:

1
const uuid = crypto.randomUUID();

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念



关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

如何训练大模型(LLM)

英文原文:How to train your own Large Language Models

概要介绍

大型语言模型,如OpenAI的GPT-4或Google的PaLM,已经席卷了人工智能领域。然而,大多数公司目前没有能力训练这些模型,并且完全依赖于只有少数几家大型科技公司提供技术支持。

在Replit,我们投入了大量资源来建立从头开始训练自己的大型语言模型所需的基础设施。在本文中,我们将概述我们如何训练LLM(Large Language Models),从原始数据到部署到用户面向生产环境。我们将讨论沿途遇到的工程挑战以及如何利用我们认为构成现代LLM堆栈的供应商:Databricks、Hugging Face和MosaicML。

虽然我们的模型主要是针对代码生成用例设计的,但所讨论的技术和教训适用于所有类型的LLMs,包括通用语言模型。在未来几周和月份中,我们计划深入探讨过程中繁琐细节系列博客文章。

为什么要训练自己的LLMs?

Replit的AI团队经常被问到“为什么要训练自己的模型?”有很多原因可以解释公司决定训练自己的LLM,包括数据隐私和安全性、对更新和改进具有更大的控制力等。

在Replit,我们主要关注定制化、降低依赖性和成本效益。

  • 定制化。训练一个定制模型使我们能够根据特定需求和要求进行调整,包括平台特定功能、术语和上下文,在通用模型(如GPT-4)或甚至代码专用模型(如Codex)中可能无法很好地覆盖。例如,我们的模型经过了针对Replit上流行的特定基于Web的语言(包括Javascript React(JSX) 和Typescript React(TSX))进行了优化。
  • 降低依赖性。虽然我们总是会根据任务选择正确的模型,但我们认为减少对少数人工智能提供商之一产生依赖是有好处的。这不仅适用于Replit还适用于更广泛开发者社区。这就是为什么我们计划开源一些模型,而没有训练它们就无法做到这一点。
  • 成本效益。尽管成本将持续下降,但LLMs对于全球开发者社区来说仍然是难以承受的。在Replit,我们的使命是将下一个十亿软件创作者带到线上。我们相信,在印度用手机编码的学生应该能够访问与硅谷专业开发人员相同的AI。为了实现这一点,我们训练定制模型,这些模型更小、更高效,并且可以大幅降低成本进行托管。

Data pipelines(数据管道)

LLMs需要大量的数据来进行训练。训练它们需要构建强大的数据管道,这些管道高度优化,同时又足够灵活,可以轻松地包含新的公共和专有数据来源。

技术栈

在整个技术栈中,我们的主要数据源可以在 Hugging Face的The Stack 获得。Hugging Face 是一个很好的数据集和预训练模型资源。他们还提供了一系列有用的工具作为 Transformers 库的一部分,包括标记化、模型推理和代码评估等工具。

The Stack由BigCode项目提供。有关数据集构建的详细信息可在Kocetkov et al. (2022)中找到。经过去重处理,数据集的1.2版本包含约350种编程语言编写的大约2.7 TB的许可源代码。

Transformers库在抽象化模型训练中处理数据规模等多个挑战方面表现出色。然而,我们发现它对于我们的流程来说还不够,因为我们需要更多地控制数据并能够以分布式方式进行处理。

llm-training

数据处理

当需要进行更高级的数据处理时,我们使用Databricks来构建我们的流水线。这种方法也使得我们能够轻松地将其他数据源(如Replit或Stack Overflow)引入到我们的过程中,在未来的迭代中计划这样做。

第一步是从Hugging Face下载原始数据。我们使用Apache Spark在每种编程语言之间并行化数据集构建器进程。然后,我们重新分区数据,并以优化设置为下游处理重写成parquet格式。

接下来,我们转向清理和预处理数据。通常情况下,去重复和修复各种编码问题很重要,但The Stack已经使用Kocetkov et al.(2022年)概述的近似去重技术为我们完成了这项工作。然而,在开始引入Replit数据到我们的流水线时,必须重新运行去重复过程。这就是拥有像Databricks这样的工具所带来好处之一,在其中可以将The Stack、Stackoverflow和Replit 数据视为较大数据湖中的三个来源,并根据需要在下游过程中利用它们。

使用Databricks 的另一个好处是可以对底层数据运行可扩展且易于跟踪分析。 我们对所有类型的统计信息进行汇总统计,并检查长尾分布,并诊断任何问题或不一致性 。所有这些都在Databricks笔记本中完成,这些笔记本还可以与MLFlow集成,以跟踪和重现我们沿途的所有分析。这一步相当于对数据进行定期X光检查,也有助于为预处理采取各种步骤提供信息。

对于预处理,我们采取以下步骤:

  • 我们通过删除任何个人可识别信息(PII),包括电子邮件、IP地址和秘密密钥来匿名化数据。
  • 我们使用多种启发式方法来检测和删除自动生成的代码。
  • 对于某些语言的子集,我们会删除无法编译或不符合标准语法解析器要求的代码。
  • 根据平均行长度、最大行长度和字母数字字符百分比过滤文件。

the-stack-db-notebook

Tokenization and vocabulary training(分词和词汇训练)

在进行分词之前,我们使用相同数据的随机子样本训练自己的定制词汇表来进行模型训练。自定义词汇表使我们的模型更好地理解和生成代码内容。这将导致改进模型性能,并加快模型训练和推断速度。

这一步是整个过程中最重要的步骤之一,因为它在我们流程(数据管道、模型训练、推断)的所有三个阶段中都被使用到。这凸显了拥有一个强大而完全集成化基础设施对于您的模型训练过程至关重要。

我们计划在未来的博客文章中深入探讨标记化。从高层次上看,需要考虑一些重要事项,如词汇量大小、特殊token以及保留用于哨兵token的空间。

一旦我们完成了自定义词汇表培训,就会对数据进行标记化处理。最后,我们构建培训数据集并将其写入优化用于输入到模型培训过程中分片格式文件当中。

Model training(模型训练)

我们使用MosaicML来训练我们的模型。在此之前,我们曾经部署过自己的训练集群,但是发现MosaicML平台为我们提供了一些关键优势。

多个云服务提供商。Mosaic使我们能够利用不同云服务提供商的GPU,而无需设置帐户和所有必需的集成开销。
LLM训练配置。Composer库具有多种针对各种模型和不同类型培训目标进行调整的配置。
托管基础设施。他们管理的基础设施为我们提供了编排、效率优化和容错(即从节点故障中恢复)。

在确定模型参数时,我们考虑到模型大小、上下文窗口、推理时间、内存占用等方面之间的各种权衡取舍。较大的模型通常具有更好性能,并且更适合于迁移学习。然而这些模型需要更高计算要求进行培训和推理, 后者对于我们尤其重要. Replit是一个云原生IDE,其性能感觉就像桌面本地应用程序一样快速, 因此代码完成模型需要非常快速. 出于这个原因, 我们通常会选择具有较小内存占用量和低延迟推理的较小模型。

除了模型参数之外,我们还可以从各种训练目标中选择,每个目标都有其独特的优点和缺点。最常见的培训目标是下一个令牌预测。这通常对于代码完成效果很好,但无法考虑文档更深层次上下文。这可以通过使用“填充-中间”目标来减轻,其中在文档中掩盖一系列令牌,并且模型必须使用周围上下文来预测它们。另一种方法是UL2(无监督潜在语言学习),它将不同的客观函数作为去噪任务进行语言模型培训,在该任务中,模型必须恢复给定输入的缺失子序列。

loss-curves-replit

一旦我们确定了模型配置和训练目标,就会在多节点GPU集群上启动训练运行。我们能够根据正在训练的模型大小以及希望完成训练过程的速度来调整为每个运行分配的节点数。运行大规模GPU集群非常昂贵,因此重要的是我们以最有效的方式利用它们。我们密切监视GPU利用率和内存,以确保从计算资源中获得最大可能使用。

我们使用Weights&Biases监控培训过程,包括资源利用情况以及培训进展情况。 我们监视损失曲线,以确保该模型在培训过程中每一步都有效地学习。 我们还观察损失峰值。 这些是损失值突然增加并通常表示底层培训数据或模型架构存在问题。 由于这些事件通常需要进一步调查和潜在调整,在我们的流程中强制执行数据确定性,因此可以更轻松地复制、诊断和解决任何此类损失峰值潜在来源问题。

Evaluation(评估)

为了测试我们的模型,我们使用了 Chen et al. (2021).所描述的 HumanEval 框架的变体。我们使用该模型生成一个 Python 代码块,给定函数签名和文档字符串。然后,我们运行一个测试用例来确定生成的代码块是否按预期工作。我们运行多个样本并分析相应的 Pass@K 数字。

这种方法最适合 Python,并配备了可用于评估器和测试用例。但是由于 Replit 支持许多编程语言,因此我们需要对各种其他语言进行模型性能评估。我们发现这很难做到,并且没有广泛采用的工具或框架提供全面综合解决方案。两个特定挑战包括在任何编程语言中召唤出可重复生产环境以及缺乏广泛使用标准化测试用例(例如 HTML、CSS 等)。幸运的是,“在任何编程语言中创建可重复生产环境” 是 Replit 的专长! 我们目前正在构建一种评估框架,允许任何研究人员插入并测试他们的多语言基准数据集。 我们将在未来发布博客文章时讨论此问题。

humaneval-results-replit

Deployment to production生产环境部署

一旦我们训练和评估了模型,就该将其部署到生产环境中。正如之前提到的那样,我们的代码补全模型应该感觉非常快速,并且请求之间延迟非常低。我们使用NVIDIA的FasterTransformer和Triton Server来加速推理过程。FasterTransformer是一个实现基于transformer神经网络推理加速引擎的库,而Triton则是一个稳定且具有易于配置性能的推理服务器。这种组合为变压器模型与底层GPU硬件之间提供了高度优化的层,并允许对大型模型进行超快速分布式推断。

在将我们的模型部署到生产环境后,我们可以使用Kubernetes基础架构自动缩放以满足需求。虽然我们在先前博客文章中已经讨论过自动缩放,但值得一提的是,在托管推理服务器时会出现一系列独特挑战,包括大量工件(即模型权重)和特殊硬件要求(即不同大小/数量GPU)。 我们设计了部署和集群配置以便能够快速可靠地交付产品。例如,我们设计集群以解决单个区域内GPU短缺问题并寻找最便宜的可用节点。

在将模型放在实际用户面前之前,我们喜欢自己测试一下并了解模型的“氛围”。我们之前计算的HumanEval测试结果很有用,但与模型一起工作才能真正感受到它,包括其延迟、建议的一致性和总体帮助程度。将模型放在Replit员工面前就像翻开一个开关那样容易。 一旦我们对此感到满意,我们会再次翻转开关,并将其推出给其他用户使用。

monitoring-replit

我们继续监控模型性能和使用指标。对于模型性能,我们监测请求延迟和GPU利用率等指标。对于使用情况,我们跟踪代码建议的接受率,并将其分解成多个维度,包括编程语言。这也使我们能够进行A/B测试不同的模型,并得到一个量化的比较一个模型与另一个模型之间的差异的方法。

Feedback and iteration反馈和迭代

我们的模型训练平台使我们能够在不到一天的时间内从原始数据转换为部署在生产环境中的模型。但更重要的是,它允许我们训练和部署模型、收集反馈,然后根据反馈快速迭代。

对于我们的流程来说,保持稳健性以适应底层数据源、模型训练目标或服务器架构上任何变化也很重要。这使我们能够利用一个日新月异且每天都似乎带来新鲜令人兴奋消息的领域中出现的新进展和功能。

接下来,我们将扩展平台以使其能够使用Replit本身来改善我们的模型。这包括诸如基于人类反馈进行强化学习(RLHF)等技术,以及使用从Replit赏金活动收集到的数据进行指导调整。

下一步

虽然我们已经取得了很大的进展,但是训练LLM仍处于非常早期阶段。我们有很多改进需要做,还有许多难题需要解决。随着语言模型的不断发展,这种趋势只会加速。与数据、算法和模型评估相关的新挑战将持续出现。

如果您对训练LLMs所涉及到的各种工程挑战感到兴奋,我们很乐意与您交流。我们喜欢反馈,并且希望听听您对我们缺失之处以及您会采取哪些不同方法的看法。

Replit AI团队一直在寻找才华横溢的工程师、研究人员和建设者。请务必查看我们职业页面上公开招聘岗位信息。如果没有合适的角色但认为自己可以做出贡献,请与我们联系; 我们很乐意收到来信!



关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

Transformer原理简明讲解

原文:What Are Transformer Models and How Do They Work?

Transformer模型是机器学习中最令人兴奋的新发展之一。它们在论文Attention is All You Need中被介绍。Transformer可以用于写故事、文章、诗歌,回答问题,翻译语言,与人类聊天,甚至可以通过对人类来说很难的考试!但是它们到底是什么呢?你会很高兴地知道,Transformer模型的架构并不复杂,它只是一些非常有用组件的串联,并且每个组件都有自己的功能。在本篇文章中,您将了解所有这些组件。

此博客文章包含简单概念性介绍。关于Transformer模型及其工作原理更详细描述,请查看Cohere公司Jay Alammar撰写的这两篇优秀文章!

简而言之,Transformer的作用是什么?想象一下你在手机上写短信。每输入一个单词后,你可能会得到三个建议的单词。例如,如果你输入“Hello, how are”,手机可能会建议下一个单词为“you”或“your”。当然,如果你继续选择手机中的建议单词,很快就会发现这些单词组成的信息毫无意义。如果你看每一组连续的3或4个单词,则它们可能有意义,但这些单词并不构成任何有意义的句子。这是因为手机使用的模型没有携带消息整体上下文,只是预测最近几个字后更可能出现哪个字。相反地,“Transformer”可以跟踪所编写内容背景,并且这就是它们编写文本使其具有意义之处所在。

img

​ 手机可以建议在短信中使用下一个单词,但没有生成连贯文本的能力。

我必须对你诚实,第一次发现Transformer逐字建立文本时,我简直不敢相信。首先,这不是人类构成句子和思想的方式。我们首先形成一个基本思想,然后开始完善它并添加单词。这也不是ML模型完成其他任务的方式。例如,图像不是以这种方式构建的。大多数基于神经网络的图形模型会形成图像的粗略版本,并逐渐完善或添加细节直到完美为止。那么为什么Transformer模型要逐字构建文本呢?一个答案是因为这样做非常有效。更令人满意的答案是因为Transformer非常擅长跟踪上下文,所以它选择下一个单词时恰好符合保持某个想法进行所需。

那么Transformer如何训练呢?使用了大量数据,事实上包括互联网上所有数据。因此当您将“你好吗”输入到转换器中时,它只需要根据互联网上所有文本知道最佳下一个单词就是“你”。如果您给出更复杂的命令,则可能会确定使用良好的下一个单词,“曾经”。然后它将该单词添加到命令中,并确定下一个好单词是“在……之上”,以此类推。逐字逐句,直到它写出一篇故事为止。

命令:Write a story.
回应:Once

下一条命令:Write a story. Once
回应:upon

下一条命令:Write a story. Once upon
回应:a

下一条命令:Write a story. Once upon a
回应:time

下一条命令: Write a story. Once upon a time
回复: there

等等…

现在我们知道了transformers的作用,让我们来看一下它们的架构。如果你已经看过transformer模型的架构,你可能会像我第一次看到时那样惊叹不已,因为它看起来非常复杂!然而,当你将其分解成最重要的部分时,就没有那么难了。Transformer有4个主要部分:

  1. 分词
  2. 嵌入
  3. 位置编码
  4. Transformer块(其中包含多个)
  5. Softmax

其中第四个——Transformer块是最复杂的。可以连接多个这样的块,并且每一个都包含两个主要组件:Attention(注意力)和Feedforward(前馈)组件。

transformer-arc

Transformer模型的架构

让我们逐个学习这些部分。

Tokenization

Tokenization (分词)是最基本的步骤。它包括一个大型的标记数据集,其中包含所有单词、标点符号等。分词步骤将每个单词、前缀、后缀和标点符号都发送到库中已知的Token。

Tokenization
Tokenization :将单词一个个装进token

例如,如果句子是“Write a story.”,那么对应的4个token将是和<.>。

Embedding

一旦输入内容被tokenized,就该将单词转换成数字了。为此,我们使用embedding(嵌入)。Embedding是任何大型语言模型中最重要的部分之一;它是实现文本与数字转换的桥梁。由于人类善于处理文本而计算机善于处理数字,因此这个桥梁越强大,语言模型就越强大。

简而言之,文本嵌入将每个文本转换为一个向量。如果两个文本片段相似,则其对应向量中的数字也相似(逐位意味着同一位置上的每对数字都相似)。否则,如果两个文本片段不同,则其对应向量中的数字也不同。如果您想了解更多信息,请查看有关文本嵌入的文章视频

尽管嵌入是数值化的,但我喜欢从几何角度来想象它们。试想一下存在一个非常简单的嵌入方式,可以将每个单词映射到长度为2(即包含2个数值) 的向量上。如果我们按照这两个数值所表示坐标定位每个单词(比如在街道和大道上),那么所有单词都站在一个巨大平面上。在这张平面上,相似的单词会靠近彼此,而不同的单词则会远离。例如,在下面这个嵌入中,“cherry”的坐标是[6,4],与“strawberry” [5,4] 接近但与“castle” [1,2] 相距较远。

embedding
Embedding:将单词(标记)转换为向量

在更大的embedding情况下,每个单词都被赋值到一个更长的向量(比如长度为4096),那么这些单词不再存在于二维平面上,而是存在于一个大的4096维空间中。然而,在这个高维大空间中,我们仍然可以认为单词之间有近有远,因此embedding概念仍然具有意义。

词embedding可以推广到文本embedding,包括整个句子、段落甚至更长的文本都会被赋值到一个向量中。然而,在transformer的情形中,我们将使用词嵌入,这意味着句子中的每个单词都会被赋值到相应的向量中。更具体地说,输入文本中的每个token都将被定位到其对应的embedding向量中。

例如,如果我们正在考虑的句子是“Write a story.”并且标记是和<.>,那么每个标记都将被赋值到一个向量中,并且我们将有四个向量。

embedding2
通常embedding将每个单词(token)赋值到一个数字列表中。

Positional encoding(位置编码)

一旦我们获得了与句子中每个token对应的向量,下一步就是将它们全部转换为一个向量进行处理。将一堆向量转换为一个向量最常见的方法是逐分量相加。也就是说,我们单独添加每个坐标。例如,如果这些(长度为2)向量分别是[1,2]和[3,4],则它们对应的总和为[1+3, 2+4],即[4,6]。这种方法可以工作,但有一个小细节需要注意:加法满足交换律,也就是说如果你以不同顺序添加相同的数字,则会得到相同的结果。在这种情况下,“我不难过我很开心”和“我不开心我很难过”两句话将得到相同的向量结果(假设它们具有相同单词但顺序不同)。这并不好。因此我们必须想出一些方法来给出两个句子不同的向量表示方式。多种方法可行,在本文中我们选择其中之一:位置编码(Positional Encoding) 。位置编码包括将预定义序列中的一系列向量添加到单词嵌入(embedding) 向量上去,并确保我们获得每个句子都有唯一表示形式且具有相似语义结构、仅单词顺序不同的句子将被分配到不同的向量。在下面的示例中,“Write”、“a”、“story”和“.”所对应的向量成为带有位置信息标签“Write(1)”,“a(2)”,“story(3)”和“. (4)”的修改后向量。

positional-encoding

位置编码会为每个单词添加一个位置向量,以便跟踪单词的位置。

现在我们知道每个句子都有一个独特的向量,这个向量携带了句子中所有单词及其顺序的信息,因此我们可以进入下一步。

Transformer block

让我们回顾一下目前为止的内容。单词被输入并转换成token(分词),然后考虑到它们的顺序(位置编码)。这给了我们每个输入模型的token一个向量。现在,下一步是预测这个句子中的下一个单词。这是通过一个非常大、非常复杂的神经网络来完成的,该网络专门训练用于预测句子中的下一个单词。

我们可以训练这样一个大型网络,但是通过添加关键步骤:Attention(注意力)组件,我们可以极大地改进它。在开创性论文《Attention is All you Need》中引入的注意力机制是Transformer模型的关键成分之一,也是它们如此有效的原因之一。下面将解释注意力机制,但现在先想象它作为一种向文本中每个单词添加上下文的方式。

在前馈网络的每个块中都添加了注意力组件。因此,如果您想象一个大型前馈神经网络,其目标是预测下一个单词,并由几个较小的神经网络块组成,则在每个这些块中都添加了注意力组件。然后,Transformer的每个组件(称为transformer 块)由两个主要组件构成:

  • 注意力组件
  • 前馈组件

Transformer是许多Transformer块的串联。

transformer-add

Transformer是许多Transformer块的串联。每个Transformer块由一个注意力组件和一个前馈组件(神经网络)组成。

Attention

Attention步骤涉及一个非常重要的问题:上下文问题。有时,同一个单词可以用不同的意思。这往往会让语言模型感到困惑,因为embedding只是将单词赋值到向量中,而不知道他们使用的单词定义。

Attention是一种非常有用的技术,可以帮助语言模型理解上下文。为了理解Attention的工作原理,请考虑以下两个句子:

句子1: The bank of the river
句子2: Money in the bank.

正如您所看到的,单词“bank”在两个句子中都出现了,但含义不同。在第一个句子中,我们指的是河流旁边的土地,在第二个句子中则指持有货币的机构。计算机对此一无所知,因此我们需要以某种方式将这些知识注入其中。什么能帮助我们呢?好吧,似乎句子中其他单词可以拯救我们。对于第一个句子,“the”和“of”这些单词对我们没有任何作用。但是,“river”这个单词让我们知道正在谈论河流旁边的土地。同样,在第二个句子中,“money”这个单词让我们明白“bank”的意思现在是指持有货币的机构。

bank

Attention有助于根据句子(或文本)中的其他单词为每个单词提供上下文。

简而言之,注意力机制的作用是将句子(或文本片段)中的单词在词嵌入中靠近。这样,在句子“Money in the bank”中,“bank”一词将被移动到“money”的附近。同样,在句子“The bank of the river”中,“bank”一词将被移动到“river”的附近。这样,两个句子中修改后的单词“bank”都会携带周围单词的某些信息,为其添加上下文。

Transformer模型中使用的注意力机制实际上更加强大,它被称为多头注意力。在多头注意力中,使用了几个不同的嵌入来修改向量并为其添加上下文。多头注意力已经帮助语言模型在处理和生成文本时达到了更高的效率水平。如果您想更详细地了解注意力机制,请查看这篇博客文章及其相应视频

The Softmax Layer

现在你已经知道一个transformer是由许多层transformer块组成的,每个块都包含一个attention和一个feedforward层,你可以将它看作是一个大型神经网络,用于预测句子中的下一个单词。Transformer为所有单词输出分数,其中得分最高的单词被赋予最有可能成为句子中下一个单词的概率。

Transformer的最后一步是softmax层,它将这些分数转换为概率(总和为1),其中得分最高对应着最高的概率。然后我们可以从这些概率中进行采样以获取下一个单词。在下面的例子中,transformer给“Once”赋予了0.5的最高概率,并给“Somewhere”和“There”赋予了0.3和0.2 的概率。一旦我们进行采样,“once”就被选定,并且那就是transformer 的输出结果。

softmax

softmax层将分数转换为概率,这些概率用于选择文本中的下一个单词。

现在怎么办?好的,我们重复这个步骤。现在我们将文本“Write a story. Once”输入模型中,很可能输出结果是“upon”。再次重复此步骤,Transformer最终会写出一个故事,例如:“Once upon a time, there was a …”(“从前有一天,有一个……”)。

Summary总结

在这篇文章中,您已经学习了transformers的工作原理。它们由几个块组成,每个块都有自己的功能,共同工作以理解文本并生成下一个单词。这些块如下:

Tokenizer:将单词转换为token。
Embedding:将token转换为数字(向量)。
Positional encoding:在文本中添加单词顺序。
Transformer block:猜测下一个单词。它由注意力块和前馈块组成。
Attention:为文本添加上下文信息。
Feedforward:是Transformer神经网络中的一个模块,用于猜测下一个单词。
Softmax函数: 将得分转换为概率以便采样出下一个单词。

重复执行这些步骤就可以写出您所看到的transformers创建的惊人文本。

Post Training(后期训练)

现在你已经知道了Transformer是如何工作的,但我们还有一些工作要做。想象一下:你扮演Transformer,“阿尔及利亚的首都是什么?” 我们希望它回答“阿尔及尔”,然后继续进行。然而,这个Transformer是在整个互联网上训练出来的。互联网很大,并不一定是最好的问题/答案库。例如,许多页面会列出长长的问题列表而没有答案。在这种情况下,“阿尔及利亚的首都是什么?”之后的下一个句子可能会是另一个问题,比如“阿尔及利亚人口数量?”,或者“布基纳法索首都在哪里?”。 Transformer不像人类那样思考他们的回应,它只是模仿它看到过(或提供过)数据集中所见到内容。

那么我们该怎样使Transformer回答问题呢?

答案就在于后期训练。就像您教导一个人完成某些任务一样,您可以让Transformer执行任务。 一旦将Transformer训练成整个互联网上使用时,则需要再次对其进行大量数据集培训以涉及各种问题和相应答案。Transformer(就像人类一样)对他们最后学到的事情有偏见,因此后期训练已被证明是帮助Transformer成功完成所要求任务的非常有用的步骤。

后期训练还可以帮助处理许多其他任务。例如,可以使用大量对话数据集来进行Transformer的后期培训,以使其作为聊天机器人表现良好,或者帮助我们编写故事、诗歌甚至代码。

了解更多

如上所述,这是一个概念性的介绍,让您了解transformers如何生成文本。如果您想要深入了解transformer背后的数学原理,请观看以下视频(YouTube)

写在最后

正如你所看到的,Transformer的架构并不复杂。它们是由几个块连接而成,每个块都有自己的功能。它们之所以能够工作得如此出色,主要原因在于它们具有大量参数,可以捕捉上下文中许多方面的信息。我们很期待看到您使用Transformer模型构建什么!






关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

AI大模型面面观,还不上车?

这可能是有史以来最大的变革

AI会把人类的工作抢完?

这波AI巨浪是由OpenAI掀起的,ChatGPT就是那个炸起巨浪的炸弹。通过ChatGPT这类对话式应用的使用,我相信大部分朋友都已经知道了这波AI和以前很不一样,这次不开玩笑!

2030

看看红杉资本去年发的一篇AI将逐步取代人类工作的预测,以目前全球AI军备竞赛的状况来看,极有可能会到来的更快。到2030年,以下工作会被取代:

  • 文字工作:影响到绝大部分的办公室白领,但是别担心,只要老板还是个人,他会留几个人聊聊天的;
  • 编程和计算:影响到程序员、架构师、数据库工程师等等;
  • 美术和设计:UI设计师、建筑设计师、非顶级艺术家、摄影师等等;
  • 视频、3D制作与游戏开发:电影工作者、3D制作工程师,以及游戏设计和开发人员。

那么问题就来了:我们的孩子们怎么办?

他们现在学的,大差不差还是我们以前学的内容,但是他们面临的工作可不是我们出社会时面对的工作了,虽然长期来看AI可以让人类更少工作、更多获得。两次工业革命、一次信息化革命,至少没有让现有知识体系产生动摇,相反让知识体系变得更加重要,学历就变得尤为重要。但这次(算第四次)革命却一锤一锤地在敲碎人类几千年的知识体系。

这个问题也困扰了我好些时间,最终,我觉得自己就做个AI降临派吧——去融入,去拥抱,去理解LLMs(大语言模型)、在工作中使用AI去提升工作,比如用好Prompt。我觉得自己学习都还不晚,那孩子们就更不用担心了,他们会深度使用好AI,就像我们熟练使用计算机和汽车一样。

不做砸珍妮纺纱机的人

好吧,那就先保持自己不落后。

飞船

我觉得目前的AI就像一个从未见过的新机器,从人类以前没有想象过的方式和速度呼啸而来。我们不能等到一年后才后知后觉,那时候已经追不上了…

我们需要现在就开始,从了解底层的技术——大模型开始,再来看看我们需要怎么去拥抱AI。

各类大模型概览

首先我们来看大模型。AILLMs

这是我自己整理的目前大模型分类,当然了肯定不止这些,但是这些对我来说相对熟悉,且本身也具备一定代表性。

行业标准OpenAI

这次冲在最前面的大模型是基于GPT(Generative Pre-trained Transformer,基于Transformer的生成式预训练模型)的GPT-3.5、GPT-4。这些模型的基础信息我就不多说了,大家可以参考之前的文章(#链接)。

OpenAI的产品当然不止ChatGPT,他们还有作图的DALL·E、语音转文字的Whisper,以及自动代码工具等。

OpenAI在GPT-2的时候是开源的,但是到GPT-3之后就闭源了,目前外界不知道OpenAI目前的最高能力已经到什么层度了,他们是这一波潮流的引领者。

搅局者LLaMa

目前在OpenAI的开源对手里面,最活跃的是Meta(原Facebook)出品的LLaMa大模型,他们拥有7B(70亿参数,后同)、13B、33B和65B等多个版本,能力也是依次上升,但是硬件要求也会越来越高。作为拥有深度学习三巨头之一杨立昆的Meta,本来应该是想在AI大模型界当一个Android的,那个最大的“在野党”,就像现在Android之于iOS。但是LLaMa目前申请权重文件非常麻烦(虽然其实已经泄露),加上不能直接用于商业,所以在商业公司层面上已经有一丝丝降温。

基于LLaMa的升级很多,先是有斯坦福大学主导的升级版——Alpaca,再是一直在宣传多么厉害多么厉害、几个大学联合制作的Vicuna。因为LLaMa的商用问题,所以我就没有去下载部署,只是在线试用了LLaMa的集大成者Vicuna。就中文来说,Vicuna可不咋滴,当然了,如果是本地部署,不断训练,肯定是会明显好于现在的能力。

另外,LLaMa大模型还有比较多的中文版或LoRA版(可以理解为简配版,为了大家更容易在消费级显卡上炼丹),比如用C++重新做了一遍的LLaMa.cpp,听说效率提升了不少。还有LLaMa的中文版,专门喂了很多中文预料来训练的,就是Chinese-LLaMa-Alpaca,还未亲测。Vicuna也有中文版,大家有兴趣可以下载安装试试,反正我对LLaMa一类的,还是继续观望,等它可以商用再说吧。

老牌劲旅

这次AI大爆发的一个核心技术组件是Transformer,它很好地解决了机器的长期记忆的问题,这个技术的发明者就是Google。但是Google却在后来移情别恋,以至于OpenAI用了大量Google挖过来的人,发布了ChatGPT之后才发觉事情不妙,仓促发布了Bard。虽然我觉得就技术和数据来说,Google都是有希望追上来的,但是他们的现有业务会不会限制他们在AI上发力(毕竟这和搜索肯定是有直接冲突的),这是一个创新者的窘境。

另外一家目前已经很不错的老牌劲旅是Claude,而且现在Claude提供免费API与Slack结合,对OpenAI已经有一些冲击了。

对于亚马逊来说,他们的Titan肯定还是基于云,买买买,我都能服务。

中国选手

下面出场的是中国选手,网络上已经有一张图了,展示了十多家国内的参赛选手。但是仔细来看,目前可申请试用的应该自有百度的文心一言和阿里的通义千问

百度有丰富的搜索数据,加上有陆奇、吴恩达等多位货真价实的国际顶级大牛,还是有一些积累的,应该是村里面第一个站出来的人。只是前面可能更多的方向是在无人驾驶(更多是在感知智能),而忘了点认知智能的科技树。

阿里这次动作有点迅速,这一块感觉并不是阿里强项,虽然同样拥有足够的数据,拥有TensorFlow和Pytorch1.0的作者之一贾扬清(已离开阿里)。

目前外界最看好的第三家可以站出来是字节跳动,有钱、有数据、有用户,还有人才。

至于其他的公司,感觉都还是比较难的,这件事情,再去找开源模型包个壳其实意义不大,因为这里还要涉及到数据、算力和训练,以及工程化地调优。数据把一大众公司挡在门外了,然后算力又把另一家巨头挡住。最后,很多公司是真的没有技术、没有人才密度的,拿项目也许没问题,但是这种拼硬实力的事情,怎么可能做得成。

这次清华大学无愧国内NO.1的存在,推出的ChatGLM还不错,特别是对于中文的支持。我自己部署了ChatGLM-6B的模型,在我的RTX-4090上跑起来还是很顺畅的,但是也只能先玩着了,因为听到他们7位数的授权费,感觉为了后面不吃官司,还是要提早找一个可商用的大模型练起来。

6b

第一个可商用的开源大模型

这周,第一个可商用大模型它就来了,是Databricks发布的Dolly2.0。首先介绍一下Databricks,它最著名的产品就是Spark,对的,就是那个Apacha Spark。

spark2

说回Dolly2,为什么它可以商用的?先从LLaMa系列大模型为什么不能商用说起。

大模型的技术开发原理其实对于大部分顶尖公司来说不难,因为前面说了GPT-2是开源的,而且牛逼的公司可以根据Google、OpenAI等发布的论文中描述的技术原理开发出大模型。但是,训练却很难,最大的问题就是训练数据,据说OpenAI里面30%的人是在做数据标准、校验等工作,而且在训练ChatGPT的时候,ChatGPT还采用众包方式,让大量肯尼亚人参与数据处理,花费了不少钱。而LLaMa、Alpaca和Vicuna等都使用了ChatGPT去获得非常有价值的原始数据(问题与配对的答案),等于转载使用,所以存在商业化问题。

而Dolly呢?他们在Dolly1的时候也是使用ChatGPT来获得训练数据的,所以依然无法商业。于是,Databricks做了一个大胆的尝试,让公司的5000员工去想,不能借助ChatGPT,也不能直接去其他网站是抄版权内容,全部要自己想,还要保证质量,为此他们还办了个内部比赛。就这样,Databricks在一周内完成1.5万条数据,平均每人3条,因此,他们的这个模型是可以商业的。这个故事在他们的官方博客上有介绍,可以查看。

关于Dolly2,代码可以去Github:databrickslabs/dolly上查看;可商用的模型在Hugging Face:dolly-v2-12b(23G的基于Pytorch的Model)。

目前看到的个别已经部署过的推友评价,中文的处理能力是够呛的,但是咱们不怕啊,数据就是咱们优势啊(后面会讲到是为什么)。

作图模型

Midjouney目前的知名度应该仅次于ChatGPT,作图最好的平台之一,但是真的要用,最好是付费(我已经交了20美元)。

对了,首图就是Midjouney上制作的,还包括下面这张图(一艘GPU飞船):

GPU飞船

目前最大对手是开源的Stable Diffusion,可以直接部署在像我这样的消费级显卡上的,而且处理速度极快。当然,在普遍的自动生成图片质量上还是不如Midjouney的。作图这个服务,对于prompt的收集、意见反馈等感觉比ChatGPT还重要,更加需要人类反馈自学习(RLHF)。

AI管家出现

前面说了这么多大模型,都还没说太多基于这些大模型的应用呢,嗯,我也不太想展开来讲应用,太太太多了。

那么面对这么多的AI应用,怎么简便地去使用呢,怎么样能让我写出更好的Prompt呢?

这时候,LongChain就出来了,一种可以自动帮助我们把一个个应用串起来的应用。

然后,微软出了一个Jarvis(贾维斯),听名字你应该知道是什么了;

这几天最火的,当然是Auto-GPT了,一个月不到,github上的stars已经过了6万。

auto

我们有弯道超车的办法

看到上面说了这么多大模型,会不会觉得头麻了。如果你是第一次接触,那就是麻妈给麻子开门,麻到家了!

但是这里面中国企业起步晚,参与少,对于AI这种必然的未来趋势,我们该怎么办呢?有办法像移动互联网一样后发先至吗?

我觉得是有的,这里非常简单的聊一下。

AI这个事情,主要是有三个决定因素:大模型、算力和数据。

大模型今天已经说了,其实开源的肯定也会不断完善,加上相关核心论文是公开的,这方面对我国来说应该不是最大的问题(君不见,国外这一波,华人比例非常之高);

算力应该也不会是长期的问题,芯片这个问题,我就不多谈了,反正这个不会成为长期的问题;

那么剩下的就是数据了。前面也说了,数据非常重要,也是模型“聪明”还是“白痴”最重要的影响因素。国内的弯道超车应该就在这里了。大家应该还有印象,今年的Q1,国家强调了数字化改革,然后由宣布组建国家数据局。我不知道大家怎么看,虽然也看到一些冷嘲热讽,但是我觉得国家数据局最终的目的是要把国内大部分的重要领域的数据都要收集上来。然后(我yy一下),进行大模型的训练。如果有充分详实的数据,以后很多事情就好办了。比如问“怎么让杭州市民用更低的价格,或者更丰富更便宜的菜品?” 如果是数据充分的AI大模型,会给出好几条方案,其中可能有(依然是yy,内容上别当真)“增加安徽、山东到杭州的高速公路建设,增加蔬菜、肉类等食材的供给,中间涉及到XX个县,根据当前土地补贴政策,整个工程造价XXXX”。

然后呢,我觉得可能只有中国在数据量(看看移动互联网我们是怎么用的,各地支付、遍地小哥)、数据汇总组织统筹能力上都是最佳的,所以在数据这个事情上,我们是可以后发先至,弯道超车的。

东市买骏马,西市买鞍鞯

对于个人呢,前面说了,要先扒上这趟车,不能再等等,等等就可能追不上了。

对于有编程基础的朋友,可以恢复Python的学习、Pytorch的学习等,把大模型研究起来,开发个基于大模型的应用玩玩;

对于产品经理,每天你需要花一个小时去看看这个世界在AI领域发生了什么,借助AI,甚至可以将所有应用重做一遍;

对于所有人,如果我今天说的大部分内容你都没有听过,那你已经接近被时代甩远的危险了,找资料好好学。比如书、比如博客,也有类似极客时间这类,大家也可以看看,每天上下班都可以听,真的别落后了。

ad1ad2

上周末,花了22400元攒了台主机,主要是为了那块RTX4090。有句话是“要做瓷器活,先要有金刚钻”,所以这台电脑,要买!

rtx4090

AI是一个新时代的战场,个人玩家也可以玩,但是,先准备好你的装备吧。木兰诗:“东市买骏马,西市买鞍鞯”,买好上战场了!

这是微信公众号的文章链接:土猛的员外






关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号

ChatGPT的原理、咒语和应用

前言

今天给大家讲讲ChatGPT的工作原理,如何使用,基于它的应用等内容。

ChatGPT的工作原理

其实要讲清楚ChatGPT的原理是很难的,因为我看的几篇能把原理讲清楚的文章基本上都在3万字以上,而且还配了大把的公式和图片。但是我不想这么写,所以就抛弃掉很多细节(特别是数学公式和数学图例),希望能用大白话给大家讲清楚,let’s go!

ChatGPT最浅显的一个工作原理就是:一个单词一个单词的输出,它会根据前面的内容猜测后面应该输出什么单词——是的,这就是ChatGPT最浅显也是最核心的原理,如果你不想深究技术方面的内容,那记住这一点是非常重要的。当ChatGPT在写一篇文章时,它实际上只是一遍又一遍地问自己:“在已有的文本基础上,下一个词应该是什么?“

jinwan

为了能够输出下一个词,你可以想象的到,ChatGPT的内部是有一个概率表的,比如上图所示的”今晚我想和你一起“和后面的词之间的概率表,然后它每次都带有一点随机的挑选概率最高的20%的词。嗯,那么接下来你可能就会问:

概率哪里来的?

首先说答案:ChatGPT”吃“下去很多文本内容,从这些文本中将发现了各个词之间的关系,做了这个统计表。

为了你可以更容易理解,我再举个例子:我们把维基百科搜索”猫“,把前100篇英文文章进行字母统计,得到a后面各单词的概率、b后面各单词的概率……统计后你可以看到如下这样一个字母对的概率表(”2-gram“):

img

根据这个表,我们可以看到,r、s、t和n几个字母后面,a出现的概率都极高,回想”今晚我想和你一起“之后的概率,我们应该可以知道概率是怎么来的了。

但是!!!字母的统计是没有用的,我们至少要把最小颗粒度变成单词,单词一个个联想才有意义。英文中目前有4万个单词,如果像上面的字母一样进行单词的二元组合的话,可以得到16亿个结果,如果是三元就是60亿个结果….如果是20个单词的文章片段呢?可能数量已经超过了宇宙中所有的粒子数量。那怎么办呢?

人类的归纳能力:模型

我们不需要去穷举每种可能性的,因为人类很善于归纳总结,我们早就已经会用模型了。

如果你不清楚什么是模型的话,我们先来看一个非常简单的模型:比如说我们要测试物体落地的距离和时间。我们可以在1楼、2楼、3楼、….、1万楼分别扔下来一个高尔夫球,测试落地时间。当然,真的这么去穷举实验的话你就傻了,我们只需要取其中的一些楼层做实验就可以了。如下图(X轴:层数,Y轴:落地时间):

img

根据这些实验,我们就可以用数学来做模型了,比如:
$$
y=a+bx+cx^2
$$

img

当然,要调整到这个样子,需要不断去调整模型,模型可大可小,有最简单的y=ax(一个参数),有上图这样的(三个参数的)一元多次函数,还有由各种简单模型不断叠加的大模型,参数就更加复杂了。比如ChatGPT-3.5的模型参数高达1750亿个,不仅仅是数值函数,还包括图像识别函数模型、自然语言处理(NLP)的模型等等。

mimapoyi

那么这1750亿的参数是怎么调整出来的?那肯定是用计算机自动调整的,不可能是人去拧这些螺丝。这里的螺丝指的就是参数,计算机出现之前,图灵做的密码破译机(计算机的原型)的参数就是用螺丝来拧的。这里面就需要引入机器学习和神经网络了。

AI的仿生:神经网络

目前最流行和最成功的方法使用神经网络。神经网络在1940年代被发明出来,其形式与今天使用的形式非常接近,可以被认为是大脑运作方式简化后的理想化模型。人类大脑中有约1000亿个神经元(神经细胞),每个神经元能够产生电脉冲高达一千次/秒。这些神经元以复杂网状连接在一起,每个神经元都有树枝状分支,使其能够向数千个其他神经元传递电信号。粗略地说,在任何给定时刻一个特定的神经元是否会产生电脉冲取决于它从其他不同连接收到了哪些“权重”不同的信号。

在计算机中,人们就发明了人工的神经元——感知机。感知机得作用就是接收三个输入条件x1、x2、x3,输出一个结果。

image-20230402110056251

这么说可能有点难理解,我们讲的例子。我们可以将感知机当做一个根据权重来做决策的机器,以周末是否去外面野餐为例,我们有可能有三个影响因素:

  1. 天气情况?
  2. 你的家人是否想去?
  3. 附近是否可以方便地停车?

$$
这三个因素可以称为x_1、x_2、x_3。如果天气好,那么x_1=1,否则x_1=0;类似的,x_2=1或x_2=0。
$$

但各个因素的重要性不同,如果下雨,那野餐的几率为零。
$$
所以我们把x_1的权重设置的更大,如w_1=6,其他权重w_2=2,w_3=2。
$$
如果我们给这个感知机的阈值设置为5(最终的结果>5就出行),那么只要天气情况好,那最终输出就是1(去野餐),其他两个条件几乎成了摆设。这里,三个x的权重和最终的阈值就是所谓的参数!!!

sigmoid1

通过这么一个多因子的权重分配系统,就把一个决定给做出来,是不是有点神奇。其实神经网络实际比这个复杂的多,因为这里仅仅是一层,实用的神经网络都具备很多层,如下图所示:

shenjingwangluo1

我们刚才做出来的野餐出行决定仅仅是其中一个人工神经元,将这个结果传递给下一层的神经元(上一层的输出结果作为下一层的输入因子)进行更复杂的判定。需要注意的是一个神经元的输出只有一个,上图其实一个神经元有多个箭头输出只是说明同一个输出可以被下一层的多个神经元接收作为输入因素。

当然,说回感知机,有些感知机是“F二代、G二代”,可以赢在起跑线上,比如我们加一个偏置值4,那么不管三个条件哪一个满足,最终的值都大于5了,都可以去野餐,这就是二代们的霸气…..这个偏置值b,以及前面的w,是不是就是我们最上面那个一元二次函数的a(偏置值)、b、c啊。

如果你认真看到这里,那么就可以知道数学函数与神经元之间的关系了。现在我们要做的事情是要让机器能自动帮我们找到权重和偏置值(w、b),使得神经网络输出的y(x)能够拟合(匹配)各种情况。这里就需要展现著名的损失函数了,有些地方也叫代价函数。
$$
C(w,b) \equiv \frac{1}{2n} \sum_x |y(x)-a|^2
$$
这里就不展开函数的具体细节了,目前常见的训练方案是使用梯度下降算法来找参数,以便最小化C(w,b)。梯度下降是一种微积分里面的求导函数,用图表来表示的话,它们在做的事情就是不管输入点(比喻成一个小球)在哪个位置,让它滚动去找最低点。整体的正地点找到了的话,那么这时候的参数就是我们要找的参数了。

img

要说明白ChatGPT的原理其实还有很多大的概念,但其实它就是一个巨大的神经网络——之前使用的是1750亿个权重版本的GPT3.5网络。在很多方面上,这个神经网络的特别之处在于处理语言的能力,并且它最显著的特点是它基于“Transformer”构建了神经网络。Transformer 的想法是对组成文本片段序列标记进行类似操作, 但不同之处在于 Transformer 引入了“注意力”概念和更加关注某些序列片段而非全部. 或许总有一天只需启动通用神经网络并通过训练进行所有自定义就足够了,但至少目前来看,在实践中“模块化”事物似乎是至关重要的——正如 Transformer 所做的那样,也可能与我们的大脑所做的类似。

ChatGPT的总体目标是根据其训练过程中看到的内容(即查看了来自Web、电子书、word等数十亿页文本),以“合理”的方式继续文本。因此,在任何给定时刻,它都有一定数量的文本,并且其目标是为下一个要添加的标记提供适当选择。

它分为三个基本阶段:首先,它获取与迄今为止已有文本相对应的标记序列,并找到表示这些标记序列的嵌入式数组(即一组数字)。然后,在神经网络中进行操作——通过在网络中连续层之间传播值——以生成新嵌入式数组(即新一组数字)。接着,它取该数组最后部分并从中生成大约50,000个值得出不同可能下一个标记概率的数组。

好了,你能感觉到越到后面我越讲不清楚了,那是因为后面需要太多的公式和图例,我直接放上来的话,懂的人自然懂,但是大部分想了解ChatGPT的朋友可能就很难理解了。不过,ChatGPT的基本概念在某种程度上相当简单:从网络、书籍等大量人类创作文本样本开始,然后训练神经网络生成“类似于这些”的文本。

和BERT的比较

这里需要补充一点就是ChatGPT中的GPT和BERT的区别。这两者都是基于Transformer的,但是BERT相对来说考虑的比较周全一些,它会去分析上下文(双向的)和做推理,我们可以认为是它对我们问的话做了一些思考,再返回。这里的返回形式,包括去召回既有的知识库,还有就是用GPT来生成。

但是GPT的模式就粗暴的多,它可不考虑这么多,直接就是根据已有的文字(比如我们输入的问题或者一些全局性的文本内容)一个单词一个单词地往下走。GPT更像是东方朔测字算命,我不想了解你,你就写几个字,我根据自己读过的“一千个北大图书馆”的知识,看字来算….

AI魔法师的咒语——Prompt

正是ChatGPT这种不求甚解,只根据我们键入的文本进行推演的工作方式,造成了我们怎么“问”就非常重要了!

这个问法,就是Prompt,也叫提示,这是现代AI魔法师的咒语啊!

我们先来看看几个简单的问法。

01.翻译

翻译1

翻译是最常见的用法之一,目前也有一些翻译插件,挺好用的,后面也会提到。

02.归纳总结

归纳

不仅仅是归纳,我还可以让它单独罗列一些内容:

归类列表

我觉得ChatGPT能力还是很强的,就这两下子,已经可以把一些“理工男”打败了。

03.润色

润色-1

再让它帮我们把文字润色一下,可以有很多种风格,包括鲁迅、李白的等等,只要你能想到,如果你写过大量文章,也可以喂给ChatGPT让它学习你的语言风格,以后可以让它用你的说话风格把一些文章给润色一下。

04.日程安排

日程安排-约会议时间

你可以把自己和朋友的日程告诉它,让它来帮助预约会议,我这里展示的是两个人,更多人的依然有效。

这只是我之前能想到的内容….然而,真正的ChatGPT魔术师的Prompt可就了不得了,我分享一下别人的内容。

image-20230402190246103

是的,这就是专业啊!原文地址

网上其实还有很多的prompt的网站,大家有兴趣可以自行搜索。

另外,OpenAI的创始人Sam Altman的想法是5年之内解决prompt的问题,以后的交互会更加简单。言外之意prompt也许是你接下来最需要学好的知识。

这五年,“用好prompt你”可以把“不使用promt的你”甩开十八条街,你信不信!!!

基于ChatGPT的应用

基于ChatGPT的应用现在太多了,除了网页版的ChatGPT之外,我自己最先用的、也算用的比较多的是翻译。

01.翻译工具

我用的是yetone开发的产品,大家可以在Chrome应用商店搜索“OpenAI Translator”找到这个插件,它除了多种语言的翻译,还可以做归纳、润色等工作。

fanyi_yetone

02.个人知识库

看到倪爽老师在用Jiayuan开发的Copilot Hub创建自己的知识库,他把自己的所有推文(大家可以理解为微博上自己发过的所有文字)都导出来,放进这个知识库中。然后,随时可以对“自己”提问,问问以前对某件事的看法,问问就自己的价值观来说,当前面临的这件事情以前的“自己”会怎么选择。是不是有点《流浪地球2》中数字意识的感觉了。

ns1 ns

Copilot Hub的地址是Copilot Hub

03.OpenAI插件

chajian

随着OpenAI开始发布插件,官方的说法是有了眼镜和耳朵,让内容更加实时了,基于OpenAI的应用肯定会出现井喷。有些应用,我相信OpenAI他们自己也没想到。未来会怎么样,值得期待!

这一波AI潮的其他优秀应用

这一波AI浪潮因ChatGPT的横空出世而被推到了大众面前,其实大部分的应用前几年就已经在发展了。虽然多次听到OpenAI与医疗、量化等行业的公司有密切合作,但下图中的大部分应用其实和OpenAI没什么关系。

img

前往查看详细AI应用列表

微软、Adobe等都已经在大步迈进AI,我们现在几乎每天都能听到很多新的基于AI的产品。嗯,当然,这也激起了一些人的紧张,比如马斯克等人联名要求暂停比GPT-4更先进的AI研发6个月。

好,不去管这些科学伦理的事情,我想介绍一位我认识的创业者,他可算是把AI用的非常溜了。

yong

以前要做数字人,制作3D是一件非常重的事情,而且价格昂贵。但是这次,他直接在家里只用一台电脑和网线,通过各种prompt就把一段数字人介绍他自己公司产品(Omniedge,一种可以连接任何设备到局域网的方案)的视频做好了,用的是他在midjourney上生成的理想代言人,还有普通话、粤语、陕西话等多个版本。数字人行业应该会被迭代,特别是制作3D模型的人,以后,真的不一定还值钱。

总结

去拥抱未来吧,何况未来已来!






关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号