我的学习笔记

土猛的员外

使用Midjourney制作专业级LOGO

原文:Best Logo Prompts for Midjourney V5 AI Image Generator: Blueprint for Amazing Logo included!
作者:Michael King

今天我们将探索一些超棒的提示,帮助你使用最新版本、最强大的Midjourney,像专业人士一样轻松制作出独特、引人注目的标志!


但等等,还有更多!作为这个美妙蛋糕上的额外樱桃,我还会提供一个Blueprint模板,您可以混合、匹配和调整它来创建自己的标志设计杰作。请耐心阅读本文直到最后,因为那里将会有这个时髦的Blueprint。

logo制作的魔法开始了

Prompt:
Design of abstract logo featuring a dog in blue on a pink background. Include lines as an additional design element. --v 5

Prompt:
Design of sketchy logo featuring a bird in green on a blue background. Include pattern as an additional design element. --v 5

Prompt:
Design of professional logo featuring a leaf in green on a brown background. Include curves as an additional design element. --v 5

Prompt:
Design of professional logo featuring a leaf in green on a brown background. Include curves as an additional design element. --v 5

Prompt:
Design of professional logo featuring a leaf in green on a brown background. Include curves as an additional design element. --v 5

Prompt:
Design of sketchy logo featuring a queen in black on a silver background. Include highlights as an additional design element. --v 5

Prompt:
Design of professional logo featuring a fish in yellow on a purple background. Include circle as an additional design element. --v 5

Prompt:
Design of sketchy logo featuring a knight in purple on a black background. Include texture as an additional design element. --v 5

Prompt:
Design of wild logo featuring a heart in multicolored on a red background. Include square as an additional design element. --v 5

Prompt:
Design of futuristic logo featuring a spaceship in white on a brown background. Include star as an additional design element. --v 5

Prompt:
Design of whimsical logo featuring a cat in silver on a gold background. Include diamond as an additional design element. --v 5

Prompt:
Design of vintage logo featuring a star in gray on a blue background. Include pattern as an additional design element. --v 5

Prompt:
Design of professional logo featuring a triangle in pink on a gold background. Include pattern as an additional design element. --v 5

Prompt:
Design of professional logo featuring a computer in blue on a yellow background. Include highlights as an additional design element. --v 5

Prompt:
Design of futuristic logo featuring a cat in purple on a gray background. Include hexagon as an additional design element. --v 5

Prompt:
Design of fancy logo featuring a galaxy in green on a gold background. Include pattern as an additional design element. --v 5

Prompt:
Design of futuristic logo featuring a zombie in green on a purple background. Include pattern as an additional design element. --v 5

Prompt:
Design of abstract logo featuring a skull in blue on a white background. Include highlights as an additional design element. --v 5

Prompt:
Design of playful logo featuring a bird in black on a black background. Include moon as an additional design element. --v 5

Prompt:
Design of watercolor logo featuring a circle in black on a purple background. Include geometric shapes as an additional design element. --v 5

现在,正如承诺的那样,这是我之前提到的超级棒的蓝图模板。使用这个家伙作为起点,并进行自定义以创建一个独一无二的标志:

Prompt Blueprint:

Design of [style风格] logo featuring a [symbol标志] in [color颜色] on a [background背景] background. Include [additional_element额外元素] as an additional design element.

这是一个Logo设计prompt的主要公式,使用下面的 YAML 内容替换方括号中的元素。

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
style:  
- modern
- minimalist
- vintage
- retro
- fun
- futuristic
- abstract
- cartoon
- geometric
- hand-drawn
- typographic
- tribal
- text-based
- illustrative
- photorealistic
- 3D
- grunge
- sketchy
- watercolor
- comic
- cute
- fancy
- feminine
- masculine
- playful
- professional
- simple
- traditional
- trendy
- vibrant
- whimsical
- wild
symbol:
- star
- circle
- triangle
- square
- heart
- diamond
- moon
- hexagon
- leaf
- tree
- wave
- cloud
- sun
- skull
- deer head
- lion head
- bear head
- cat
- dog
- car
- train
- pizza
- coffee cup
- coffee bean
- cupcake
- ice cream cone
- computer
- phone
- camera
- book
- rocket
- airplane
- house
- castle
- sword
- crown
- flower
- bird
- fish
- butterfly
- dragon
- unicorn
- mermaid
- alien
- robot
- spaceship
- planet
- galaxy
- zombie
- ghost
- witch
- wizard
- knight
- princess
- queen
- king
- wheel
- gear
- clock
color:
- red
- orange
- yellow
- green
- blue
- purple
- pink
- brown
- black
- white
- gray
- silver
- gold
- multicolored
background:
- red
- orange
- yellow
- green
- blue
- purple
- pink
- brown
- black
- white
- gray
- silver
- gold
- multicolored
additional_element:
- gradient
- shadow
- border
- pattern
- texture
- geometric shapes
- lines
- curves
- highlights
- 3d effects
- star
- circle
- triangle
- square
- heart
- diamond
- moon
- hexagon

大家看到了吗!有了这些技巧、诀窍和超棒的蓝图,你就可以用Midjourney V5 AI图像生成器征服标志设计世界。记住,熟能生巧,所以不要害怕尝试并让你的创造力奔放!

但在你开始创作标志杰作之前,请帮我一个忙关注我的频道。我承诺会继续提供最新的技巧和诀窍,让您娱乐和知情。

愉快的设计,下次再见并保持好奇心!

作为对您耐心的小小奖励,这里有一个Python脚本,可以动态随机地创建标志提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import random  
import yaml


def get_options(file_path):
with open(file_path, 'r') as file:
options = yaml.load(file, Loader=yaml.FullLoader)
return options

input_data = get_options('input.yaml')

command = input_data['command']
for key in input_data.keys():
if key != 'command':
command = command.replace(f'[{key}]', input_data[key][random.randint(0, len(input_data[key]) - 1)])

print(command)

关于Midjourney的其他文章:






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

公众号:土猛的员外


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



一分钟制作Chatgpt插件

员外注释:Wolfram的ChatGPT插件貌似是目前最好的几个ChatGPT Plugin之一,这篇文章从动手实现,到我意想不到的反向操作电脑(这还可以延伸到各类硬件),最后说原理,对于开发Plugin的开发者应该有一些帮助。

中文译文:ChatGPT的即时插件:介绍Wolfram ChatGPT插件套件

英文原文:Instant Plugins for ChatGPT: Introducing the Wolfram ChatGPT Plugin Kit

Instant Plugins for ChatGPT: Introducing the Wolfram ChatGPT Plugin Kit

