Svelte-入门和启动指南-全-

Svelte 入门和启动指南(全)

原文:zh.annas-archive.org/md5/e96f946280935a201b4993c4d37278eb

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

你好,欢迎来到 SvelteKit 入门!这本书旨在成为一本易于阅读的指南,介绍你了解名为 SvelteKit 的清新简单 JavaScriptJS)框架的核心概念。它被写成了一本旨在突出我认为框架的核心概念的书。由于 JS 生态系统变化非常频繁,因此不应将本文视为 SvelteKit 的权威指南。相反,这本书背后的意图是提供一个资源,我希望在我学习这个有希望的新框架时就能得到。因为我经常发现自己先想学习高级概念,然后再转向更细致的细节,所以这本书也是如此。然而,在我们开始之前,我们必须先覆盖一些高级概念。

什么是 SvelteKit?

SvelteKit 是一个使用 Svelte 组件开发 Web 应用程序的一站式解决方案。如果你熟悉 React,那么你很可能听说过 Create React App。虽然 Svelte 和 React 都可以用来创建用 JS 构建的可重用组件,但每种方法在提供了利用这些组件的手段时更有用。

默认情况下,SvelteKit 提供了处理路由的机制;服务器端渲染SSR)支持 TypeScript,并通过诸如 热模块替换HMR)等特性丰富了开发体验,这些特性会在检测到应用程序中的更改时自动刷新浏览器。这个特性以及许多其他特性都是通过 SvelteKit 与 Vite 的紧密耦合实现的。

Vite 如何简化开发?

没有 Vite,SvelteKit 以及像 HMR 这样的功能将无法实现,而 Vite 本身也离不开 Rollupesbuild。Vite 通过提供一个利用现代网络浏览器中本地的 ECMAScript 模块ESM)可用性的闪电般的开发服务器来将自己与其他打包工具区分开来。Vite 通过将应用程序代码分为两部分——依赖项和源代码来实现这一点。

依赖项

Vite 可以通过预先使用 esbuild 将每个项目依赖项打包成其自己的 ES 模块来提供一个快速的开发服务器。这些依赖项是 CommonJS通用模块定义UMD)还是 ESM 格式,对 Vite 来说无关紧要,因为它会在应用程序的初始构建中将它们全部转换为 ESM。这样做意味着浏览器只需为每个依赖项发出一个 HTTP 请求,而不是为每个导入语句发出请求。这可以大大提高开发服务器的性能,尤其是在考虑依赖项密集型应用程序中请求如何快速累积的情况下。

源代码

与依赖项不同,源代码在开发过程中经常发生变化。为了使源代码的开发者(你)保持满意,Vite 采用了一些巧妙的方法。由于现代浏览器支持原生的 ES 模块,Vite 可以将打包工作卸载到浏览器上,并且只需要在将其提供给浏览器之前将源代码转换成原生 ES 模块。适当的源代码只有在必要时才会被浏览器加载 – 也就是说,如果它在当前显示的屏幕上被使用。

为了避免每次源代码更改时重新打包整个应用程序的低效,Vite 支持热模块替换(HMR)。本质上,HMR 是只替换更改过的 ES 模块的做法。这保持了开发服务器的快速响应,无论应用程序是单页还是数千页。

到目前为止,我们已经讨论了 Vite 及其使用 esbuild 的方式,但 Rollup 是如何融入其中的呢?Vite 在构建过程中使用 Rollup – 也就是说,我们不是直接发送源代码,而是将我们的 Svelte 组件转换成纯 JavaScript,这使得浏览器可以轻松读取。Rollup 通过树摇(只包含模块中使用的部分)、代码拆分(将代码拆分成块)和懒加载(仅在需要时加载资源)等特性来管理项目中可能包含的数千个模块。这些特性的使用导致了更小、性能更好的网络应用程序。

这本书是为谁准备的?

SvelteKit Up and Running 旨在帮助那些希望通过学习下一个流行的基于 JS 的框架来提升技能的网页开发者。无论你是决定学习哪个 JS 框架,还是想通过在简历中添加另一种流行的技术来在求职者中脱颖而出,这本书都将帮助你获得开始成为 SvelteKit 大师旅程所需的所有基本知识。要最大限度地发挥这些资料的价值,需要具备 HTML、CSS、JS 和 Svelte 的工作知识。

这本书涵盖了哪些内容?

第一章初始设置和项目结构,解释了如何安装新的 SvelteKit 项目并设置开发环境。它还涵盖了 SvelteKit 项目的逻辑结构以及组织源代码的最佳实践。

第二章配置和选项,介绍了如何通过 SvelteKit 和 Vite 的配置选项来自定义应用程序。

第三章与现有标准的兼容性,详细介绍了 SvelteKit 如何与现代网络标准(如 Fetch、FormData 和 URL 应用程序编程接口(APIs))协同工作。

第四章, 有效路由技术,讨论了基本路由技术。这些技术对于创建从最简单的页面和 API 端点到动态 URL(随内容变化)的一切都是必不可少的。它还涵盖了如何使用布局为应用程序创建一个统一的用户界面。

第五章, 深入数据加载,详细解释了如何通过使用 SvelteKit 的load()函数将数据加载到我们的页面和组件中。它还分解了在该函数中开发者可用的数据。

第六章, 表单和数据提交,涵盖了开发者如何通过使用 HTML 表单元素从用户那里接收数据。从那里,它解释了如何使用操作来分割与表单相关的逻辑,以及如何增强这些表单以减少用户经常遇到的反感。

第七章, 高级路由技术,讨论了 SvelteKit 路由机制中一些较少使用但更强大的功能的细节。它涵盖了开发者在路由中处理可选参数、处理具有未知数量参数的路由以及遇到冲突时路由的优先级。

第八章, 构建和适配器,解释了开发者如何为不同的环境准备应用程序。它提供了示例,说明如何为部署到 Cloudflare Pages、Node.js 环境以及静态托管解决方案准备 SvelteKit 应用程序。

第九章, 钩子和错误处理,涵盖了服务器钩子和共享钩子之间的区别,以及它们如何被用来操纵流入和流出应用程序的数据。此外,它还解释了开发者如何管理预期的错误以及如何处理那些从未预料到的错误。

第十章, 管理静态资源,详细说明了如何最好地管理图像或全局 CSS 文件等资源。它解释了 Vite 在这一过程中的核心作用以及最佳实践。

第十一章, 模块与秘密,讨论了本书其他部分未涉及的一些模块。它为您提供了 SvelteKit 附带的一些其他工具和功能的一般概述。它还涵盖了负责管理 API 密钥或数据库密码等秘密的模块。

第十二章, 增强可访问性和优化 SEO,将展示如何使应用程序更容易对更广泛的受众开放。不仅将这些实践纳入应用程序可以使它们与屏幕阅读器等技术更兼容,而且还能提供增强搜索引擎提供商排名的额外好处。

附录: 示例和支持,为您提供访问官方和非官方社区维护资源的途径。所提供的如何将其他前端技术集成到 SvelteKit 中的示例展示了这有多么简单,尤其是在利用社区项目来加快开发过程时。本节提供的资源对于解决问题和在 SvelteKit 社区中交朋友都非常有价值。

为了充分利用这本书

为了充分利用这本书并确保提供的信息被保留,建议您在阅读时与材料一起工作。在您的设备上键入命令和代码,但也请随意在过程中实验和玩弄代码。为了充分利用这本书,您需要具备 Svelte、JS、HTML 和 CSS 的工作知识,因为它专注于 SvelteKit。

本书涵盖的软件/硬件 操作系统要求
SvelteKit 1.16.3 或更高版本 Windows、macOS 或 Linux
JS
HTML 和 CSS

如果您使用的是本书的数字版,我们建议您自己键入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将有助于您避免与代码的复制和粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/SvelteKit-Up-and-Running。如果代码有更新,它将在 GitHub 仓库中更新。

我们还提供我们丰富的图书和视频目录中的其他代码包,可在 github.com/PacktPublishing/ 获取。查看它们!

下载彩色图像

我们还提供了一份包含本书中使用的截图和图表的彩色 PDF 文件。您可以从这里下载:packt.link/1zRGE

使用约定

本书使用了多种文本约定。

文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“在这个新版本中,我们仍然导入了 bcrypt 模块,但我们还添加了 user.json 的导入。”

代码块设置如下:

import bcrypt from 'bcrypt';
export const actions = {
  login: async ({request}) => {
    const form = await request.formData();
    const hash = bcrypt.hashSync(form.get('password'), 10);
    console.log(hash);
    console.log(crypto.randomUUID());
  }
}

任何命令行输入或输出都按以下方式编写:

npm install bcrypt

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“在 Firefox 中,您可以在存储 | 会话存储下找到它。”

小贴士或重要注意事项

它看起来像这样。

联系我们

我们读者的反馈始终受到欢迎。

一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。

勘误表:尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告此错误。请访问 www.packtpub.com/support/errata 并填写表格。

copyright@packt.com 并附有材料链接。

如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问 authors.packtpub.com

分享您的想法

读完 SvelteKit Up and Running 后,我们非常希望听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本

感谢您购买此书!

您喜欢在路上阅读,但无法携带您的印刷书籍到处走?

您的电子书购买是否与您选择的设备不兼容?

别担心,现在,您每购买一本 Packt 书籍,都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。

按照以下简单步骤获取优惠:

  1. 扫描二维码或访问以下链接

packt.link/free-ebook/9781804615485

  1. 提交您的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。

第一部分 – SvelteKit 入门

在这部分,我们将通过快速安装向您介绍 SvelteKit,然后是我们都喜欢和欣赏的标准“Hello, World!”示例,这将帮助您熟悉项目结构。然后,我们将探讨可用的各种配置选项,以进一步自定义 SvelteKit 应用程序。从那里,我们将探讨 SvelteKit 如何利用现有标准来提供一个小巧而强大的框架。

本部分包含以下章节:

  • 第一章初始设置和项目结构

  • 第二章, 配置和选项

  • 第三章, 与现有标准的兼容性

第一章:初始设置和项目结构

我现在已经开发了近十年的网络应用程序,自从我开始以来,这个领域发生了巨大的变化。为了参考这一点,我自从 JavaScript 在主流浏览器中支持得如此糟糕,以至于 jQuery 成为了构建交互式前端体验的既定标准以来,就开始构建网站了。但随着时间的推移,我们看到越来越多的浏览器愿意支持 ECMAScript 标准,那些不支持的标准已经消失(再见了,Internet Explorer)。JavaScript 随后重新成为了一种可行的语言。随着 Node.js 的兴起,开发者终于可以在单一编程语言中构建整个应用程序,包括前端和后端。JavaScript 已经接管了网络开发世界,并牢固地确立了其地位。

随着技术的成熟,开发体验也日益完善。随着 SvelteKit 1.0 的到来,我们开发者得到了一个直观的体验,允许我们将前端和后端逻辑结合起来,让我们不禁想问:“我们以前是如何做到这一点的?”在我们深入那个体验之前,我们需要先了解一些事情。

首先,我们将介绍使用 SvelteKit 开发应用程序的先决条件。然后,我们将继续介绍如何安装 SvelteKit,并讨论项目通常的结构。从那里,我们将构建一个“Hello, World!”应用程序,以便我们可以看到一切的实际操作。

总结来说,我们将在本章讨论以下主题:

  • 先决条件

  • 安装 SvelteKit

  • SvelteKit 的项目结构

  • “Hello, World!”

在覆盖所有这些材料之后,你应该能够合理地设置一个新的 SvelteKit 应用程序用于你的下一个项目。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter01

先决条件

为了充分利用这本书并确保你记住提供的信息,建议你在阅读时与材料一起工作。将显示的命令和代码输入到你的项目中,但也请随意进行实验。为了有效地进行这项工作,你需要一台运行 Windows、macOS 或基于 Linux 的操作系统以及访问终端或命令行界面的计算机。大多数能够运行上述操作系统的最新版本的现代计算机硬件应该能满足你的需求。具体来说,你需要一个至少有 1 GB RAM 和至少 1.6 GHz 处理器的系统。这应该足以用于使用 SvelteKit 进行开发,尽管性能可能会因操作系统而异。

就像许多其他 Web 开发项目一样,你还需要一个网络浏览器。建议使用最新版本的 Firefox、Chrome 或 Safari。你还需要安装 Node.js。建议使用与标准 Node.js 安装一起提供的最新 npm。你可以用 yarn 代替 npm,但可能更简单直接使用 npm。如果你担心性能,pnpm 也可以使用。

最后需要的工具是一个文本编辑器或通过 ext install svelte.svelte-vscode 安装扩展,然后按 Enter

总结来说,你需要以下内容:

  • 基于 macOS、Windows 或 Linux 的计算机

  • 现代网络浏览器(Firefox、Chrome 或 Safari)

  • 终端访问权限

  • Node.js 18+ LTS

  • 包管理器(npm 随 Node.js 一起安装)

  • 文本编辑器/集成开发环境(推荐使用 Svelte 扩展)

安装 SvelteKit

首先,打开你的终端或命令行界面,导航到一个你愿意开始新项目的目录。从那里,我们将运行三个简单的命令。第一个将创建一个新的 SvelteKit 项目,并会有各种提示来初始化应用程序,第二个将安装依赖项,第三个将启动我们的开发服务器:

npm create svelte@latest
npm install
npm run dev

当运行第一个命令时,你会看到几个提示。其中第一个会询问你是否要安装 create@svelte,你应该回答 y 表示是。当被提示选择一个目录来安装项目时,请保持选项为空以使用当前目录(或者如果你愿意,也可以指定目录)。然后你会被询问要使用哪个模板。我们将主要使用 骨架项目 选项,但请随时在另一个目录中尝试 SvelteKit 示例应用

下一个提示与 TypeScriptTS)的使用有关,SvelteKit 对其有很好的支持。然而,为了使本书的焦点保持在 SvelteKit 本身,并吸引那些可能还不熟悉 TS 的开发者,我们将使用纯 JavaScript。因此,为了正确地跟随本文,你应该选择 。如果你对 TS 感到足够自信,那么当然可以选择 。务必选择 以启用 ESLint 和 Prettier 支持,因为它们很可能为你节省麻烦并进一步改善你的开发体验。还建议包括支持的测试包,但本书不会涵盖测试策略。

在使用 npm install 安装项目依赖后,我们运行 npm run dev 来启动我们的开发服务器。命令的输出应该类似于 图 1**.1 中所示。

图 1.1 – 展示 Vite 开发服务器的输出

图 1.1 – 展示 Vite 开发服务器的输出

注意 Vite 启动我们的开发服务器有多快。尽管这是一个骨架应用,其他打包工具可能需要几秒钟的时间,而 Vite 在一秒内就准备好了。如终端中显示的输出所示,可以通过在浏览器中导航到 http://localhost:5173/ 来查看网站。如果您想从除开发机器以外的设备访问网站,例如在移动设备上,那么您可以在 package.json 项目文件中找到的相应 npm 脚本后附加 –-host。在 scripts 条目下,新命令将看起来像 "dev": "vite dev –-host"

我们刚刚介绍了 SvelteKit 的安装过程。到现在为止,你应该能够轻松地安装自己的基于 SvelteKit 的项目。我们已经介绍了 create@svelte 包中的各种提示如何让你根据喜好自定义项目。现在,让我们看看一个典型的 SvelteKit 项目是如何结构的。

SvelteKit 的项目结构

安装新的 SvelteKit 项目后,在您首选的编辑器中打开项目目录。在该文件夹中,您会注意到在 JavaScript 应用程序的根项目文件夹中常见的文件,如 package.json.gitignoreREADME.md,以及与 SvelteKit 相关的配置文件(svelte.config.js)和 Vite(vite.config.js)。您还会注意到三个子文件夹:static/tests/src/。以下几节将详细探讨它们。

static/

这个文件夹是放置静态资源的地方,例如 robots.txt(为搜索引擎网站爬虫提供的指南)、静态图像,如 favicon,甚至全局 CSS 样式表。这些文件应该能够“原样”提供服务。位于此文件夹中的文件将作为如果它们存在于项目根目录中一样,对应用程序逻辑可用,即 /robots.txt。您也可以通过在文件路径前加上 %sveltekit.assets% 来访问它们。请注意,如果这里的文件被更改,您可能需要手动刷新页面以查看更改。在某些情况下,您甚至可能需要重新启动开发服务器,因为 Vite 对缓存有强烈的看法。您不应尝试以编程方式访问此目录中包含的文件。相反,路径应该在包含这些资产的地方“硬编码”。

tests/

从逻辑上讲,Playwright 包(在我们说“是”的各种提示中包含)的测试位于此处。要运行 Playwright 浏览器测试,请使用 npm 脚本 npm run test。Vitest 的单元测试将与您的源代码一起包含。例如,如果您包含了一个名为 utilities.js 的文件,那么它的单元测试将作为 utilities.test.js 与它一起存在。Vitest 是 Vite 开发者提供的包,它使基于 Vite 的应用程序的简单测试成为可能。测试驱动开发TDD)是一种确保代码按预期执行的优秀实践。然而,这超出了本书的范围。

src/

您将在大多数时间里在这个文件夹中工作,因为这是 SvelteKit 应用程序核心逻辑所在的地方。现在应该注意一些文件和目录:

  • routes/

  • lib/

  • app.html

路由/

需要注意的第一个子文件夹是 src/routes/。这个目录将包含管理 SvelteKit 文件路由机制所需的大部分文件。它的同级文件夹 src/params/ 将稍后介绍,但到目前为止,假设与您的应用程序路由管理相关的逻辑主要位于此处。作为一个简短的例子,如果您想添加一个静态的“关于”页面,那么您可以通过创建包含适当标记和文本的 src/routes/about/+page.svelte 来实现。

lib/

Svelte 组件和各种其他实用工具可以放置在 src/lib/ 子文件夹中。这个文件夹可能不在骨架项目模板中,所以您需要自己添加它。它将在 SvelteKit 示例应用中展示。通过将组件放在这里,您可以在稍后的 import 语句中轻松引用它们,因为 $lib/ 别名将在整个应用程序中可用。

app.html

还有更多文件将在稍后介绍,但到目前为止,最后的提及是 app.html。这个文件作为您应用程序构建的基础。当打开时,您会注意到它包含对 %sveltekit.head% 的引用,SvelteKit 使用它来注入各种脚本和链接标签,以及 %sveltekit.body%,它用于注入为应用程序生成的标记。

回顾一下,static/ 目录包含不经常更改的文件,tests/ 目录包含 Playwright 包中的测试,而 src/ 目录包含您项目的源代码。您创建的大多数 Svelte 组件和其他实用工具都可以放在 src/lib/ 中,以便通过 import 语句中的 $lib/ 别名轻松访问。如果您想向应用程序 URL 添加新路由,可以在 src/routes/ 中相应名称的目录内创建一个 +page.svelte 文件。最后,您的应用程序需要一个基础来构建。这就是 app.html 的作用。我相信您已经迫不及待地想要最终构建一些东西了,所以让我们开始吧。

Hello World 应用程序

现在我们已经对在全新初始化的 SvelteKit 项目中查看的内容有了一定的了解,似乎构建一个“Hello, World!”应用程序是合适的。我们将首先在编辑器中打开src/routes/+page.svelte文件。在这个阶段,它应该只包含基本的 HTML 代码:

<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

由于此文件直接位于src/routes/目录中,而不是子目录中,因此它作为 URL 的根路由(即/)在浏览器中可用。如果我们要在路由目录中创建一个新的文件夹(即src/routes/hello/),并在其中放置另一个+page.svelte文件(即src/routes/hello/+page.svelte),那么我们就会使/hello路由作为有效的 URL 对我们的应用程序可用。我们将在后面的章节中介绍更高级的路由技术,但到目前为止,只需知道要添加新路由,你需要在routes目录中创建一个使用所需路由名称的文件夹,以及一个+page.svelte文件。

Svelte 组件

熟悉 Svelte 的读者会注意到+page.svelte文件扩展名是 Svelte 组件的扩展名。这是因为它就是一个 Svelte 组件!因此,我们可以调整其中的 HTML,使用<style>标签自定义外观,在<script>标签内编写 JS,并导入其他 Svelte 组件。如果你对 Svelte 组件不熟悉,建议在继续之前至少学习基础知识。可以查看Allessandro Segala的《Svelte 3 Up and Running》或访问官方 Svelte 网站(svelte.dev)获取更多信息。

让我们对src/routes/+page.svelte文件进行一些修改,看看发生了什么。将<h1>标签的内部文本更改为Hello, World!,如下所示:

<h1>Hello, World!</h1>

多亏了 Vite,我们在浏览器中的页面在保存后立即更新。如果你的设置中有双显示器,一个显示代码,另一个显示浏览器,你将很快看到热模块替换HMR)是多么有价值。我们做出的更改都是好的,但如果用户不能更改文本,这并不是一个真正的“Hello, World!”应用程序。为了一个真正的“Hello, World!”示例,我们需要显示一些由用户提供的文本。以下代码展示了这一点:

<script>
  let name = 'World';
</script>
<form>
  <label for="name" >What is your name?</label>
  <input type="text" name="name" id="name" bind:value={name} />
</form>
<h1>Hello, {name}!</h1>

这个简单的 Svelte 组件创建了一个名为name的变量,默认值为“World”。从那里,HTML 给我们一个基本的表单,将文本输入值绑定到我们的变量,并在 HTML <h1>标签中输出文本。多亏了 Svelte 的响应性和文本输入值与name变量的绑定,提供的文本会立即显示,即使在输入时也是如此。

图 1.2 – “Hello, World!”组件的输出

图 1.2 – “Hello, World!”组件的输出

摘要

在本章中,我们讨论了一些开始使用 SvelteKit 所需的先验知识和工具。你可能已经拥有一台运行着强大操作系统的计算机,并且安装了浏览器。你可能甚至已经安装了带有 npm 软件包管理器的最新 LTS 版本的 Node.js。我们还简要介绍了通过安装 Svelte 特定扩展来准备你的编辑器。

我们继续介绍了 SvelteKit 的安装过程。安装过程中提供的提示使得设置新的 SvelteKit 项目变得简单,并且可以轻松地根据开发者的喜好进行定制。

在项目安装之后,我们从高层次上审视了 SvelteKit 的项目结构。虽然 tests/static/ 目录相当直接,但 src/ 文件夹中存在一些细微差别。例如,将各种 Svelte 组件和其他实用工具放在 src/lib/ 文件夹中可以帮助项目避免变得难以导航。位于那里的组件也可以通过 $lib/ 别名轻松地在应用程序代码中访问。

我们还创建了一个基本的“Hello, World!”应用程序。它展示了 SvelteKit 如何使从头开始构建应用程序变得简单。

在下一章中,我们将介绍一些你可能需要在 SvelteKit 和 Vite 中调整的各种配置选项,以便根据你的需求进行定制。

资源

第二章:配置和选项

在我看来,没有什么比在项目上辛勤工作几周,结果在生产环境中因为一个看似微不足道的配置选项而出现 bug 更令人沮丧的了。这就是为什么本章将努力在您深入研究之前向您介绍可用的各种选项。我知道您已经准备好学习 SvelteKit 了。我保证,我对“真正开始构建东西”的热情与您一样,但我向您保证,早期学习这些概念是有价值的。如果您希望快速开发高性能的 Web 应用程序,那么我相信在学习您特定的应用程序之前学习这些概念对您最有利。

为了开始本章,我们将首先查看如何使用svelte.config.js文件中的选项来管理您的项目配置。然后,我们将简要地查看一些使用基本适配器的配置,以便您开始。我们还将查看您在vite.config.js文件中可用的选项。在关于 SvelteKit 的书中如此多地讨论 Vite 可能看起来有些奇怪,但如果没有 Vite,SvelteKit 就不会是现在的工具。

在本章中,我们将涵盖以下主题:

  • 配置 SvelteKit

  • 配置 Vite

在我们讨论了配置及其如何影响您的工具之后,您应该会感到更自在地做出适当的更改,以满足您下一个 SvelteKit 项目的需求。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter02

配置 SvelteKit

SvelteKit 项目的核心配置位于svelte.config.js文件中。了解您可用的各种选项将使您能够充分利用 SvelteKit。虽然我们无法在如此简短的章节中涵盖所有可用的选项,但我们的目标是涵盖您可能认为有用的选项。有关更多配置选项,请参阅本章末尾的进一步阅读部分以获取更多资源。

要开始,请打开编辑器中骨架项目中的svelte.config.js文件。请注意,目前它相当简单。基本上,它从@sveltejs/adapter-auto包中导入adapter函数,在config常量的kit属性中指定该函数,并最终导出config对象。我们还通过config.kit属性提供了一个类型注解,我们将在这里添加各种其他属性来自定义配置。它应该看起来像这样:

import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
      kit: {
           adapter: adapter()
       }
};
export default config;

adapter-auto 包在通过 npm create svelte@latest 安装时默认包含。它将在构建时自动适应你的项目,以便可以轻松部署到 Vercel、Cloudflare Pages、Netlify 或 Azure Static Web Apps。因此,adapter-auto 被视为 devDependencies。这样做将允许你自定义特定于该环境的选项,因为 adapter-auto 不接受任何选项。

我们将在后面的章节中详细讨论适配器;我们将介绍如何使用 adapter-static 包生成静态网站,以及如何使用 adapter-node 在 Node.js 服务器上运行 SvelteKit 应用程序。现在,让我们看看你可能在以下章节中需要的一些其他潜在配置选项。

alias

虽然通过 $lib/ 别名访问 src/lib/ 路径非常有用,但有时你可能想访问其外部的内容。为此,你可以在 config.kit 中添加 alias 属性。例如,如果你的项目有一个用于连接和管理数据库的特定文件,你可以创建一个别名以简化导入。或者,你可能想轻松地从常用文件夹中访问图像。以下代码片段展示了这些概念:

src/db.js

const db = {
     connection: 'DB connection successful!',
     // database logic goes here…
};
export default db;

svelte.config.js

import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
      kit: {
           adapter: adapter(),
           alias: {
                 db: '/src/db.js',
                 img: '/src/images'
                 }
       },
};
export default config;

src/routes/+page.svelte

<script>
      import db from 'db';
      import url from 'img/demo.png';
      let status = db.connection;
</script>
<p>{status}</p>
<img src={url}>

在这个例子中,我创建了一个名为 connection 的模拟数据库对象。在 svelte.config.js 文件中,我添加了两个别名——一个用于模拟数据库对象,另一个指向一个图像文件夹。然后,在 +page.svelte 文件中,从这两个别名中进行了导入,以说明如何使用别名导入单个文件,以及如何从目录中导入特定文件。

