开始我们的 Svelte 待办事项应用程序

现在我们已经对 Svelte 的工作原理有了基本的了解,我们可以开始构建我们的示例应用程序:一个待办事项列表。在本文中,我们将首先了解应用程序所需的的功功能,然后创建一个 Todos.svelte 组件,并添加静态标记和样式,为开始开发待办事项列表应用程序功能做好准备,这些功能将在后续文章中进行介绍。

我们希望用户能够浏览、添加和删除任务,以及将任务标记为已完成。这将是我们将在本教程系列中开发的基本功能,我们还将在过程中介绍一些更高级的概念。

先决条件

至少,建议您熟悉核心 HTMLCSSJavaScript 语言,并了解 终端/命令行

您需要一个安装了 node + npm 的终端来编译和构建您的应用程序。

目标 学习如何创建 Svelte 组件,将其渲染到另一个组件中,使用道具将数据传递到组件中,并保存其状态。

与我们一起编写代码

Git

使用以下命令克隆 GitHub 存储库(如果您尚未完成):

bash
git clone https://github.com/opensas/mdn-svelte-tutorial.git

然后,要进入当前应用程序状态,请运行:

bash
cd mdn-svelte-tutorial/02-starting-our-todo-app

或者直接下载文件夹的内容

bash
npx degit opensas/mdn-svelte-tutorial/02-starting-our-todo-app

请记住运行 npm install && npm run dev 以在开发模式下启动应用程序。

REPL

要使用 REPL 与我们一起编码,请从以下位置开始:

https://svelte.net.cn/repl/b7b831ea3a354d3789cefbc31e2ca495?version=3.23.2

待办事项应用程序功能

这是我们的待办事项列表应用程序准备就绪后的样子

typical to-do list app, with a title of 'what needs to be done', an input to enter more to-dos, and a list of to-dos with checkboxes

使用此 UI,我们的用户将能够

  • 浏览他们的任务
  • 将任务标记为已完成/待办事项,而不会删除它们
  • 删除任务
  • 添加新任务
  • 按状态筛选任务:所有任务、活动任务或已完成任务
  • 编辑任务
  • 将所有任务标记为活动/已完成
  • 删除所有已完成的任务

构建我们的第一个组件

让我们创建一个 Todos.svelte 组件。它将包含我们的待办事项列表。

  1. 创建一个新文件夹 - src/components

    注意:您可以将组件放在 src 文件夹中的任何位置,但 components 文件夹是一个公认的约定,可以让您轻松找到组件。

  2. 创建一个名为 src/components/Todos.svelte 的文件,内容如下:
    svelte
    <h1>Svelte to-do list</h1>
    
  3. public/index.html 中的 title 元素更改为包含文本 Svelte 待办事项列表
    svelte
    <title>Svelte to-do list</title>
    
  4. 打开 src/App.svelte 并将其内容替换为以下内容:
    svelte
    <script>
      import Todos from "./components/Todos.svelte";
    </script>
    
    <Todos />
    
  5. 在开发模式下,当指定组件中不存在的道具时,Svelte 将在浏览器控制台中发出警告;在本例中,我们在 src/main.js 中实例化 App 组件时指定了 name 道具,该道具在 App 中未使用。控制台目前应该会向您显示类似于“<App> was created with unknown prop 'name'”的消息。要消除此问题,请从 src/main.js 中删除 name 道具;它现在应该如下所示:
    js
    import App from "./App.svelte";
    
    const app = new App({
      target: document.body,
    });
    
    export default app;
    

现在,如果您检查测试服务器 URL,您将看到我们的 Todos.svelte 组件被渲染

basic component rendering which a title that says 'Svelte to-do list'

添加静态标记

目前,我们将从应用程序的静态标记表示开始,这样您就可以看到它是什么样的。将以下内容复制并粘贴到我们的 Todos.svelte 组件文件中,替换现有内容