在一分钟内构建一个新插件…

几周前,我们与OpenAI合作发布了Wolfram插件,使ChatGPT可以使用Wolfram语言和Wolfram|Alpha作为工具,在ChatGPT内部自动调用。可以将其视为向ChatGPT添加广泛的“计算超能力”,从而让它获得 Wolfram语言和Wolfram|Alpha中所有通用的计算功能和计算知识。

但是如果您想制作自己的特殊插件,进行特定的计算或者访问仅在您自己的电脑或电脑系统上可用的数据或服务怎么办呢?今天,我们发布了第一个版本的开发套件来实现这一点。借助整个Wolfram Language技术栈构建,并且我们已经成功地使整个过程变得非常简单——至少基本定制 ChatGPT 插件现在只需要不到一分钟就可以轻松部署。

需要进行一次(非常简单)设置-验证OpenAI身份并安装Plugin Kit。然后你就可以开始创建你的第一个插件。

要运行这里的示例,您需要:

  • 开发者可以访问ChatGPT的OpenAI插件系统

  • 可以访问Wolfram语言系统(包括免费的Wolfram Engine for Developers、Wolfram Cloud Basic等)

  • 您现在还需要使用PacletInstall[“Wolfram/ChatGPTPluginKit”]安装ChatGPT Plugin Kit(插件套件)。

这是一个非常简单的例子。假设你为一个单词创造了一个“强度”的概念,将其定义为“字母数字”的总和(“a”为1,“b”为2等)。在 Wolfram 语言中,您可以计算如下:

img

而且作为十多年来的标准,您可以立即将这样的计算部署为 Wolfram Cloud 中的 Web API——可通过 Web 浏览器、外部程序等立即访问:

img

但是现在有一种新的部署形式:作为ChatGPT的插件。首先,您需要获取插件工具包:

img

然后您就可以立即部署您的插件了。只需要:

img

最后一步是您需要告诉ChatGPT有关您的插件。在Web界面中(按照当前配置),选择“Plugins > Plugin store > Develop your own plugin”(“插件”>“插件商店”>“开发自己的插件”),并将来自ChatGPTPluginDeployment的URL插入到你的对话框中 (通过点击复制按钮获取 img) :

ChatGPT plugin dialog

现在一切准备就绪。您可以开始与ChatGPT谈论“单词强度”,它将调用您的插件(默认情况下称为“WolframCustom”)来计算它们:

img

展开这个对话气泡,可以了解ChatGPT与插件之间的通信:

img

没有插件,它就不知道“字母强度”是什么。但有了插件,它就能够做出各种(相当惊人的)事情——比如这样:

img

关于角马属性的修饰很迷人,但如果打开聊天气泡,就可以看到它是如何得出答案的——它只是开始尝试不同的动物(“狮子”,“斑马”,“角马”):

img

软件工程师会立即注意到,我们设置的插件正在本地主机上运行,也就是在您自己的计算机上执行。正如我们将要讨论的那样,这通常是一件非常有用的事情。但是您也可以使用Plugin Kit创建纯粹在Wolfram Cloud(或者阿里云/腾讯云/华为云…)中执行的插件(例如,您不必在计算机上安装Wolfram Engine)。

你所要做的就是使用ChatGPTPluginCloudDeploy——然后你会得到一个Wolfram Cloud中的URL,你可以告诉ChatGPT这个插件的位置:

img

实际上,您可以直接在网络浏览器中完成整个设置过程,无需进行任何本地 Wolfram 安装。只需在 Wolfram Cloud 中打开一个笔记本,并从那里部署您的插件即可:

Wolfram Cloud notebook

让我们来做一些其他的例子。接下来,让我们发明一个“地理影响圆盘(geo influence disks)”的概念,然后部署一个插件来呈现这样的东西(稍后我们将谈论一些正在进行的细节):

img

现在我们可以安装这个新插件,然后开始向ChatGPT询问“地理影响圆盘(geo influence disks)”:

img

ChatGPT 成功调用了插件,并返回了一张图片。有趣的是,它猜测(事实上是正确的)“地理影响圆盘”应该是什么意思。请记住,它无法看到图片或读取代码,因此它的猜测只能基于 API 函数名称和提出的问题。当然,在调用 API 函数时,它必须有效地理解至少一点内容——x 应该表示位置,而 radius 则表示距离。

再举个例子,我们来制作一个插件,可以向用户(即部署插件的人)发送短信:

img

现在只需要说“发送一条信息…”(“send me a message…”)

img

一条短信将会到达——在这种情况下,它还带有ChatGPT的小装饰:

Rhinoceros alert

这是一个插件,它还会发送提到的动物的“警报图片”:

img

是的,需要许多技术才能实现这一点:

img

Gnu alert

作为另一个例子,让我们创建一个插件来检索我的个人数据——这里是我在 Wolfram databin数据中积累了数年的心率数据:

img

现在我们就可以使用ChatGPT来对这些数据提出自己的问题了:

img

而且,如果安装了主要的Wolfram插件,我们可以通过ChatGPT的“语言用户界面”立即对这些数据进行实际计算:

img

这个例子使用了 Wolfram Data Drop 系统。但是,我们也可以用类似 SQL 数据库(关系型数据库)的东西来做同样类型的事情。如果你已经设置好插件以访问私有数据库,那么通过 ChatGPT 和 Wolfram 插件真正令人惊叹的事情就能够实现。

通过Plugins(插件)操控你的电脑

当您通过ChatGPT的标准Web界面使用它时,ChatGPT是在OpenAI的服务器上运行。但是,通过插件,您可以“回溯”——通过您的Web浏览器——以使事情发生在自己的本地计算机上。我们稍后将讨论这个“引擎盖下”的工作原理,但现在可以说一下:当您使用ChatGPTPluginDeploy(而不是ChatGPTPluginCloudDeploy)部署插件时,在插件中实际运行的Wolfram语言代码将在本地计算机上运行。因此,这意味着它可以访问计算机上的本地资源,如摄像头、扬声器、文件等。

例如,这里我正在设置一个插件来使用我的计算机摄像头拍照(使用 Wolfram 语言的 CurrentImage[ ] 函数)—然后将图片与我指定的任何颜色混合(我们稍后会讨论 CloudExport 的用法):

img

安装插件后,我对ChatGPT说:“想象一下我穿着绿色的衣服!”然后,ChatGPT会立即调用插件,让我的电脑拍摄我的照片,并将其与绿色混合(包括我的“我不知道这是否有效”的表情):

译者:这真的好吗???

