SvelteKit 入门

Avatar of Adam Rackis
Adam Rackis

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

SvelteKit 是我所说的下一代应用程序框架的最新成员。它当然会为您搭建应用程序,包含 Next 一直以来提供的基于文件的路由、部署和服务器端渲染功能。但 SvelteKit 还支持嵌套布局、同步页面数据的服务器端变异以及其他一些我们即将深入了解的实用功能。

本文旨在对 SvelteKit 进行高级概述,希望能够激发从未使用过 SvelteKit 的任何人的兴趣。这将是一次轻松的巡礼。如果您喜欢看到的内容,可以访问 完整文档

从某种意义上说,这是一篇具有挑战性的文章。SvelteKit 是一个应用程序框架。它存在的目的是帮助您构建……好吧,应用程序。这使得演示变得困难。在博文中构建整个应用程序是不可行的。因此,我们将稍微发挥一下想象力。我们将构建应用程序的骨架,设置一些空的 UI 占位符和硬编码的静态数据。目标不是构建一个实际的应用程序,而是向您展示 SvelteKit 的各个组件如何工作,以便您能够构建自己的应用程序。

为此,我们将构建经过验证的待办事项应用程序作为示例。但请放心,这将更多地关注 SvelteKit 的工作原理,而不是创建另一个待办事项应用程序。

本文中所有内容的代码都可以在 GitHub 上找到。该项目也已 部署到 Vercel,以便进行实时演示。

创建项目

启动一个新的 SvelteKit 项目非常简单。在终端中运行npm create svelte@latest your-app-name并回答问题提示。请务必选择“Skeleton Project”,但对于 TypeScript、ESLint 等,您可以根据需要进行选择。

项目创建完成后,运行npm inpm run dev,开发服务器应该会启动并运行。在浏览器中打开localhost:5173,您将看到骨架应用程序的占位符页面。

基本路由

请注意src下的routes文件夹。它包含我们所有路由的代码。其中已经有一个+page.svelte文件,其中包含根/路由的内容。无论您在文件层次结构中的哪个位置,该路径的实际页面名称始终为+page.svelte。考虑到这一点,让我们为/list/details/admin/user-settingsadmin/paid-status创建页面,并为每个页面添加一些文本占位符。

您的文件布局应如下所示

Initial files.

您应该能够通过更改浏览器地址栏中的 URL 路径来导航。

Browser address bar with localhost URL.

布局

我们希望在应用程序中添加导航链接,但我们当然不想在创建的每个页面上都复制它们的标记。因此,让我们在routes文件夹的根目录中创建一个+layout.svelte文件,SvelteKit 将将其视为所有页面的全局模板。让我们向其中添加一些内容

<nav>
  <ul>
    <li>
      <a href="/">Home</a>
    </li>
    <li>
      <a href="/list">To-Do list</a>
    </li>
    <li>
      <a href="/admin/paid-status">Account status</a>
    </li>
    <li>
      <a href="/admin/user-settings">User settings</a>
    </li>
  </ul>
</nav>

<slot />

<style>
  nav {
    background-color: beige;
  }
  nav ul {
    display: flex;
  }
  li {
    list-style: none;
    margin: 15px;
  }
  a {
    text-decoration: none;
    color: black;
  }
</style>

一些基本的导航和样式。特别重要的是<slot />标签。这不是用于 Web Components 和 Shadow DOM 的插槽,而是 Svelte 的一项功能,指示在何处放置内容。页面渲染时,页面内容将滑入插槽所在的位置。

现在我们有了导航!我们的设计可能无法赢得任何比赛,但我们也不是为了这个目的。

Horizontal navigation with light yellow background.

嵌套布局

如果我们希望所有管理页面都继承我们刚刚构建的普通布局,并且还共享所有管理页面(但仅限管理页面)的一些共同内容,该怎么办?没问题,我们在admin根目录中添加另一个+layout.svelte文件,它将被其下所有内容继承。让我们执行此操作并添加以下内容

<div>This is an admin page</div>

<slot />

<style>
  div {
    padding: 15px;
    margin: 10px 0;
    background-color: red;
    color: white;
  }