svelte
<!-- Todos.svelte -->
<div class="todoapp stack-large">
  <!-- NewTodo -->
  <form>
    <h2 class="label-wrapper">
      <label for="todo-0" class="label__lg"> What needs to be done? </label>
    </h2>
    <input type="text" id="todo-0" autocomplete="off" class="input input__lg" />
    <button type="submit" disabled="" class="btn btn__primary btn__lg">
      Add
    </button>
  </form>

  <!-- Filter -->
  <div class="filters btn-group stack-exception">
    <button class="btn toggle-btn" aria-pressed="true">
      <span class="visually-hidden">Show</span>
      <span>All</span>
      <span class="visually-hidden">tasks</span>
    </button>
    <button class="btn toggle-btn" aria-pressed="false">
      <span class="visually-hidden">Show</span>
      <span>Active</span>
      <span class="visually-hidden">tasks</span>
    </button>
    <button class="btn toggle-btn" aria-pressed="false">
      <span class="visually-hidden">Show</span>
      <span>Completed</span>
      <span class="visually-hidden">tasks</span>
    </button>
  </div>

  <!-- TodosStatus -->
  <h2 id="list-heading">2 out of 3 items completed</h2>

  <!-- Todos -->
  <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading">
    <!-- todo-1 (editing mode) -->
    <li class="todo">
      <div class="stack-small">
        <form class="stack-small">
          <div class="form-group">
            <label for="todo-1" class="todo-label">
              New name for 'Create a Svelte starter app'
            </label>
            <input
              type="text"
              id="todo-1"
              autocomplete="off"
              class="todo-text" />
          </div>
          <div class="btn-group">
            <button class="btn todo-cancel" type="button">
              Cancel
              <span class="visually-hidden">renaming Create a Svelte starter app</span>
            </button>
            <button class="btn btn__primary todo-edit" type="submit">
              Save
              <span class="visually-hidden">new name for Create a Svelte starter app</span>
            </button>
          </div>
        </form>
      </div>
    </li>

    <!-- todo-2 -->
    <li class="todo">
      <div class="stack-small">
        <div class="c-cb">
          <input type="checkbox" id="todo-2" checked />
          <label for="todo-2" class="todo-label">
            Create your first component
          </label>
        </div>
        <div class="btn-group">
          <button type="button" class="btn">
            Edit
            <span class="visually-hidden">Create your first component</span>
          </button>
          <button type="button" class="btn btn__danger">
            Delete
            <span class="visually-hidden">Create your first component</span>
          </button>
        </div>
      </div>
    </li>

    <!-- todo-3 -->
    <li class="todo">
      <div class="stack-small">
        <div class="c-cb">
          <input type="checkbox" id="todo-3" />
          <label for="todo-3" class="todo-label">
            Complete the rest of the tutorial
          </label>
        </div>
        <div class="btn-group">
          <button type="button" class="btn">
            Edit
            <span class="visually-hidden">Complete the rest of the tutorial</span>
          </button>
          <button type="button" class="btn btn__danger">
            Delete
            <span class="visually-hidden">Complete the rest of the tutorial</span>
          </button>
        </div>
      </div>
    </li>
  </ul>

  <hr />

  <!-- MoreActions -->
  <div class="btn-group">
    <button type="button" class="btn btn__primary">Check all</button>
    <button type="button" class="btn btn__primary">Remove completed</button>
  </div>
</div>

再次检查渲染结果,您将看到类似于以下内容:

A to-do list app, but unstyled, with a title of what needs to be done, inputs, checkboxes, etc.

上面的 HTML 标记样式不是很好,而且在功能上也毫无用处。不过,让我们看看标记并了解它与我们所需的功能之间的关系

  • 用于输入新任务的标签和文本框
  • 三个按钮用于按任务状态进行筛选
  • 一个标签,显示任务总数和已完成的任务数量
  • 一个无序列表,其中包含每个任务的列表项
  • 当任务正在被编辑时,列表项包含一个输入框和两个按钮,用于取消或保存修改
  • 如果任务未被编辑,则有一个复选框用于设置已完成状态,以及两个用于编辑或删除任务的按钮
  • 最后,有两个按钮用于选中/取消选中所有任务,以及删除已完成的任务

在后续文章中,我们将让所有这些功能都正常运行,甚至还会添加更多功能。

待办事项列表的可访问性功能