img

好的,让我们尝试一个稍微复杂一些的例子。在这里,我们将创建一个插件来让ChatGPT在我的电脑上打开笔记本,并开始写入内容。为了实现这一点,我们将定义几个API端点(并将整个插件命名为“NotebookOperations”):

img

首先,让我们告诉ChatGPT创建一个新笔记本

img

然后在我的屏幕上弹出了一个新的笔记本:

My cat notebook

如果我们查看从中部署插件的 Wolfram 语言会话中的符号 nb,我们将发现它是由 API 设置的:

img

现在让我们使用其他的API端点向笔记本添加内容:

img

这就是我们可以获得的结果:

Cat pictures

这段文字是由ChatGPT编写的;图片则来自于网络图像搜索。 (我们也可以使用Wolfram语言中的新ImageSynthesize[]函数制作全新的猫咪图片。)

最后,让我们请ChatGPT展示一张用CurrentNotebookImage从电脑屏幕上捕捉到的笔记本截图:

img

我们还可以添加另一个端点,使用CloudPublish将笔记本发布到云中,并可能通过电子邮件发送URL。

我们可以将前面的示例视为在笔记本中累积结果。但是我们也可以只在Wolfram语言符号的值中累积结果。这里,我们将符号“result”初始化为空列表。然后,我们定义了一个API来附加到此列表,但是给出了提示,仅当有单词结果时才执行此附加操作:

img

让我们为ChatGPT设置一个“练习”:

img

此时,result仍为空:

img

现在让我们来问第一个问题:

img

ChatGPT并不会直接显示答案。但它会调用我们的API并将其附加到result中:

img

让我们来问问另一个问题:

img

现在 result 中包含了两个答案:

img

如果我们在笔记本中放置Dynamic[result],每当ChatGPT调用API时,我们会看到它动态地改变。

在最后一个例子中,我们从ChatGPT内部修改了符号的值。如果我们感觉勇敢,可以让ChatGPT评估计算机上的任意代码,例如使用调用ToExpression的API。但是,是的,赋予ChatGPT执行自己制作的任意代码的能力似乎会使我们面临某种“天网风险”(并使我们更加想知道“AI宪法”之类的事情)。

但比执行任意代码更安全的做法是让ChatGPT有效地“四处查找”我们的文件系统。让我们设置以下插件:

img

首先,我们设置一个要操作的目录:

img

现在让我们问ChatGPT关于那些文件:

img

使用 Wolfram 插件,我们可以让它制作这些文件类型的饼图:

img

现在我们要求它做一些非常“LLM-ey”的事情,并总结每个文件的内容(在API中,我们使用Import导入文件的纯文本版本):

img

还有很多事情可以做。这里有一个插件,可以计算从您的电脑到服务器的ping时间:

img

img

或者,作为另一个例子,您可以设置一个插件来创建定时任务,在指定的时间提供电子邮件(或短信等)提醒:

img

ChatGPT 负责认真排队任务:

img

然后,每隔大约10秒钟,我的邮箱里就会弹出一个(也许有些可疑的)动物笑话:

Animal jokes

最后一个例子,让我们考虑一下在我的电脑上播放曲调的本地任务。首先,我们需要一个能够解码音符并播放它们的插件(”ChatGPTPluginDeploy”用于告诉ChatGPT插件已经完成了工作——因为ChatGPT本身无法知道这一点):

img

在这里,我们给ChatGPT想要的音符——是的,这立即在我的电脑上演奏出来:

img

现在,作为对一位著名虚构人工智能的致敬,让我们尝试演奏另一首曲子:

img

是的,ChatGPT整理了一些笔记,并将它们打包到插件中;然后插件播放了它们:

img

依然可以运行:

img

但是…等一下!那是什么曲调?ChatGPT似乎还不能像HAL那样(可疑地)宣称:

“没有[HAL] 9000电脑曾经犯过错误或扭曲信息。按任何实际定义的词语,我们都是防错和无误的。“

说说工作原理【敲黑板】

我们现在已经看到了许多使用ChatGPT插件工具包的示例。但是它们是如何工作的?底层是什么?当您运行ChatGPTPluginDeploy时,实际上正在设置一个Wolfram语言函数,可以从ChatGPT内部调用该函数,以便在需要时使用。为了使这个过程顺利进行,需要使用Wolfram语言独特能力的广泛谱系,并与ChatGPT中某些“聪明”的功能相结合。

从软件工程角度来看,ChatGPT插件基本上就是一个或多个Web API(连同告诉ChatGPT如何调用这些API的“清单”)。那么如何在Wolfram语言中设置Web API呢?十年前我们发明了一种极其简单易行的方法。

像Wolfram语言中的所有内容一样,Web API也由符号表达式表示,在这种情况下形式为APIFunction[…]. 那么APIFunction里面有什么呢?其中有两个部分:一段实现所需功能的Wolfram Language代码和规范说明,在将字符串传递给APIFunction之前应该如何解释这些字符串(例如来自Web API)。

以下是一小段 Wolfram Language 代码, 在此案例中用于否定颜色并使其变浅: =>

img

如果我们愿意,我们可以将其重构为一个应用于两个参数的“纯函数”:

img

纯函数本身只是一个符号表达式,它会被计算为其自身:

img

如果需要的话,我们可以给纯函数的参数命名,并将它们与其名称作为键一起提供到一个关联 (img) :

img

但是假设我们想要从 Web API 调用我们的函数。Web API 中的参数始终为字符串。那么,如何将字符串(例如“lime green”)转换为 Wolfram 语言可以理解的符号表达式呢?好吧,我们必须使用 Wolfram 语言的自然语言理解能力。

以下是一个示例,其中我们正在说希望将字符串解释为颜色:

img

那个颜色样本到底是什么?就像 Wolfram 语言中的其他所有内容一样,它只是一个符号表达式:

img

好的,现在我们准备将所有内容打包成APIFunction。第一个参数表示我们要表示的API有两个参数,并描述了我们希望如何解释这些参数。第二个参数给出了实际的Wolfram语言函数,该API计算它们。单独使用,APIFunction只是一个符号表达式,其本身就可以评估:

img

但如果我们为参数提供值(这里使用关联),它将被计算:

img

目前,所有这些都只是在我们的 Wolfram 语言会话中发生。但要获得实际的 Web API,我们只需“云部署”APIFunction

img

现在我们可以从Web浏览器中调用此Web API:

Web API

是的,那就是符号表达式的结果。如果我们想要一些可视化的东西,我们可以告诉APIFunction将其结果作为PNG输出:

img

现在它将显示为网络浏览器中的图像:

Purple box in web browser

(请注意,CloudDeploy 部署的 Web API 默认情况下权限设置为只有我可以运行。如果使用 CloudPublish,则任何人都可以运行它。)

好的,那么我们如何设置我们的 Web API 以便作为 ChatGPT 插件调用呢?一个直接的问题是,在最简单的级别上,ChatGPT 只处理文本,因此我们必须将结果转换为文本。所以让我们进行一些 Wolfram 语言编程来实现这个目标。以下是 Wolfram 知识库中常见颜色值和名称列表:

img

当然,我们也知道许多其他命名颜色的集合,但在这里不必担心它们:

img

现在我们可以使用Nearest函数来找到最接近我们所得颜色的常见颜色:

img

现在让我们将其放入APIFunction中(这里我们已经“图标化”了颜色列表;我们也可以定义一个单独的函数来查找最近的颜色,它会自动随CloudDeploy一起带来):

img

现在我们已经准备好使用ChatGPTPluginDeploy了。 ChatGPT插件的工作方式是,我们必须为与我们的API对应的“端点”命名。 这个名称以及我们在API中用于参数的名称将被ChatGPT用来确定何时以及如何调用我们的插件。 但在这个例子中,我们只想为端点使用某种唯一名称,这样我们就可以在聊天中引用它而不会让ChatGPT将其与其他内容混淆。 所以让我们称之为ColorMangle。 现在让我们进行部署:

img

到目前为止,我们关于APIFunction及其调用的所有内容在ChatGPTPluginDeploy和ChatGPTPluginCloudDeploy中都是相同的。但接下来要说的就不同了。因为ChatGPTPluginDeploy设置API函数在本地计算机上执行,而ChatGPTPluginCloudDeploy则将其设置为在Wolfram Cloud(或Wolfram Enterprise Private Cloud等)上运行。

本地部署和云端部署都有优缺点。在本地运行可以让您获得计算机的本地功能,如相机、文件系统等。在云端运行则允许其他人也能运行您的插件(但目前除非您将插件注册到OpenAI,否则一次只有有限数量的人能安装您的插件)。

好吧,让我们来谈谈本地插件部署。ChatGPTPluginDeploy 在您的计算机上有效地设置了一个最小化 Web 服务器(使用10行 Wolfram 语言代码实现),在 ChatGPTPluginDeploy 选择的端口上运行,并且每当它收到对 API URL 的请求时调用 Wolfram 引擎与您的 API 函数进行交互。

这是 ChatGPTPluginDeploy 正在使用的操作系统套接字(是的,Wolfram 语言将套接字——像其他所有东西一样——表示为符号表达式):

img

好的,但是ChatGPT怎么知道你的API呢?首先,您需要通过ChatGPT UI的 Plugins > Plugin store > Develop your own plugin(插件>插件商店>开发自己的插件)告诉它您正在使用的端口。您可以通过单击ChatGPTPluginDeployment对象中的 img 图标或以编程方式找到端口:

img

您输入此URL,然后告诉ChatGPT“查找清单文件”:

Find manifest file

让我们看一下它找到了什么:

img

这是一个“清单”,它告诉插件安装程序的相关信息。我们没有指定太多内容,所以大部分都是默认值。但清单中重要的一部分是提供API规范URL的部分:http://localhost:59353/.well-known/openapi.json

访问该URL后,我们可以找到这个“OpenAPI规范”:

Validated OpenAPI spec

最后,点击Install localhost plugin,该插件将出现在您的ChatGPT会话中已安装插件列表中

img

当ChatGPT启动时安装了插件,它的“系统提示”中会包含一个额外的部分,让它可以“学习”如何调用该插件:

System prompt

现在我们可以开始使用这个插件了:

img

是的,它有效。但这里有一些魔法。ChatGPT不得不“拆开”我们所要求的内容,意识到API端点称为ColorMangle是相关的,然后确定其颜色参数应该是“青柠绿”,级别应该是“0.5”。打开盒子,我们可以看到它所做的:

img

现在我们可以开始在其他地方使用“颜色混合”——尽管ChatGPT急于告诉我们,“颜色混合”是一种“虚构的操作”,可能是为了避免被指责不尊重某个国家的旗帜颜色:

img

在我们处理的这个案例中,ChatGPT成功地将文本片段正确“连接”到API中的适当参数。而且它是从我们为参数使用的名称(以及我们给出的端点名称)所获得的信息碎片中实现了这一点(相当惊人)。

但有时候我们需要告诉它更多信息,我们可以通过在ChatGPTPluginDeploy内指定插件提示来实现:

img

现在我们不仅需要谈论颜色:

img

起初,它没有成功地“解开”“冰岛的颜色”,但后来它纠正了自己,并得到了答案。(是的,如果我们写一个更好的提示,也许我们本可以避免这种情况。)

实际上,您可以提供多个级别的提示。您可以为整个插件包括一个相当长的提示。然后,您可以为每个单独的API端点提供较短的提示。最后,您可以通过将“color”→ “Color”替换为类似于以下内容来帮助ChatGPT解释API中单个参数:

img

当您设置插件时,它可以包含许多端点,执行不同的操作。此外,在共享提示之外,这特别方便的原因之一是(出于安全原因),任何给定子域只能有一个关联的插件。因此,如果想要具有各种功能,则必须通过使用不同的端点来实现。

对于ChatGPTPluginCloudDeploy,每个子域仅限一个插件意味着任何给定用户一次只能部署一个云插件。但对于本地插件规则略有不同,并且ChatGPTPluginDeploy可以通过在不同端口上运行它们来部署多个插件-事实上,默认情况下ChatGPTPluginDeploy每次调用时都会选择随机未使用的端口。

但是本地插件如何工作?它如何“回到”您的计算机?魔法基本上发生在ChatGPT Web前端中。所有插件工作方式都是当将要调用该插件时,LLM逐标记生成过程停止,并且“外循环”的下一个动作是调用该插件-然后将其提供给LMM在下一步中将添加到字符串中。好吧,在本地插件的情况下,“外循环”使用聊天窗口前端中JavaScript向指定了localhost 端口 的计算机发送请求。(顺便说一下,一旦ChatGPTPluginDeploy打开端口,它将保持打开状态,直到您在其套接字对象上明确调用Close。)

当使用本地插件时,它们在部署插件的Wolfram语言会话中运行其Wolfram语言代码。这意味着例如(如我们在某些情况下看到的那样),设置的值在进行另一个调用时仍然存在。