此示例未能考虑通常用于数据库操作所需的任何异步代码。直接从路由调用数据库也不是最佳实践,但我们在后面的章节中会回到如何做到这一点。虽然这个例子相当基础,但它的目的是展示能够为特定文件和目录添加自己的别名是有用的。

appDir

此属性默认为 _app 值,并用于确定导入的资产(如 CSS 和 JS)将从构建的应用程序中提供。

csp

为了帮助保护你的应用程序及其用户免受恶意 config.kit.csp 的侵害,以下是一些措施:

  • mode: 'hash' | 'nonce' | 'auto':这指定了是否应使用哈希或非 ces 来强制执行 CSP。使用 auto 将为动态渲染的页面使用非 ces,为预渲染的页面使用哈希。

  • directives: 在此处指定的指令将被添加到 Content-Security-Policy 头部。有关可用选项的完整列表,请参阅官方 SvelteKit 文档。

  • reportOnly:这个选项用于报告错误,但仍然允许脚本加载。这里指定的指令将被添加到Content-Security-Policy-Report-Only头中。有关可用选项的完整列表,请参阅官方 SvelteKit 文档。

虽然这可以帮助防止从外部来源加载脚本,但您可能故意将来自%sveltekit.nonce%的脚本包含到包含脚本的nonce属性中,如下所示:

<script src='YOUR_EXT_URL/script.js' nonce='%sveltekit.nonce%'>

CSRF

为了帮助保护您的应用程序免受来自另一个源对您的应用程序的POST请求。如果您需要禁用它,可以通过将config.kit.csrf.checkOrigin设置为false来实现,如下所示:

const config = {
  kit: {
    adapter: adapter(),
    csrf: {
      checkOrigin: false
    }
  },
};

环境变量

如果您在项目中包含了.env文件,您可以使用以下选项进一步指定哪些可以公开以及它们存储在哪个目录中:

  • dir:一个默认为“.”(项目根目录)的字符串值。

  • publicPrefix:一个默认为PUBLIC_的字符串值。这个前缀表示.env文件中定位的环境变量是否安全地暴露给客户端代码。这些环境变量随后可以通过从整个应用程序中的$env/static/public模块导入来访问。例如,假设我们正在处理项目base目录中的一个.env文件。添加PUBLIC_EXTERNAL_API=https://api.nasa.gov/planetary/apod值将在客户端代码中可导入,而INTERNAL_API=AN_INTERNAL_IP_ADDRESS值则只能在服务器代码中导入。

预渲染

在您的应用程序中,很可能至少有一部分内容是可以预渲染的——也就是说,它将为每个访问它的用户显示完全相同的 HTML、CSS 和 JS。一个常见的例子是“关于”页面。是否希望特定页面进行预渲染将在后面的章节中讨论,但为了进一步自定义预渲染,请考虑以下可以添加到config.kit.prerender中的选项:

  • entries:一个字符串数组,用于指定每个应该进行预渲染的路由。默认值是*特殊字符,将包括所有非动态路由。

  • concurrency:可以同时预渲染的页面数量。由于 JavaScript 是单线程的,因此只有在您的项目需要在预渲染过程中进行网络请求时,此选项才会生效。

  • crawl:此选项默认为true。它告诉 SvelteKit 是否应该预渲染entries数组中找到的页面。

  • handleHttpError:此选项默认为fail。其他选项包括ignorewarn或一个特定的 SvelteKit details对象,该对象实现了PrerenderHttpErrorHandler。如果在预渲染过程的任何点上失败,此选项将确定如何处理它。

  • handleMissingId:这与 handleHttpError 相同,但它接受的 details 对象是从 PrerenderMissingIdHandler 实现的。链接到另一页上的特定点是通过 URL 中的哈希实现的——一个 # 字符后跟目标页面上元素的 ID。如果目标页面上找不到指定 ID 的元素,那么此选项将决定构建过程应该如何行为。

  • origin:默认为 kit.svelte.dev/docs/page-options。如果你想在渲染内容中使用应用程序的 URL,在这里指定它可能会有所帮助。

希望这个选项列表不会让你感到过于繁重,并为你提供了如何根据需要自定义 SvelteKit 的见解。SvelteKit 将继续发展,因此始终建议查看官方文档,因为新功能可能会在未来的版本中添加或删除。

现在我们已经查看了一些可用于自定义 SvelteKit 的选项,让我们来看看如何修改 Vite 的行为。与 SvelteKit 一样,Vite 对项目应该如何配置有自己的看法。这些看法对于广泛的用例非常有用,但总会有需要调整的情况。

配置 Vite

如前所述,Vite 是使 SvelteKit 成为可能的项目构建工具,因此了解如何配置 Vite 与了解 SvelteKit 一样重要。话虽如此,Vite 具有高度的可配置性,因此为了简洁(以及您的注意力集中),我们将仅限于提供一个对可用选项的高级概述。本节的目的不是提供一个详尽的列表,而是快速浏览一下您可用的选项。如需进一步阅读,请参阅本章末尾的资源。

在其核心,SvelteKit 只是一个 Vite 插件。显然,它不仅仅是一个插件,但当你在一个新创建的 SvelteKit 项目中打开 vite.config.js 文件时,你就会明白我的意思。类似于 svelte.config.js,这个配置导出了一个 config 常量,其中设置了一些属性——导入的 SvelteKit 插件和要包含的测试。如果你在 create@svelte 提示中回答了“否”关于测试的问题,你可能看不到 test 属性。以下代码片段展示了骨架应用程序的配置:

import { sveltekit } from '@sveltejs/kit/vite';
const config = {
      plugins: [sveltekit()],
      test: {
           include: ['src/**/*.{test,spec}.{js,ts}']
     }
};
export default config;

插件

从我们的 Vite 配置的plugins属性开始似乎很合理,因为这本书的整个前提都是基于一个特定的插件。随着 Vite 变得越来越受欢迎,其采用率也在上升,其生态系统正在扩展,随之而来的是更多的插件。许多这些插件都是由社区开发的,用于解决常见问题。由于 Vite 扩展了 Rollup,许多 Rollup 插件也可以与 Vite 无缝工作,尽管并非所有。Vite 开发团队维护了几个官方的 Vite 插件,包括一个支持旧版浏览器支持的插件。许多功能和功能可以通过这些插件添加到你的应用程序中。一旦你找到了满足你要求的插件并安装了它,你将需要将其导入并包含在config.plugins数组中,就像之前展示的 SvelteKit 一样。插件的配置各不相同,但通常,选项是通过导入函数调用的参数传递的。要查找更多可用的 Vite 插件,请查看 GitHub 上的 Awesome Vite 项目github.com/vitejs/awesome-vite

server

由于 Vite 正在运行你用 SvelteKit 工作的开发服务器,你可能需要更改该服务器的一些操作方式。例如,你可能需要更改默认端口,通过另一个服务器代理,添加对 HTTPS 的支持,或者甚至禁用 HMR 以使用旧浏览器进行测试。在这些情况下以及更多情况下,你将想要相应地调整config.server

build

当你在生产环境中构建应用程序时,你可能需要做一些调整。无论你需要确保应用程序满足特定的浏览器兼容性,还是你想自定义 Rollup 的选项,这些设置都可以在config.build下找到。

preview

一旦你为你的生产环境构建了应用程序,你应该使用 Vite 的预览功能对其进行测试。可以通过管理config.preview来更改预览服务器的选项。

optimizeDeps

有时,你可能想测试多个依赖项或依赖项的版本与你的代码一起工作,以查看它们对你的项目有多好。要包括或排除依赖项以进行预捆绑,或进一步修改esbuild使用的选项,请开始配置config.optimizeDeps

ssr

如果你需要防止依赖在你的服务器环境中运行,你可能需要管理在config.ssr下可用的选项。

摘要

在本章中,我们介绍了 SvelteKit 可用的许多配置选项。我们讨论了config.kit.alias可能如何用于导入频繁访问的文件的一个例子。虽然我们没有涵盖所有选项,但我们简要地看了appDircspcsrfenvprerender选项以及它们如何影响我们的应用程序。

我们也浏览了一些在 vite.config.js 中可用的配置选项。大部分情况下,SvelteKit 会根据安装提示中的选择设置一个适合我们的配置,但对于进一步的调整和插件,我们现在知道在哪里开始查找官方 Vite 文档。

在下一章中,我们将讨论一些现有的网络标准以及 SvelteKit 的设计如何考虑到这些标准。存在于网络浏览器中的现有标准正迅速在其他环境中得到支持,包括基于 Node 的环境,甚至是 Deno 和 Cloudflare Workers。通过利用这些标准而不是在它们之上构建新的标准,SvelteKit 对更多开发者变得可访问。

进一步阅读

  • SvelteKit 文档 – 在你的故障排除之旅中,官方 SvelteKit 文档应该是你的第一站:kit.svelte.dev/docs

  • 出色的 Vite – 一系列官方和社区维护的资源或插件,使使用 Vite 进行开发变得愉快:github.com/vitejs/awesome-vite

  • Vite 文档 – 需要修改你的开发服务器或构建过程?请在此查看官方推荐:vitejs.dev/config/

第三章:兼容现有标准

一些框架试图通过为你提供工具和功能来简化你的开发工作,例如处理网络请求或管理表单提交的数据。虽然这种策略的意图是高尚的,但它可能产生意想不到的后果。例如,当学习一个新的框架时,开发者必须掌握其所有复杂性才能有效。阅读关于另一种制作网络请求的方式可能会减慢开发者的速度,因为阅读文档的时间就是没有在构建的时间。它也可能防止代码的可移植性。当为应用A编写的代码特定于框架X时,那么在将其用于使用框架Y构建的应用B之前,代码需要修改。

SvelteKit 针对这个问题有一个解决方案,那就是什么都不做。嗯,不是真的什么都不做,而是不提供需要你每次使用时查阅文档的包装器和函数,SvelteKit 鼓励使用现有的 Web 应用程序编程接口APIs)和标准。通过不重新发明轮子,更多的开发者可以快速开始使用 SvelteKit,因为他们不需要学习他们已经熟悉的标准的抽象。这不仅将阅读文档的时间降到最低,而且还需要更少的代码来驱动框架。

本章将介绍一些基本的Web APIs用法以及它们如何与 SvelteKit 交互。具体来说,我们将查看以下当前网络标准的示例:

  • 获取

  • FormData

  • URL

对这些标准的深入探讨超出了本章的范围,但如果你想了解更多信息,相关信息将在本章末尾提供。在这些示例之后,你应该能够舒适地使用你对各种网络标准的现有知识来使用 SvelteKit。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter03

Fetch

首先,让我们看看最常用的 Web APIs 之一,fetch。假设你的开发环境运行在 Node.js(v18+)的最新 LTS 版本上,你将无需安装额外的包如node-fetch就能在服务器上使用fetch。那个包是最广泛使用的包,它提供了功能,允许开发者在fetch被纳入 Node.js 的核心功能之前进行网络请求。由于fetch现在也支持每个主要浏览器,我们可以在客户端和服务器端环境中安全地使用它来发起外部和内部请求。因为 SvelteKit 可以在浏览器和服务器环境中运行,所以我们可以说,我们实际上“让 fetch 发生了。”

为了说明fetch在浏览器和服务器上如何工作,让我们看看一个例子。我们将通过添加src/routes/fetch/+page.svelte文件来创建一个新的路由,作为我们的演示页面。我们还将创建src/routes/fetch/+page.js来从实际的fetch请求中获取数据。在这个例子中,我们将利用位于api.nasa.gov/的 NASA 免费“每日天文图片”API。建议获取一个 API 密钥以供常规使用;然而,以下示例中提供的演示 API 密钥足以满足我们的需求。这个例子向 NASA API 发出网络请求,并在我们的页面上显示接收到的数据。

请注意,样式绝对不是必需的,只是提供以使我们的示例在浏览器中查看时更容易理解。我们可以添加很多样式,使这个例子看起来很棒,但这会占用我们在这页面上有限的更多空间。此外,你在这里是为了学习 SvelteKit,而不是 CSS:

src/routes/fetch/+page.svelte

<script>
  export let data;
</script>
<h1>Astronomy Picture of the Day</h1>
<h2>{data.pic.title}</h2>
<div class='wrap'>
  <a href={data.pic.hdurl}><img src={data.pic.url}></a>
</div>
<p>{data.pic.explanation}</p>
<style>
  h1, h2 {
    text-align: center;
  }
  .wrap {
    max-width: 75%;
    margin: 0 auto;
    text-align: center;
  }
  img {
    max-width: 100%;
  }
</style>

在这个文件中,我们只是设置了数据的标记,以便填充。要访问由兄弟文件+page.js提供的数据,我们必须包含export let data;。如果你熟悉 React 或 Vue,可以将这视为 SvelteKit 在组件之间传递属性的方法。如果你熟悉 React 或 Vue,可以将这视为 SvelteKit 在组件之间传递属性的方法。完成这些后,我们可以使用对象中提供的数据来填充图像标题、链接到高分辨率文件、链接到外部托管图像,并显示图像描述。当然,我们还添加了一些最小化样式。真正的魔法,以及我们的fetch请求,发生在下一个+page.js片段中,我们在那里调用外部 API:

src/routes/fetch/+page.js

const key = 'DEMO_KEY'; // your API key here
export function load() {
  const pic = fetch(`https://api.nasa.gov/planetary/apod?api_key=${key}`)
  .then(response => {
    console.log('got response');
    return response.json();
  });
  return {pic};
}

+page.js中,我们创建了一个常量,你可以在这里放置你自己的 API 密钥。然后,我们导出了load函数,这是告诉 SvelteKit 在渲染兄弟文件+page.svelte之前必须运行的函数。它可以async也可以不是;SvelteKit 并不关心,会相应地处理每种情况。然后我们创建了一个常量pic,我们可以将fetch调用返回的 promise 分配给它。在fetch调用中,我们提供了带有附加 API 密钥的 URL,作为第一个也是唯一的参数。如果你的 API 需要在头部指定选项、设置方法或可能需要一个认证 cookie,你可以通过在第二个参数中提供一个对象来实现。记住,SvelteKit 旨在与现有的网络标准兼容,所以实现将至少包含标准化的功能。有关如何以这种方式利用fetch的更多信息,请参阅本章末尾的资源。

继续使用fetch调用接收到的承诺,我们运行console.log()来演示代码在浏览器和服务器环境中都会运行。如果你的开发环境还没有运行,你可以使用npm run dev命令启动它。记住,它将在提供的 URL 中在浏览器中可用。你可以通过检查你的浏览器控制台输出(在开发者工具中)以及你的终端中的 Vite 开发服务器输出来确认这一点。在这两种情况下,你应该看到显示的“收到响应”输出。因为我们的请求不需要任何认证,所以在客户端运行是安全的。

最后,我们将转换load函数,这些函数在服务器和浏览器上都会运行。对于通用load函数,它们必须返回一个对象。

加载数据

我们已经确定从+page.js运行的代码实际上在浏览器和服务器上都会运行。如果我们想让代码只在客户端运行,我们就可以将其放在+page.svelte中。同样,如果我们想让这段代码只在服务器上运行,我们可以将文件名更改为+page.server.js。第二种用法更适合进行认证数据库调用或访问环境变量,例如 API 密钥(就像我们在之前的例子中所做的那样)。SvelteKit 将识别这个文件是打算只在服务器环境中运行的,并将采取适当的步骤来分离该逻辑。服务器load函数与通用load函数的工作方式略有不同,但我们将更深入地探讨这一点,在后面的章节中。

在完成所有这些操作之后,我们现在可以在浏览器中导航到 /fetch,查看图片、标题和描述,甚至可以点击图片查看全分辨率文件。调用各种内部或外部 API 非常有帮助,但我们的应用程序可能需要在某个时候从用户那里接收数据。在下一节中,我们将介绍如何访问和操作Requests中提供的FormData对象。

FormData

在构建 Web 应用程序时,通常通过使用表单来接受用户数据。可以通过+page.svelte+page.server.js访问这些数据,这些都在新的评论路由下。和之前一样,+page.svelte文件包含用于构建表单的 HTML 和最小样式。为了获取从服务器发送的数据,我们必须在我们的客户端<script>代码中包含读取export let form;的行。这让我们可以查看从+page.server.js返回的对象的状态,并通过 Svelte 提供的模板系统将状态反馈给用户:

src/routes/comment/+page.svelte

<script>
  export let form;
</script>
<div class='wrap'>
  {#if form && form.status === true}
    <p>{form.msg}</p>
  {/if}
  <form method='POST'>
    <label>
      Comment
      <input name="comment" type="text">
    </label>
    <button>Submit</button>
  </form>
</div>
<style>
  .wrap {
    width: 50%;
    margin: 0 auto;
    text-align: center;
  }
  p {
    color: green;
  }
  label {
    display: block;
    margin: 1rem;
  }
</style>

src/routes/comment/+page.server.js

export const actions = {
  default: async (event) => {
    const form = await event.request.formData();
    const comment = form.get('comment');
    if(comment) {
      // save comment to database
      return {
        status: true,
        msg: `Your comment has been received!`
      }
    }
  }
}

如前所述,+page.server.js+page.js文件的工作方式相同,但它在服务器上才会运行。就像我们可以在fetch示例中导出load()一样,服务器端文件也可以导出actions。这些导出的actions只能通过POST请求访问。我们将在后面的章节中探讨如何进一步利用actions,但到目前为止,请注意我们正在创建一个单独的动作,default。这意味着当表单提交时,POST请求将由/comment端点处理。

default动作中,这个例子访问了位于event中的Request对象,然后访问该Request中的FormData。一旦FormData对象被分配给form变量,我们使用get()通过名称从表单输入中检索值。通常,我们接下来会进行类似数据库调用的操作,以保存评论和唯一的用户标识符。为了简洁起见,我们只需返回一个包含statusmsg属性的简单对象。

当页面重新加载时,这些属性将通过 Svelte 模板语法进行检查,并向用户显示他们的评论已成功提交的消息。尽管这个例子只使用了FormDataget()方法,但官方标准中记录的所有方法对我们也都是可用的。如果我们想遍历所有提交的值和键,我们可以使用for...of循环,并通过form.values()form.keys()访问值。

现在我们知道了如何从提交的表单中获取数据,我们也应该看看另一种可能被忽视的从用户那里接收数据的方式——URL。

URL

event对象,就像request对象一样。让我们继续使用之前的评论示例。如果我们想在其他网站的网络中构建一个评论服务,我们可能想向用户报告他们刚刚在哪个网站上发表了评论。让我们扩展之前的例子并做到这一点。我们不需要对src/routes/comment/+page.svelte做任何修改。相反,我们将相应地调整我们的服务器动作。

这个例子与上一个例子相同,只是做了一些改动。通过event.url访问 URL API,并将其分配给url常量。然后使用console.log(url)将其输出到服务器,以显示你可以访问的各种只读属性。为了演示,我们通过url.hostname获取主机名,并在模板字面量中使用它,这被分配给返回对象的msg属性:

src/routes/comment/+page.server.js

export const actions = {
  default: async (event) => {
    const form = await event.request.formData();
    const url = event.url;
    console.log(url);
    const comment = form.get('comment');
    if(comment) {
      // save comment to DB here
      return {
        status: true,
        msg: `Your comment at ${url.hostname} has been 
          received!`
      }
    }
  }
}

如果你切换到浏览器窗口并发表评论,你现在会看到它报告了hostname属性。在生产环境中,这应该是你的域名,但在我们的开发环境中,你会看到消息http://localhost,你将看到URL对象:

示例事件.url 对象

URL {
  href: 'http://127.0.0.1:5173/comment',
  origin: 'http://127.0.0.1:5173',
  protocol: 'http:',
  username: '',
  password: '',
  host: '127.0.0.1:5173',
  hostname: '127.0.0.1',
  port: '5173',
  pathname: '/comment',
  search: '',
  searchParams: URLSearchParams {},\
  hash: ''
}

这个URL对象展示了获取其信息是多么简单。所有显示的只读属性都可以通过点符号轻松访问。不再需要使用正则表达式解析整个URL以提取所需的部分。现在,如果您想获取查询字符串中设置的值,可以轻松地使用for...of循环遍历event.url.searchParams。在您的浏览器中添加一些 URL 选项。一个例子可能看起来像http://127.0.0.1:5173/comment?id=5&name=YOUR_NAME_HERE。现在,console.log(url)函数调用将输出第一个?之后和每个随后的&设置的名字和值。

摘要

在本章中,我们介绍了一个fetch的示例用例,以及一个两部分的演示,展示了如何使用URLFormData。虽然这里提供的示例并不代表您将能够访问的所有各种 Web API 的全貌,但它们应该说明了使用 SvelteKit 使用它们是多么简单。如果您希望使用 SvelteKit 构建应用程序,那么熟悉这些现代 Web API 非常重要,因为它们在 SvelteKit 的开发过程中被广泛使用。SvelteKit 鼓励您依赖现有的知识。通过这样做,SvelteKit 可以向您提供更少的代码,从而使您可以向用户提供更少的代码。

在下一章中,我们将从使用 SvelteKit 所必需的背景信息中脱离出来,开始构建一个类似应用程序的东西。我们将介绍各种路由技术以及如何在应用程序中构建一致的用户界面。

资源

  • MDN Web 文档 – 要了解fetch()formData()URL等更多信息,请访问developer.mozilla.org

第二部分 – 核心概念

网络导航是一种非常典型的体验,SvelteKit 的开发者已经将路由作为框架的核心。在本部分中,我们将更详细地检查之前介绍的路线技术。之后,我们将看到 SvelteKit 如何将数据移动到组件中,并通过 HTML 表单元素从它们那里接受数据。最后,我们将看到一些更高级的路由技术,这些技术承诺可以覆盖在路由中遇到的几乎所有边缘情况。

本部分包含以下章节:

  • 第四章, 有效路由技术

  • 第五章, 深入数据加载

  • 第六章, 表单和数据提交

  • 第七章, 高级路由技术

第四章:有效的路由技术

我们已经花费了很多时间来介绍背景信息。我知道这些主题并不总是最令人兴奋的,但现在我们已经介绍了它们,我们可以进入真正的乐趣。到目前为止,我们只是简要地提到了通过在src/routes/内部创建一个名为所需路由名的目录来添加新路由,并在其中添加一个+page.svelte文件。我们还简要地探讨了创建服务器页面。但是,当然,路由并不总是这么简单。我们如何构建应用程序编程接口API)?我们如何在不重复每个页面的样式的情况下,在整个应用程序中创建一个一致的用户界面UI)?当我们的应用程序抛出错误时会发生什么?

在本章中,我们将通过讨论 SvelteKit 路由的某些核心点来回答这些问题。首先,我们将看看我们如何创建具有动态内容的新页面。然后,我们将更详细地研究+page.server.js文件的工作方式。然后,我们将展示如何创建可以接受各种类型 HTTP 请求的 API 端点。最后,我们将介绍如何使用布局在整个应用程序中构建一个一致的 UI。

在本章中,我们将涵盖以下主题:

  • 创建动态页面

  • 创建服务器页面

  • 创建 API 端点

  • 创建布局

到本章结束时,你应该对 SvelteKit 的基于文件的路由机制的路由概念有一个舒适的理解。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter04

创建动态页面

在前面的章节中,我们已经介绍了创建新页面的过程。为了刷新你的记忆,它就像在src/routes/内部创建一个名为所需路由名的新目录一样简单。在那个目录内部,我们创建+page.svelte,它只是一个 Svelte 组件,然后作为在浏览器中显示的页面进行渲染。一个令人尴尬的简单的关于页面可能看起来像这样:

src/routes/about/+page.svelte

<div class='wrapper'>
  <h1>About</h1>
  <p>
    Lorem ipsum dolor sit amet...
  </p>
</div>

这个例子仅仅说明了添加新页面是多么简单。在其中,我们看到一个div,一个h1标题标签,和一个带有lorem ipsum样本文本的段落p标签。当然,在现实世界的场景中,它会有更多内容以及一些样式。这个例子只存在是为了展示添加新静态页面是多么简单。

但如果你需要创建一个你不知道内容是什么的页面呢?如果我们想创建一个具有动态 URL 的页面呢?例如,当在线查看新闻文章时,用户通常可以直接分享到文章的链接。这意味着每篇文章都有自己的独特 URL。如果我们想创建一个显示从数据库中提取的文章的单个模板,我们就需要找到一种方法来管理每个 URL。

在这些情况下,SvelteKit 的基于文件的路由机制有一个特殊的语法可以使用。当创建显示内容的模板时,我们在一个目录中创建它,其中路由的动态部分被方括号[ ]包围。方括号内的名称将成为一个参数,允许我们动态加载数据。为了使此参数可选,请使用双方括号[[ ]]

这可能一开始听起来有些令人困惑,所以让我们看看一个示例,展示您如何管理新闻文章或博客文章。在这个示例中,我们需要创建一些文件。我们不会连接到实际的数据库,而是使用 JSON 文件来存储一些示例数据并直接从该文件中提取:

src/lib/articles.json

{
  "0": {
    "title": "First Post",
    "slug": "first-post",
    "content": "Lorem ipsum dolor…"
  },
  "1": {
    "title": "Effective Routing Techniques",
    "slug": "effective-routing-techniques",
    "content": "Lorem ipsum dolor…"
  }
}

此文件基本上是一个包含两个对象的单一对象,每个对象都具有titleslugcontent属性。您可以随意调整此文件或添加您想看到的条目。其目的是作为一个占位符数据库来展示以下示例。

接下来,新闻页面通常有一个着陆页,用户可以滚动查看最新的文章。根据既定惯例,我们将创建一个+page.server.js文件来加载数据并将其提供给作为页面渲染数据的+page.svelte模板:

src/routes/news/+page.server.js

import json from '$lib/articles.json';
export function load() {
  return { json };
}

由于它不包含任何环境变量或秘密,也不调用真实的数据库,这个文件也可以是一个+page.js文件。它仅用于从 JSON 示例文件加载数据。本质上,它导入该文件,然后在导出的load()函数中将其作为对象返回,使其对下一个显示的 Svelte 模板可用。实际上,这样的事情也可以在 Svelte 模板的<script>标签中完成,但请记住,这个示例旨在作为一个真实数据库的替代品。

现在我们已经加载了我们的文章,我们需要一个地方来展示它们,所以现在让我们创建一个用于新闻的着陆页。为了简洁起见,省略了样式:

src/routes/news/+page.svelte

<script>
  export let data;
</script>
<h1>
  News