您可能会注意到此处有一些不寻常的属性。例如

svelte
<button class="btn toggle-btn" aria-pressed="true">
  <span class="visually-hidden">Show</span>
  <span>All</span>
  <span class="visually-hidden">tasks</span>
</button>

在此,aria-pressed 告诉辅助技术(如屏幕阅读器)按钮可以处于两种状态之一:pressedunpressed。将它们视为打开和关闭的类似物。将值设置为 true 表示该按钮默认处于按下状态。

visually-hidden 目前没有任何效果,因为我们还没有包含任何 CSS。但是,一旦我们添加了样式,则具有此类的任何元素都将对有视力的人隐藏,而对屏幕阅读器用户仍然可用 - 这是因为这些词对有视力的人来说是不必要的;它们在那里是为了为没有额外视觉环境的屏幕阅读器用户提供有关按钮功能的更多信息。

往下看,您会发现以下 <ul> 元素

svelte
<ul
  role="list"
  class="todo-list stack-large"
  aria-labelledby="list-heading">

role 属性有助于辅助技术解释元素具有的语义值是什么 - 或其用途是什么。<ul> 默认情况下被视为列表,但我们将要添加的样式会破坏该功能。此角色将“列表”含义还原到 <ul> 元素。如果您想详细了解为什么需要这样做,您可以查看 Scott O'Hara 的文章 "Fixing Lists" (2019)。

aria-labelledby 属性告诉辅助技术,我们将 idlist-heading<h2> 视为描述其下方列表用途的标签。建立这种关联可以为列表提供更具信息性的上下文,这有助于屏幕阅读器用户更好地理解列表的用途。

现在似乎是讨论 Svelte 如何处理可访问性的时候了;让我们现在就来做。

Svelte 无障碍性支持

Svelte 特别强调可访问性。其目的是鼓励开发人员“默认情况下”编写更易访问的代码。作为一个编译器,Svelte 可以静态分析我们的 HTML 模板,以便在编译组件时提供可访问性警告。

可访问性(缩写为 a11y)并不总是容易做对,但 Svelte 会在您编写不可访问的标记时发出警告。

例如,如果我们在 todos.svelte 组件中添加一个 <img> 元素,但没有相应的 alt 属性

svelte
<h1>Svelte To-Do list</h1>

<img height="32" width="88" src="https://w3org.cn/WAI/wcag2A" />

编译器将发出以下警告

bash
(!) Plugin svelte: A11y: <img> element should have an alt attribute
src/components/Todos.svelte
1: <h1>Svelte To-Do list</h1>
2:
3: <img height="32" width="88" src="https://w3org.cn/WAI/wcag2A">
   ^
created public/build/bundle.js in 220ms

[2020-07-15 04:07:43] waiting for changes...

此外,即使在调用编译器之前,我们的编辑器也可以显示此警告

A code editor window showing an image tag, with a popup error message saying that the element should have an alt attribute

您可以使用以 svelte-ignore 开头的 注释 告诉 Svelte 忽略下一块标记中的此警告,如下所示:

svelte
<!-- svelte-ignore a11y-missing-attribute -->
<img height="32" width="88" src="https://w3org.cn/WAI/wcag2A" />

注意:使用 VSCode,您可以通过单击 快速修复… 链接或按 Ctrl + . 自动添加此忽略注释。

如果您想全局禁用此警告,可以在 rollup.config.js 文件中为 Svelte 插件添加此 onwarn 处理程序,如下所示:

js
plugins: [
  svelte({
    dev: !production,
    css: (css) => {
      css.write("public/build/bundle.css");
    },
    // Warnings are normally passed straight to Rollup. You can
    // optionally handle them here, for example to squelch
    // warnings with a particular code
    onwarn: (warning, handler) => {
      // e.g. I don't care about screen readers -> please DON'T DO THIS!!!
      if (warning.code === "a11y-missing-attribute") {
        return;
      }

      // let Rollup handle all other warnings normally
      handler(warning);
    },
  }),

  // …
];

在设计上,这些警告是在编译器本身中实现的,而不是作为您可以选择添加到项目的插件。其想法是默认情况下检查标记中的 a11y 问题,并让您选择退出特定警告。