在云中,它不会立即以这种方式工作,因为每个API调用实际上是独立的。但是可以轻松地将状态保存在云对象中(例如使用CloudPutCloudExpression等)以便可以跨多个API调用具有“持久性内存”。

ChatGPT内部的LLM(目前)仅设置为处理文本。那么图像怎么办?好吧,插件可以将它们放入Wolfram Cloud中,并将其URL传递给ChatGPT。 ChatGPT已经设置能够直接呈现特定类型的东西-如图像。

因此-正如我们上面所看到的-要从插件“输出”图像(或几个图像),我们可以使用CloudExport将每个图像放入云对象中,在PNG格式中进行操作。然后ChatGPT可能需要一些提示来显示嵌入式输出中的图片。

部署Wolfram Language插件于ChatGPT中存在一些稍微棘手的“管道”,其中大部分在ChatGPTPluginDeployChatGPTPluginCloudDeploy中自动处理。但是通过基于Wolfram语言的基本符号结构(及其集成部署功能)来构建,可以非常简单地为ChatGPT创建复杂的自定义Wolfram语言插件,并为LLM和Wolfram语言周围不断发展的生态系统做出贡献。






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

公众号:土猛的员外


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



适合全栈工程师的7个网页设计技巧

这是一篇帮助非专业网页设计师快速改善设计感的文章,7个设计技巧,对于全栈工程师也是非常有用的。

这里是原文链接

每个网页开发者都不可避免地会遇到需要做出视觉设计决策的情况,无论他们是否喜欢。

也许你所在的公司没有全职设计师,需要自己实现新功能的用户界面。或者你正在进行一个副业项目,并且想要它看起来比其他基于Bootstrap的网站更好。

很容易就会放弃并说:“我永远无法做出好看的东西,我不是艺术家!”但事实证明,有很多技巧可以提升你的工作水平,而这些技巧并不需要具备图形设计背景。

以下是七个简单的想法,可以用来改善你今天的设计。

1.使用颜色和粗细来创建层次结构,而不是字体大小

1_KYZikUrx9F02cJU9kpn_gQ

在设计 UI 文本时,常见的错误是过度依赖字体大小来控制层次结构。

“这段文本重要吗?让它变大一点。”

“这段文本次要吗?让它变小一点。”

与其完全依靠字体大小,不如尝试使用颜色或者字重来完成同样的工作。

“这段文本重要吗?让它加粗。”

“这段文本次要吗?用浅色调。”

尽量只使用两到三种颜色:

  • 一个深(但不是黑)的颜色用于主内容(例如文章标题)
  • 一个灰色用于辅助内容(例如文章发布日期)
  • 一个浅灰色用于附属内容(例如页脚版权声明)

1_2YuCOOCjdMEJxg-Lb6G2FA

同样,对于 UI 设计工作来说,通常只需要两种字重:

  • 大多数文本使用普通字重(400 或 500,具体取决于字体)
  • 强调文本使用较粗的字重(600 或 700)

1_AHrVF0vTtj-yoyhmBNHNLA

在 UI 设计中避免使用小于 400 的字重;它们可以用于大标题,但在较小的尺寸下阅读起来太困难。如果您考虑使用较轻的字重来弱化某些文本,请改用较浅的颜色或更小的字号。

2.不要在有色背景上使用灰色文字

1_ajjrhpp-l3GDG7ne7Am8fw

将文本变成浅灰色是在白色背景下减弱其重要性的好方法,但在有色背景上看起来并不那么好。

这是因为我们实际上看到的效果是灰色对白色的对比度降低了。

让文本更接近背景颜色才能帮助创建层次结构,而不是使它变成浅灰色。

1_CNaej5BrPr9lWTMAfllfdw

当你在使用色彩丰富的背景时,有两种方法可以降低对比度:

1.降低白色文本的不透明度

使用白色文本并降低不透明度。这样可以让背景颜色透过一点,以某种方式减弱文本与背景之间的冲突。

1_OMntEW2V5jPXrZR6CjrBFQ

2.根据背景颜色手动选择一种颜色

当你的背景是图像或图案时,这比降低不透明度更有效;或者当降低不透明度会使文本感觉过于沉闷或褪色时。

1_OMntEW2V5jPXrZR6CjrBFQ

选择与背景相同色调的颜色,通过调整饱和度和亮度使其看起来合适。

3.Offset阴影

1_LisFGBtYOvR-3cwFTzTDUw

不要使用大的模糊和扩散值来使盒子阴影更加明显,而是添加垂直偏移。

这样看起来更自然,因为它模拟了一个从上方照射下来的光源,就像我们在现实世界中习惯看到的那样。

这也适用于内部阴影,例如您可能会在井或表单输入上使用:

1_qWSsYovqBDKF87f1IVMHsQ

如果你对阴影设计感兴趣,Material Design指南是一个很好的入门材料。

4.使用更少的边框

1_fNm6hXxnBvIcHGp9DQRdRQ

当你需要在两个元素之间创建分隔时,尽量不要立即使用边框。

虽然边框是区分两个元素的好方法,但它们并不是唯一的方法。过多地使用边框会让你的设计感觉繁忙和杂乱。

下次当你想要使用边框时,可以尝试以下这些替代方案:

1.使用盒子阴影

盒子阴影可以很好地勾勒出像边框一样的元素,但更加微妙,并且能够达到相同的目标而不会分散注意力。

1_Pm5PyS0vZ65GuGu8erPRfA

2.使用两种不同的背景

给相邻的元素略微不同的背景颜色通常就足以区分它们。如果您已经使用了不同的背景颜色,另外还加上了边框,请尝试去掉边框;也许您并不需要它。

1_9j89WYXMqsnb_A1v8heXaw

3.添加额外的间距

有什么比增加元素之间的距离更好的方法来创建分隔呢?将事物彼此拉得更远是一种非常好的方式,可以在不引入任何新UI的情况下创建元素组之间的区别。

1_7CEsoYdtFPjMBqpDB58HqQ

5.不要把本来应该很小的图标放大

1_57g05Gl-FjDtcCUtaPPOLw

如果你正在设计一些需要大图标的东西(比如一个落地页的“特性”部分),你可能会本能地选择免费的图标集,例如 Font Awesome 或 Zondicons,并将它们的大小增加到符合你的需求。

毕竟,它们是矢量图像,所以如果你增加了它们的尺寸,质量不会受到影响,对吧?

虽然矢量图像确实不会因为尺寸变大而降低质量,但是那些在 16-24 像素下绘制的图标,在放大到原来大小的 3 倍或 4 倍时永远都不会看起来很专业。它们缺乏细节,并且总是感觉过于“粗壮”。