</h1>
<ul>
  {#each Object.entries(data.json) as [key, value]}
    <li>
      <a href='/news/{value.slug}'>{value.title}</a>
    </li>
  {/each}
</ul>

这个 Svelte 组件导出data变量,使我们能够访问从我们的模拟数据库在load()中返回的数据。然后它添加一个h1标题标签,接着是一个无序列表,列出我们模拟数据中的每个条目。之后,它使用{#each} Svelte 模板语法遍历从Object.entries(data.json)返回的数组中的每个条目。对于每个条目(我们的两个示例对象),我们用列表项标签将其包围,并在<a>标签中显示title属性,通过slug属性链接到文章。

接下来,我们需要创建一个页面来显示文章内容,但请注意,我们希望路由中包含一个参数,所以我们将使用方括号[ ]来包围文章的 slug:

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

<script>
  export let data;
</script>
<h1>News</h1>
<h2>{data.title}</h2>
<div class='content'>
  {data.content}
</div>
<a href='/news'>Back to news</a>

此文件导出data变量,以便我们可以访问关于我们的文章的信息。然后,它在一个<h1>标题标签内显示News,接着是一个带有文章标题的<h2>标题标签,一个包含文章内容的div,以及一个返回新闻页面的链接。所有文章信息都是通过在下一个文件中对我们的数据库进行另一次调用而加载的:

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

import json from '$lib/articles.json';
import { error } from '@sveltejs/kit';
export function load({ params }) {
  let found = {};
  Object.keys(json).forEach((id) => {
    if(json[id].slug === params.slug) {
      found = json[id];
    }
  });
  if(Object.keys(found).length !== 0) {
    return found;
  }
  throw error(404, 'Whoops! That article wasn\'t found!');
}

就像之前一样,我们已经导入我们的模拟数据库来访问完整的文章内容。此代码还导入了 SvelteKit 的error模块,这将在稍后很有用。然后我们导出了load()函数,以便我们可以将加载的数据返回到渲染的页面。在load()函数内部,代码初始化一个名为found的空变量,然后开始遍历 JSON 数据中的每个对象。在循环中,它检查我们数据中的任何 slugs 是否与 URL 中给出的 slug 匹配。如果找到匹配项,它将被分配给found变量。循环结束后,我们检查found不是一个空对象。如果不是空的,我们返回一个包含found变量的对象。如果是空的,我们抛出一个404 Not Found错误。

当你在浏览器中打开你的开发站点并导航到/news时,你应该看到列出两个文章标题。点击它们将使用户重定向到相应的文章。这个例子以简单的方式说明了参数化路由,这在大多数情况下是有效的。但是,当它不起作用时我们该怎么办?你尝试导航到一个不存在的文章了吗?现在就试试看;我会在这里等着:

图 4.1 – 在 SvelteKit 中抛出错误时显示一个通用错误页面

文章不存在,用户会看到一个通用的错误页面。如果我们完全不抛出错误,页面将渲染显示undefined值。相反,我们应该向用户展示一个合适的错误页面。正如 SvelteKit 为我们提供了+page.svelte+page.js+page.server.js文件,我们也可以创建一个+error.svelte模板,当应用程序抛出错误时可以使用。通过在src/routes/news/[slug]/目录中指定它,错误页面模板将被本地化到该特定路由。如果我们想构建一个在整个应用程序中通用的错误页面,我们可以在应用程序的根路由(src/routes/+error.svelte)放置一个+error.svelte模板。让我们在src/routes/news/[slug]/中创建一个模板,这样用户就不会因为我们的沟通不清而感到困惑:

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

<script>
  import { page } from '$app/stores';
</script>
<h1>News</h1>
<h2>{$page.status}</h2>
<div class='content'>
  {$page.error.message}
</div>
<style>
  * {
    font-family: sans-serif;
  }
  h1, h2, .content {
    text-align: center;
    color: #358eaa;
  }
</style>

对于这个错误模板,我们导入page存储模块来访问有关此特定请求的信息。该模块以及许多其他模块在整个应用程序中都可以使用。由于这个模块使用了 Svelte 的存储,我们可以通过在前面加上美元符号($)来访问它包含的关于页面的值。这个模板的其余部分相当直接。它包括标记为News<h1>标题标签,后面跟着我们在+page.server.js中抛出错误时传递的状态码和错误消息。一些样式被包括进来,以显示这个模板与 SvelteKit 中显示的默认模板的不同。将通用 SvelteKit 错误模板的图 4.1与我们的自定义版本图 4.2进行比较:

图 4.2 – 定制的错误页面模板

到目前为止,你应该能够舒适地创建应用程序的基本路由,无论是静态的还是动态的。你也应该能够根据路由显示错误页面。关于高级路由的内容将在稍后介绍,但现在,让我们更仔细地看看+page.server.js文件是如何工作的。

创建服务器页面

在之前的示例中,我们使用了+page.js+page.server.js文件来加载数据。通常,它们可以互换使用,但何时使用哪一个才是最佳时机?在本节中,我们将分析这两个文件之间的部分差异,并讨论+page.server.js文件中可用的各种功能。我们将将其分解为以下主题:

  • load()

  • 页面选项

  • 操作

load()

正如我们在之前的示例中所见,数据可以通过在页面中导出data属性来加载到+page.svelte组件中。+page.js+page.server.js都可以用来将数据加载到该页面模板中,因为它们都可以导出load()函数。何时使用哪个文件取决于你计划如何加载数据。当在+page.js文件中运行时,load()将在客户端和服务器上同时运行。如果你能够在这里加载数据,这是推荐的,因为 SvelteKit 可以管理通过fetch()调用获取数据。当预加载数据(预测用户可能的行为并在他们实际操作前开始处理)时,这尤其有用。

然而,有时这是不可能的。例如,如果你需要调用需要身份验证或数据库的 API,你很可能不希望你的连接秘密暴露给客户端。这样,任何有权访问你的应用程序的人都可以下载你的秘密并代表你向该 API 或数据库发出请求。在这些情况下,你需要在服务器上存储这些秘密。由于这些秘密在服务器上,你需要访问服务器的文件系统。服务器需要调用以获取适当的数据,并将这些数据传递给客户端。在这些情况下,最好使用+page.server.js文件来满足数据加载需求。

页面选项

+page.js+page.server.js文件不仅用于加载数据。它们还可以导出针对其同级页面的特定选项。某些选项允许您配置与页面渲染相关的功能。这些特定的选项是布尔值,这意味着可以通过将它们设置为true来启用,或通过将它们设置为false来禁用。它们如下所示:

  • 预渲染

  • ssr

  • csr

预渲染

虽然可以在svelte.config.js项目中自定义预渲染,但您可能会发现自己需要根据页面逐个显式启用或禁用它。要在页面上启用预渲染,您可以设置export const prerender = true;。相反,将其值设置为false将禁用该页面的预渲染。何时预渲染页面应根据页面的 HTML 内容是否静态来决定。如果页面上显示的 HTML 应该对任何查看者都相同,则该页面被认为是安全的预渲染。通过预渲染页面,HTML 将在构建时生成,并且对于每个特定路由的请求,将静态 HTML 发送到客户端。这导致最终用户的加载时间更快,从而提供更好的体验。

服务器端渲染

+page.js文件或+page.server.js文件中,export const ssr = true;。SvelteKit 默认启用此选项,因此如果您需要更改它,您可能会发现自己通过将其值设置为false来禁用它。

客户端渲染

而不是在服务器上渲染页面并将其发送到客户端,通过启用export const csr = false;,就像您对其他可用的页面选项所做的那样。

如果您发现自己正在构建一个 SPA、一个静态 HTML 网站,或者尝试在客户端渲染静态内容,您可能会发现这些渲染相关的选项很有用。

行为

因为+page.js文件也在客户端运行,所以它们不能导出行为。行为允许您接收通过 POST HTTP 方法发送的form元素提交的数据。我们在第三章中看到了一个例子,我们讨论了 SvelteKit 与FormData API 的兼容性。在那个例子中,我们导出了一个默认行为。默认行为将由未指定action属性的表单元素提交时触发。然而,我们不仅限于只有默认行为,因为 SvelteKit 还允许我们创建命名行为。命名行为将在相同的路由上工作,但通过在查询字符串中提供路由后跟行为名称来区分彼此。默认行为可能不会与命名行为共存,因此您必须删除它或更改其名称,并在使用它的form元素上设置action属性。

基于我们之前的示例,让我们看看我们如何实现一些与在线评论相关的更多操作。我们可能想要创建的一些附加功能包括允许用户对评论进行star或回复。由于我们将添加更多命名操作,我们将default操作更改为create

src/routes/comment/+page.server.js

export const actions = {
  create: async (event) => {
    const form = await event.request.formData();
    …
  },
  star: async () => {
    return {
      status: true,
      msg: 'You starred this comment!'
    }
  },
  reply: async () => {
    return {
      status: true,
      msg: 'You replied!'
    }
  }
}

注意default操作已被更改为createcreate中的代码已被省略,因为自上次示例以来它没有变化。我们还添加了starreply操作。目前,它们除了返回一个对象,该对象将输出我们的消息,展示当点击相应的按钮时它们都会被调用。在现实世界的场景中,这些操作可能会调用数据库,增加“star count”,或保存回复评论的内容和被回复评论的唯一标识符。

关于表单本身,我们可以为每个新功能创建单独的表单,并指定POST方法和每个新功能的action属性。然而,一个更直观的用户体验会将功能整合在一起,并将它们都保留在一个统一的组件中。我们不会创建多个表单,而是为新的功能创建一个按钮,并为每个按钮指定一个formaction属性。这样做将保持父表单中指定的 HTTP 方法,但允许根据点击的按钮发送到不同的操作:

src/routes/comment/+page.svelte

<script>
  import { enhance } from '$app/forms';
  export let form;
</script>
<div class='wrap'>
  {#if form && form.status === true}
    <p>{form.msg}</p>
  {/if}
  <form method='POST' action='?/create' use:enhance>
    <label>
      Comment
      <input name="comment" type="text">
    </label>
    <button>Submit</button>
    <button formaction='?/star'>Star</button>
    <button formaction='?/reply'>Reply</button>
  </form>
</div>
<style>
…
</style>

从我们上次遇到这个示例时,首先要注意的变化是我们添加了import { enhance } from '$app/forms';。这个添加将允许我们使用 JavaScript 逐步增强我们的表单。然后,在每次表单提交后,页面将不需要重新加载。这个模块在下面的<form>元素中使用 Svelte 的use:指令。尝试运行没有它的示例,并观察现在 URL 将根据点击的哪个按钮包含查询字符串。

说到按钮,我们在本例中添加了两个。每个按钮都设置了formaction属性,这允许我们指定从+page.server.js中调用哪个命名操作。请注意,我们必须通过指定一个查询参数后跟一个/字符来调用这些操作。我们还设置了表单操作为?/create。由于我们导出了命名操作,我们不能再有名为default的操作,必须在form元素上指定要调用的操作。如果我们想调用位于另一个路由的操作,我们可以通过将formaction设置为所需的路由名称后跟?/,然后是操作名称来轻松实现。

你现在应该对何时使用+page.server.js而不是+page.js、如何自定义页面渲染以及如何轻松地从form元素接受数据有信心。在下一节中,我们将介绍如何创建接受不仅仅是 POST 请求的 API 端点。

API 端点

我们已经介绍了+page.svelte+page.js+page.server.js文件,但我们还没有讨论+server.js文件。这些文件使我们能够接受不仅仅是POST请求。作为网络应用开发者,我们可能需要支持各种平台。拥有一个 API 简化了我们服务器与其他平台之间数据的传输。许多 API 可以接受 GET 和 POST 请求,以及 PUT、PATCH、DELETE 或 OPTIONS。

一个+server.js文件通过导出一个函数来创建一个 API 端点,该函数的名称是你希望它接受的 HTTP 请求方法。导出的函数将接受 SvelteKit 特定的RequestEvent参数,并返回一个Response对象。例如,我们可以创建一个简单的端点,允许我们为博客创建帖子。如果我们使用移动应用来撰写和发布,这可能很有用。请注意,+server.js文件不应与页面文件一起存在,因为它旨在处理所有 HTTP 请求类型:

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

import { json } from '@sveltejs/kit';
export function POST({ request }) {
  // save post to DB
  console.log(request);
  return json({
    status: true,
    method: request.method
  });
}
export function GET({ request }) {
  // retrieve post from DB
  console.log(request);
  return json({
    status: true,
    method: request.method
  });
}

此文件从@sveltejs/kit包中导入json模块,这对于发送 JSON 格式的Response对象很有用。然后我们导出名为 POST 和 GET 方法的函数,每个函数都只将Request对象输出到控制台,然后返回一个 JSON 格式的Response。如果我们愿意,我们还可以导出其他 HTTP 动词(如 PUT、PATCH 或 DELETE)的函数。

你应该通过在浏览器中导航到api/post/路由来演示这个示例。打开页面后,观察输出到你的开发服务器的Request对象中可用的其他属性。如果你不理解你看到的内容,那没关系,因为我们在下一章中会更深入地探讨它。回到你的浏览器,打开返回的 JSON 格式对象中设置为 POST 的method属性。如果你的浏览器不允许你编辑请求,OWASP ZAP、Burp Suite、Postman 或 Telerik Fiddler 等代理工具将允许你自定义 HTTP 请求。请参阅本章末尾的资源链接。

现在你已经知道了如何创建你自己的 API,让我们看看如何通过布局来统一你应用程序的用户体验。

创建布局

到目前为止,我们已经在本章中介绍了很多内容,但我们仍然只为每个特定页面添加了样式和标记。这是重复的,并且不是我们时间的高效利用。为了减少重复,我们可以利用布局。一个+layout.svelte组件可以通过利用 Svelte 的<slot>指令来统一用户体验。布局文件将任何兄弟页面组件和子路由嵌套在其中,使我们能够在整个应用程序中显示持久标记。就像+page.svelte一样,我们可以在我们的路由层次结构的任何级别包含+layout.svelte组件,允许布局嵌套在布局中。因为每个布局也是一个 Svelte 组件,样式将局部化到该特定组件,并且不会传播到嵌套在其内的组件。让我们看看我们如何使用布局来为现有的代码创建一个一致的布局和导航菜单:

src/routes/+layout.svelte

<script>
  import Nav from '$lib/Nav.svelte';
</script>
<div class='wrapper'>
  <div class='nav'>
    <Nav />
  </div>
  <div class='content'>
    <slot />
  </div>
  <div class='footer'>
    This is my footer
  </div>
</div>
<style>
  .wrapper {
    min-height: 100vh;
    display: grid;
    grid-template-rows: auto 1fr auto;
  }
  .footer {
    text-align: center;
    margin: 20px 0;
  }
</style>

由于此+layout.svelte组件位于我们路由的根级别,它将应用于所有子路由。我们的组件首先做的事情是导入我们的自定义导航组件(如下所示)。其次,它创建将容纳我们应用程序其余部分的标记,包括此文件的兄弟+page.svelte。其标记由几个具有不同类名的<div>元素组成,这些元素表示功能。具有.wrapper类的<div>元素包裹了所有其他元素,这样我们就可以应用在组件的<style>部分中找到的粘性页脚样式。具有.nav类的<div>包含我们的自定义Nav组件,.content <div>包含我们的 Svelte <slot>指令,而.footer是我们放置网站页脚信息的地方。现在让我们看看我们在根布局中导入的自定义Nav组件:

src/lib/Nav.svelte

<nav>
  <ul>
    <li><a href='/'>Home</a></li>
    <li><a href='/news'>News</a></li>
    <li><a href='/fetch'>Fetch</a></li>
    <li><a href='/comment'>Comment</a></li>
    <li><a href='/about'>About</a></li>
    <li><a href='/api/post'>API</a></li>
  </ul>
</nav>
<style>
  ul {
    list-style: none;
    text-align: center;
  }
  ul li {
    display: inline-block;
    padding: 0;
    margin: 1em .75em;
  }
</style>

此组件仅包含 HTML,其中包含指向我们已创建的所有路由的链接和一些基本的样式。它由具有href属性设置为相对路由的<a>元素组成,这些元素嵌套在一个无序列表的列表项中,该列表项位于<nav>元素内。再次强调,这个例子过于简单,但就我们的目的而言,它是有效的。现在,您可以将本书后面创建的任何新路由添加到导航菜单中,以便在浏览器中测试时可以轻松访问。

相对路由

注意提供的路由是相对的,并且不以域名开头。如果我们要将我们的生产应用程序部署到子目录,而不是域名的根目录,这些路由将失败。相反,我们可以在 svelte.config.js 中设置应用程序的基本路径。具体来说,我们将设置 config.kit.paths.base 为我们的子目录路径,以 / 开头。然后在组件和路由中,我们可以使用 import { base } from $app/paths 并在所有路由前加上 {base}/。这样,我们的应用程序就会知道它存在于一个子目录中。尝试在你的开发项目中这样做,并观察 Vite 和 SvelteKit 如何自动从这个目录中提供服务!

为了进一步练习与布局相关的概念,尝试创建 src/routes/news/[slug]/+layout.svelte 以使文章具有一致的外观。就像我们看到的 +page.svelte 文件一样,+layout.svelte 文件可以与 +layout.js+layout.server.js 文件一起使用。它们的功能与页面对应文件相同,但它们返回的数据将可用于 +layout.svelte 以及任何并存的 +page.svelte 页面。页面选项也可以在布局文件中设置,并且这些选项将“渗透”到嵌套组件中。

根据提供的信息,你现在应该具备为你的 SvelteKit 应用程序生产一致且健壮的 UI 所需的技能。布局在创建各种 UI 元素时非常有用,尤其是那些需要在应用程序的各个部分保持一致性的元素。

摘要

在本章中,我们介绍了如何创建静态和动态路由以及管理这些路由的自定义错误模板。我们还看到了开发者如何通过单个表单调用多个命名操作来接受通过 <form> 元素提交的数据。我们学习了如何利用 SvelteKit 的路由机制构建 API,这对于需要从除网络浏览器以外的平台访问应用程序尤其有用。然后我们通过布局统一了应用程序的 UI。有了这些布局,我们看到了它们如何被利用来在应用程序的各个部分保持导航元素的可预测位置。在这几页中要吸收这么多信息确实很多,所以我们将在这接下来的几章中更详细地探讨一些这些概念。

在下一章中,我们将学习更多关于管理我们加载到页面上的数据的方法。我们还将介绍更多高级的数据加载方法。

资源

HTTP 代理/发送工具:

第五章:深入探讨数据加载

每个创建过的应用程序都是由数据驱动的。没有数据来处理,应用程序实际上是没有用的。这就是为什么开发者对如何管理他们应用程序的数据检索有一个牢固的理解非常重要。当使用 SvelteKit 时,这是通过在页面或布局文件中导出load()函数来完成的。

在上一章中,我们简要介绍了load()。在本章中,我们将通过讨论其工作原理以及查看更多实际、真实世界的示例来进一步分析它。我们将创建一个仅在客户端强制执行load()的示例,并介绍使用load()时需要记住的一些关键细节。我们还将使用load()在布局中展示它如何使数据在我们的应用程序中可移植。最后,我们将查看一个示例,展示如何利用服务器load()函数提供的一些数据,这些数据在通用load()函数中不可用。

本章我们将涵盖以下主题:

  • 在客户端加载

  • 在布局中加载

  • 解构 RequestEvent

完成本章学习后,你将能够熟练地在 SvelteKit 应用程序中以各种方式加载数据。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter05.

在客户端加载

在上一章讨论创建服务器页面时,我们介绍了从+page.js导出的load()函数将在客户端和服务器上运行。当我们想要确保load()只在服务器上运行时,我们将它移动到+page.server.js。但如果你正在尝试构建一个离线准备好的应用程序呢?你可能正在构建一个仅在客户端上运行的load()函数,而不是在服务器上。当+page.js中的load()函数在两个环境中运行时,我们如何做到这一点?

再次回想一下上一章中创建服务器页面部分,我们讨论了页面选项,并且你会记得ssr选项。当导出时,这个常量将禁用或启用+page.js中的load()函数,使其仅在客户端运行,我们可以添加export const ssr = false;。让我们回到我们的fetch示例第三章并修改它以展示这一点。

在进行此调整之前,请确保console.log('got response')函数仍然存在。在浏览器中打开/fetch路由,并确认输出显示在浏览器控制台和你的开发服务器上。完成这些操作后,通过导出ssr页面选项来禁用页面上的 SSR:

src/routes/fetch/+page.js

const key = 'DEMO_KEY'; // your API key here
export const ssr = false;
export function load() {
  const pic = fetch(`https://api.nasa.gov/planetary/apod?api_key=${key}`)
    .then(response => {
      console.log('got response');
      return response.json();
    });
  return {pic};
}

这个例子与我们之前看到的例子相同,只是在第 3 行,我们添加了 export const ssr = false;。这个页面选项有效地禁用了页面的 SSR(服务器端渲染),意味着 load() 只会在客户端运行。你会注意到 console.log() 调用不再输出到开发服务器,但会在浏览器控制台中显示。

从现在开始,我们将区分 load() 函数,即 load() 函数是在服务器上的 +page.server.js 中运行的,这意味着通用 load() 函数是从 +page.js 中运行的。从高层次来看,它们在功能上是相同的。但还有一些特殊之处需要提及:

  • 通用和服务器 load() 函数都可以访问与调用它的请求相关的数据。

  • 服务器 load() 函数将能够访问更多请求数据,例如 cookie 和客户端 IP 地址。

  • 通用 load() 函数始终返回一个对象。该对象的值可以是几乎任何东西。

  • 服务器 load() 函数必须返回可以被 devalue 包序列化的数据(基本上,任何可以转换为 JSON 的东西)。更多关于 devalue 的信息请访问 github.com/rich-harris/devalue

通用负载时间

应该提到的是,在第一次渲染时,load() 将在服务器和客户端执行。然后,每个后续请求都将在客户端执行。为了演示这种行为,请将你的浏览器导航到一个从 +page.js 文件中运行 load() 的路由,例如我们来自 第三章/fetch 示例。观察在浏览器中首次打开 /fetch 页面时服务器和客户端的输出控制台。导航到另一个路由然后再返回将只显示客户端的输出。

关于调用 load() 的最后一点说明;除非你指定页面应该通过页面选项预先渲染,否则它将在运行时始终被调用。如果你决定预先渲染页面,那么 load() 将在构建时被调用。记住,页面应该只在静态 HTML 对于每个访问页面的用户都相同的情况下才进行预先渲染。

我们刚刚介绍了如何强制 load() 只在客户端运行以及它的一些工作细节。结合所有这些新信息以及前几章的信息,你应该对 load() 的基础知识感到相对舒适。让我们进一步探讨,看看它如何在布局模板中使用。

加载布局

到目前为止,我们只看了 load()+page.js+page.server.js 文件中的使用,但它也可以在 +layout.js+layout.server.js 文件中使用。虽然布局不能导出操作,但它们在其他方面与页面文件功能相同。这意味着之前提到的页面选项(如 ssr)和 load() 函数将适用于布局内部嵌套的任何组件。关于 load() 函数的另一个重要特性是,由于它们在 SvelteKit 中并发运行,单个页面将在所有请求完成之前不会渲染。在页面上以及布局上都有 load() 函数将防止渲染,直到两者都完成。但由于它们将同时运行,任何延迟都应该微不足道。

在布局中加载数据的最明显优势是能够访问在兄弟和子页面中的数据。这意味着布局加载的任何数据都可以在导出 data 变量的继承的 +page.svelte 文件中访问。SvelteKit 还会跟踪应用程序中加载的数据,并且只有在它认为绝对必要时才会触发 load()。在我们想要强制重新加载数据的情况下,我们可以导入 $app/navigation 提供的 invalidateinvalidateAll 模块。

为了演示这些概念,让我们创建一个与导航并列的组件,以便向用户提醒未读通知。该组件将持久存在于应用头部,以便易于访问。这为展示从布局中加载数据的理想场景提供了可能。我们还将创建另一个页面,显示通知的完整列表,以演示从布局加载数据如何在子组件中使用。

让我们从 +layout.js 中的 load() 函数开始。为了简单起见,我们将在函数调用中直接返回数据,而不是调用一个假想的数据库或 API:

src/routes/+layout.js

export function load() {
  console.log('notifications loaded');
  return {
    notifications: {
      count: 3,
      items: [
        {
          type: `comment`,
          content: `Hi! I'm Dylan!`
        },
        {
          type: `comment`,
          content: `Hi Dylan. Nice to meet you!`
        },
        {
          type: `comment`,
          content: `Welcome to the chapter about load()!`
        }
      ]
    },
  }
}

此文件仅包含导出的 load() 函数,它返回一个包含另一个 notifications 对象的对象。请记住,通用的 load() 函数可以导出任何东西,只要它位于对象内部。notifications 对象相当简单,它由两个属性组成;一个值为 3count 属性,另一个标记为 items 的属性,它只是一个包含三个其他对象的数组。为了显示数据不是每次导航到新页面时都会加载,我们包含了一个 console.log() 调用,输出文本 notifications loaded

接下来,我们将对我们的根布局模板进行一些更改,以便它实际上可以使用我们刚刚加载的数据。大部分将保持不变,但我们需要添加一些可以显示数据的标记以及一些基本的样式来传达通知徽章的概念:

src/routes/+layout.svelte

<script>
  import Nav from '$lib/Nav.svelte';
  import Notify from '$lib/Notify.svelte';
  export let data;
</script>
<div class='wrapper'>
  <div class='nav'>
    <div class='menu'>
      <Nav />
    </div>
    <div class='notifications'>
      <Notify count={data.notifications.count}/>
    </div>
  </div>
  <div class='content'>
    <slot></slot>
  </div>
  <div class='footer'>
    This is my footer
  </div>
</div>
<style>
  .wrapper {
    min-height: 100vh;
    display: grid;
    grid-template-rows: auto 1fr auto;
  }
  .footer {
    text-align: center;
    margin: 20px 0;
  }
  .nav {
    text-align: center;
  }
  .menu {
    display: inline-block;
  }
  .notifications {
    float: right;
  }
</style>

在这个版本的 +layout.svelte 中有一些重要的更改需要注意:

  • 导入了新的Notify组件(如下所示)。

  • 我们导出data变量以使用从src/routes/+layout.js返回的数据。

  • notificationscount属性发送到Notify组件。

  • .menu.notifications元素的标记添加到.navdiv 元素中。这允许我们在页面的右上角显示Notify组件。

  • 为具有.nav.menu.notifications类的元素添加了新的样式,以装饰我们的新标记。

接下来,让我们看看我们刚刚导入的Notify组件。此组件将包含显示我们的通知计数和链接到/notification路由的标记:

src/lib/Notify.svelte

<script>
  export let count = 0;
</script>
<a href='/notifications'>
  {count}
</a>
<style>
  a {
    padding: 15px;
    color: white;
    text-decoration: none;
    background-color: #ea6262;
  }
</style>

此组件相对简单。首先,它导出count变量并为其设置默认值0。这是必要的,因为虽然这个组件在布局中使用,但它并不位于我们之前创建的+layout.js文件之下或旁边,因此它无法访问布局load()函数提供的信息。接下来,这个组件创建一个链接标签来包含count变量。最后,它包含了一些基本的样式来装饰我们的通知徽章。

最后,让我们看看通知页面。因为这个文件位于+layout.js的层次结构之下,我们可以像从与其并存的+page.js文件加载的数据一样访问data

src/routes/notifications/+page.svelte

<script>
  export let data;
</script>
{#if data.notifications.count > 0}
  <ul>
  {#each data.notifications.items as item}
    <li>{item.content}</li>
  {/each}
  </ul>
{/if}

此页面使用了 Svelte 指令:{#if}{#each}。由于我们在组件顶部导出了data变量,因此我们可以在此组件中使用从src/routes/+layout.js加载的数据。如果notifications对象的count属性大于零,它将创建必要的标记来生成一个无序列表。然后,它在一个列表项中输出每个评论项的content属性。

现在,当你用浏览器打开你的项目时,你应该在应用的右上角看到一个新通知徽章,显示notification对象中count属性的值。尝试选择导航菜单中的某些项,看看每次点击链接时文本notifications loaded是否都会输出。它在开发服务器和浏览器控制台中首次加载时显示,但不会再次运行。这是因为正在加载的数据尚未更改,SvelteKit 能够识别这一点。

让我们看看如何在点击通知徽章时强制重新加载数据。我们可以通过使用从$app/navigation导入的invalidateAll来实现这一点。如果load()函数使用了fetch(),那么使用invalidate模块是有意义的。在那个例子中,我们会通过将fetch()调用中指定的 URL 传递给invalidate()来强制重新加载。由于我们只是返回一个对象,我们需要使用invalidateAll()来触发重新加载:

src/lib/Notify.svelte

<script>
  import { invalidateAll } from '$app/navigation';
  export let count = 0;
</script>
<a href='/notifications' on:click={() => invalidateAll()}>
  {count}
</a>

Notify.svelte组件中,我们添加了对invalidateAll的导入。当点击通知链接徽章时,它会调用invalidateAll(),通知 SvelteKit 重新运行上下文中的所有load()函数。现在,当你点击页面顶部的通知链接时,你应该会在浏览器控制台看到输出notifications loaded。导航到其他页面,如关于新闻主页,则不会产生输出。

在未来,如果你发现自己正在构建将在应用程序界面中显示动态数据的组件,请考虑我们刚刚讨论的概念。通过在布局文件中加载数据,你可以减少 HTTP 请求或数据库查询的数量,这可以显著提高用户的应用程序体验。如果你需要强制重新加载数据,你将知道如何使数据无效,以便 SvelteKit 重新运行适当的load()函数。接下来,让我们看看如何进一步利用load()来构建更高级的功能。

解构RequestEvent

当涉及到load()函数时,服务器似乎比客户端拥有更多信息。在如此多的数据中,很难确切知道所有可用的信息。简而言之,服务器的load()函数使用 SvelteKit 特定的RequestEvent进行调用。以下是该对象中可用的属性(prop)和函数(fn)的快速概述:

  • cookies (prop) – 请求期间发送的 cookie。

  • fetch (fn) – 在第三章中讨论的 Web API fetch()函数的兼容变体。它带来了额外的优势,允许基于相对路由进行请求,并在同一服务器上传递 cookie 和头部信息。

  • getClientAddress (fn) – 返回客户端的 IP 地址。

  • locals (prop) – 通过 SvelteKit 的handle()钩子插入到请求中的任何自定义数据。我们将在后面的章节中介绍。

  • params (prop) – 当前路由特有的参数,例如上一章新闻示例中传递给文章的 slug。

  • platform (prop) – 环境适配器添加的数据。

  • request (prop) – 实际请求数据,表示为一个对象。

  • route (prop) – 请求路由的标识符。

  • setHeaders (fn) – 允许在返回的Response对象中操作头部。

  • url (prop) – 关于请求 URL 的数据,我们在第三章中讨论过。

请求事件演示

要亲自查看这些信息,请创建一个包含console.log()函数的src/routes/+layout.server.js文件,该函数输出传递给load()的单个参数。通过在根布局中创建它,你将能够看到基于从浏览器访问的不同路由,属性是如何变化的。然后,数据将在你的开发控制台中显示。

一个你可能需要利用这些数据的实际例子是在用户认证的情况下。通常,用户认证后,他们会被分配一个 cookie(因为他们输入密码做得很好 – 这是个双关语)存储在他们的设备上,这确保了他们的认证将在访问期间持续有效。如果他们离开应用程序,它可以在以后用来确认他们的身份,这样他们就不需要再次进行认证。让我们观察一下 SvelteKit 是如何实现这一点的。由于本章是关于load(),我们将构建实际的表单并在下一章讨论如何设置 cookie。现在,我们只需检查用户是否设置了 cookie,并在浏览器中手动设置一个。

首先,让我们将src/routes/+layout.js重命名为src/routes/+layout.server.js。如果我们打算访问 cookie 数据,我们需要访问RequestEvent提供的数据。通过将逻辑添加到我们的根服务器布局中,我们有一个额外的优势,即在整个应用程序中保持认证检查:

src/routes/+layout.server.js

export function load({ cookies }) {
  const data = {
    notifications: {
      count: 3,
      items: [
        {
          type: `comment`,
          content: `Hi! I'm Dylan!`
        },
         …
      ]
    }
  };
  if(cookies.get('identity') === '1') {
    // lookup user ID in database
    data.user = {
      id: 1,
      name: 'Dylan'
    }
  }
  return data;
}

在这个新的根布局逻辑版本中,我们解构了传递给load()的参数,因为我们目前只需要访问cookies属性。我们保留了之前创建的notifications对象,但将其放入一个名为data的新变量中。为了简洁起见,这段文本省略了一些条目。从那里,我们检查发送到我们应用程序的请求是否包含一个名为user且值为1的 cookie。如果是,我们将一些假用户信息插入到data对象的user属性中。通常在这种情况下,我们会将 cookie 值与数据库中的有效会话进行比对,如果找到,则检索相应的用户数据并将其发送回客户端,但我们正在尝试保持简单。完成所有这些后,data对象从load()返回。

接下来,我们需要实际展示用户已经成功认证。为此,我们将创建一个新的路由,让我们的用户可以登录:

src/routes/login/+page.svelte

<script>
  export let data;
</script>
{#if data.user}
  <p>
    Welcome, {data.user.name}!
  </p>
{/if}

如我们在上一节中所述,从+layout.js+layout.server.js返回的数据通过导出data变量在子组件中可用。一旦完成,我们使用 Svelte 的{#if}指令检查是否设置了user属性。如果找到,我们则显示data.username属性。

当然,在这个例子中,我们从未设置任何 cookie。我们将在下一章中介绍这一点,所以现在,让我们手动在我们的浏览器中创建 cookie。在这样做之前,导航到/login路由并验证页面上没有显示任何内容。一旦确认它是一个空白页面,请按照以下步骤为您的浏览器创建 cookie:

  • identity

  • 双击1

  • 使用相同的步骤确保/

  • identity

  • 选择1

  • 使用相同的步骤确保/

按照这些步骤操作后,你现在应该已经在浏览器中拥有了正确的 cookie。完成此操作后,在浏览器中刷新/login页面,你会看到一个欢迎用户的消息,其中包含由name属性指定的值。这个例子相当简单,而实际的基于 cookie 的登录系统在功能上稍微复杂一些;然而,概念是相同的。

尽管我们覆盖的例子只使用了RequestEvent中的cookies属性,但我们看到了如何轻松地访问其他任何属性,例如urlparams,或者甚至使用setHeaders函数设置我们自己的头部信息。有了所有这些数据可供我们使用,我们可以在应用程序中构建的内容的可能性几乎是无限的。

摘要

在本章中,我们讨论了关于load()的大量信息。我们首先讨论了它只能在客户端完成,然后转向一些关于其工作原理的更详细的内容。之后,我们探讨了在布局中使用load()以最小化每个页面加载时发出的请求数量,并最大化方便地访问可能需要应用范围内的数据。我们还探讨了在需要重新加载数据的情况下如何使数据失效。最后,我们介绍了服务器load()函数是如何通过RequestEvent调用的,这为我们提供了访问大量有价值信息的机会。这些信息可以让我们为我们的应用程序构建基于 cookie 的登录功能。

在本章中,我们学习了关于load()背后的一些更详细的细节,你应该感到轻松,可以休息一下。如果你手头有烤制的饼干,我建议你从书中休息一下,给自己一些奖励。你已经应得它了。

但请务必回来,因为在下一章中,我们将探讨更多关于通过使用表单从用户那里接收数据背后的细节,使表单变得有趣,并通过利用快照来减少数据输入的摩擦。

资源

devalue: github.com/rich-harris/devalue

第六章:表单和数据提交

在上一章中,我们探讨了 SvelteKit 中加载数据的一些细节。虽然加载数据很重要,但同样重要的是我们要了解如何让用户提交数据。这就是为什么本章将探讨 SvelteKit 中表单和动作的一些细节。虽然并非所有应用程序都必须接受用户的数据,但那些以直观方式这样做的一般会脱颖而出。毕竟,一些最好的用户体验被认为是理所当然的,因为它们只是简单地工作。只有当事情出错时,用户才会开始关注它们。

在本章中,我们将学习如何利用<form>元素来保持我们的应用程序的可访问性和代码的简洁性。将这些表单与易于实现的动作集成,使我们能够根据需要处理提交的数据。最后,我们将探讨如何通过添加渐进式增强来软化围绕表单的标准用户体验的一些边缘。为了完成所有这些,我们将对之前开始的登录表单进行最后的润色。

本章将涵盖以下内容:

  • 表单设置

  • 动作分析

  • 提高表单功能

完成本章后,你应该能够轻松地创建自己的登录表单,并且你会知道如何继续前进并接受你基于 SvelteKit 的应用程序的用户的所有类型的数据。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter06

表单设置

我们在第四章中简要地看了如何一起使用表单和动作。虽然在上一章中涵盖了RequestEvent,我们开始创建必要的代码以使用 cookie 在我们的应用程序中验证用户。然而,在那个例子中,我们从未给用户提供提供用户名或密码的手段。我们也从未在应用程序中创建 cookie。相反,我们选择使用浏览器的开发者工具手动创建一个。现在是时候将所有这些整合在一起了。由于我们已经涵盖了与load()相关的逻辑以及与RequestEvent相关的细节,我们可以继续构建之前的例子。一个不错的起点是登录表单本身。毕竟,如果没有提供一个地方让他们登录,我们就无法让用户登录。

但在我们创建表单之前,让我们先在我们的导航中添加一个指向登录页面的链接:

src/lib/Nav.svelte

<nav>
  <ul>
    <li><a href='/'>Home</a></li>
    <li><a href='/news'>News</a></li>
    <li><a href='/fetch'>Fetch</a></li>
    <li><a href='/comment'>Comment</a></li>
    <li><a href='/about'>About</a></li>
    <li><a href='/api/post'>API</a></li>
    <li><a href='/login'>Login</a></li>
  </ul>
</nav>

这个更改就像复制一个现有的<li>元素,并替换<a>内的路由和文本一样简单。这将使导航和测试我们的登录功能变得更加简单。

接下来,让我们从实际的表单开始。回顾一下我们创建的用于根据用户的 cookie 显示成功登录的文件,我们需要进行一些修改。首先,我们将从 $app/forms 中导入 enhance 模块。我们将在本章后面讨论这个背后的部分魔法,所以现在不用担心。接下来,我们将想要导出 form 变量,以便我们可以向用户指示他们的登录状态。最后,我们需要创建一个带有适当输入的 <form> 元素,并给它一些样式:

src/routes/login/+page.svelte

<script>
  import { enhance } from '$app/forms';
  export let data;
  export let form;
</script>
{#if form?.msg}
  {form.msg}
{/if}
{#if data.user}
  <p>
    Welcome, {data.user.name}!
  </p>
{/if}
<form use:enhance method="POST" action="?/login">
  <label for="username">Username</label>
  <input name="username" id="username" type="text"/>
  <label for="password">Password</label>
  <input name="password" id="password" type="password"/>
  <button>Log In</button>
</form>
<style>
  form {
    display: flex;
    flex-direction: column;
    justify-content: center;
    width: 25%;
    margin: 0 auto;
  }
  input {
    margin: .25em 1em 1em;
    display: block;
  }
  label {
    margin: 0 .5em;
  }
</style>

现在您已经看到了所有的添加内容,让我们来讨论一下。除了新的导入和导出之外,您接下来会注意到的下一个变化是 Svelte 指令检查 form?.msg 是否已设置。如果已设置,我们将显示该消息。

数据与表单

form 属性来自我们的登录操作返回的数据(将在下一节中创建)。记住,我们包含 export let data; 以获取 load() 返回的数据属性。同样,我们包含 export let form; 以检索由表单操作返回的数据。返回的数据也可以通过应用程序中的 $page.form 存储在任何地方检索。

下一个重大变化是添加了 <form> 元素。它使用了我们之前导入的 enhance 模块,将 HTTP 方法设置为 POST,并将数据发送到位于 src/routes/login/+page.server.jslogin 操作。我们必须将 HTTP 方法设置为 POST;否则,我们的表单将尝试通过 GET 请求提交数据,我们不希望在不安全的情况下发送密码。

我们随后包括了输入、标签和按钮的适当标记。目前,我们只引用了一个表单操作来管理用户的登录。如果我们想启用注册或密码重置功能,并保持后续操作与我们的登录操作在同一文件中,我们可以利用 formaction 属性。然而,formaction 属性旨在用于您有多个按钮引用同一 <form> 元素内的不同端点时。在密码重置场景中,我们可能需要另一个 <form> 元素来指定发送密码重置链接的电子邮件。同样,在注册的情况下,我们可能需要获取用户的电子邮件,以及他们的用户名和密码,因此在只接受用户名和密码详情的表单中包含这两者几乎没有意义。在这些情况下,为每个功能创建一个单独的表单并直接在 <form> 元素上指定操作会更有意义。出于项目组织的目的,仍然可能有必要将有关身份验证的逻辑保持在单个 +page.server.js 文件中。

创建表单的实际标记相对简单易行。我们刚刚看到,我们需要指定方法以及要调用的动作。为了获取从我们的动作返回的信息,我们需要在页面上包含export let form;并利用从动作返回的数据。现在你已经看到了它的几种变体,你应该能够轻松地创建表单以从用户那里获取数据。当然,如果我们不利用提交的数据,表单就没有什么用处。在下一节中,我们将创建一个动作来处理表单收集的数据。为了确保我们的动作能够顺利工作,我们需要设置一个数据库并讨论一些安全最佳实践。

分析动作

第四章中,我们花了一部分时间来探讨动作是如何工作的。现在,是时候更深入地研究它们以及它们在底层是如何工作的了。但在我们开始之前,我们需要设置另一个模拟数据库并简要讨论安全问题。一旦我们完成这些,我们将完成向我们的应用程序添加逻辑并验证有效用户的工作。本节将涵盖以下内容:

  • 数据库设置

  • 密码和安全

  • 登录动作

经过所有这些,你将大致了解如何最终为你的 SvelteKit 应用程序创建一个登录表单。

数据库设置

当然,这并不是一个真正的数据库。我们将利用另一个 JSON 文件来存储我们的用户数据,并帮助我们模拟查找用户及其散列密码。它看起来应该像这样:

src/lib/users.json

[
  {
    "id": "1",
    "username": "dylan",
    "password": "$2b$10$7EakXlg...",
    "identity": "301b1118-3a11-...",
    "name": "Dylan"
  },
  {
    "id": "2",
    "username": "jimmy",
    "password": "$2b$10$3rdM9VQ...",
    "identity": "62e3e3cc-adbe-...",
    "name": "Jimmy"
  }
]

此文件是一个简单的数组,包含两个用户对象以及与我们的用户相关的各种属性。在这个演示中,这些属性的值是微不足道的,但identitypassword的值对我们特别感兴趣,正如我们将在下一节中看到的。identity属性通常对应于存储在另一个表中的用户会话 ID。它应该使用一个唯一的标识符,并且不容易被猜到。如果它很容易被猜到,任何人都可以通过在他们的设备上创建带有有效会话 ID 的标识符 cookie 来认证我们的应用程序作为任何用户。在这个例子中,identity使用了Crypto Web API来生成一个随机的通用唯一标识符UUID)。不应该使用 Crypto Web API 进行密码散列。在这个演示中,我们只会用它来创建一个将被保存在用于认证用户的 cookie 中的 UUID。为了测试目的,这个值可以是任何唯一的字符串,但这个例子旨在相对真实。为了使这些材料不偏离学习 SvelteKit 的指导方针,我们只需要包括这些内容来构建我们的模拟用户数据库。

密码和安全

由于身份验证是网络应用中非常常见的功能,如果不进一步阐述如何正确实现它,那就太不合适了。而且因为如果实现不当,可能会产生灾难性的后果,我们将学习如何安全地实现它。虽然我们目前还没有连接到真实的数据库,而是将用户密码存储在 JSON 文件中(除了演示目的之外,强烈建议不要这样做),但我们将观察如何使用另一个通过 npm 安装的包正确地哈希密码。

要继续前进,我们需要安装bcrypt。在你的终端中,在项目目录中运行以下命令:

npm install bcrypt

完成这些后,我们可以使用以下代码生成哈希。这段代码将是临时的,因为它将为我们提供一个方便的方法来生成密码的哈希以及用户对象identity属性的 UUID。然后可以将这些添加到你的users.json文件中,模拟从数据库中查找用户密码。我们将在之后演示登录功能来利用它:

src/routes/login/+page.server.js

import bcrypt from 'bcrypt';
export const actions = {
  login: async ({request}) => {
    const form = await request.formData();
    const hash = bcrypt.hashSync(form.get('password'), 10);
    console.log(hash);
    console.log(crypto.randomUUID());
  }
}

这个+page.server.js文件导入了我们刚刚使用 npm 安装的bcrypt模块。然后它创建了一个登录操作,我们的<form>元素会将数据提交到这个操作。它通过crypto.randomUUID();从开发者工具的控制台检索登录表单提交的表单数据。当你在浏览器中导航到/login,填写表单的密码字段,提交它,然后在你的终端中打开服务器控制台时,你将能够将哈希和随机生成的 UUID 复制到每个用户的users.json文件中相应的passwordidentity属性中。这样,你就可以为你的用户创建密码。如果你正在使用本书 GitHub 仓库中的代码,每个用户的哈希都是从以下字符串派生出来的:

  1. password

  2. jimmy

注意

不言而喻,这些被认为是糟糕的密码。在任何情况下都不应该尝试使用这些密码或它们的哈希,除非是在这个演示中。

在讨论安全性的话题时,我们应该抓住这个机会指出一些应避免的做法。重要的是我们开发者不要使用共享变量来存储敏感数据。为了澄清,这并不意味着不能使用变量来存储敏感数据。相反,我们应该避免在表单操作中设置变量,这样变量随后可能会在load()函数中可用。以一个不良做法的例子来说,考虑一个开发者在一个+page.server.js文件的最高作用域级别声明一个变量来存储聊天消息,在表单操作中将消息数据赋值给它,然后在同一文件的load()函数中返回相同的变量。这样做可能会让用户 B 查看用户 A 的聊天消息。通过立即将数据返回到页面,可以避免这种数据泄露。这些相同的指南也适用于 Svelte 存储。在服务器上管理数据时,我们永远不应该设置存储的状态,因为在服务器上这样做可能会使所有该服务器上的用户都能访问到。

现在我们已经知道了如何创建哈希密码和 UUID,我们可以遵循一些关于安全性的基本最佳实践。如果你有任何疑问,请查阅官方 SvelteKit 文档。随着技术的变化,最佳实践也可能随之改变。在下一节中,我们将看到如何通过创建一个将所有这些联系在一起的操作来最终完成登录表单。

登录操作

在完成所有这些设置之后,我们终于做到了。现在我们可以完成表单使用的操作来让我们的用户登录并在他们的浏览器中设置一个 cookie。我相信你现在已经准备好了,让我们深入探讨吧。

在这个文件中,我们之前创建了一些代码来生成密码的哈希值以供测试。我们可以去掉这些代码,用代码替换它,该代码将在我们的数据库中查找匹配的用户名,将提供的密码与找到的用户哈希密码进行比较,并在用户的设备上设置一个 cookie:

src/routes/login/+page.server.js

import bcrypt from 'bcrypt';
import users from '$lib/users.json';
export const actions = {
  login: async ({request, cookies}) => {
    const form = await request.formData();
    const exists = users.filter(user => user.username === form.
      get('username'));
    const auth = exists.filter(user => bcrypt.compareSync(form.
      get('password'), user.password));
    if(!exists.length || !auth.length) {
      return {msg: 'Invalid login!'};
    }
    cookies.set('identity', auth[0].identity, {path: '/'});
    return {msg: 'success!'}
  }
}

在这个新版本中,我们仍然导入了bcrypt模块,但我们还添加了user.json的导入。然后我们向解构的RequestEvent参数中添加了cookies。在设置login操作后,我们获取由<form>元素提交的数据,并将其放入form常量中。接下来,我们使用filter()来检查users数组中每个元素的用户名。任何匹配项都被添加到exists常量中。然后我们再次使用filter()来检查提交的密码与exists中每个用户的哈希密码。如果找到匹配项,它将被添加到auth常量中。如果existsauth的数组中没有项目,我们返回一条消息,表示登录尝试无效。

对抗账户枚举

我们绝不应该返回一个消息,告诉用户他们的用户名(或电子邮件)是正确的,但提供的密码失败了。这样做将允许恶意行为者枚举有效账户,本质上是在短时间内连续猜测用户名。一旦完成,攻击者就可以轻松地编制一个有效用户名的列表,并开始在真实账户上尝试暴力破解密码。由于用户并不以创建强密码而闻名,这可能导致多个账户的账户接管。这就是为什么我们只返回一个消息,告诉用户他们的登录尝试失败了。无论他们的用户名或密码是否正确,这是他们需要自己弄清楚的事情。

如果用户成功登录,我们使用cookies.set()发送Set-Cookie头部信息,告诉客户端将identitycookie 设置为用户的会话 ID,在域名的根目录下。我们必须在选项中指定根路径;否则,我们的 cookie 将默认只在设置的最高级别路由上工作——在这种情况下,仅在诸如/login之类的页面上工作。你可以想象这对用户来说是多么令人沮丧。然后我们可以检查用户是否有权访问应用程序中各个位置的功能。要删除相同的 cookie 并注销用户,我们可以使用cookies.delete(),同时传入 cookie 的名称以及我们的路径。

最后,为了向用户显示他们的登录尝试是否成功,我们需要对我们的根服务器布局进行一些调整。如果你还记得,我们之前只检查了identity === '1'。在实现了假数据库之后,我们可以改为与我们的用户 JSON 文件进行比对:

src/routes/+layout.server.js

import users from '$lib/users.json';
export function load({ cookies }) {
  const data = {
    notifications: {
      count: 3,
      items: […]
    }
  };
  const exists = users.filter(user => user.identity === cookies.get('identity'));
  if(exists.length) {
    const {password, …user} = {...exists[0]};
    data.user = user;
  }
  return data;
}

由于我们需要将identitycookie 的值与users.json中存在的值进行比对,我们首先需要导入它。目前我们不需要对data常量进行任何更改,因此我们可以保留与通知相关的代码。然后我们必须使用filter()来查找是否有任何用户具有从身份 cookie 获取的值,并将找到的这些用户分配给exists常量。如果exists有值,我们就获取第一个找到的值,并利用解构赋值的强大功能来避免将用户的密码传递给data.user。这样做是为了防止包含敏感数据。

现在我们已经将所有这些放在一起,我们可以通过在浏览器中导航到/login并输入适当的详细信息来验证登录是否正常工作。如果你创建了自定义的散列,你需要使用你提供的字符串来成功进行身份验证。提交表单后,我们应该看到状态消息,以及来自src/routes/login/+page.svelte的欢迎消息。

回顾一下,在本节中,我们使用 JSON 文件创建了一个假的用户数据库。我们在该文件中包含了我们的安全密码散列以供检查。当从src/routes/login/+page.svelte中的<form>元素提交用户名和密码时,使用src/routes/login/+page.server.js检索这些数据。然后我们检查用户名以及该用户的散列密码;如果找到匹配项,我们通过cookies.set()发送响应中的Set-Cookie头,并发送一个成功状态消息。如果登录尝试与用户名或密码不匹配,我们返回一个无效登录状态消息。现在我们已经知道如何创建表单并将我们的数据提交到适当的操作,让我们来探讨一些可以改善我们应用程序用户体验的方法。

增强表单

为了减少网页表单中固有的摩擦,SvelteKit 为我们提供了一些选项。我们之前在设置登录表单的use:enhance时看到了这些选项中的第一个。这个选项非常适合防止页面重定向,因为它可以在后台提交表单数据,这意味着我们的页面不需要重新加载。我们还没有看到的是 SvelteKit 所说的快照。在本节中,我们将探讨这两个工具以及它们如何帮助改善您应用程序的体验:

  • enhance

  • 快照

完成本节后,您将能够构建出直观且流畅的表单供您的用户使用,这将大大增加用户接受您应用程序的可能性。

enhance

通过从$app/forms导入enhance,我们可以逐步增强<form>元素的处理流程。这意味着我们可以在不要求页面重新加载的情况下提交数据,这在提交<form>元素时通常是必须的。我们已经看到这个动作出现了几次,但在两种情况下我们都没有讨论它是如何工作的。

enhance采取的第一步是更新form属性。我们通过位于src/routes/login/+page.svelte中的 Svelte 指令观察到这一点,该指令检查form?.msg是否已设置。由于 Svelte 是响应式的,当enhance更新form时,我们可以立即查看更改并显示我们的消息。enhance还会更新$page.form$page.status,这两个都是 Svelte $page存储的属性。这个存储提供了有关当前显示页面的信息。$page.form将包含来自表单操作的相同数据,而$page.status将包含 HTTP 状态码数据。我们第一次在第四章动态路由部分看到了$page存储的例子。

在收到成功响应后,enhance 将重置 <form> 元素,并通过调用 invalidateAll() 强制适当的 load() 函数重新运行。然后,它将解析任何重定向,渲染最近的 +error.svelte(如果发生错误),并将焦点重置到正确的元素,就像页面是第一次被加载一样。

如果 enhance 应用于具有指向完全不同路由的动作的 <form> 元素,enhance form$page。这是因为它的目的是模拟原生浏览器行为,并且像这样跨路由提交数据通常会触发页面刷新。要强制更新这些属性,您需要传递一个回调函数给 enhance。然后,回调函数可以使用 applyAction 来相应地更新存储。applyAction() 函数接受一个 SvelteKit ActionResult 类型,也可以从 $app/forms 中导入。

快照

用户经常遇到的一种令人沮丧的经历是在填写完一个大型表单后,但在提交该表单之前离开页面。无论原因如何,丢失花费了大量时间输入的数据都是痛苦的。

通过在快照中持久化 <form> 数据,我们可以使用户更容易地继续他们离开的地方。用户头痛的减少意味着使用我们的应用程序的体验更好。而且实现起来非常简单,我们只需要导出一个带有 capturerestore 属性设置的 snapshot 常量。要看到它的实际效果,让我们持久化我们之前构建的评论表单数据:

src/routes/comment/+page.svelte

<script>
  import { enhance } from '$app/forms';
  export let form;
  let comment = '';
  export const snapshot = {
    capture: () => comment,
    restore: (item) => comment = item
  }
</script>
<div class='wrap'>
  {#if form && form.status === true}
    <p>{form.msg}</p>
  {/if}
  <form method='POST' action='?/create' use:enhance>
    <label>
      Comment
      <input name="comment" type="text" bind:value={comment}>
    </label>
    <button>Submit</button>
    <button formaction='?/star'>Star</button>
    <button formaction='?/reply'>Reply</button>
  </form>
</div>

在这个新版本中,我们只做了三项重要的更改:

  1. 添加了 let comment = ''; 以便我们可以捕获并恢复到我们的 JS 中的输入值。

  2. 添加了带有 export 的快照对象 const snapshot

    1. 当在导航离开页面之前更新页面时,capture 属性会调用一个匿名函数。这个函数只需要返回我们希望捕获并在稍后恢复的值——在这种情况下,与 comment 相关的值。

    2. restore 在页面加载后立即调用,并将调用它的参数分配给 comment

  3. 我们将评论的 <text> 输入的值绑定到 comment 变量,以便在捕获时检索输入的值,并在恢复时设置。

实施这些更改后,您可以通过在浏览器中打开 /comment 路由,输入一个测试评论,然后导航到另一个页面来测试它们。当您在浏览器中点击 后退 时,您将观察到恢复的数据正如您离开时一样。因为快照将它们捕获的数据持久化到会话存储中,您可以通过打开浏览器开发者工具来观察这些数据。在 Firefox 中,您可以通过点击菜单中的链接找到它,但不会触发恢复,因为它被视为导航到新页面。

了解如何逐步且无缝地增强你的表单可以带来一个让用户不断回访的体验。通过在后台运行表单提交,我们可以利用 Svelte 的响应性为用户提供即时且有用的反馈。并且通过使用快照,我们可以保存用户在简单或复杂表单上的进度。有了这些知识,你现在可以着手将直观的体验构建到你的应用程序中。

摘要

我们本章开始时构建了一个简单的 <form> 元素,用于接受用户名和密码。在同一页面组件中,我们将身份验证尝试的状态反馈给用户。之后,我们创建了一个表单操作,用于查找用户名并比较提供的密码与哈希值。如果成功,我们在用户的设备上设置一个 cookie 来登录用户。如果失败,我们通知用户他们的尝试失败了。我们还简要讨论了围绕使用我们的应用程序对用户进行身份验证的一些安全最佳实践。然后我们考察了如何通过使用 enhance 和快照来改进 <form> 元素的体验。完成所有这些后,我们可以对在未来的 SvelteKit 项目中实施的任何表单充满信心。

到目前为止,我们已经涵盖了所有内容,你应该能够构建一个基本的网站或应用程序。在下一章中,我们将介绍更多高级功能,这些功能可以真正展示使用 SvelteKit 构建的力量。我们将探讨更高级的路由概念,指出它们是如何利用我们已经讨论过的功能,并解释一些尚未涉及的内容。

资源

以下为本章的资源:

第七章:高级路由技术

到目前为止,我们已经涵盖了所有内容,你现在可以放下这本书,使用 SvelteKit 构建一个简单的网站。但如果你想在应用程序中构建更高级的功能,你可能会发现自己难以找到合适的路由层次结构。这是因为当我们谈到动态路由时,我们只是触及了表面。在第四章中,我们讨论了通过传递给我们的路由的参数创建动态页面。在那个例子中,我们通过提供的 slug 加载文章,并将其与我们在演示数据库中找到的匹配。我们无法提前知道 slug 是什么,为每篇文章创建一个新的路由将会过于复杂。相反,我们研究了基于访问的 URL 接收到的 slug 参数。

这只是对动态路由的简要介绍。在本章中,我们将探讨一些更高级的技术,可以帮助你提升你的路由逻辑。我们将研究带有可选参数的路由、未知长度的参数、如何使用正则表达式匹配参数、在路由逻辑冲突的情况下哪些路由将具有优先级,以及更高级的布局技术,包括如何从中退出。

本章将分为以下主题:

  • 使用可选参数

  • 剩余参数

  • 匹配、排序和编码——哦,我的天!

  • 高级布局

到本章结束时,你将掌握 SvelteKit 中可用的各种路由技术。无论你的下一个 SvelteKit 项目的需求如何,你都将拥有解决和应对任何复杂路由难题所需的知识。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter07

使用可选参数

自从我们在第四章创建动态页面部分提到了可选参数,让我们从这里开始。在创建路由中的可选参数时,有一些事情需要考虑。例如,它们不能与另一个路由共存,因为这会导致路由规则冲突。在创建可选路由时,最佳做法是当路由的最后一部分可以有一个默认选项时。许多应用程序会根据用户选择的语言更改 URL。在我们的例子中,我们将展示如何通过选择我们演示商店运营的北美国家来创建一个可选参数。我们实际上不会构建一个完整的商店,而是用它来展示本章中高级路由概念。

首先,让我们在我们的导航中创建一个新的路由,就像我们之前的示例一样:

src/lib/Nav.svelte

<nav>
  <ul>
    <li><a href='/'>Home</a></li>
    ...
    <li><a href='/login'>Login</a></li>
    <li><a href='/store'>Store</a></li>
  </ul>
</nav>

我们需要做的只是再在导航菜单中添加一个链接到我们新的路由的另一个列表项。完成之后,我们可以创建store目录,这个目录将包含本章的所有示例:

src/routes/store/+layout.svelte

<h2>Store</h2>
<ul>
  <li><a href="/store/locations/">Locations</a></li>
  <li><a href="/store/products/">Products</a></li>
</ul>
<slot />

这个简单的布局将允许我们导航本章中涵盖的各种概念。在添加链接后,我们使用在早期章节中提到的 Svelte <slot>元素。花点时间创建所有必要的目录。接下来,我们还将为/store路由创建一个简单的着陆页:

src/routes/store/+page.svelte

<h3>Welcome to the Store!</h3>

在创建了/store路由和locations目录所需的文件之后,我们现在将创建另一个目录。与这个目录的主要区别在于它使用双方括号([[country]])作为名称。这是 SvelteKit 区分可选路由和不可选路由的方式。因为我们正在创建一个带有可选参数的页面,所以我们不需要在locations目录中创建+page.svelte。相反,我们将它添加到[[country]]目录中。为了继续,我们将创建适当的+page.svelte+page.js文件:

src/routes/store/locations/[[country]]/+page.svelte

<script>
  export let data;
</script>
<h2>You're viewing the {data.country.toUpperCase()} store.</h2>
<ul>
  <li><a href="/store/locations">North America</a></li>
  <li><a href="/store/locations/ca">Canada</a></li>
  <li><a href="/store/locations/me">Mexico</a></li>
  <li><a href="/store/locations/us">United States</a></li>
</ul>

到现在为止,这应该看起来非常熟悉。我们使用export let data;以便在下一个文件中访问load()提供的信息。我们使用这些数据来告知用户他们正在查看哪个国家的商店位置,并显示大写缩写。然后我们创建一个无序列表,其中包含指向我们将在下一个文件中提供的各种允许路由的链接。

[[country]]/+page.js中,我们需要将路由提供的参数与我们的商店运营的国家列表进行比对。我们可以使用以下代码来完成:

src/routes/store/locations/[[country]]/+page.js

export function load({ params }) {
  const codes = [
    'na',
    'ca',
    'me',
    'us'
  ];
  const found = codes.filter(country_code => country_code === params.country);
  return {country: found[0] ?? 'na'};
}

到目前为止,导出load()函数应该对你来说也很熟悉。在这个特定的函数中,我们只需要访问params,所以我们解构了传递给load()RequestEvent对象。然后我们声明了一个codes数组,它作为一个允许路由的列表。在下一行,我们通过在codes上运行filter()来检查提供的路由是否在允许路由的数组中。然后filter()返回一个包含所有匹配项的数组,并将其分配给found常量。然后我们可以返回一个包含country属性的数组,它被分配给found中的第一个值。如果found的第一个值是空的,我们将默认显示所有北美地区的值。在这种情况下,na

一旦我们完成所有这些,我们就可以打开我们的应用程序,点击 load()。当选择其他任何选项时,缩写会相应更新。正如之前提到的,可选参数在路由的最后一部分可以有一个默认选项时效果最佳。如果可选参数被包含在路由中间的某个位置,那么路由机制将理解随后的路由部分为可选参数。

在这个例子中,我们使用双方括号 [[ ]] 创建了一个新的路由。虽然这个例子距离作为一个完整的存储还有很长的路要走,但它应该可以阐明如何在路由中使用可选参数。现在,既然你已经理解了可选参数,让我们看看我们如何可以处理未知长度的路由。

剩余参数

就像 JavaScript 函数可以使用剩余参数操作符()接受剩余参数一样,我们的路由也可以这样做。通过在单个方括号内使用剩余参数操作符,我们可以允许指定路由具有可变长度。当创建类似文件浏览器这样的东西时,URL 应该匹配一个路径,然后通过 URL 使页面内容可共享,这个特性非常有用。

为了看到这个概念的实际应用,让我们在我们的存储中创建一个 products 路由。首先添加 src/routes/store/products/+layout.svelte,这样我们就可以轻松地导航产品:

src/routes/store/products/+layout.svelte

<h3>Products</h3>
<ul>
  <li><a href="/store/products/shirts">Shirts</a></li>
  <li><a href="/store/products/shirts/mens">Mens Shirts</a></li>
  <li><a href="/store/products/shirts/mens/tshirts">Men's T-shirts
    </a></li>
  <li><a href="/store/products/shirts/mens/tshirts/cotton">Men's 
    Cotton T-shirts</a></li>
  <li><a href="/store/products/shirts/mens/tshirts/cotton/
    graphic">Men's Graphic Cotton T-shirts</a></li>
</ul>
<slot />

这个 Svelte 组件相当简单。它由标题、无序列表、列表项以及指向各种产品的链接组成。同样,我们使用了 Svelte 的 <slot /> 元素来保持我们在页面上的导航,当我们点击时。接下来,让我们创建一个可以处理我们刚刚提供的不同长度的产品端点。为此,我们将创建一个使用方括号和剩余参数操作符作为目录名称的文件夹。在这个例子中,我们将使用 [...details] 作为目录名称。现在,让我们看看 +page.js+page.svelte 文件:

src/routes/store/products/[...details]/+page.js

export function load({ params }) {
  return params;
}

由于我们不是构建整个存储,我们可以保持这个存储非常简单。因为我们试图展示 SvelteKit 路由机制中剩余参数的工作方式,我们将在 load() 中简单地返回 params。一个更健壮且实用的例子可能会从 params 中获取值,并使用它来过滤从数据库检索的产品列表。然后,这些数据可以从 load() 返回,以便在下一个文件中渲染每个产品。

现在,为了展示剩余参数值的变化,我们将添加以下 +page.svelte

src/routes/store/products/[...details]/+page.svelte

<script>
  export let data;
</script>
<h4>Product page</h4>
{#if data.details}
  <p class='red'>{data.details}</p>
{:else}
  <p>No product selected! Try clicking one or adding your own URL.
{/if}
<style>
  .red {
    color: red;
    font-weight: bold;
  }
</style>

再次强调,我们保持简单。我们不是显示 data 中可能可用的所有产品,而是简单地使用 Svelte 的 {#if}{:else} 指令来演示 details 参数如何变化。如果 data.details 为空,我们显示一个默认消息。如果它有值,我们以粗体红色文本显示它。如果我们给目录起了一个不同的名字,那么这个名字就是访问参数的方式。尝试点击一些指向各种产品的链接,注意浏览器中的 URL 如何变化,红色中的值也是如此。如果你在 /store/products/ 之后向 URL 添加自己的值会发生什么?

使用这些高级路由技术时,我们必须考虑一些影响。例如,正如可选参数在 URL 的末尾部分工作时效果最佳一样,剩余参数后面不能跟一个可选参数。如果我们尝试在剩余参数之后提供可选路由部分,它们将被剩余参数消耗。要查看 Vite 开发服务器抛出的错误,尝试在 /[...details]/ 内创建一个可选目录。你不必担心意外这样做,因为 Vite 会为你留意,但在规划应用程序的路由时了解这一点仍然很重要。

如果你发现自己正在将未知长度的路由构建到应用程序中,考虑使用 SvelteKit 的剩余参数来创建它们。它们不仅处理那些不确定的长度,而且逻辑可以轻松地融入现有的 SvelteKit 应用程序流程中。

匹配、排序和编码——哦,我的天啊!

如果你不太熟悉 SvelteKit 更高级的路由技术细节,它可能会很快变得难以控制。为了应对意外情况,我们将探讨一些你可以使用的策略,以确保你的应用程序的路由按预期工作。在本节中,我们将介绍如何确保参数的类型是你所期望的。然后,我们将检查 SvelteKit 如何处理可以解析到多个路由的 URL。最后,我们将提供一些关于 URL 编码的信息。你可以期待看到以下子章节:

  • 匹配

  • 排序

  • 编码

一旦完成,你将更接近掌握 SvelteKit 应用程序的路由。

匹配

我们已经探讨了如何在路由中使用可选和剩余参数。但回想一下我们在 第四章 中创建的示例,它涉及到动态路由。在新闻部分,我们只检查提供的 [slug] 参数是否存在于我们的数据库中。如果我们想确保传递给数据库的值实际上是一个 slug,我们可以创建一个自定义匹配器来完成这项工作。

要使用 SvelteKit 创建匹配器,我们向src/params/添加一个具有描述性名称的 JS 文件。如果该目录尚不存在,请不要担心!您现在可以简单地创建它。这里的文件导出一个单个函数:match()。该函数接受一个字符串参数并返回一个布尔值。因为传递给函数的值是字符串,我们将使用正则表达式regex)来确保传入的参数是我们希望在路由上强制执行的类型。正则表达式可能一开始看起来令人畏惧,但网上有大量工具可以帮助创建和学习正则表达式规则。请参阅本章末尾以获取更多资源。现在让我们为我们的新闻文章创建一个匹配器,以确保在执行数据库查找之前传递了正确的 slug:

src/params/slug.js

export function match(str) {
  return /^[a-z0-9]+(?:-[a-z0-9]+)*$/gim.test(str);
}

很明显,匹配器不需要过于复杂。它只需要导出match()函数,该函数接受一个字符串参数。然后,此匹配器将该字符串与正则表达式字面量进行比较,对于匹配返回 true,对于不匹配返回 false。此正则表达式测试一个或多个字符串或数字字符后跟一个字符,该字符必须后跟一个或多个字符串或数字字符。以字符结尾的字符串被认为是无效的。

应用匹配器

当将匹配器应用于特定路由时,=字符后面的值是给定匹配器的名称。另一个例子可能包括创建一个测试整数的匹配器。该规则可以通过设置参数如下来应用于动态路由:[param=integer],其中params/integer.js是匹配器文件的名称。

要将我们刚刚创建的匹配器应用于我们的新闻文章,我们需要将src/routes/news/[slug]重命名为src/routes/news/[slug=slug]。一旦我们相应地调整了路由中的参数,我们就可以像之前一样查看我们的新闻文章。当然,现有的文章将匹配得很好,因为它们包含有效的 slugs。为了测试在运行数据库查找之前是否应用了此匹配器,我们可以在src/lib/articles.json中创建一个新文章。新文章对象的内容和标题无关紧要,但通过创建一个具有无效 slug 的文章,我们可以确认匹配器正在工作。一旦您创建了一个具有不良 slug 的文章,尝试查看它。您应该收到404 未找到错误,尽管文章存在。这是因为传递给动态参数的值没有匹配我们提供的正则表达式。

虽然正则表达式可能难以处理,但知道 SvelteKit 使开发者能够利用其背后的力量是令人欣慰的。能够这样做确保我们的应用程序按预期工作。然而,仍然可能存在 SvelteKit 路由以意外方式到达端点的情况。为了避免这些情况,让我们看看哪些路由规则比其他规则具有优先级。

排序

由于 URL 完全可能匹配多个路由,因此了解哪些路由规则将以何种顺序执行非常重要。类似于 CSS 规则被赋予不同的权重,SvelteKit 的路由规则也是如此。那么,在何时以及如何避免冲突?

  1. 更具体的路由总是优先于不那么具体的路由。没有参数的路由被认为是最高级别的。例如,src/routes/about/+page.svelte 将在 src/routes/[param]/+page.svelte 之前执行。

  2. 将匹配器应用于动态参数将赋予它比没有匹配器的参数更高的优先级。调用 src/routes/news/[slug=slug]/+page.svelte 将比 src/routes/news/[slug]/+page.svelte 具有更高的优先级。

  3. 可选参数和剩余参数的优先级最低。如果它们不是路由的最后一部分,则会被忽略。例如,src/routes/[x]/+page.svelte 将在 src/routes/[...rest]/+page.svelte 之前执行。

  4. 冲突解决者由参数的字母顺序决定。也就是说,src/routes/[x]/+page.svelte 将在 src/routes/[z]/+page.svelte 之前执行。

如果你计划利用 SvelteKit 更高级的路由功能,那么理解这些规则是绝对必要的。尝试自定义项目中创建的路由,并调整它们以创建冲突。看看你是否能自己解决冲突或预测哪些页面将在其他页面之前被调用。接下来,我们将探讨如何通过编码管理 URL 中的特殊字符。

编码

在他们的职业生涯中,每个开发者都遇到过编码问题,但没有人花时间去完全理解它们。既然你是一位忙碌的开发者,渴望开始构建,而且你可能不是来这本书上听关于编码的讲座的,所以我们会尽量简短。为了防止在构建使用特殊字符的路由时产生严重的挫败感,SvelteKit 允许我们编码路由,以便它们可以在 URL 中使用。当然,一些字符,如 [ ] ( ) #% 在 SvelteKit 或浏览器中具有特殊含义,因此它们大多被禁止使用。然而,当在路由机制中正确编码并在浏览器中进行 URL 编码时,它们仍然可以使用。

在 SvelteKit 中创建包含特殊字符的路由时,特殊字符的写法类似于动态参数,放在方括号 [ ] 内。然而,它们前面会加上 x+ 前缀,后面跟着字符的十六进制值。一个例子是在创建指向 /.well-known/ 目录的路由时,它可以表示为:src/routes/[x+2e]well-known/+page.svelte。在大多数情况下,此路由应该没有问题,编码也不必要,但我们使用它来演示。请继续在你的项目中创建它。在浏览器中,导航到开发站点,并将 /.well-known/ 路由附加到地址栏以确认它是否工作。现在尝试创建路由 /?-help/。因为 src/routes/[x+3f]-help/+page.svelte。但我们将无法访问 /?-help/ 的网页。相反,我们需要在 /%3f%-help/ 访问该特定路由。在路由中使用特殊字符时,请考虑事先使用十六进制值对其进行编码。

要获取字符的十六进制值,你可以使用以下 JS 片段:':'.charCodeAt(0).toString(16); 其中 : 是你想要获取十六进制值的特殊字符。我们不仅限于只使用十六进制值来处理简单的文本。SvelteKit 的路由也支持 [u+xxxx],其中 xxxx 是 Unicode 码点。

为了确保我们的应用程序按预期运行,了解如何正确编码特殊字符是至关重要的。我们还探讨了如何将匹配器应用于路由,以确保动态参数的类型符合我们的预期。并且通过简要了解路由是如何优先于其他路由的,你应该可以放心地在应用程序的路由中探索更高级的技术。

高级布局

应用程序越复杂,其结构也越复杂。为了保持应用程序逻辑的有序性,我们可以利用更高级的路由机制,例如布局组和断开。通过使用布局组,我们可以组织各种布局组件,而不会使应用程序的 URL 变得杂乱。并且通过在页面和模板中插入简单的语法,我们可以将布局或页面从其层次结构中分离出来,同时保持我们应用程序的结构完整。

由于我们将应用程序组件组织成逻辑分组,因此将应用程序功能组织成逻辑分组也是有意义的。为了使用实际示例进行演示,考虑那些仅对已登录用户可用而对匿名用户不可用的界面组件。当用户登录时,他们可以通过评论与其他用户互动,更改他们的个人资料信息或查看自己的通知。未登录的网站用户不应看到这些组件中的任何一项。根据我们到目前为止对布局的了解,为每种类型的用户创建不同的布局可能会使我们遇到影响应用程序干净 URL 的问题。这就是我们可以利用 SvelteKit 的布局组的地方。

在创建布局组时,使用括号 ( ) 包围目录名称。该布局组内部的所有内容都将包含在该组中,并槽位在找到那里的 +layout.svelte 文件中。为了演示布局组,我们将创建两个组:(app)(site)。在 (app) 中,我们将移动与应用程序功能相关的逻辑,在 (site) 中,我们将移动在基本网站上常见的一些逻辑。我们新的 routes 目录结构应该看起来像这样:

src/routes/

src/
|_routes/
   |_(app)/
   |  |_comment/
   |  |_login/
   |  |_notifications/
   |  |_store/
   |
   |_(site)/
   |  |_about/
   |  |_fetch/
   |  |_news/
   |
   |_api/
   |_+layout.server.js
   |_+layout.svelte
   |_+page.svelte

在调整文件夹后,我们可以为每个新的布局组创建一个布局:

src/routes/(app)/+layout.svelte

<div class='app_layout'>
  <slot />
</div>
<style>
  .app_layout {
    background: #cac4c4;
    padding: 1rem;
  }
</style>

在这个布局文件中,我们将要在 Svelte <slot /> 指令中渲染的所有内容包裹在一个应用背景颜色的 <div> 元素中。为了简化,我们只是尝试展示不同的布局组是如何工作的。下一个文件会做完全相同的事情,但应用不同的颜色:

src/routes/(site)/+layout.svelte

<div class='site_layout'>
  <slot />
</div>
<style>
  .site_layout {
    background: #83a8ee;
    padding: 1rem;
  }
</style>

保存这些布局后,你会注意到当在浏览器中导航到 comment/login/notifications/store/ 时,应用程序显示的背景颜色与导航到 about/fetch/news/ 时显示的颜色不同。然而,我们的 URL 完全相同!

对于想要从现有层次结构中跳出特定布局或页面的情况,我们可以在文件名中附加 @ 字符。例如,+page@+layout@。然后我们可以跟随着我们希望它直接继承的目录名称。如果没有提供 @ 字符后面的名称,则将使用根布局。我们可以通过将 src/routes/(app)/store/products/[...details]/+page.svelte 重命名为 src/routes/(app)/store/products/[...details]/+page@(app).svelte 来看到这个功能的效果。这样做将产品页面从产品和存储布局中移出。尝试将其重命名为 +page@store.svelte 以保留存储布局,或重命名为 +page@.svelte 以将其完全返回到根布局。当然,我们的产品链接不再可见,因为显示它们的标记包含在 src/routes/(app)/store/products/+layout.svelte 中,但我们只是试图展示如何跳出页面的直接布局。这个功能对于将应用程序逻辑分离到管理或认证部分,同时保持 URL 不受影响非常有用。

我们刚刚看到,如何通过在 Svelte 组件命名约定中使用 @ 符号来跳出布局。当我们包含 @ 符号后跟我们想要的布局名称时,文件将直接继承该布局名称,而不是所有布局之间的布局。我们还看到了如何创建布局组以保持我们的项目结构,同时不破坏应用程序的 URL。涵盖了一切之后,你应该能够满足任何 SvelteKit 项目的最复杂的路由需求。

摘要

在本章节之前的章节中,我们介绍了核心路由概念。在本章中,我们探讨了 SvelteKit 中可用的更高级技术。这些技术可以帮助我们进一步自定义应用程序并解决边缘情况。在路由方面,我们现在理解了如何创建具有默认值的可选参数。我们还看到了如何使用剩余参数创建未知长度的可共享 URL。匹配被证明对于确保我们的应用程序正在接收预期类型的参数非常有用。我们还看到了 SvelteKit 如何优先考虑某些路由规则,这对于理解当 URL 匹配多个路由时的执行顺序非常有帮助。在介绍了如何在路由中编码特殊字符之后,我们探讨了如何创建布局组,甚至跳出布局层次结构,同时保持应用程序逻辑完整。如果你完成了这一章,并且对所学内容感到满意,你将能够处理在构建 SvelteKit 应用程序的路由时遇到的任何最奇怪的边缘情况。

在下一章中,我们将短暂地休息一下路由,分析各种 SvelteKit 适配器和它们所使用的环境。我们还将更仔细地研究页面选项,并尝试第一次为生产环境构建我们的应用程序。

资源

第三部分 - 补充概念

本部分旨在介绍 SvelteKit 的额外必需概念。它首先向您展示如何使用 SvelteKit 的适配器系统轻松地为生产环境生成构建,并展示了如何做到这一点。然后,它涵盖了如何使用钩子来操作进入和离开 SvelteKit 应用程序的数据。从那里,它讨论了如何利用 Vite 最好地导入静态资源。然后,它解释了 SvelteKit 中可用的各种模块,这些模块使整个框架成为可能。该部分接着介绍了如何确保 SvelteKit 应用程序能够提供给尽可能多的用户,同时提高搜索引擎排名。最后,它总结了各种对任何试图用 SvelteKit 为其下一个项目提供动力的开发者都非常有价值的资源。

本部分包含以下章节:

  • 第八章构建和适配器

  • 第九章钩子和错误处理

  • 第十章管理静态资源

  • 第十一章模块和密钥

  • 第十二章增强可访问性和优化 SEO

  • 附录 示例和支持

第八章:构建和适配器

在前四章中,我们花费了大量时间讨论各种路由技术。从简单的路由到可以渲染为静态 HTML 并加快我们应用速度的页面,再到更复杂的策略,通过使用正则表达式确保数据是预期的类型。虽然这些技术代表了 SvelteKit 的核心功能,但它们并不是全部。SvelteKit 中另一个有用的特性是它能够通过适配器在几乎任何环境中运行。在我们深入探讨各种适配器和它们的配置之前,我们将分析创建 SvelteKit 应用程序生产构建所涉及的过程。

首先,我们需要解决如何为生产环境构建我们的应用程序。Vite 使这一步变得简单,然后我们将转向如何使用不同的适配器调整不同环境的构建。虽然每个适配器都有自己的要求,但讨论它们所有很快就会变得重复。相反,我们将专注于三个不同的适配器,它们各自适合独特的环境。

本章将涵盖以下主题:

  • 创建构建

  • 适配应用

一旦完成,你将能够构建和部署你自己的应用程序到多种平台类型,包括静态主机、Node.js 以及众多无服务器环境。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter08

创建构建

在其他关于 Web 开发框架的书籍中,关于创建应用程序生产构建的部分通常要到书的最后才出现。但鉴于 SvelteKit 和 Vite 使这一过程变得如此简单,实际上没有等待的理由。我相信你急于尽快让你的应用程序可用,所以让我们直接进入正题。

到目前为止,我们只在我们开发环境中工作过我们的应用程序。本质上,当我们运行npm run dev命令时,Vite 在本地机器上启动了一个开发服务器。要关闭开发服务器,我们使用Ctrl + C。为了准备我们应用程序的生产构建,我们可以使用提供的npm脚本:

npm run build

如果你打开项目的package.json文件,你会注意到这个特定的脚本运行了vite build命令。它确实做了它听起来像的事情,通过启动 Vite 的构建过程,这涉及到 Rollup 的打包过程、打包静态资源以及运行配置的适配器。命令完成后,你会在终端中看到各种文件及其大小。如果有任何构建错误,它们将在这里显示。

默认情况下,完全构建的应用程序将被输出到 .svelte-kit/ 目录。要更改此目录,我们可以打开 svelte.config.js 文件,并将首选的目录名称传递给 config.kit.outDir 选项。当打开 build 文件夹时,您会注意到构建的应用程序结构与我们的源代码不同。这当然是出于设计考虑,并且根据我们使用的适配器而有所不同。为了确认我们的生产构建工作正常,我们可以使用以下命令进行预览:

npm run preview

运行后,Vite 将提醒我们应用程序可以访问的新 URL 和端口。我们可以在那里查看和使用应用程序,就像运行我们的开发服务器时一样。不同之处在于,文件更改不会自动更新,因为用于运行此版本应用程序的文件需要在每次构建时重新生成——除非在 vite.config.js 项目中设置了 build.watch

在运行我们应用程序的第一个官方构建之后,在开始讨论适配器之前,有一些事情需要注意。首先,回想一下第四章,我们在根目录的 +layout.js 文件中介绍了 ssr。只需记住,预渲染发生在应用程序构建过程中。因此,在预渲染页面上发生的任何 fetch() 请求都会在构建时发生。我们可以通过简单更改我们的 fetch 示例来确认这一点。在我们之前设置 ssr = false 的地方,现在将其更改为 prerender = true;

src/routes/(site)/fetch/+page.js

const key = 'DEMO_KEY'; // your API key here
export const prerender = true; // change this line
export function load() {
  const pic = fetch(`https://api.nasa.gov/planetary/apod?api_key=${key}`)
    .then(response => {
      console.log('got response');
      return response.json();
    });
  return {pic};
}

一旦我们在这个特定路由上启用了预渲染,我们就可以继续重新构建应用程序。现在,当我们打开我们的 build 文件夹时,我们应该看到位于 .svelte-kit/output/prerendered/pages/fetch.html 路径的文件。打开此文件后,我们将看到图像元素以及从 NASA API 拉取的所有其他数据,这些数据以静态 HTML 的形式显示。这证实了预渲染发生在构建过程中。这对于开发者在为生产环境准备应用程序时需要非常注意的事情。

在构建您的应用程序时,还有一些其他的特点需要注意:

  • 禁用 csrssr 将导致渲染一个空白的页面。

  • 依赖于表单操作的页面无法进行预渲染,因为页面依赖于 HTTP POST 请求。

  • 使用 url.searchParams. 的页面也无法进行预渲染。

  • 基于路由中的动态参数加载数据的页面可以进行预渲染;然而,SvelteKit 需要观察这些端点的链接才能进行预渲染。

如演示所示,为生产环境准备应用程序相当简单。Vite 快速打包依赖项,甚至提供了预览应用程序的方法。尽管如此,在打包应用程序之前,还有一些事情需要考虑。在下一节中,我们将查看一些常用的适配器以及它们如何为预期的环境生成特定的构建。

适配应用程序

现在我们已经知道了如何构建我们的应用程序,我们可以探索一些将应用程序转换为特定环境的适配器。在我们整个项目开发过程中,我们一直在使用@sveltejs/adapter-auto包。到目前为止,我们没有遇到任何问题,但如果我们想要构建并部署我们的应用程序到真实的生产环境,我们需要更加具体。虽然adapter-auto很好,但它不接受任何配置选项,并且仅与 Cloudflare Pages、Vercel、Netlify 和 Azure Static Web Apps 兼容。如果我们想在 Node.js 服务器或其他地方托管我们的应用程序,我们需要使用不同的适配器。让我们在下一节中探索一些可用的不同选项。我们将查看的适配器如下:

  • adapter-node

  • adapter-cloudflare

  • adapter-static

这个列表并不全面,因为 SvelteKit 项目支持许多其他环境。同时,SvelteKit 社区继续为各种平台创建和发布适配器。在构建自己的适配器之前,请务必检查 Svelte 社区资源。

adapter-node

首先,我们将从adapter-node开始,因为大多数 Web 开发者至少对 Node.js 环境有些熟悉。可以通过运行以下命令来安装此适配器:

npm install -D @sveltejs/adapter-node

安装后,我们可以通过将其导入到我们的svelte.config.js文件中并相应地指定适配器来将其添加到我们的项目中:

svelte.config.js

import adapter from '@sveltejs/adapter-node';
const config = {
    kit: {
        adapter: adapter(),
        alias: {
            db: '/src/db.js',
            img: '/src/lib/images'
        }
    },
};
export default config;

与之前一样,这个配置的不同之处在于它导入的是adapter-node而不是adapter-auto

一旦更换了适配器,我们就可以通过在终端中运行npm run build来为 Node.js 环境构建应用程序。默认情况下,此适配器将构建的应用程序输出到build/目录。

到目前为止,在这个项目中,我们只包含了一个依赖项,那就是 bcrypt。如果我们没有包含这个依赖项,我们甚至不需要进行下一步。但是,由于找到不使用其他依赖项的项目似乎很少见,所以我们最好还是涵盖这一点。为了确保我们的 Node.js 生产环境能够访问所有必需的依赖项,我们需要安装它们。我们通过将 package.jsonpackage-lock.json 复制到 build/ 目录来实现这一点。为了确保我们的构建能够成功运行,我们将模拟自动化部署。我们可以通过将整个 build/ 目录(现在也包括 package.jsonpackage-lock.json)复制到机器上的另一个位置来实现这一点。一旦完成,我们就可以在相同的目录中使用 npm 的 clean install 命令来下载所需的生成依赖项。这是自动化部署和持续集成环境的推荐安装方法。我们不需要开发依赖项,因为 SvelteKit 现在已经打包成纯 JS,所以命令看起来是这样的:

npm ci --omit dev

在构建所在的同一目录下运行此命令将下载所需的依赖项。

然后,我们可以使用以下命令启动应用程序:

ORIGIN=http://0.0.0.0:3000 node build

在这种情况下,build 是我们希望 Node.js 定位的目录名称。我们还指定了 ORIGIN 环境变量,以便 adapter-node 可以正确确定应用程序的 URL。如果没有这个,SvelteKit 会阻止任何 POST 请求,作为对 0.0.0.0:3000 的保护措施。要更改 IP 地址或监听端口,我们可以在运行 node build 之前设置这些环境变量。例如,在 127.0.0.1:8000 上启动应用程序将看起来像这样:HOST=127.0.0.1 PORT=8000 ORIGIN=http://127.0.0.1:8000 node build

为了进一步自定义构建,此适配器提供了以下选项:

  • out – 一个字符串,指定构建应该输出的目录。默认为 build,且不应在末尾包含 / 字符。

  • precompress – 一个默认为 false 的布尔值,控制是否使用 gzipbrotli 对资源和预渲染页面进行压缩。

  • envPrefix – 一个字符串值,指定应用于环境变量的前缀,这在你的托管提供商不提供标准环境变量访问(如 HOSTPORT)时非常有用。设置此值将允许你创建自己的环境变量。

  • polyfill – 一个默认为 true 的布尔值,允许你指定构建是否应该包含添加了在旧版 Node.js 版本中不可用功能的 polyfills。

在我们刚刚讨论的所有内容之后,你应该能够将你的 SvelteKit 应用程序部署到几乎任何 Node.js 环境中。如果目标环境限制了你的控制权限,你应该能够使用各种选项自定义构建。由于 Node.js 环境如此普遍,不解释adapter-node背后的某些功能以及它如何简化你的部署过程将是一个巨大的错误。

adapter-cloudflare

虽然adapter-node在运行 Node.js 应用程序时很棒,但还有一些适配器允许我们无需管理、配置或维护服务器即可部署。例如,Vercel、Netlify 和 Cloudflare 等平台都提供这些服务,并赋予开发者比以往任何时候都更快地交付代码的能力。为了演示目的,让我们看看部署到 Cloudflare Pages 有多简单。

首先,我们将像安装其他包一样安装适配器:

npm i -D @sveltejs/adapter-cloudflare

完成后,我们可以更改我们的svelte.config.js以反映新的适配器。同样,我们只需要导入新的适配器:

svelte.config.js

import adapter from '@sveltejs/adapter-cloudflare';
const config = {
    kit: {
        adapter: adapter(),
    }
};
export default config;

在深入探讨之前,重要的是要注意,我们迄今为止构建的应用程序bcrypt包之前,我们的应用程序将无法构建,因为bcrypt广泛使用了 Node.js API。为了成功构建和部署应用程序,我们需要做一些更改。

如果你不想在你的应用程序中做出这些更改,你可以在每章开头列出的技术要求部分下找到这本书的代码仓库的分支创建一个分支。在那个仓库中,有一个标记为cloudflare的分支,其中包含了所有必要的更改。在创建分支时,请确保选择cloudflare分支。至于必要的更改,让我们先简要地考察一下:

  1. 使用npm uninstall命令卸载bcrypt

  2. 如下代码片段所示,在src/routes/(app)/login/+page.server.js中移除对bcrypt的引用。

  3. src/lib/users.json中的密码更改为纯文本值:

src/routes/(app)/login/+page.server.js

import users from '$lib/users.json';
export const actions = {
  login: async ({request, cookies}) => {
    const form = await request.formData();
    const exists = users.filter(user => user.username === form.get('username'));
    const auth = exists.filter(user => user.password === form.get('password'));
    if(!exists.length || !auth.length) {
      return {msg: 'Invalid login!'};
    }
    cookies.set('identity', auth[0].identity, {path: '/'});
    return {msg: 'success!'}
  }
}

这个+page.server.js的新版本只是简单地移除了对bcrypt包的引用,并改为将password字段中提供的文本与src/lib/users.json文件中的纯文本值进行比较。从安全角度来看,这有多么糟糕是毋庸置疑的,我信任你永远不会在这个 特定演示 之外这样做

现在我们已经移除了所有需要 Node.js 的依赖项,我们可以继续将我们的部署到 Cloudflare。首先,我们应该登录到 Cloudflare 账户,导航到这里的cloudflare。通常,这将是main分支。

从那里,我们可以设置几个选项。Cloudflare 通过询问我们是否使用框架来简化下一步,而我们确实在使用框架。从框架预设下拉菜单中选择SvelteKit将自动填充构建命令构建输出目录字段。更多信息请参见图 8.1

图 8.1 – Cloudflare Pages 项目设置

图 8.1 – Cloudflare Pages 项目设置

由于我们尝试部署的项目源代码位于 Git 仓库的子目录中,我们必须在环境变量部分指定该目录的NODE_VERSION。尽管 SvelteKit 只需要 Node.js 16.14 或更高版本,但我们指定了版本 18,因为这也是 SvelteKit 当前的最新长期支持版本,也是本书开发过程中使用的版本。一旦所有内容都填写正确,你就可以保存并部署项目了!

Cloudflare 将检查你的代码并尝试构建它。如果一切顺利,你将获得一个指向你应用程序的 URL。为了参考,我们迄今为止创建的应用程序版本可在sveltekit-up-and-running.pages.dev/上找到。将更新部署到应用程序就像将代码推送到项目仓库的适当分支一样简单。

如你所见,一旦在无服务器平台如 Cloudflare Pages 上设置好,部署 SvelteKit 应用程序几乎可以毫不费力。Vercel 和 Netlify 适配器有类似的过程,你可以在空闲时探索。虽然我们的特定项目遇到了一个包含的依赖项需要 Node.js 的轻微问题,但这个例子希望展示了使用正确的适配器可以简化部署。

adapter-static

尽管我们之前只遇到了一个小的适配器问题,但我们肯定会遇到adapter-static的问题。原因在于这个适配器旨在仅在可以托管静态内容的平台上使用——也就是说,在这些平台上不存在服务器后端逻辑。如果你有一个可以提供静态 HTML、CSS 和 JS 的主机,你可以使用这个适配器在那里托管整个 SvelteKit 应用程序。一个常见的例子是 GitHub pages,但 Cloudflare 和其他许多也支持这种方法。托管静态应用程序的好处是速度更快,因为没有服务器后端需要与之通信。

由于我们无法在我们的项目中使用adapter-static,我们不会尝试在我们的现有项目中安装它。但这是一个值得讨论的适配器。就像其他适配器一样,它可以通过以下命令轻松安装:

npm install -D @sveltejs/adapter-static

再次强调,它可以在 svelte.config.js 中导入。此适配器与其他适配器不同,因为它预渲染整个应用程序。它能够做到这一点,因为我们将在应用程序最低级别的布局中插入 export let prerender = true;。在每种情况下,这将是 src/routes/+layout.js

为了自定义此适配器生成的构建,我们提供了一些选项。这些选项通过 svelte.config.js 传递给适配器:

  • pages – 一个默认为 build 的字符串值,它确定预渲染页面将被输出到何处。

  • assets – 一个默认为 pages 提供的值的字符串值,它确定静态资源应该输出到何处。

  • fallback – 一个字符串值,指定当全局禁用 SSR 时使用的回退文件。全局禁用 SSR 将启用 index.html200.html404.html

  • precompress – 一个布尔值,确定是否应该使用 brotligzip 压缩算法压缩文件。

  • strict – 一个布尔值,防止应用程序在预渲染时某些端点不存在时构建。如果你的应用程序使用了仅在特定情况下存在的页面,禁用此功能可能很有用。

如果你有兴趣亲自尝试 adapter-static,考虑使用 skeleton 模板创建一个新的 SvelteKit 应用程序。你可以尝试使用 演示应用程序,但会遇到服务器路由无法预渲染的问题。这些可以通过删除来解决以使其工作,但如果你的目标是简单地了解静态适配器的工作原理,从 skeleton 模板部署可能会更容易。再次强调,构建静态应用程序的步骤相当简单:

  1. 使用 npm install -``D @sveltejs/adapter-static 安装适配器。

  2. svelte.config.js 中导入适配器。

  3. 通过在 src/routes/+layout.js 中添加 export let prerender = true; 确保应用程序完全可预渲染。

  4. 运行 npm run build 命令!

一旦你的应用程序构建完成,你可以简单地将其复制到任何可以提供静态文件的服务器上。当然,某些主机可能有它们自己的要求,所以请务必阅读它们的文档。

通过与 SvelteKit 一起工作是一种了解它的好方法。如果你还没有个人网站,可以考虑使用 SvelteKit 静态适配器来创建一个。这不仅不需要数据库或后端,而且几乎可以部署到任何托管提供商。如果你想知道从哪里开始,Josh Collinsworth 使用静态适配器创建了一个出色的项目。它允许用户使用 Markdown 添加博客文章,同时仍然作为一个静态网站存在。这意味着可以在 GitHub Pages 等平台上免费托管。你可以在本章末尾的 资源 部分找到 Josh 项目的链接。虽然我们的项目以及许多其他项目无法使用 adapter-static,因为它们不是可预渲染的,但这个特定适配器在生成静态网站方面的价值是显而易见的。

摘要

在了解了 SvelteKit 构建过程的工作原理后,我们观察了如何在本地预览我们的构建。我们还探讨了页面选项如何影响我们的构建。我们学习了构建过程,并看到如何通过选择正确的适配器来调整我们的应用程序以适应各种平台。我们构建的应用程序最适合 Node.js 环境,但我们也已经看到将其部署到 Cloudflare Pages、Netlify 或 Vercel 等平台是多么简单。通过使用正确的适配器和开发策略,我们甚至可以将我们的应用程序转变为静态网站。现在你已经看到了如何为不同的生产环境准备你的应用程序,你可以继续发布你的 SvelteKit 应用程序到野外。

在下一章中,我们将学习如何通过使用钩子(hooks)来操作整个应用程序中的请求。我们还将讨论如何利用这些钩子来帮助管理错误。因为没有任何应用程序是完美的,所以我们将探讨 SvelteKit 如何让我们在出现问题时自定义用户体验。

资源

第九章:钩子和错误处理

虽然从任何页面发起 API 请求可能非常有用,但想象一下在 每个 页面上尝试对用户进行外部 API 验证的麻烦。可能可以创建一个自定义助手,为每个请求添加特定的头或 cookie 来协助这一过程。幸运的是,SvelteKit 提供了在整个框架中操作请求和响应对象的方法。它是通过所谓的 钩子 来实现的。这些钩子可以非常强大地管理进出我们应用的数据。它们还可以帮助管理错误。由于我们之前对错误处理的接触非常简短,所以我们将在介绍钩子之后更详细地研究错误处理。

在本章中,我们将涵盖以下主题:

  • 使用钩子

  • 错误处理

作为实际示例,我们将构建一个简单的界面,允许我们 star GitHub 上的官方 SvelteKit 仓库。您将需要验证您的个人账户,但如果您在 GitHub 上没有账户,请不要担心,因为概念足够简单,可以轻松跟随。到练习结束时,我们将涵盖 SvelteKit 中可用的钩子,以及如何利用它们来帮助管理错误。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter09

建立示例需要 GitHub 账户:github.com/signup

使用钩子

与其他未命名的 JS 框架不同,SvelteKit 将需要记住的钩子列表保持简短和简单。在撰写本文时,只有两种类型的钩子 – +page.server.js 只在服务器上运行,而 +page.js 则在服务器或客户端上运行。服务器和共享钩子都放在 src/ 目录中,无论是在 src/hooks.server.js 还是 src/hooks.client.js,这取决于我们打算在哪个环境中运行钩子。我们将把这个部分分解成以下子部分:

  • 服务器钩子

  • 共享钩子

到本节结束时,您将能够修改所有进入和离开您的 SvelteKit 应用程序的请求。

服务器钩子

只能在服务器上运行的钩子有 handleFetch()handle()。正如我们所期望的,handleFetch() 具有操作 SvelteKit 内置的 fetch() 方法所发起请求的能力,这个方法可以在 load() 或通过操作调用。另一个钩子 handle();可以在 SvelteKit 的路由器接收请求时操作数据。可以这样理解,handleFetch() 操作的是即将离开应用的数据,而 handle() 操作的是进入应用的数据。作为服务器钩子,两者都可以添加到 src/hooks.server.js

为了开始本章的示例,展示钩子有多么有用,让我们先设置一些东西。首先,我们需要一种方法来使用 GitHub 认证我们的个人账户。我们将通过向适当的 GitHub API 端点发送一个个人访问令牌来实现这一点。一旦生成,这个令牌就可以添加到 HTTP 请求头中。非常重要的一点是,我们要像对待密码一样对待令牌,因此我们将通过环境变量安全地导入这个令牌,这将在后面的章节中进一步讨论。

要生成你的令牌,请访问github.com并登录。然后,你可以通过点击右上角的个人头像并选择SvelteKit star repo、一个过期日期和完整的repo范围来导航到你的个人资料设置。完成操作后,点击生成令牌并复制提供的值。

然后,我们可以打开我们的 SvelteKit 项目,并在项目的根目录中创建一个.env文件。在这个文件中,我们将添加我们的令牌,如下所示:

.env

GITHUB=YOUR_GITHUB_TOKEN_HERE

你可以保存并关闭文件。我们将在后面的章节中详细介绍如何管理秘密,但到目前为止,只需知道我们已经给我们的令牌起了一个名字,我们可以用这个名字将其导入到我们的代码中。

接下来,让我们创建一个新的路由并将其添加到Nav.svelte中:

src/lib/Nav.svelte

<nav>
  <ul>
    <li><a href='/'>Home</a></li>
    ...
    <li><a href='/github'>GitHub</a></li>
  </ul>
</nav>

再次强调,这是一个简单的添加操作,我们为链接添加了标记,并将其添加到我们不断增长的链接列表中。一旦我们为新的路由添加了标记,我们就会创建一个适当的+page.svelte文件来渲染这个路由。这个页面将包含一个简单的表单,有按钮允许我们星标取消星标GitHub 上的 SvelteKit 仓库,以及一些显示仓库当前有多少星标的文本:

src/routes/(app)/github/+page.svelte

<script>
  import { enhance } from '$app/forms';
  import { invalidate } from '$app/navigation';
  export let data;
  export let form;
  const reload = () => {
    invalidate('https://api.github.com/repos/');
  };
</script>
{#if form && form.message }
  {form.status}
  {form.message}
{/if}
<p>
  Stargazers on the official SvelteKit repo: {data.repo.stargazers_count}
</p>
<form method='POST' use:enhance>
  <button formaction='?/star' on:click={reload}>Star</button>
  <button formaction='?/unstar' on:click={reload}>Unstar</button>
</form>

这里发生了很多事情,但我们之前都见过。从文件的末尾开始,我们可以看到这个页面包含一个<form>元素,由两个按钮组成。一个按钮调用/github?/star操作,另一个调用/github?/unstar操作。表单使用enhance模块,这意味着请求在后台进行,不会触发页面刷新。表单中的每个按钮都调用分配给常量reload的函数表达式。这个匿名函数利用了invalidate模块,它强制任何使用fetch()方法的load()函数重新运行指定的 URL。这很有用,因为显示仓库有多少星的段落标签将相应地更新。最后要注意的代码部分是 Svelte 的{#if}指令,它将通知我们请求的状态。

保存+page.svelte后,我们需要继续处理+page.server.js,因为那里将包含我们大部分的逻辑。重要的是,它不会包含任何与认证相关的代码,因为所有这些都将驻留在钩子中:

src/routes/(app)/github/+page.server.js

const star_url = 'https://api.github.com/user/starred/sveltejs/kit';
export function load({ fetch }) {
  const repo = fetch( 'https://api.github.com/repos/sveltejs/kit' )
    .then( response => response.json() );
  return { repo };
}
export const actions = {
  star: async({ fetch }) => {
    const response = fetch(star_url, {
      method: 'PUT',
      headers: {
        'Content-Length': '0',
      }
    })
    .then(response => {
      const status = response.status;
      return {
        status: status,
        message: (status === 204 ? 'Success!' : 'Error')
      }
    });
    return response;
  },
  unstar: async({ fetch }) => {
    const response = fetch(star_url, {
      method: 'DELETE'
    })
    .then(response => {
      const status = response.status;
      return {
        status: status,
        message: (status === 204 ? 'Success!' : 'Error')
      }
    });
    return response;
  },
}

这里有很多事情在进行,但同样,这并不是我们之前没有见过的。第一行声明了一个常量 star_url,我们可以在之后的 fetch() 请求中引用它。这是我们用来通知 GitHub 我们想要 starunstar 仓库的端点。紧接着的代码块创建了一个熟悉的 load() 函数。这个端点实际上不需要认证,并将返回有关指定仓库的一般信息。

从那里,我们可以检查我们创建的两个操作 – starunstar。这两个操作都解构了 SvelteKit 附加的 fetch() 方法。然后它们都使用 fetch()star_url 端点发送请求,但根据 GitHub API 规范,它们在使用的 HTTP 方法类型上有所不同。我们还为 star 请求添加了一个额外的头部,因为官方 GitHub API 文档规定,对于这个特定的请求,Content-Body 应该设置为 0。这两个操作随后返回一个对象,包含请求状态和一个消息,告知我们请求是否成功通过,或者是否收到了错误。

在这一点上,我们可以在浏览器中导航到 http://localhost/github 并查看官方 SvelteKit 仓库的星标数量。然而,尝试为其添加星标将会导致错误,因为我们没有使用 GitHub API 的适当权限。为了进行认证,我们需要在网络的请求头部提供我们的访问令牌。虽然我们可以在每个操作的 +page.server.js 文件中这样做,但如果我们要构建更多需要与 GitHub 进行认证的路由,这会很快变得麻烦。例如,如果我们想构建允许我们读取和评论问题的功能呢?相反,我们可以使用 handleFetch() 来捕获所有即将离开我们应用程序的 fetch() 请求,并在一个位置进行认证。为此,我们需要创建 src/hooks.server.js

src/hooks.server.js

import { GITHUB } from '$env/static/private';
export async function handleFetch( { request, fetch  } ) {
  if (request.url.startsWith('https://api.github.com/')) {
    request.headers.set('Accept', 'application/vnd.github+json');
    request.headers.set('Authorization', 'Bearer ' + GITHUB);
    request.headers.set('X-GitHub-Api-Version', '2022-11-28');
  }
  return fetch(request);
}

我们首先导入之前创建的个人访问令牌。再次强调,我们将在后面的章节中详细探讨其工作原理,所以现在只需确认我们已经导入了保存在 .env 中的令牌。然后,我们为 handleFetch() 函数创建函数定义。由于 Fetch 是一个异步 API,因此我们需要指定这个函数也将是 async 的。我们随后解构了 RequestEvent 对象以提取 requestfetch。在钩子中,我们检查请求的 URL 是否为 api.github.com/。如果是,我们将设置必要的 AcceptAuthorizationX-GitHub-Api-Version 头部。虽然 AcceptX-GitHub-Api-Version 是简单的预定字符串,但 Authorization 头部的值是连接字符串 bearer 以及导入的个人访问令牌。

现在,我们可以在浏览器中打开 http://localhost/github 并点击 star 按钮。因为我们已经在 GitHub API URL 上使用了 invalidate(),所以我们应该观察到计数增加一个。SvelteKit 的开发者们确实赢得了我们的点赞,但如果您对这个简单的慈善行为感到不舒服,尝试点击 unstar 按钮并再次观察变化。

需要注意的是,handleFetch() 只会钩入 SvelteKit 提供的 fetch() 函数,而不是标准的 fetch() 函数。这可以通过从 src/routes/(app)/github/+page.server.js 中的 *star**unstar* 动作中移除解构的 fetch 参数来观察到。然后,尝试标记仓库将返回 GitHub API 的 未授权 错误,因为除非我们使用 SvelteKit 提供的 fetch() 方法,否则 handleFetch() 钩子不会被调用。这也可以通过从 +page.server.js 中的 load() 函数中移除解构的 fetch 来观察到。假设您没有超过 GitHub 的速率限制,标准的 fetch() 函数将继续提供数据,因为调用端点不需要身份验证。正因为如此,强烈建议尽可能使用 SvelteKit 的 fetch()

如我们所见,能够操纵离开我们应用程序的请求是非常强大的,但关于进入我们平台的请求呢?为此,我们需要利用 handle()。为了这个例子,我们将通过添加 src/hooks.server.jshandle() 钩子来演示:

src/hooks.server.js

import { GITHUB } from '$env/static/private';
export async function handleFetch( { request, fetch  } ) {
  if (request.url.startsWith('https://api.github.com/')) {
    request.headers.set('Accept', 'application/vnd.github+json');
    request.headers.set('Authorization', 'Bearer ' + GITHUB);
    request.headers.set('X-GitHub-Api-Version', '2022-11-28');
  }
  return fetch(request);
}
export async function handle({ event, resolve }) {
  event.setHeaders({'X-NOT-FROM-GITHUB': 'our value'});
  const response = await resolve(event);
  response.headers.set('X-ANOTHER-HEADER', 'something else');
  return response;
}

在这个版本中,我们在文件末尾添加了 handle() 函数的定义。这个函数接受一个 RequestEvent 对象以及一个 resolve() 函数。在我们的版本中,我们添加了一个自定义的头部,该头部将被添加到我们应用程序内的所有请求中。这个头部是 X-NOT-FROM-GITHUB,并将包含 我们的值。一旦头部被添加到 event 对象中,我们就通过将事件传递给 resolve() 并返回承诺来完成请求。我们还添加了另一个方法来演示如何在请求完成后使用 resolve() 修改头部。可以通过在应用程序中的任何页面上打开浏览器,打开 开发者工具,导航到 网络 选项卡,并刷新页面来观察这些头部。打开第一个请求将显示我们在 响应头部 中的自定义头部和值。

当然,我们不仅限于设置头。选择头只是为了演示目的。我们还可以通过将 event.locals 设置为我们选择的任何对象来设置将在 load() 和服务器页面中可用的数据。我们甚至可以利用 event.cookies 来修改 cookie 值。然而,如果我们使用 SvelteKit 的内置 fetch() 方法,只要它位于有权访问 cookie 的域中,这些 cookie 将自动在应用程序中传递。最后,需要注意的是,handle()handleFetch() 都将在渲染或预渲染过程中运行。如果你尝试使用 adapter-static 生成静态网站,这一点尤其重要要记住。

正如我们刚刚演示的,服务器钩子可以在需要自定义请求和响应头时成为一个强大的工具。虽然修改头数据只是它们的一个用例,但我们提供的示例应该突出它们可以解决的许多可能的解决方案。接下来,让我们看看我们可以在客户端和服务器上使用的钩子。

共享钩子

在刚刚介绍了服务器环境中的两个钩子之后,我们现在在客户端环境中只有 handleError() 可用。这个钩子对于捕获未通过 SvelteKit 的 error 模块抛出的错误非常有用——也就是说,任何关键的应用程序错误,或者使用 throw new Error() 抛出的错误。记录应用程序错误非常有用。它还允许我们控制用户在遇到应用程序的严重问题时会看到什么。为了演示共享钩子的用法,让我们将其添加到 src/hooks.server.jssrc/hooks.client.js 中。在这个例子中,我们将服务器上的错误记录到文件中,并在客户端使用 console.log() 记录错误:

src/hooks.client.js

export async function handleError({ error, event }) {
  console.log('client handled error' + error.message);
  console.log(event.url);
  return {message: 'Whoops, looks like you found an error! Sorry about that.'};
}

在这个例子中,我们使用 console.log() 在浏览器控制台中显示各种消息。如果 handleError() 能够在客户端遇到任何意外错误时运行,那将是非常好的;然而,实际情况并非如此,因为它只有在客户端的 load() 过程中或客户端渲染时遇到错误才会运行。因此,我们需要在另一个位置抛出错误以观察其效果。为了触发这个错误,我们将对 src/routes/(site)/fetch/+page.js 进行一些小的修改:

src/routes/(site)/fetch/+page.js

import { browser } from '$app/environment';
const key = 'DEMO_KEY'; // your API key here
export const prerender = true;
export function load() {
  if(browser) {
    throw new Error('in the browser');
  }
  const pic = fetch(`https://api.nasa.gov/planetary/apod?api_key=${key}`)
    .then(response => {
      console.log('got response');
      return response.json();
    });
  return {pic};
}

我们做的第一个修改是从 $app/environment 中导入 browser 模块。一旦完成,我们检查正在运行的代码是否在浏览器中,然后抛出一个错误,其余的代码保持不变。当我们导航到应用程序中的 /fetch 路由时,我们将看到来自 src/hooks.client.js 的消息输出。通过这个钩子,我们可以确保在客户端错误期间不会输出任何敏感数据。我们还可以相应地调整返回的错误对象中的消息。

要在服务器上演示 handleError(),我们可以将错误消息写入日志文件,这样开发人员或安装在服务器上的守护进程可以在稍后轻松阅读:

src/hooks.server.js

import { GITHUB } from '$env/static/private';
import fs from 'fs';
export async function handleFetch( { request, fetch  } ) {
  if (request.url.startsWith('https://api.github.com/')) {
    request.headers.set('Accept', 'application/vnd.github+json');
    request.headers.set('Authorization', 'Bearer ' + GITHUB);
    request.headers.set('X-GitHub-Api-Version', '2022-11-28');
  }
  return fetch(request);
}
export async function handle({ event, resolve }) {
  event.setHeaders({'X-NOT-FROM-GITHUB': 'our value'});
  const response = await resolve(event);
  response.headers.set('another', 'custom value');
  return response;
}
function today() {
  const current = new Date();
  return current.getDate() + "-" +
         current.getDay() + "-" +
         current.getFullYear() + " " +
         current.getHours() + ":" +
         current.getMinutes() + ":" +
         current.getSeconds();
}
export async function handleError({ error, event }) {
  const log = today() + ' ' + error.message + ' @ ' + event.request.url;
  fs.appendFile('./app.log', log + '\n', (err) => {
    if(err) {
      console.log(err);
    }
  });
  return {
    error: error.message
  };
}

在这个例子中,首先要注意的更改是我们导入了 fs 模块。这是一个用于访问文件系统的 Node 特定模块。这值得注意,因为包含它将阻止我们在其他环境中构建我们的应用程序。接下来要注意的更改是我们添加了 today() 函数,该函数返回当前日期和时间作为字符串。理想情况下,这个函数应该位于 utilities 文件夹中,可能位于 src/lib/,但在这个演示中,我们可以将其包含在这里。我们还添加了 handleError() 到文件的末尾。它接受一个 error 对象以及一个 event 对象。然后我们将 today() 函数的输出与错误消息以及错误发生的位置信息连接起来。然后使用 fs.appendFile() 将所有内容写入我们的日志文件。如果在写入文件时遇到错误,我们将将其输出到控制台。最后,我们返回一个包含错误消息的对象。

要触发 handleError(),我们可以在我们的操作或 load() 中抛出一个错误。精确的位置并不特别重要,但我们可以通过在 +page.server.js 中添加 throw new Error('我们的自定义错误'); 来看到我们的日志功能在行动。实际上,尝试从我们迄今为止创建的各种 load() 函数或操作中抛出错误。你甚至可以尝试与请求提供的数据进行实验。例如,记录各种标题可能很有用,例如客户端 User-Agent,因为当调试浏览器兼容性时,这些信息可能很有帮助。一旦你抛出了几个错误,打开项目目录根目录下的 app.log 并观察输出。这个简单的日志机制可以根据项目的需求进行定制。

通过利用 handleError(),我们可以轻松地启动自己的日志机制。我们还可以自定义客户端渲染或 load() 期间显示的错误消息,这对于我们的应用程序是单页应用时特别有帮助。当然,handleError() 非常有用,但它只对意外错误有帮助。我们应该如何管理我们完全预期的错误呢?

错误处理

有时,讨厌的用户试图访问他们不应该访问的资源。或者,也许他们只是用户,没有注意到他们点击的地方。无论如何,我们每个人都至少遇到过这样的用户。作为开发者,我们最好确保我们的代码在用户不可避免地遇到错误时给出有意义的消息。为了我们的理智和未来的自己,我们应该在 SvelteKit 中讨论错误。

我们使用 handleError() 创建的所有错误都被视为 意外错误。这是因为我们没有使用 SvelteKit 的错误模块。使用此模块创建的错误被视为 预期错误。通过导入模块,我们可以向 SvelteKit 发送自定义的状态码和错误消息,然后这些消息可以被 +error.svelte 组件捕获和使用。这使我们能够对用户显示的消息有更大的控制权。

第四章 中,我们简要探讨了路由机制将如何与最近的 +error.svelte 模板交互。然而,我们并没有真正讨论 error 模块或如何使用它。首先,它需要从 @sveltejs/kit 中导入。一旦导入,我们就可以使用 throw error() 抛出错误,并将两个参数传递给函数。第一个参数是一个整数,表示分类错误的 HTTP 状态码。例如,401 代码表示一个 客户端未授权 错误。传递给 error() 的第二个参数是一个包含 message 属性的对象,其中包含我们想要传达的消息。

为了看一个例子,让我们对我们的先前代码做一些修改:

src/routes/(app)/github/+page.server.js

import { error } from '@sveltejs/kit';
const star_url= 'https://api.github.com/user/starred/sveltejs/kit';
export function load({ fetch }) {
  throw error(401, {
    message: 'You don\'t have permission to see this!',
    id: crypto.randomUUID()
  });
  const repo = fetch( 'https://api.github.com/repos/sveltejs/kit' )
    .then( response => response.json() );
  return { repo };
}
export const actions = {
    // omitted for brevity
}

在这个例子中,我们从 @sveltejs/kit 中导入了 error 模块。然后在 load() 函数中,我们立即使用该模块抛出一个错误。对于我们的错误,我们提供了 401 状态码和一个包含 message 属性的对象,以及一个自定义的错误 id。同样,这有助于错误报告和日志记录,因为我们可以用我们想要提供的信息自定义错误对象。如果我们不想创建整个对象,我们可以提供一个包含错误消息的字符串,甚至只是 HTTP 状态码!我们将保持此文件中的其余代码不变。

而不是在错误周围构建传统的 try catch 语句,我们将让 SvelteKit 通过提供一个定制的 +error.svelte 组件来处理捕获:

src/routes/(app)/github/+error.server.js

<script>
  import { page } from '$app/stores';
</script>
{$page.error.message}
{#if $page.error.id}
  <p>
    Error ID: {$page.error.id}
  </p>
{/if}

要显示我们的错误消息,我们必须利用 Svelte 存储库 page 中的数据。这个存储库是从 $app/stores 导入的,并且可以通过在存储库名称前加上 $ 符号来访问其内部数据。page 存储库包含有关访问页面的各种属性和数据。然后我们显示提供的错误消息,接着是一个 Svelte {#if} 指令,用于显示错误 ID,如果存在的话。

然后,我们可以导航到 http://localhost/github 并查看我们新的错误消息以及错误 ID 号码。为了玩转这个示例并了解更多关于错误处理的知识,尝试将 src/routes/(app)/github/+error.svelte 移动到 src/routes/(app)/+error.svelte。如果你将它完全移动到 src/routes/+error.svelte 会发生什么?你应该在自己的项目中尝试这个操作,简而言之,Svelte 的路由机制足够强大,能够将错误传递到它找到的最近的 +error.svelte 组件。

在探索了错误处理之后,你现在应该能够向应用程序用户展示与他们遇到的错误相关的相关信息。以信息化的方式这样做应该会最小化处理支持票证所需的时间。

摘要

在探索了 SvelteKit 中可用的各种钩子以及错误处理之后,我们已经涵盖了大量的内容。我们首先看到了如何通过在 handleFetch() 钩子中更改熟悉的 RequestEvent 对象来修改利用 SvelteKit 内置 fetch() 的出站 fetch() 请求。我们还看到了如何通过 handle() 调整进入我们应用程序的数据。然后,我们探讨了 handleError() 共享钩子及其如何被用来构建丰富的日志机制或与另一个服务结合使用。最后,我们回到如何使用 SvelteKit 的错误路由设备来管理预期错误,这些设备允许我们通过自定义 Svelte 组件来自定义外观和感觉。

现在我们已经牢固掌握了如何管理错误,接下来我们将进入下一章,评估我们如何最好地管理静态资产。

资源

第十章:管理静态资源

当涉及到管理静态资源时,SvelteKit 与这个过程几乎没有关系。实际上,整个过程都交给了打包工具 Vite。通过利用 Vite 来管理静态资源,我们作为开发者无需再学习另一个框架特定的策略。相反,我们可以依赖 Vite 的高效打包和构建过程。因为 Vite 自动管理导入的资源,我们无需担心为缓存文件进行哈希处理。在本章中,我们将探讨如何利用 Vite 来管理静态资源,如图片、字体、音频、视频和 CSS 文件。一旦我们考察了如何实现这一点,我们还将讨论一些与静态资源相关的细节。

本章将分为以下部分:

  • 导入资源

  • 其他信息

到我们完成时,我们将对在 SvelteKit 应用程序中包含可以“原样”提供的服务文件的最佳实践有一个牢固的掌握。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter10

导入资源

如果你过去十年从事过 Web 开发,那么你可能会记得一个时期,那时样式要么是内联编写的,要么是在层叠样式表CSS)中编写的。这些CSS文件对于创建应用程序的一致外观和感觉非常有帮助。当然,它们的集中化性质也带来了自己的缺点。它们通常变得很大且难以导航,这可能导致包含未使用的样式规则。当宝贵的毫秒可以决定用户转化或失去销售的时候,不向客户发送未使用的资源是非常重要的。此外,如果我们用 SvelteKit 构建 Web 应用程序,我们真的应该使用 Svelte 方法,并将样式隔离在每个组件中。但有时保留一个应用全局样式的样式表是有用的。例如,想象一下需要将特定样式应用到每个段落元素上。将相同的简单样式规则应用到应用程序中的每个组件可能会导致代码重复。甚至可能存在我们忘记包含规则的情况,导致应用程序中样式不一致。虽然像Tailwind CSSBootstrap这样的项目很棒,但它们可能并不适合每个项目,这就是为什么我们将介绍如何在 SvelteKit 应用程序中包含全局样式表。

首先,我们需要一些样式。请注意,这些样式将应用于整个应用程序。通常,在 Svelte 组件内创建样式时,这些样式会被隔离到特定的组件中,这意味着它们不会应用于父组件或子组件。许多现代浏览器会为 HTML 元素应用它们自己的默认样式,因此,为了统一应用程序的体验,通常的做法是创建一个 reset.css 文件。此文件通过将浏览器为常见元素应用的样式重置为可预测的样式,确保在不同浏览器之间的一致性体验。在我们的例子中,我们将使用由 Josh W. Comeau 编写的简洁而全面的 自定义 CSS 重置 的略微修改版本。请参阅本章末尾的资源,以获取解释其工作原理的文章链接:

src/reset.css

/* https://www.joshwcomeau.com/css/custom-css-reset/ */
*, *::before, *::after {
  box-sizing: border-box;
}
* {
  margin: 0;
}
html, body {
  height: 100%;
  font-family: sans-serif;
}
body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}
input, button, textarea, select {
  font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

从本质上讲,此 CSS 文件的规则是为各种 HTML 元素设置更合理和可预测的默认样式。例如,box-sizing 被设置为 border-box,这适用于所有元素以及 ::before::after 伪元素。这条规则意味着元素的填充将在计算该元素宽度时被包含。这些 CSS 规则允许在浏览器之间提供一致和可靠的体验。当然,我们可以自由地向此 CSS 添加任何内容。为了使我们的更改更加明显,我们还对 htmlbody 元素设置了 font-family: sans-serif;

要将此 CSS 包含到我们的应用程序中,我们需要打开 src/routes/+layout.svelte 并像导入 JS 模块一样导入它。如果你还记得第二章,我们曾使用相同的方法导入图像路径!

src/routes/+layout.svelte

<script>
  import '/src/reset.css';
  import Nav from '$lib/Nav.svelte';
  import Notify from '$lib/Notify.svelte';
  export let data;
</script>
...

显然,我们在此文件中需要做的唯一更改是第一行,其中我们导入 reset.css。为了简洁起见,文件中的其余代码已被省略。导入 CSS 文件后,请注意我们的样式立即被应用。我们不需要创建 <link><style> 标签,因为 Vite 识别样式表并将其自动为我们应用。方便的是,文件导入路径可以是相对的或绝对的,因为 Vite 不会区分这两者。

为了突出使用此方法导入样式表的好处,让我们将其与另一种包含全局样式表的方法进行比较。这种方法应用于 SvelteKit 的 1.0 版本之前的预发布版本,它通过手动将 <link> 标签添加到 src/app.html<head> 部分来实现。然后,它使用 %sveltekit.assets% 占位符引用 static/ 目录中的文件。这种方法并不可取,但让我们分析它以考虑其缺陷:

src/app.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%sveltekit.assets%/favicon.png" />
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" href="%sveltekit.assets%/global.css" />
      %sveltekit.head%
  </head>
  <body data-sveltekit-preload-data="hover">
    <div style="display: contents">%sveltekit.body%</div>
  </body>
</html>

如我们所见,这种方法将static/global.css文件包含在src/app.html的 head 标签中,这是应用程序的入口点。app.html文件作为整个应用程序构建的基础框架,因此我们可以合理地认为,我们可以在这里包含任何额外的脚本或外部资产,就像 favicon 一样。这种方法依赖于%sveltekit.assets%占位符来包含static/目录中的 CSS 文件。然而,这种方法没有考虑到 Vite 的 HMR 功能。每当对static/global.css进行更改时,整个开发服务器都需要重新启动以反映这些更改,因为 Vite 不会处理任何包含在静态资产中的文件。同时考虑一下常见的场景,即对.css文件进行压缩,对.scss.sass.less文件进行预处理。在这些情况下,我们需要 Vite 比从 SvelteKit 的static/目录中包含的文件采取更积极的做法。而且由于 Vite 可以通过在静态资产的文件名后附加哈希来管理缓存文件,很明显,像导入 JS 模块一样导入文件符合我们的最佳利益。

第二章中,我们看到了如何导入一个图片 URL。现在我们也看到了 Vite 如何允许我们直接将全局 CSS 文件导入到我们的 Svelte 组件中。既然我们已经展示了如何动态地最佳利用静态资产,让我们讨论一下这个过程中的一些细节。

其他信息

我们现在知道如何导入静态文件,但在这样做时,还有一些细节需要注意。以下是我们需要覆盖的各个项目的分解:

  • 图片与样式

  • 自定义导入

  • 文件路径

  • SvelteKit 配置选项

  • Vite 配置选项

现在让我们回顾一下关于我们每个导入背后发生的一些重要信息。

图片与样式

当我们在第二章中导入图片时,我们收到了 URL,然后我们在<img>标签的src属性中引用它。当我们导入 CSS 文件时,我们只需要导入它以应用样式。这是因为 Vite 预先配置为自动将 CSS 文件中的样式注入到执行导入的组件中。这就是为什么导入是在根+layout.svelte文件中执行的原因。Vite 还支持 CSS @importurl()语句以及 CSS 模块。CSS 模块可以在代码中将样式规则作为对象导入时很有用,而@importurl()允许开发者构建一个中心 CSS 文件,该文件可以引用位于其他地方的较小的 CSS 文件。当导入 CSS 文件时,除了导入之外不需要采取任何其他操作。当导入其他媒体,如字体、音频或视频文件时,我们需要将导入的资产 URL 设置为相应 HTML 元素的src属性。

自定义导入

当从 Vite 导入静态资源时,我们可以通过在文件导入名称后附加适当的后缀来自定义它们的导入方式。例如,要将文件作为字符串导入,我们可以在文件名后附加?raw。正如预期的那样,这将给我们导入文件的原始内容。对于前面显示的reset.css示例,它可以通过import reset from '/src/reset.css?raw';来包含,其中 reset 变量包含reset.css的内容。然后我们需要找到一种方法将此内容包含在<style>标签内。以类似的方式,如果我们想将文件作为不在标准媒体类型中找到的 URL 导入,我们可以在文件导入语句后附加?url。这有助于将来自?worker的文件包含到文件名中!

文件路径

当运行 Vite 的开发服务器时,我们可以在浏览器开发者工具中观察网络请求,并注意到导入的文件是从项目源代码中的位置提供的。例如,来自第二章的图片位于src/lib/images/demo.svg,并且也是从该位置提供的。然而,当我们运行npm run build然后运行npm run preview时,我们会观察到路径已更改。它被赋予的路径是_app/immutable/assets/demo.dd76856a.svg。此路径是构建的 SvelteKit 应用程序特有的,通常在应用程序构建后我们不会编辑它。但请花点时间注意,文件名中包含了一个哈希值。如果文件内容发生变化,我们会注意到构建的资产将包括附加到文件名上的不同哈希值。

我们也可以利用这个时刻来观察包含的 favicon 文件。我们会看到在devbuild/preview中,它都是从域名根目录/提供的。这是因为它位于static/目录中,Vite 提供和构建应用程序,使得任何位于那里的文件都将从应用程序的根级别提供。

SvelteKit 配置选项

这些选项可以在svelte.config.js中进行自定义:

  • files.assets – 此选项指定了静态资源将被存储的目录。SvelteKit 会自动将此选项设置为static,并覆盖由publicDir指定的 Vite 同级设置(默认为public)。通常适合这里的文件有robots.txtfavicon.ico,因为它们很少改变。要在源代码中引用此处定位的文件,只需在文件名前加上/即可。例如,我们可以在任何组件中通过添加<img src='/favicon.png' />来显示默认的 favicon 图标。

  • paths.assets – 此选项接受一个字符串,指定从哪里提供应用程序文件的绝对路径。它默认为空字符串。

  • paths.base – 此选项也默认为空字符串。如果你的应用程序是从子目录中提供的,你可以在这里指定根相对路径。然后,你可以使用从 $app/paths 导入的 base 模块来适当地修改硬编码的路径。

  • paths.relative – 此选项接受一个布尔值。当 true 时,$app/paths 模块中提供的 baseassets 的值将相对于构建的资产。当 false 时,这些相同的值将是根相对的。

Vite 配置选项

此选项可以在项目的 vite.config.js 中进行自定义:

  • assetsInclude – 许多常见的媒体类型都由 Vite 自动处理,但如果项目需要扩展默认列表以将不常见的文件类型视为资产,则此选项可能很有用。此选项允许自定义允许的静态资产文件类型。它可以是字符串、正则表达式或 picomatch 模式。

我们刚刚看到了如何自定义静态资产的导入。如果我们需要强制将资产导入为 URL,我们知道我们会在导入文件的末尾追加 ?url。我们还了解到 CSS 文件会自动注入到导入到其中的组件中。结合一些配置选项,这些细节提供了关于如何在 SvelteKit 应用程序中使静态资产的管理无压力的见解。

摘要

当在 SvelteKit 应用程序中包含图像、CSS 文件或其他媒体类型时,很明显我们应该利用 Vite 来导入资产,就像我们导入 JS 模块一样。这样做的好处是简单,同时也允许优化缓存。它使我们的开发体验保持流畅,因为 Vite 的 HMR 会自动在浏览器中显示更改。它也非常灵活,允许通过 URL 或原始内容导入各种媒体类型。

现在我们知道了如何管理静态资产,我们应该回到如何管理秘密的话题。如果你还记得上一章,我们在 .env 文件中添加了一个个人访问令牌,这使得我们可以使用 GitHub API 进行身份验证。在下一章中,我们将进一步探讨这一点,并介绍使管理秘密变得轻松的各种模块。

资源

第十一章:模块和秘密

虽然路由对于 SvelteKit 来说确实很重要,但它远不止于此。在这本书的整个过程中,我们使用了多个不同的 SvelteKit 模块。例如,我们已从 $app/forms$app/environment$app/stores 等模块中导入绑定,仅举几个例子。但我们尚未探讨这些模块是什么,以及它们是如何工作的。在本章中,我们将简要概述我们之前看到的一些模块以及我们尚未看到的一些模块。我们还将涵盖一些用于管理秘密的模块以及何时使用哪个模块。

在本章中,我们将检查以下内容:

  • SvelteKit 模块摘要

  • 保护秘密安全

在涵盖了各种模块,并检查了我们之前存储 GitHub API 秘密的示例之后,我们将清楚地了解何时最好利用每个单独模块的力量。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter11

SvelteKit 模块摘要

在前面的章节中,我们使用了几个不同的模块,但只提供了简短的说明。虽然本节的分析也将是简短的,但它应该提供足够广泛的洞察,让潜在的 SvelteKit 开发者熟悉现有模块的工作原理。我们已经遇到了这里列出的几个,但还有一些我们尚未涉及。对于更深入的解释,请参阅本章末尾的资源。

$app/environment

为了开始对模块的分析,让我们从一个我们相对近期使用过的模块开始。在 第九章 中,当我们试图在客户端抛出错误时,我们使用了 $app/environment 模块并导入了 browser。正如我们预期的那样,根据 SvelteKit 命名约定,从这个模块导出的所有绑定都与应用程序环境相关。这使得通过它们的名称识别每个绑定的目的变得非常简单。例如,我们看到 browser 返回一个布尔值,基于它正在运行的运行环境是否是客户端。同样,buildingdev 将根据代码是否在构建过程中运行或在我们的开发环境中运行分别返回布尔值。$app/environment 的最后一个导出是 version。虽然 version 的命名仍然很合适,但重要的是要注意,它并不指代 SvelteKit 的版本。相反,它指的是应用程序构建的版本。version 的值由构建时的 Unix 时间戳确定。SvelteKit 使用这个字符串值在客户端导航遇到错误时检查新版本,并通过执行完整页面加载来默认使用标准导航实践。

$app/forms

当我们在 第四章 中使用 $app/formsenhance 时,我们看到了它如何通过在提交表单时在后台发送数据而不是要求整个页面重新加载来 渐进式增强 我们的表单提交。如果 enhance 的默认行为没有完全满足我们的需求,它可以被自定义。如果我们决定改变其默认行为并提供一个自定义的回调函数,可以使用 applyAction 来更新组件 form 属性中的数据。我们可以从 $app/forms 中导入的最后一个绑定是 deserialize,这是一个在反序列化表单提交的响应数据时非常有用的函数。

$app/navigation

如预期,$app/navigation 提供了与导航相关的工具。我们之前从这个模块中使用了 invalidateAll第五章invalidate第九章。在这两个实例中,我们这样做是为了强制 load() 再次运行。从这个模块中还有其他一些有用的绑定,如 afterNavigatebeforeNavigate。这两个绑定在 SvelteKit 生命周期中的特定时间运行回调函数。使用 afterNavigate,提供的回调函数在当前组件挂载后以及用户导航到新 URL 后执行。beforeNavigate 的回调函数在导航到新 URL 前运行。

从这个特定模块中可用的其他绑定包括 disableScrollHandlinggotopreloadCodepreloadData。如果我们想改变 SvelteKit 管理浏览器窗口滚动位置的方式,我们可以通过调用 disableScrollHandling() 完全禁用它。goto() 函数为开发者提供了在导航到另一个 URL 的同时,包括允许管理浏览器历史记录、保持特定元素的关注焦点以及触发 load() 函数重新运行的功能。preloadCode() 函数允许开发者在使用特定路由之前导入该路由的代码,而 preloadData() 将执行相同的功能,同时也会调用路由的 load() 函数。当 <a> 元素被赋予 data-sveltekit-preload-data 属性时,也会发现 preloadData() 的相同行为。

$app/paths

当我们需要操作文件路径或为位于子目录中的应用程序创建路由链接时,我们可以依赖 $app/pathsassetsbase 都将返回一个字符串值,该值可以在 svelte.config.js 中的 config.kit.paths 下自定义。如果应用程序是从子目录中提供的,我们可以手动将 {base} 文本添加到路由之前,以确保我们的应用程序从正确的目录中提供路由,而不是尝试从域名根目录提供路由。base 提供的值必须始终是一个以 / 开始但永不以 / 结尾的根相关路径。相反,assets 将始终是应用程序文件的绝对路径。

$app/stores

SvelteKit 开发者非常慷慨,为开发者提供了一些可读的存储,这些存储可以为我们提供有用的信息。当我们从 $app/stores 导入时,我们将能够访问 navigatingpageupdatedgetStores。在导航期间,navigating 将填充一个包含元数据的对象,例如导航事件开始的位置、目的地以及导航类型。类型可以从表单提交到链接点击,甚至浏览器的前进和后退事件。一旦导航完成,此存储的值将返回 null。同时,page 作为包含与当前查看页面相关信息的存储。我们曾在 第四章 中使用它来显示当前页面的错误消息。updated 存储在 SvelteKit 检查是否检测到应用程序的新版本后,将返回一个布尔值。所有这些存储都可以通过调用 getStores() 来检索,但这样做并不推荐,并且仅在需要暂停存储订阅直到组件挂载之后时才应这样做。否则,可以通过在存储名称前加上 $ 符号来自动处理订阅过程来访问所有存储值。

$service-worker

当构建一个 $service-worker 模块时,它只能在服务工作者中使用。当导入绑定时,我们将能够访问 basebuildfilesprerenderedversion。再次强调,合理的命名约定是我们的救星,并且主要解释了从每个绑定返回的数据类型。我们可以从 base 接收一个字符串值,它的工作方式与 $app/paths 中的 base 类似。即使应用程序位于子目录中,它也会为我们提供应用程序的基本路径。build 返回的字符串数组将由 Vite 在构建过程中生成的 URL 组成。另一个字符串数组 files 将提供有关静态文件或包含在 config.kit.files.assets 中的文件的详细信息。对于预渲染的路径名,我们可以从 prerendered 数组中检索字符串。最后,我们可以使用 version 在我们的服务工作者代码中确定应用程序版本。

这个模块列表绝对不是全面的。在过度依赖它们之前,建议阅读本章末尾列出的官方 SvelteKit 文档。现在我们已经检查了一些这些模块,让我们继续探讨那些帮助我们保护敏感信息的模块。

保护机密信息

当涉及到保护像 API 密钥或数据库凭证这样的机密信息时,SvelteKit 为我们提供了保障。正如我们在 第九章 中所看到的,我们可以从 $env/static/private 导入 .env 机密信息。但这并不是唯一允许我们导入环境变量的模块。在本节中,我们将探讨更多模块以及它们如何帮助我们导入环境变量以及机密信息。

$env/static/private

从我们已使用的模块开始,$env/static/private 非常适合导入像 .env 文件中指定的或启动运行时设置的变量。例如,如果我们使用 API_KEY="" node index.js 命令启动我们的应用程序,API_KEY 变量将可用,就像我们在 第九章 中导入 $env/static/privateGITHUB 可用一样。在启动应用程序时提供的任何环境变量都将覆盖 .env 文件中提供的值。这个模块中的变量在构建时由 Vite 静态捆绑到应用程序代码中。这意味着捆绑的代码更少,但也意味着机密信息将包含在应用程序构建中。我们可以通过以下构建命令来确认这一点:

npm run build
grep -r 'YOUR_API_KEY' .svelte-kit/

YOUR_API_KEY 替换为 GitHub 提供的个人访问令牌将显示我们的构建中注入了 API 键的文本和文件。因此,我们应该谨慎不要将构建提交到版本控制,因为这样做可能会暴露机密信息。幸运的是,这个模块只会在服务器端代码中工作,如果我们尝试将其导入客户端代码,它会导致构建失败。

$env/static/public

我们可以将 $env/static/public 视为 $env/static/private 的兄弟模块。主要区别在于,这个模块只会导入以 config.kit.env.publicPrefix 中设置的值开头的变量。默认情况下,这个值是 PUBLIC_。这允许我们将所有机密信息都保存在同一个 .env 文件中,同时允许我们指定哪些是安全地暴露给客户端的,哪些不是。例如,以下 .env 文件将允许将 PUBLIC_GITHUB_USER 导入客户端代码,但不允许 GITHUB_TOKEN。尽管这两个值都存在于同一个文件中,但我们可以放心,SvelteKit 不会将我们的机密信息告诉除我们信任的服务器之外的任何人:

.env

GITHUB_TOKEN=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN
PUBLIC_GITHUB_USER=YOUR_USERNAME_HERE

$env/dynamic/private

正如我们不能将 $env/static/private 导入客户端代码中一样,我们也不能将 $env/dynamic/private 导入客户端代码中。这两个模块之间的区别在于,虽然 $env/static 模块可以访问 .env 文件,但 $env/dynamic 可以访问适配器指定的环境变量。回想一下第八章,当我们将应用程序部署到 Cloudflare 时。当我们这样做时,我们指定了环境变量 NODE_VERSION。获取 Node 版本可能对我们来说并不非常有用,但使用 $env/dynamic/private 允许我们访问在 Cloudflare Pages 项目设置中设置的其它秘密。这在团队合作中非常有用,因为可以在平台上共享秘密,而不是传递 .env 文件。当项目将要部署的平台有自己的设置环境变量的方式时,你很可能要依赖 $env/dynamic 来访问它们。

$env/dynamic/public

按照既定的模式,本模块应该只需要很少的解释。使用 $env/dynamic/public,我们只能导入以 PUBLIC_ 或者在 config.kit.env.publicPrefix 中指定的任何值开头的环境变量。这与 $env/static/public 的工作方式相同。当然,它与 $env/static/public 不同,因为这是一个另一个动态模块,这意味着它是特定于配置的环境适配器的。例如,如果我们把 PUBLIC_GITHUB_USER 添加到我们的 Cloudflare Pages 环境变量中,那么我们就可以使用这个模块来访问该环境变量的值。

摘要

在 SvelteKit 中有大量的模块可用。希望这个简要概述已经提供了足够的洞察力,以便我们知道如何开始使用每一个模块。在下一章中,我们将探讨一些增强可访问性的最佳实践,这可以带来提高搜索引擎优化SEO)的额外好处。

资源

第十二章:提高可访问性和优化 SEO

当涉及到构建 Web 应用程序时,我们不能忽视确保应用程序对所有用户可访问的重要性。通过赋权依赖于屏幕阅读器等辅助技术的用户,我们可以进一步扩大我们应用程序的影响。不仅使应用程序对更广泛的受众可用可以带来更多用户,而且还可以影响搜索引擎优化SEO)。因此,忽视 SvelteKit 如何帮助我们从一开始就使应用程序可访问将是疏忽的。

为了了解我们如何最好地赋权我们的用户,以及这样做如何帮助我们提高 SEO,我们应该检查几个概念。首先,我们将看到内置的编译时检查如何通过我们端的小配置来提高我们应用程序的可访问性。我们还将了解如何最好地宣布路由更改,这可以惠及屏幕阅读器等工具。然后,我们将简要介绍一些可以惠及可访问性的技巧,并以一些简单的 SEO 优化技巧结束。我们将所有内容分解为以下部分:

  • 编译时检查

  • 宣布路由

  • 可访问性增强

  • SEO 技巧

完成本章后,我们将涵盖确保您的 SvelteKit 应用程序对广大受众可访问性的基本要素。遵循这里概述的最佳实践将带来提高 SEO 排名的额外好处。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter12

编译时检查

当我们安装我们的 SvelteKit 项目时,它自带了一些有见地的增强功能。在这些增强功能中,编译时检查特别有用,可以警告我们有关格式不佳或缺少属性的元素。在做出建议的更改后,我们会注意到这些警告消失了。

如果你注意到了eslint的建议或构建输出的结果,你可能已经注意到了一些关于A11y的警告。这是指代可访问性的缩写术语。它指的是字母A,接下来的 11 个字符,以及字母y。在认识到使应用程序可访问的重要性时,Svelte 开发者选择默认包含合理的默认行为,因为这有助于构建一个更加开放的互联网。在因常规警告而感到沮丧之前,考虑一下在无需寻求我们自己的解决方案的情况下,让应用程序检查 a11y 错误带来的便利。不仅考虑 a11y 构建有助于用户,而且有助于开发者通过识别哪些模式是可访问的,哪些不是,来变得更好。

如果你还没有看到这些问题,我们可以回到我们最早的例子之一,并从<img>元素中移除alt属性:

src/routes/+page.svelte

<script>
  import db from 'db';
  import url from 'img/demo.svg';
  let status = db.connection;
  let name = 'World';
</script>
<form>
  <label for="name" >What is your name?</label>
  <input type="text" class='name' bind:value={name} />
</form>
<h1>Hello, {name}!</h1>
<p>{status}</p>
<img src={url}>
// A11y: <img> element should have an alt attribute

在这个例子中,所做的唯一更改是在<img>标签的最后移除alt属性。大多数现代编辑器应该会直接在文件中提醒您,但如果您没有看到这个警告,您可以在终端中运行npm run build来直接查看构建输出。在观察build输出后,我们将能够确定问题的确切位置并查看推荐的修复方案。

这些警告不仅适用于 HTML 元素的缺失属性。如果表单标签与控件相关联,如果某些媒体类型有字幕,如果属性赋予不正确的值,以及更多无法合理列出的警告,我们也会收到警告。有关完整列表,请参阅本章末尾的资源。正如我们所见,编译时无障碍性检查可以在帮助开发者向尽可能多的用户提供可访问的应用程序方面非常有用。

遵循编译时检查警告的建议并不是我们提高应用程序无障碍性的唯一方法。我们还可以通过更新每个页面的标题来通知用户导航事件。通过以下方式宣布路由更改,我们可以在保持客户端导航的同时,提醒屏幕阅读器用户。

宣布路由

确保我们应用程序的无障碍性的另一种策略是宣布我们的路由。这意味着我们的所有页面都包含一个标题,以便屏幕阅读器可以向用户宣布新页面。在典型的 SSR 应用程序中,导航包括在导航到时加载每个新页面。在 SvelteKit 中,导航由客户端处理,因此并不总是需要完整的页面重新加载。这对屏幕阅读器来说是一个困境,因为它们依赖于每个链接点击时都存在新的标题元素,以便向用户宣布页面。

为了更好地与屏幕阅读器协同工作,我们可以使用<svelte:head>指令将标题插入到我们创建的每个新页面中:

src/routes/+page.svelte

<script>
  import db from 'db';
  import url from 'img/demo.svg';
  let status = db.connection;
  let name = 'World';
</script>
<svelte:head>
  <title>Home</title>
</svelte:head>
<form>
  <label for="name" >What is your name?</label>
  <input type="text" class='name' bind:value={name} />
</form>
<h1>Hello, {name}!</h1>
<p>{status}</p>
<img src={url} alt='demo'>

在我们的应用程序着陆页面上,我们已经重新添加了alt属性到我们的<img>标签,但更重要的是,我们已经将页面标题设置为<svelte:head>标签。让我们在另一个文件中也进行类似的更改,以便我们可以观察这如何影响浏览体验:

src/routes/(site)/about/+page.svelte

<svelte:head>
  <title>About</title>
</svelte:head>
<div class='wrapper'>
  <h1>About</h1>
  <p>
    Lorem ipsum dolor ...
  </p>
</div>

在我们应用程序的<title>元素中,使用适当的文本,并用<svelte:head>标签包围它。这些标签将内容放置在文档的头部。为了了解这如何影响浏览体验,请打开应用程序的开发版本,并注意在浏览器中显示的主页页面标题。然后,点击关于,观察浏览器标签中显示的标题如何变化。

对于屏幕阅读器应用程序,这个小小的改变允许它们提醒用户他们已经导航到一个新的路由。即使对于不使用屏幕阅读器的用户,这也是对之前只显示站点名称的文本的一个明显的改进。如果用户在这个网站上标记了一个页面,现在的默认文本将更准确地反映该页面。这不仅对所有人类用户有帮助,而且还可以大大提高我们应用程序的 SEO,因为许多搜索引擎在索引帖子时会考虑页面标题。

通过宣布路由,我们可以极大地改善我们应用程序的用户体验。接下来,让我们看看一些其他的小调整,这些调整可以带来很大的改进。

无障碍性增强

并非所有开发人员以及所有使用屏幕阅读器的用户都将英语作为他们的母语。因此,我们应该能够相应地调整我们的应用程序。这样做相当直接,通过在内容提供语言上做一小笔记,我们可以极大地改善全球辅助技术用户的体验。默认情况下,SvelteKit 将语言设置为英语,但我们可以通过更改src/app.html中的<html>元素的lang属性来快速调整:

src/app.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <link rel="icon" href="%sveltekit.assets%/favicon.png" />
        <meta name="viewport" content="width=device-width" />
        %sveltekit.head%
    </head>
    <body data-sveltekit-preload-data="hover">
         <div style="display: contents">%sveltekit.body%</div>
    </body>
</html>

注意文件顶部的<html>元素,其中设置了lang属性。将lang属性设置为适当的语言,例如fr代表法语或ar代表阿拉伯语,确保屏幕阅读器可以正确发音或翻译内容。

我们可以在我们的应用程序中实现的最后一个无障碍性改进是允许 SvelteKit 操纵 HTML 元素上的焦点。通常,当应用程序在服务器上渲染时,每个新的导航事件都会重置焦点。但在客户端渲染的应用程序中,浏览器可能无法检测到导航事件的发生,因此,焦点将保持在当前聚焦的元素上。为了考虑到无障碍性,SvelteKit 将焦点重置到<body>元素上——也就是说,除非某个元素设置了autofocus属性,在这种情况下,该元素将被给予焦点。让 SvelteKit 的行为控制焦点的好处是让屏幕阅读器的用户知道已经发生了导航事件。

当谈到使我们的应用程序更容易被更广泛的受众使用时,我们不需要付出太多努力。这样做可以改善用户体验,上述所有改进也可以提高 SEO 排名。

SEO 技巧

除了在我们的应用程序中做一些小的 a11y 改进外,我们还可以记住一些其他建议。首先,我们应该尽可能使用 SvelteKit 的 服务器端渲染SSR)。这样做可以确保应用程序快速交付,同时使内容更容易被搜索引擎解析。当然,现在许多搜索引擎都有索引客户端渲染内容的能力,但 SSR 的速度和可靠性不容忽视。我们只有在有充分理由的情况下才应该禁用 SSR。

另一个值得考虑的有用技巧是考虑我们应用程序的性能。在大多数情况下,我们可以依赖 Vite 从我们的构建中摇树干未使用的代码。更小的包大小意味着需要发送给客户端的代码行数更少,许多搜索引擎根据资产交付时间来排名结果。请参阅本章末尾的 资源 部分,了解可以提供关于页面速度洞察力的工具。

提高搜索引擎优化(SEO)的最后一个有用技巧是在路由名称中省略尾部斜杠。额外的斜杠可能会对页面排名产生负面影响,所以除非你有充分的理由,否则考虑让页面选项 trailingSlash 属性保持不变。遵循这些少数几个技巧,我们可以确保我们的 SvelteKit 应用程序在搜索引擎结果中排名很高。

摘要

当谈到构建一个成功的网络应用程序时,我们必须努力使其对所有用户都易于访问。这样做的原因可能是出于纯粹的自私,试图尽可能多地占领市场,或者出于平等主义,试图包括来自网络各个领域的用户。可能你只是想在高搜索引擎结果中排名很高。无论原因如何,使用 SvelteKit 的过程相当直接。我们已经看到了编译时提供的警告,并且我们已经了解了在创建独特的页面标题时,SEO 和 a11y 的好处。只要记住一些 SEO 小技巧,我们的可访问应用程序就很容易为世界所知。

在完成本章后,我们几乎涵盖了关于 SvelteKit 所有的讨论内容。然而,技术发展迅速,所以我们永远无法真正停止学习。到这本书出版时,SvelteKit 很可能已经引入了更多的改进和变化。为了确保您拥有最新的信息,下一章将提供更多资源、社区和值得探索的示例。

资源

附录:示例和支持

由于学习是一个永无止境的过程,而且技术发展迅速,本章的最终目的是为你提供继续使用 SvelteKit 旅程所需的资源。在 Web 开发的世界里,几乎每个项目都会集成多个工具和技术,因此我们将探讨如何轻松地将 SvelteKit 与其他前端工具集成。我们还将看到一些官方和社区资源,这些资源在解决问题、提升知识或发现新组件时非常有价值。之后,我们将以作者的感谢语结束。让我们以下列章节结束这本书:

  • 集成

  • 更多阅读和资源

  • 总结

之后,你将拥有所有必要的工具和知识,可以继续构建酷炫的 SvelteKit 项目。

技术要求

本章的完整代码可在 GitHub 上找到:github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/tailwind

集成

当谈到构建现代 Web 应用程序时,使用大量技术并不罕见。每个工具都有其位置,并且开发者可能更熟悉特定的前端框架,而不是标准 CSS。这是可以的,只要这些工具能够很好地与其他工具集成,它就可以加快开发速度。幸运的是,SvelteKit 与其他工具配合得相当好。

在撰写本文时,Tailwind CSS 已经变得非常流行。Tailwind CSS 的目标是减少发送的 CSS 量,只提取使用的那部分。这对于减少发送给客户端的资产数量和加快加载时间非常有用。为了展示如何简单地将 Tailwind CSS 这样的工具集成到我们现有的 SvelteKit 项目中,让我们来操作一下。这些步骤也可以在官方 Tailwind CSS 文档中找到。建议在开始此过程之前在你的仓库中创建一个新的分支,因为它将破坏我们的一些现有样式。如果你正在跟随这本书的仓库,这些示例可以在 tailwind 分支上找到。首先,我们可以使用以下命令安装 Tailwind 以及其他几个依赖项:

npm install -D tailwindcss postcss autoprefixer

当然,tailwindcss 将包含在项目中使用 Tailwind CSS 所需的工具。postcss 依赖项将允许我们操作 CSS 文件,而 autoprefixer 是一个 postcss 插件,它将自动将适当的供应商前缀注入到我们生成的 CSS 中。一旦我们将依赖项添加到我们的开发环境中,我们可以使用以下命令来初始化我们的 Tailwind 项目。它将创建必要的 tailwind.config.jspostcss.config.js 文件:

npx tailwindcss init -p

在初始化 tailwindcss 之后,我们可以打开 svelte.config.js 并导入 vitePreprocess 模块。这将使我们能够处理 Svelte 组件中的 <style> 标签:

svelte.config.js

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';
const config = {
    kit: {
        adapter: adapter(),
        alias: {
            db: '/src/db.js',
            img: '/src/lib/images'
        }
    },
    preprocess: vitePreprocess()
};
export default config;

现在我们已经导入了 vitePreprocess,我们可以确保 Tailwind CSS 了解我们的组件路径。我们可以通过更新 tailwind.config.js 来做到这一点,如下所示:

tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./src/**/*.{html,js,svelte,ts}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

显然,我们只需要更改 content 数组属性中可用的路径,指向我们的 src/ 目录,并确保 .svelte 文件类型被识别,以及其他标准文件类型。

然后,我们可以创建一个单独的 app.css 文件,在那里我们可以使用 @tailwind 指令导入 Tailwind 的所有功能:

src/app.css

@tailwind base;
@tailwind components;
@tailwind utilities;

如果你一直很注意,下一步应该很简单。然后我们将 src/app.css 导入到我们的根布局组件中:

src/routes/+layout.svelte

<script>
  import '/src/reset.css';
  import '/src/app.css';
  import Nav from '$lib/Nav.svelte';
  import Notify from '$lib/Notify.svelte';
  export let data;
</script>
<div class='wrapper'>
  <div class='nav'>
    <div class='menu'>
      <Nav />
    </div>
    <div class='notifications'>
      <Notify count={data.notifications.count}/>
    </div>
  </div>
  <div class='content bg-orange-300'>
    <slot />
  </div>
  <div class='footer'>
    This is my footer
  </div>
</div>
<!-- <style> omitted for brevity -->

当然,我们已导入 reset.css,因此我们的项目中的现有 CSS 将会有冲突。确保你的开发环境正在运行 npm run dev。为了防止完全破坏我们的项目,我们只将 .content 元素的背景设置为 Tailwind CSS 提供的浅橙色,但我们肯定会注意到项目中的其他变化。如果你还没有这样做,现在是一个探索以实用为先的 CSS 实践的绝佳时机。

我们看到了手动集成另一个工具(如 Tailwind CSS)的方法,但我们谈论的是 SvelteKit,在那里事情总是按预期进行。如果这些步骤太难记住,有一个更简单的方法。尝试在你的项目仓库的 main 分支上创建另一个分支,并使用以下命令执行我们刚才所做的基本相同的事情。再次提醒,如果你正在跟随本书的仓库,这段代码可以在 tailwind-add 分支中找到:

npx svelte-add@latest tailwindcss

我们可以按照提示进行操作,一旦我们使用 npm install 安装了依赖项,我们的项目就会集成 Tailwind CSS!通过使用社区维护的 svelte-add 项目,我们可以快速轻松地导入与我们的 SvelteKit 项目集成的模板。例如,如果你在编写 CSS 时更喜欢使用 SCSS/Sass 风格,你可以使用 scss 自定义添加器,如下所示:

npx svelte-add@latest scss

如我们所见,使用 SvelteKit 集成不同的技术并不困难。虽然我们可以手动集成这些其他工具链,但使用社区提供的资源也同样容易。让我们看看更多的社区资源,看看还有哪些其他选择!

更多阅读和资源

如前所示,围绕 SvelteKit 的社区资源可以非常有效地节省我们的时间和精神负担,使我们能够专注于构建我们的应用程序。如果没有 SvelteKit 的社区,这本书就不可能完成。如果你想要扩展你的 SvelteKit 知识,帮助他人,或者创建你自己的 SvelteKit 扩展,请考虑以下列出的各种资源!

SvelteKit 文档

在官方 SvelteKit 网站上提供的文档可能是你找到有关框架信息的最佳资源。它非常详尽,并且不断更新以反映框架内的变化。确保在有任何关于 SvelteKit 的问题时从这里开始:

kit.svelte.dev

SvelteKit 教程

为了彻底测试你的 SvelteKit 知识并学习这本书无法涵盖的内容,请查看官方 SvelteKit 教程:

learn.svelte.dev/tutorial

Svelte 和 SvelteKit 聊天

有问题或只是想与其他使用 SvelteKit 的人聊天?官方 Discord 服务器是你要去的地方:

svelte.dev/chat

独立创作者

有太多优秀的作家和创作者在与 SvelteKit 合作,这里无法一一列举,但作者最喜欢的两位是罗德尼·约翰逊和乔希·柯林斯沃斯。柯林斯沃斯提供了我们在第八章中看到的出色的 SvelteKit 静态博客起始模板,而约翰逊则创作了信息丰富的教程视频和文章:

Svelte Society

当谈到寻找 Svelte 和 SvelteKit 社区资源时,Svelte Society 会为你提供全方位的支持,无论你是寻找模板、组件、插件,还是更多。他们甚至还会组织 Svelte 活动,所以如果你想在你的地区遇见其他 Svelte 开发者,你应该从这里开始:

sveltesociety.dev/

SvelteKit 仓库

就像许多开源项目一样,SvelteKit 背后的代码在 GitHub 上免费提供供查看。如果你认为你发现了一个针对框架的特定错误,请在这里搜索问题,如果你没有看到你的问题被列出,请通过提交它来贡献!SvelteKit 开发者不断接受拉取请求,并欣赏他们能得到的任何帮助:

github.com/sveltejs/kit

就像许多开源项目一样,社区和文档可以成就或毁掉一个项目。由于 SvelteKit 背后出色的支持,很难想象一个人们不会不断宣扬 SvelteKit 和 Svelte 的未来。

总结

如果你已经读到这儿,那么感谢你一直陪伴着我。我希望这里提供的材料和知识能够对你的 SvelteKit 项目有所帮助。如果你喜欢这本书,那么请与对学习新 JS 框架感兴趣的朋友、同事和熟人分享。作为我的第一本书,这无疑是一次旅程,我在写作过程中学到了很多。如果你对我的更多技术性文本感兴趣,我会在 https://www.closingtags.com 上写关于网页开发和相关技术的文章。如果你用 SvelteKit 做出了什么酷炫的东西,我很乐意听到你的分享。你可以通过我网站上的联系表单联系到我。再次感谢,并期待看到你创造的作品。

摘要

就这样,你已经完成了这本书!如果你对 SvelteKit 的各种功能还有疑问,可以查看之前提供的社区资源。你将找到所有扩展你的 SvelteKit 知识和了解社区其他成员所做的事情所需的一切。由于 SvelteKit 与许多其他工具集成得很好,将其融入现有的工作流程应该会非常容易。我期待看到你用它创造的作品。再次感谢!

资源

posted @ 2025-10-24 10:05  绝不原创的飞龙  阅读(67)  评论(0)    收藏  举报