注意:只有在您有充分理由这样做时才应该禁用这些警告,例如在构建快速原型时。成为一个好的网络公民并让您的页面对尽可能广泛的用户群可访问非常重要。

Svelte 检查的可访问性规则取自 eslint-plugin-jsx-a11y,这是一个 ESLint 插件,它对 JSX 元素中的许多可访问性规则提供静态检查。Svelte 旨在在其编译器中实现所有这些规则,并且其中大多数规则已经移植到 Svelte。在 GitHub 上,您可以查看 哪些可访问性检查仍然缺失。您可以通过单击其链接来查看每条规则的含义。

样式化我们的标记

让我们让待办事项列表看起来更好看一些。用以下内容替换文件 public/global.css 的内容

css
/* RESETS */
*,
*::before,
*::after {
  box-sizing: border-box;
}
*:focus {
  outline: 3px dashed #228bec;
  outline-offset: 0;
}
html {
  font: 62.5% / 1.15 sans-serif;
}
h1,
h2 {
  margin-bottom: 0;
}
ul {
  list-style: none;
  padding: 0;
}
button {
  border: none;
  margin: 0;
  padding: 0;
  width: auto;
  overflow: visible;
  background: transparent;
  color: inherit;
  font: inherit;
  line-height: normal;
  -webkit-font-smoothing: inherit;
  -moz-osx-font-smoothing: inherit;
  appearance: none;
}
button::-moz-focus-inner {
  border: 0;
}
button,
input,
optgroup,
select,
textarea {
  font-family: inherit;
  font-size: 100%;
  line-height: 1.15;
  margin: 0;
}
button,
input {
  overflow: visible;
}
input[type="text"] {
  border-radius: 0;
}
body {
  width: 100%;
  max-width: 68rem;
  margin: 0 auto;
  font:
    1.6rem/1.25 Arial,
    sans-serif;
  background-color: #f5f5f5;
  color: #4d4d4d;
}
@media screen and (min-width: 620px) {
  body {
    font-size: 1.9rem;
    line-height: 1.31579;
  }
}
/*END RESETS*/

/* GLOBAL STYLES */
.form-group > input[type="text"] {
  display: inline-block;
  margin-top: 0.4rem;
}
.btn {
  padding: 0.8rem 1rem 0.7rem;
  border: 0.2rem solid #4d4d4d;
  cursor: pointer;
  text-transform: capitalize;
}
.btn.toggle-btn {
  border-width: 1px;
  border-color: #d3d3d3;
}
.btn.toggle-btn[aria-pressed="true"] {
  text-decoration: underline;
  border-color: #4d4d4d;
}
.btn__danger {
  color: #fff;
  background-color: #ca3c3c;
  border-color: #bd2130;
}
.btn__filter {
  border-color: lightgrey;
}
.btn__primary {
  color: #fff;
  background-color: #000;
}
.btn__primary:disabled {
  color: darkgrey;
  background-color: #565656;
}
.btn-group {
  display: flex;
  justify-content: space-between;
}
.btn-group > * {
  flex: 1 1 49%;
}
.btn-group > * + * {
  margin-left: 0.8rem;
}
.label-wrapper {
  margin: 0;
  flex: 0 0 100%;
  text-align: center;
}
.visually-hidden {
  position: absolute !important;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px 1px 1px 1px);
  clip: rect(1px, 1px, 1px, 1px);
  white-space: nowrap;
}
[class*="stack"] > * {
  margin-top: 0;
  margin-bottom: 0;
}
.stack-small > * + * {
  margin-top: 1.25rem;
}
.stack-large > * + * {
  margin-top: 2.5rem;
}
@media screen and (min-width: 550px) {
  .stack-small > * + * {
    margin-top: 1.4rem;
  }
  .stack-large > * + * {
    margin-top: 2.8rem;
  }
}
.stack-exception {
  margin-top: 1.2rem;
}
/* END GLOBAL STYLES */