3icons1

如果你只有小图标,可以尝试将它们放在另一个形状内,并给该形状添加背景色:

3icons2

这样可以让您保持实际图标更接近其预期大小,同时填充更大的空间。

如果您有预算,还可以使用专为较大尺寸设计的高级图标集,例如HeroiconsIconic

6.使用重音符号边框为平淡的设计增添色彩

6-1

如果你不是一名平面设计师,那么如何为你的用户界面增添视觉上的吸引力呢?

一个简单的技巧就是在原本感觉有些单调的界面部分添加彩色边框。

例如,在警告信息旁边加上一条彩色边框:

6-01

或者用于突出显示活动导航项:

6-02

甚至可以跨越整个布局的顶部:

6-03

在UI中添加一个彩色矩形并不需要任何平面设计技能,但它可以大大提升你的网站的“设计感”。

挑选颜色有困难吗?试试从像Dribbble这样受限调色板中选择颜色,以避免被传统调色板无尽可能性所压倒。

7.并非每个按钮都需要有背景颜色。

7-1

当页面上有多个用户可以执行的操作时,很容易陷入仅基于语义设计这些操作的陷阱。

像Bootstrap这样的框架在您添加新按钮时会提供一系列语义样式可供选择,从而鼓励了这种行为:

7-2

“这是一个积极的操作吗?把按钮变成绿色。”

“这会删除数据吗?把按钮变成红色。”

语义是按钮设计中重要的一部分,但有一个更重要的维度经常被忽视:层次结构。

页面上的每个操作都处于一个重要性金字塔中。大多数页面只有一个真正的主要操作,几个不太重要的次要操作和几个很少使用的第三级操作。

在设计这些动作时,重要的是传达它们在层次结构中所处的位置。

  • 主要操作应该显而易见。在这里,实心、高对比度的背景颜色效果非常好。
  • 次要操作应该清晰但不突出。轮廓样式或低对比度的背景颜色是很好的选择。
  • 第三级操作应该可以被发现但不会打扰用户。将这些操作设计成链接通常是最好的方法。

7-3

破坏性的操作呢,它们不应该总是红色吗?

并非如此!如果破坏性操作不是页面上的主要操作,则最好将其作为次要或第三按钮处理。

7-4

将大号、红色和加粗的样式保留给那些在界面中作为主要操作的负面行为,例如确认对话框:

7-5






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

公众号:土猛的员外


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



HuggingFace Transformers Agent介绍

本文比较了HuggingFace出品的Transformers Agent与LangChain Agent。

原文链接:Hugging Face Transformers Agent 作者:Sophia Yang。

还有一个Youtube视频,如果可以看且英文尚可的朋友可以直接看,视频更清楚。

以下为翻译正文:


就在两天前,Hugging Face发布了Transformers Agent——一款利用自然语言从精选工具集中选择工具并完成各种任务的代理。这听起来很熟悉吧?没错,它与LangChain Tools和Agents非常相似。在本篇博客文章中,我将介绍Transformers Agent以及与LangChain Agent的比较。

Transformers Agents是什么?

简而言之,它在transformers的基础上提供了一个自然语言API:我们定义了一组策划工具并设计了一个代理来解释自然语言并使用这些工具。

我可以想象HuggingFace的工程师们会这样说:我们在HuggingFace上托管了许多令人惊叹的模型。我们能否将它们与LLMs集成?我们能否使用LLMs来决定使用哪个模型、编写代码、运行代码并生成结果?基本上,没有人需要再学习所有复杂的任务特定模型了。只需给出一个任务,LLMs(代理)就会为我们完成一切。

下面是所有步骤:

img

来源: https://huggingface.co/docs/transformers/transformers_agents

  • Instruction:用户提供的prompt
  • Prompt:一个带有具体指令的提示模板,其中列出了多个可用工具
  • Tools:经过筛选的转换器模型,例如Flan-T5用于问答
  • Agent:一个LLM解释问题、决定使用哪些工具,并生成代码以使用这些工具执行任务
  • 受限制的Python解释器:执行Python代码

它是如何工作的?

Step 1: 实例一个agent.

第一步是实例化一个agent。agent只是一个LLM,可以是OpenAI模型、StarCoder模型或OpenAssistant模型。

使用OpenAI模型需要OpenAI API密钥,并且使用不免费。我们从HuggingFace Hub加载StarCoder模型和OpenAssistant模型,这需要HuggingFace Hub API密钥并且免费使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from transformers import HfAgent

# OpenAI
agent = OpenAiAgent(model="text-davinci-003", api_key="<your_api_key>")

from transformers import OpenAiAgent
from huggingface_hub import login
login("<YOUR_TOKEN>")

# Starcoder
agent = HfAgent("https://api-inference.huggingface.co/models/bigcode/starcoder")

# OpenAssistant
agent = HfAgent(url_endpoint="https://api-inference.huggingface.co/models/OpenAssistant/oasst-sft-4-pythia-12b-epoch-3.5")

Step 2: 运行agent.

agent.run是一种单次执行方法,可以自动选择任务所需的工具,例如,选择图像生成器工具来创建图像。

img

agent.chat 保留聊天记录。例如,在这里,它知道我们之前生成了一张图片,并且可以转换图像。

img

和LangChain Agent有什么不同?

Transformers Agent仍处于实验阶段。它的范围更小,灵活性较低。目前Transformers Agent的主要重点是使用Transformer模型和执行Python代码,而LangChain Agent则“几乎”可以完成所有任务。让我们分解一下比较Transformers和LangChain Agents之间不同组件:

工具集(Tools)

  • HuggingFace Transfomers Agent 拥有一个惊人的工具列表,每个工具都由Transformer模型驱动。这些工具提供了三个重要优势:
    • 1)尽管Transformers Agent目前只能与少数几个工具交互,但它有潜力与超过100,000个HuggingFace模型进行通信。它拥有完整的多模态功能,包括文本、图像、视频、音频和文档;
    • 2)由于这些模型是专门为特定任务而构建的,因此利用它们可能比仅依赖LLMs更直接,并且可以产生更准确的结果。例如,我们可以简单地部署BART来执行文本分类而不是设计LLM的提示来执行该任务;
    • 3)这些工具解锁了LLMs无法完成的功能。以BLIP为例,它使我们能够生成引人入胜的图像标题——这是LLMs所不能做到的。.
  • LangChain工具都是外部API,例如Google搜索、Python REPL。实际上,LangChain通过load_huggingface_tool函数支持HuggingFace工具。LangChain已经可以完成Transformers Agent所能做的一切。另一方面,Transformers Agents也有可能整合所有的LangChain工具。.
  • 在这两种情况下,每个工具都只是一个Python文件。您可以在此处找到Hugging Face Transformers Agent工具的文件,以及LangChain工具的文件。正如您所看到的,每个Python文件包含一个表示一个工具的类。