</style>

我们添加了一个红色横幅,指示这是一个管理页面,然后像之前一样,一个<slot />表示我们希望页面内容显示的位置。

我们之前的根布局会渲染。根布局内部有一个<slot />标签。嵌套布局的内容将进入根布局的<slot />。最后,嵌套布局定义了自己的<slot />,页面内容将渲染到其中。

如果导航到管理页面,您应该会看到新的红色横幅

Red box beneath navigation that says this is an admin page.

定义数据

好的,让我们渲染一些实际数据——或者至少看看我们如何渲染一些实际数据。有数百种方法可以创建和连接到数据库。但是,本文的重点是 SvelteKit,而不是管理 DynamoDB,因此我们将“加载”一些静态数据。但是,我们将使用与处理真实数据时相同的机制来读取和更新它。对于真实的 Web 应用程序,请将返回静态数据的函数替换为连接和查询您正在使用的任何数据库的函数。

让我们在lib/data/todoData.ts中创建一个非常简单的模块,它返回一些静态数据以及模拟真实查询的人工延迟。您会看到此lib文件夹通过$lib在其他地方导入。这是 SvelteKit 对该特定文件夹的功能,您甚至可以添加自己的别名

let todos = [
  { id: 1, title: "Write SvelteKit intro blog post", assigned: "Adam", tags: [1] },
  { id: 2, title: "Write SvelteKit advanced data loading blog post", assigned: "Adam", tags: [1] },
  { id: 3, title: "Prepare RenderATL talk", assigned: "Adam", tags: [2] },
  { id: 4, title: "Fix all SvelteKit bugs", assigned: "Rich", tags: [3] },
  { id: 5, title: "Edit Adam's blog posts", assigned: "Geoff", tags: [4] },
];

let tags = [
  { id: 1, name: "SvelteKit Content", color: "ded" },
  { id: 2, name: "Conferences", color: "purple" },
  { id: 3, name: "SvelteKit Development", color: "pink" },
  { id: 4, name: "CSS-Tricks Admin", color: "blue" },
];

export const wait = async amount => new Promise(res => setTimeout(res, amount ?? 100));

export async function getTodos() {
  await wait();

  return todos;
}

export async function getTags() {
  await wait();

  return tags.reduce((lookup, tag) => {
    lookup[tag.id] = tag;
    return lookup;
  }, {});
}

export async function getTodo(id) {
  return todos.find(t => t.id == id);
}

一个返回待办事项列表的扁平数组、标签的查找表以及获取单个待办事项的函数(我们将在“详细信息”页面中使用最后一个函数)。

加载数据

我们如何将这些数据放入 Svelte 页面中?有许多方法,但现在,让我们在list文件夹中创建一个+page.server.js文件,并将以下内容放入其中

import { getTodos, getTags } from "$lib/data/todoData";

export function load() {
  const todos = getTodos();
  const tags = getTags();

  return {
    todos,
    tags,
  };
}

我们定义了一个load()函数,该函数提取页面所需的数据。请注意,我们没有getTodosgetTags异步函数的调用使用await。这样做会创建一个数据加载瀑布,因为我们需要等待待办事项加载完成后才能加载标签。相反,我们从load返回原始 Promise,SvelteKit 会执行必要的await工作。

那么,我们如何从页面组件中访问这些数据呢?SvelteKit 为我们的组件提供了一个data属性,其中包含数据。我们将使用反应式赋值从其中访问我们的待办事项和标签。

我们的列表页面组件现在如下所示。

<script>
  export let data;
  $: ({ todo, tags } = data);
</script>