.todoapp {
  background: #fff;
  margin: 2rem 0 4rem 0;
  padding: 1rem;
  position: relative;
  box-shadow:
    0 2px 4px 0 rgb(0 0 0 / 20%),
    0 2.5rem 5rem 0 rgb(0 0 0 / 10%);
}
@media screen and (min-width: 550px) {
  .todoapp {
    padding: 4rem;
  }
}
.todoapp > * {
  max-width: 50rem;
  margin-left: auto;
  margin-right: auto;
}
.todoapp > form {
  max-width: 100%;
}
.todoapp > h1 {
  display: block;
  max-width: 100%;
  text-align: center;
  margin: 0;
  margin-bottom: 1rem;
}
.label__lg {
  line-height: 1.01567;
  font-weight: 300;
  padding: 0.8rem;
  margin-bottom: 1rem;
  text-align: center;
}
.input__lg {
  padding: 2rem;
  border: 2px solid #000;
}
.input__lg:focus {
  border-color: #4d4d4d;
  box-shadow: inset 0 0 0 2px;
}
[class*="__lg"] {
  display: inline-block;
  width: 100%;
  font-size: 1.9rem;
}
[class*="__lg"]:not(:last-child) {
  margin-bottom: 1rem;
}
@media screen and (min-width: 620px) {
  [class*="__lg"] {
    font-size: 2.4rem;
  }
}
.filters {
  width: 100%;
  margin: unset auto;
}
/* Todo item styles */
.todo {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}
.todo > * {
  flex: 0 0 100%;
}
.todo-text {
  width: 100%;
  min-height: 4.4rem;
  padding: 0.4rem 0.8rem;
  border: 2px solid #565656;
}
.todo-text:focus {
  box-shadow: inset 0 0 0 2px;
}
/* CHECKBOX STYLES */
.c-cb {
  box-sizing: border-box;
  font-family: Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  font-weight: 400;
  font-size: 1.6rem;
  line-height: 1.25;
  display: block;
  position: relative;
  min-height: 44px;
  padding-left: 40px;
  clear: left;
}
.c-cb > label::before,
.c-cb > input[type="checkbox"] {
  box-sizing: border-box;
  top: -2px;
  left: -2px;
  width: 44px;
  height: 44px;
}
.c-cb > input[type="checkbox"] {
  -webkit-font-smoothing: antialiased;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  margin: 0;
  opacity: 0;
}
.c-cb > label {
  font-size: inherit;
  font-family: inherit;
  line-height: inherit;
  display: inline-block;
  margin-bottom: 0;
  padding: 8px 15px 5px;
  cursor: pointer;
  touch-action: manipulation;
}
.c-cb > label::before {
  content: "";
  position: absolute;
  border: 2px solid currentcolor;
  background: transparent;
}
.c-cb > input[type="checkbox"]:focus + label::before {
  border-width: 4px;
  outline: 3px dashed #228bec;
}
.c-cb > label::after {
  box-sizing: content-box;
  content: "";
  position: absolute;
  top: 11px;
  left: 9px;
  width: 18px;
  height: 7px;
  transform: rotate(-45deg);
  border: solid;
  border-width: 0 0 5px 5px;
  border-top-color: transparent;
  opacity: 0;
  background: transparent;
}
.c-cb > input[type="checkbox"]:checked + label::after {
  opacity: 1;
}

对标记进行样式设置后,现在一切都看起来更好了

Our to-do list app, styled, with a title of 'what needs to be done', an input to enter more to-dos, and a list of to-dos with checkboxes

迄今为止的代码

Git

要查看本文结束时代码的状态,请访问您的存储库副本,方法如下:

bash
cd mdn-svelte-tutorial/03-adding-dynamic-behavior

或者直接下载文件夹的内容

bash
npx degit opensas/mdn-svelte-tutorial/03-adding-dynamic-behavior

请记住运行 npm install && npm run dev 以在开发模式下启动应用程序。

REPL

要在 REPL 中查看代码的当前状态,请访问以下网址:

https://svelte.net.cn/repl/c862d964d48d473ca63ab91709a0a5a0?version=3.23.2

总结

有了标记和样式,我们的待办事项列表应用程序开始成型,我们已经做好了准备,可以开始专注于我们必须实现的功能。