Agent

  • HuggingFace Transformers Agent使用此提示模板根据工具的描述确定要使用哪个工具。它会要求LLM提供解释,并在提示中提供一些few-shot学习示例。
  • LangChain默认使用ReAct框架来确定基于工具描述要使用哪个工具。该框架在这篇论文中有所描述。它不仅可以做出决策,还提供思考和推理,类似于Transformers Agent使用的解释方式。此外,LangChain有四种代理类型

自定义Agent

在这两种情况下,创建自定义代理并不太困难::

  • 请查看此 Colab 结尾处的 HuggingFace Transformer Agent 示例。
  • 在这里查看LangChain的示例

“Code-execution”

  • Hugging Face Transformers Agent在LLM选择工具并生成代码后,将“代码执行”作为其中一步。这限制了Transformers Agent的目标仅限于执行Python代码。
  • LangChain包括“代码执行”作为其工具之一,这意味着执行代码不是整个过程的最后一步。这提供了更多灵活性,可以根据任务目标来决定如何使用它:可以执行Python代码,也可以像进行Google搜索并返回搜索结果等其他操作。.

结论

在这篇博客文章中,我们探讨了Hugging Face Transformers代理的功能,并将其与LangChain代理进行了比较。我期待着在Transformers代理方面看到更多的发展和进步。

. . .

By Sophia Yang on May 12, 2023

Sophia Yang 是一位高级数据科学家。欢迎在领英推特YouTube上关注她,并加入DS/ML读书俱乐部❤️。






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

公众号:土猛的员外


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



Enabling conversational interaction on mobile with LLMs 中文版

译者注:

本文为《Enabling conversational interaction on mobile with LLMs》的中文翻译稿,Google发布的这篇文章对大语言模型在应用侧的发展有很积极的意义。LLM目前还是聚焦在文本上,因为是语言模型啊。但是这篇文章在研究的是如何让LLM看懂UI,并可以和文本内容进行转化相互转换:比如,看到UI内容进行提问、理解屏幕展现的主要内容是什么、让大模型看图阅读理解,还有一个就是像人一样去操作(这个我觉得是RPA的增强版)。

英文原文:Enabling conversational interaction on mobile with LLMs

-------------------------- 以下为正文 ----------------------------

2023年5月12日星期五
由谷歌Research的学生研究员Bryan Wang和研究科学家Yang Li发布

移动设备上的智能助手已经显著提升了基于语言的交互,例如设置定时器、打开手电筒等简单日常任务。尽管取得了进展,但这些助手在支持移动用户界面(UI)中的对话式交互方面仍存在很大局限。例如,无法回答关于屏幕上显示的特定信息的用户问题。助手需要具有图形用户界面(GUI)的计算理解能力,才能实现这种功能。

先前研究调查了几个重要技术构建块,以启用与移动端 UI 的对话式交互,包括为用户总结移动屏幕以快速理解其目的、将语言指令映射到 UI 操作和建模 GUI 使其更适合基于语言进行交互。然而,每个构建块只涉及对话式交互中有限方面,并需要大量精心策划大规模数据集和训练专用模型。此外,在移动 UI 上可能发生各种各样的对话式交互。因此,必须开发一种轻量级、通用化方法来实现对话式交互

在 CHI 2023 上发布《利用大型语言模型实现与移动 UI 的对话式交互》,我们调查了利用大型语言模型(LLMs)实现与移动 UI 的多样化对话式交互的可行性。最近的预训练LLM(如PaLM)已经展示了自适应各种下游语言任务的能力,只需提供少量目标任务的示例即可。我们提出了一套Prompt技术,使交互设计师和开发人员能够快速原型化和测试与用户的新语言交互,从而节省了在投资专用数据集和模型之前的时间和资源。由于 LLMs 只接受文本token作为输入,因此我们贡献了一种生成移动 UI 文本表示形式的新算法。我们的结果表明,该方法使用每个任务仅两个数据示例即可实现竞争性表现。更广泛地说,我们展示了LLMs从根本上改变未来的“会话交互设计”流程的潜力。

LLM4Mobile 1

展示我们在使用LLMs实现移动UI各种对话交互的工作的动画。

通过UI对LLMs提示

通过prompt,LLMs支持上下文少样本学习——而不是为每个新任务微调或重新训练模型,只需使用目标任务的几个输入和输出数据示例即可提示LLM。对于许多自然语言处理任务(例如问答或翻译),少样本prompt的表现与针对每个任务训练模型(穷举)的基准方法效果是差不多的。但是,语言模型只能接受文本输入,而移动端UI却是多模式的,在其视图层次结构数据中包含文本、图像和结构信息(即包含UI元素详细属性的结构化数据)和截图。此外,将移动端屏幕的视图层次结构数据直接输入到LLMs中并不可行,因为它包含过多信息,如把每个UI元素的详细属性等内容都写出来,那可能超过LLMs输入的token长度限制。

为了解决这些挑战,我们开发了一系列让大语言模型(LLMs)可以“看懂”移动端UI的技术。我们提供了一种算法,使用深度优先搜索遍历将Android UI 的视图层次转换成HTML语法生成移动UI 的文本表示形式。我们还利用思维链提示技术,将一些生成的中间结果合理拼接起来,一并推给LLM以得出最终的推理能力。

LLM4Mobile 2

展示使用移动UI进行少样本提示LLMs过程的动画。

我们的快速设计始于一个前言,它说明了prompt的目的。前言后面是多个实例,包括输入、思维链(如果适用)和每个任务的输出。每个实例其实就是一屏(移动端屏幕)的元素转化为HTML的各种语法输入。根据输入,可以提供思维链以引导LLMs进行逻辑推理。由于这一步骤是可选项,所以在上面的动画中没有显示。任务输出是目标任务所需的结果,例如屏幕摘要或用户问题答案等。通过在prompt中包含多个示例,可以实现少量提示功能。在预测过程中,我们将新输入屏幕附加到prompt末尾并将其馈送给模型。

实验

我们进行了四项关键建模任务的全面实验:

  • (1)屏幕提问生成
  • (2)屏幕摘要
  • (3)屏幕问答
  • (4)将指令映射到UI操作。