<table cellspacing="10" cellpadding="10">
  <thead>
    <tr>
      <th>Task</th>
      <th>Tags</th>
      <th>Assigned</th>
    </tr>
  </thead>
  <tbody>
    {#each todos as t}
    <tr>
      <td>{t.title}</td>
      <td>{t.tags.map((id) => tags[id].name).join(', ')}</td>
      <td>{t.assigned}</td>
    </tr>
    {/each}
  </tbody>
</table>

<style>
  th {
    text-align: left;
  }
</style>

这应该会渲染我们的待办事项!

Five to-do items in a table format.

布局组

在继续“详细信息”页面和数据变异之前,让我们快速了解一下 SvelteKit 的一项非常棒的功能:布局组。我们已经看到了所有管理页面的嵌套布局,但如果我们希望在文件系统相同级别的任意页面之间共享布局,该怎么办?特别是,如果我们希望仅在“列表”页面和“详细信息”页面之间共享布局,该怎么办?我们已经在该级别上有一个全局布局。相反,我们可以创建一个新目录,但使用括号中的名称,如下所示

File directory.

现在我们有一个布局组,涵盖了我们的列表和详情页面。我将其命名为(todo-management),但您可以随意命名。需要明确的是,此名称**不会**影响布局组内页面的 URL。URL 将保持不变;布局组允许您向页面添加共享布局,而无需它们都包含routes中目录的全部内容。

我们**可以**添加一个+layout.svelte文件和一些简单的<div>横幅,上面写着“嘿,我们正在管理待办事项”。但让我们做一些更有趣的事情。布局可以定义load()函数,以便为其下的所有路由提供数据。让我们使用此功能来加载我们的标签——因为我们将在details页面中使用我们的标签——以及我们已经拥有的list页面。

实际上,仅仅为了提供单个数据片段而强制使用布局组几乎肯定是不值得的;最好在每个页面的load()函数中复制该数据。但是对于这篇文章,它将为我们提供一个查看新 SvelteKit 功能的借口!

首先,让我们进入我们的list页面的+page.server.js文件并从中删除标签。

import { getTodos, getTags } from "$lib/data/todoData";

export function load() {
  const todos = getTodos();

  return {
    todos,
  };
}

我们的列表页面现在应该会产生错误,因为没有tags对象。让我们通过在我们的布局组中添加一个+layout.server.js文件来修复此问题,然后定义一个加载我们的标签的load()函数。

import { getTags } from "$lib/data/todoData";

export function load() {
  const tags = getTags();

  return {
    tags,
  };
}

就这样,我们的列表页面再次渲染了!

我们正在从多个位置加载数据

让我们更详细地说明这里发生了什么

  • 我们为我们的布局组定义了一个load()函数,我们将其放在+layout.server.js中。
  • 这为布局服务的所有页面提供数据——在本例中,这意味着我们的列表和详情页面。
  • 我们的列表页面还定义了一个load()函数,该函数位于其+page.server.js文件中。
  • SvelteKit 会承担繁重的工作,将这些数据源的结果合并在一起,并在data中同时提供两者。

我们的详情页面

我们将使用我们的详情页面来编辑待办事项。首先,让我们在列表页面中的表格中添加一列,该列链接到包含待办事项 ID 的查询字符串中的详情页面。

<td><a href="/details?id={t.id}">Edit</a></td>

现在让我们构建我们的详情页面。首先,我们将添加一个加载器来获取我们正在编辑的待办事项。在/details中创建一个+page.server.js,内容如下

import { getTodo, updateTodo, wait } from "$lib/data/todoData";

export function load({ url }) {
  const id = url.searchParams.get("id");

  console.log(id);
  const todo = getTodo(id);

  return {
    todo,
  };
}

我们的加载器带有一个url属性,我们可以从中提取查询字符串值。这使得查找我们正在编辑的待办事项变得很容易。让我们渲染该待办事项,以及编辑它的功能。

只要您使用表单,SvelteKit 就会拥有很棒的内置变异功能。还记得表单吗?这是我们的详情页面。为了简洁起见,我省略了样式。

<script>
  import { enhance } from "$app/forms";

  export let data;

  $: ({ todo, tags } = data);
  $: currentTags = todo.tags.map(id => tags[id]);
</script>

<form use:enhance method="post" action="?/editTodo">
  <input name="id" type="hidden" value="{todo.id}" />
  <input name="title" value="{todo.title}" />

  <div>
    {#each currentTags as tag}
    <span style="{`color:" ${tag.color};`}>{tag.name}</span>
    {/each}
  </div>

  <button>Save</button>
</form>

我们像以前一样从布局组的加载器中获取标签,从页面的加载器中获取待办事项。我们从待办事项的标签 ID 列表中获取实际的tag对象,然后渲染所有内容。我们创建一个表单,其中包含一个用于 ID 的隐藏输入和一个用于标题的真实输入。我们显示标签,然后提供一个提交表单的按钮。

如果您注意到了use:enhance,它只是告诉 SvelteKit 使用渐进增强和 Ajax 来提交我们的表单。您可能总是会使用它。

我们如何保存我们的编辑?

注意表单本身上的action="?/editTodo"属性?这告诉我们我们希望将编辑后的数据提交到哪里。对于我们的情况,我们希望提交到一个名为editTodo的“操作”。

让我们通过将以下内容添加到我们已经为详情页面拥有的+page.server.js文件(当前具有load()函数,用于获取我们的待办事项)中来创建它。

import { redirect } from "@sveltejs/kit";

// ...

export const actions = {
  async editTodo({ request }) {
    const formData = await request.formData();

    const id = formData.get("id");
    const newTitle = formData.get("title");

    await wait(250);
    updateTodo(id, newTitle);

    throw redirect(303, "/list");
  },
};

表单操作为我们提供了一个request对象,该对象提供了对我们的formData的访问权限,formData具有用于我们各种表单字段的get方法。我们添加了用于 ID 值的隐藏输入,以便我们可以在此处获取它,以便查找我们正在编辑的待办事项。我们模拟延迟,调用一个新的updateTodo()方法,然后将用户重定向回/list页面。updateTodo()方法仅仅更新我们的静态数据;在现实生活中,您将在您正在使用的任何数据存储中运行某种更新。

export async function updateTodo(id, newTitle) {
  const todo = todos.find(t => t.id == id);
  Object.assign(todo, { title: newTitle });
}

让我们试一试。我们首先进入列表页面

List page with to-do-items.

现在,让我们点击其中一个待办事项的“编辑”按钮,以在/details中调出编辑页面。

Details page for a to-do item.

我们将添加一个新的标题

Changing the to-do title in an editable text input.

现在,点击“保存”。这应该会将我们带回/list页面,并应用新的待办事项标题。

The edited to-do item in the full list view.

新标题是如何显示出来的?它是自动的。一旦我们重定向到/list页面,SvelteKit 就会像往常一样自动重新运行我们所有的加载器。这是下一代应用程序框架(如 SvelteKit、RemixNext 13)提供的关键进步。它们不会仅仅为您提供一种方便的渲染页面方法,然后祝您好运获取您可能需要更新数据的任何端点,而是将数据变异与数据加载集成在一起,使两者能够协同工作。

您可能想知道一些事情……

**此变异更新似乎并不太令人印象深刻。**加载器会在您导航时重新运行。如果我们没有在表单操作中添加重定向,而是停留在当前页面上会怎样?SvelteKit 将像以前一样在表单操作中执行更新,但**仍然**会重新运行当前页面的所有加载器,包括页面布局中的加载器。

**我们可以有更具针对性的方法来使我们的数据失效吗?**例如,我们的标签没有被编辑,因此在现实生活中我们不想重新查询它们。是的,我向您展示的只是 SvelteKit 中的默认表单行为。您可以通过use:enhance提供回调来关闭默认行为。然后 SvelteKit 提供手动失效函数

**在每次导航时加载数据可能会很昂贵,而且没有必要。**我可以像使用react-query之类的工具那样缓存这些数据吗?是的,只是方式不同。SvelteKit 允许您设置(然后遵守)Web 已经提供的缓存控制标头。我将在后续文章中介绍缓存失效机制。

我们在本文中所做的一切都使用静态数据并在内存中修改值。如果您需要恢复所有内容并重新开始,请停止并重新启动npm run dev Node 进程。

总结

我们只是触及了 SvelteKit 的皮毛,但希望您已经看到了足以让您兴奋的东西。我不记得上次我发现 Web 开发如此有趣是什么时候了。通过开箱即用的捆绑、路由、SSR 和部署等功能,我可以将更多时间花在编码上而不是配置上。

以下是一些您可以用作学习 SvelteKit 的后续步骤的更多资源