实验结果表明,我们的方法在每个任务只使用两个数据示例的情况下就达到了比较好的性能。

image7

任务一:屏幕提问生成

给定一个移动UI屏幕,屏幕提问生成的目标是综合相关用户输入所需的UI元素,产生连贯、语法正确的自然语言问题。

我们发现LLMs可以利用UI上下文来为相关信息生成问题。相比于启发式方法(基于模板的生成),LLMs在提问的质量方面表现显著优异。

image8

LLM生成的示例屏幕提问。 LLM可以利用屏幕上下文来生成与移动UI上每个输入字段相关的语法正确的问题,而模板方法在理解方面则不足。

我们还展示了LLMs将相关的输入字段组合成一个问题以实现高效沟通的能力。例如,要求最低和最高价格的筛选器被合并为一个问题:“价格范围是多少?

image4

我们观察到LLM可以利用其先前的知识,将多个相关的输入字段组合起来询问一个问题。

在一次评估中,我们征求了人类对提问的语法正确性(Grammar)和与其生成所需输入字段相关性(Relevance)的评分。除了人工标注的语言质量外,我们还自动检查了LLMs覆盖生成问题所需所有元素的能力(Coverage F1)。我们发现,由LLM生成的问题几乎完美无缺地符合语法规范(4.98/5),并且高度相关于屏幕上显示的输入字段(92.8%)。此外,LLM在全面涵盖输入字段方面表现出色(95.8%)。

Template 2-shot LLM
Grammar 3.6 (out of 5) 4.98 (out of 5)
Relevance 84.1% 92.8%
Coverage F1 100% 95.8%

任务2:屏幕摘要

屏幕摘要是自动生成描述性语言概述,涵盖移动屏幕的基本功能。该任务有助于用户快速了解移动端UI的目的,特别是当UI在视觉上不可访问时。

我们的研究结果表明,LLMs可以有效地总结移动端UI的基本功能,它们可以生成比我们之前使用UI特定文本介绍的Screen2Words基准模型更准确的摘要,如下面着色文本和框所示。

image10

2-shot LLM生成的示例摘要。我们发现LLM能够利用屏幕上的特定文本来组成更准确的摘要。

有趣的是,我们观察到LLMs在创建摘要时,利用他们的先前知识来推断未在UI中呈现的信息

在下面的例子中,LLM推断地铁站属于伦敦地铁系统,而输入UI并不包含此信息。

image3

LLM利用其先前的知识来帮助总结屏幕内容。

人类评估认为LLM摘要比基准更准确,但它们在BLEU(是一种用于评估机器翻译从一种自然语言到另一种自然语言的文本质量的算法)等指标上得分较低。感知质量和度量分数之间的不匹配反映了最近的研究结果,即尽管自动度量没有反映出来,LLM撰写的摘要更好。

image2
image2-1

上图:自动度量标准下的屏幕摘要表现。下图:由人类评估者投票确定的屏幕摘要准确性。

任务三:屏幕问答

给定一个移动端UI和一个开放式问题,要求提供有关UI的信息,模型应该提供正确的答案。我们专注于事实性问题,这些问题需要基于屏幕上呈现的信息来回答。

image1

屏幕QA实验的示例结果。LLM明显优于现成的QA基线模型。

我们使用四个指标来报告性能:完全匹配(预测答案与真实答案完全相同)、包含真实答案(回答完整地包含了真实答案)、子字符串匹配(回答是真实答案的子字符串)以及基于整个数据集中预测和真实答案共享单词的微型F1分数。

我们的结果表明,LLM可以正确回答与用户界面相关的问题,例如“标题是什么?”。 LLM比基线QA模型DistillBERT表现显著更好,达到了66.7%的完全正确率。值得注意的是,0-shot LLM取得了30.7% 的精确匹配分数,这表明该模型具有固有的问答回复能力。

Models Exact Matches Contains GT Sub-String of GT Micro-F1
0-shot LLM 30.7% 6.5% 5.6% 31.2%
1-shot LLM 65.8% 10.0% 7.8% 62.9%
2-shot LLM 66.7% 12.6% 5.2% 64.8%
DistillBERT 36.0% 8.5% 9.9% 37.2%

任务4:将指令映射到UI操作

给定一个移动端UI屏幕和自然语言控制UI的指令,模型需要预测执行指示动作的对象ID。例如,当使用“打开Gmail”进行指导时,模型应正确识别主屏幕上的Gmail图标。这个任务对于使用语音输入等语言输入来控制移动应用程序非常有用。我们之前介绍过这个基准任务。

image1-2

使用PixelHelp数据集中的数据作为示例。该数据集包含常见UI任务(如打开wifi)的交互跟踪。每个跟踪都包含多个步骤和相应的说明。

我们使用Seq2Act论文中的Partial和Complete指标评估了我们方法的性能。Partial指正确预测单个步骤的百分比,而Complete则衡量准确预测整个交互跟踪的部分。虽然我们基于LLM的方法没有超过在大规模数据集上训练得到的基准结果,但仅仅使用两个提示数据示例就取得了显着表现。

Models Partial Complete
0-shot LLM 1.29 0.00
1-shot LLM (cross-app) 74.69 31.67
2-shot LLM (cross-app) 75.28 34.44
1-shot LLM (in-app) 78.35 40.00
2-shot LLM (in-app) 80.36 45.00
Seq2Act 89.21 70.59

结论与总结

我们的研究表明,在移动端UI上原型设计新语言交互可以像设计数据示例一样容易。因此,交互设计师可以快速创建功能模拟以测试与最终用户的新想法。此外,开发人员和研究人员可以在投入大量精力开发新数据集和模型之前探索目标任务的不同可能性。

我们调查了促使LLMs能够在移动端UI上实现各种对话交互的可行性。我们提出了一套提示技术,用于适应LLMs到移动UIs中。我们进行了广泛的实验来评估我们方法的有效性,并针对四个重要建模任务进行了比较。结果显示,与传统机器学习流水线(包括昂贵的数据收集和模型训练)相比,使用LLMs可以快速实现新颖基于语言的交互,并取得竞争性能。

致谢

感谢本文合著者Gang Li, 以及同事Chin-Yi Cheng、Tao Li、Yu Hsiao、Michael Terry 和 Minsuk Chang 的讨论和反馈意见。特别感谢Muqthar Mohammad 和Ashwin Kakarla 在协调数据收集方面提供宝贵帮助。感谢John Guilyard为博客创建动画和图形。






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

公众号:土猛的员外


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



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编程语言--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支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



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支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



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支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



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支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!