客户端框架介绍 - 学习 Web 开发

客户端框架介绍
=======

* Overview: Client-side JavaScript frameworks
* 下一页

在本章节我们开始大致了解框架，简要回顾 JavaScript 和框架的历史，为什么框架会存在以及它们提供了什么，如何开始考虑选择一个框架并学习，以及对于客户端框架还有什么替代方案。

|  |  |
| --- | --- |
| 前提： | 熟悉 HTML, CSS, 以及 JavaScript 语言的核心。 |
| 目标： | 了解客户端 JavaScript 框架是如何存在的，它们能解决什么问题，还有哪些替代方案，以及如何选择一个框架。 |

简史
--

当 JavaScript 在 1996 发布以后，它给 web 增加了少许的交互性和乐趣，在那个时候，网页仍由静态文档组成。web 应该不仅仅是阅读的地方，更是创造的地方。JavaScript 变得越来越流行。使用 JavaScript 的开发者们创造工具来解决他们遇到的问题，并且将其打包成称为**库**的可复用组件，这样就能和他人共享解决方案。这种共享库的体系帮助塑造了 web 的增长。

现在，JavaScript 是 web 的一部分，95% 的网页都在使用，而且 web 又是现代生活的一部分。用户在 web 上写文章，管理预算，听音乐，看电影，以及和相隔万里的人通过文字，音频，视频聊天来实时交流。web 让我们能够做到那些过去只能在电脑上安装本地应用程序才能做到的事。这些现代的，复杂的，具有交互性的网站通常被称为 **web 应用程序**。

现代 JavaScript 框架的到来加快了打造高度动态化和交互性强的应用程序的速度。**框架**就是提供该如何构建应用程序的意见的库。这些意见能使应用具有可预测性和同质性。可预测性让软件能在扩展到很大规模的同时仍保持可维护性。可预测性和可维护性对于一个软件的长久健康运行是十分重要的。

在现代 web 中，JavaScript 框架为众多令人印象深刻的软件提供支持——包括许多你可能每天都使用的网站。你正在阅读的这个 MDN Web 文档页面，就是使用 React / ReactDOM 框架驱动前端的。

有哪些框架？
------

有很多框架可供你选择，但以下主要介绍目前公认的“四大框架”。

### Ember

Ember 于 2011 年 12 月发布，最初作为 SproutCore 项目的延续而开始。比其新式的替代品（例如 React 和 Vue），作为老框架，它的用户人数要少得多。但因其稳定性、社区支持以及编程原则都非常良好，它仍然享有很高的知名度。

开始学习 Ember (en-US)

### Angular

Angular 是一个开源 Web 应用程序框架，正式发布于 2016 年 9 月 14 日。它由构建 AngularJS 的团队完全重写，并由 Google 的 Angular 团队社区共同领导。

Angular 是一种基于组件的框架，使用声明式的 HTML 模板。在应用构建时，框架的编译器将 HTML 模板转换为优化好的 JavaScript 指令，这一过程对开发者是透明的。Angular 使用 TypeScript，它是 JavaScript 的超集，我们将在下一章中对其进行更多介绍。

开始学习 Angular

### Vue

在学习了 AngularJS 的源码之后，Evan You（尤雨溪）在 2014 年第一次发布 Vue 。Vue 是“四大框架”中最年轻的，但在最近，它的人气迅速上升。

Vue，就像 AngularJS，用它自己的代码拓展了 HTML。除此之外，它还主要依赖于现代的、标准化的 JavaScript。

开始学习 Vue

### React

Facebook 在 2013 年发布了 React。在当时 React 已经被用于 Facebook 内部用来解决许多问题。严格来说 React 本身并不是框架，而是一个用来渲染用户界面组件的库。React 被用来组合*其他*用来构建应用的库——React 和 React Native 让开发者能够用 JavaScript 构建移动应用；React 和 ReactDOM 使他们能够被用来制作 web 应用程序等。

因为 React 和 ReactDOM 被经常放在一起使用，通俗地讲，React 可以被理解为是一个 JavaScript 框架。当你通读了这个模块时，我们将使用这种口语化的理解进行描述。

React 用类似 HTML 的语法的 JSX 拓展了 JavaScript。

开始学习 React

框架为何会存在？
--------

我们已经讨论了因为什么契机而创造了框架，但我们仍不知道为什么开发者认为有必要创造它。要知道这个问题的答案，我们首先需要来看一看软件开发中的各种挑战。

设想一个很常见的软件：一个待办事项清单创建器，在接下来的章节中我们会使用各种框架来实现它。这个应用应让用户可以完成诸如呈现任务列表、添加和删除任务等操作，且在完成这些操作的同时能可靠地跟踪和更新应用程序的底层数据。在软件开发中，这种底层数据被称为状态（state）。

上述每个目标理论上都很简单。我们可以遍历数据来列出清单，添加一个对象来创建新任务，使用标识符来查找、编辑和删除任务。需要注意的是，用户都是在浏览器中使用应用的这些功能，然而这就引出了一些问题： **每当我们修改应用的数据时，我们都需要更新用户界面以使其匹配。**

我们可以通过待办事项清单应用的一个功能来展现这个问题的难点：呈现任务清单。

冗长的 DOM 操作
----------

构建 HTML 元素并适时在浏览器中呈现它们需要大量的 JavaScript 代码。假设我们的状态——底层数据，是一个结构如下的对象数组：

```js
const state = [
  {
    id: "todo-0",
    name: "Learn some frameworks!",
  },
];
```

如何向用户展示其中一个任务？我们想将每个任务表示为一个列表项——无序列表元素（`<ul>`）中的一个 HTML `<li>` 元素。我们该如何实现？可以像下面这样：

```js
function buildTodoItemEl(id, name) {
  const item = document.createElement("li");
  const span = document.createElement("span");
  const textContent = document.createTextNode(name);

  span.appendChild(textContent);

  item.id = id;
  item.appendChild(span);
  item.appendChild(buildDeleteButtonEl(id));

  return item;
}
```

在这里，我们使用 `document.createElement()` 方法来创建我们的 `<li>` 元素，以及另外几行代码来创建它需要的属性和子元素。

该代码片段的第十行引用了另一个构建函数：`buildDeleteButtonEl()`。它遵循与我们用来构建列表项元素时类似的模式：

```js
function buildDeleteButtonEl(id) {
  const button = document.createElement("button");
  const textContent = document.createTextNode("Delete");

  button.setAttribute("type", "button");
  button.appendChild(textContent);

  return button;
}
```

这个按钮只有当我们决定实现删除功能时，才会生效。要在页面上渲染我们的列表项，代码看起来可能是这样的：

```js
function renderTodoList() {
  const frag = document.createDocumentFragment();
  state.tasks.forEach((task) => {
    const item = buildTodoItemEl(task.id, task.name);
    frag.appendChild(item);
  });

  while (todoListEl.firstChild) {
    todoListEl.removeChild(todoListEl.firstChild);
  }
  todoListEl.appendChild(frag);
}
```

我们现在已经有超过 30 行*仅*服务于用户界面的代码——*只*作用于在 DOM 中渲染某些内容的步骤——并且我们还没有为列表添加样式呢！

如本例所示，直接使用 DOM，我们需要了解有关 DOM 工作原理的许多内容：如何创建元素；如何改变它们的属性；如何将元素放在一起；如何让它们出现在页面上。这些代码实际上都没有处理用户交互，或添加删除之类任务。如果我们添加这些功能，我们必须记得在正确的时间以正确的方式更新我们的用户界面。

JavaScript 框架的出现是为了使这些工作变得更容易——它们的存在是为了提供更好的*开发体验*。它们没有给 JavaScript 带来新的功能；但它们使你可以更轻松地使用 JavaScript 来构建现代的 web。

如果你想查看本节中的代码示例，可以查看 CodePen 上该应用的可用版本, 该版本还允许用户添加和删除新任务。

有关本节中使用的 JavaScript 的更多信息：

* `document.createElement()`
* `document.createTextNode()`
* `document.createDocumentFragment()`
* `eventTarget.addEventListener()`
* `node.appendChild()`
* `node.removeChild()`
另一种打造用户界面的方式
------------

JavaScript 框架都提供了一种更具以*声明性*的方式地编写用户界面的方法。也就是说，它们允许你编写描述用户界面的代码，然后框架将你编写的描述用户界面的代码通过在幕后的 DOM 显现出来。

原生 JavaScript 使用循环来构建新 DOM 元素的方法乍一看很难理解。相对地，让我们看一看使用 Vue 来呈现任务清单的方式：

```html
<ul>
  <li v-for="task in tasks" v-bind:key="task.id">
    <span>{{task.name}}</span>
    <button type="button">Delete</button>
  </li>
</ul>
```

你没看错。以上用了 JavaScript 框架的代码用了 6 行就实现了刚刚需要 32 行代码才能实现的功能。如果你对这里的大括号和 `v-` 属性不熟悉，没关系；你会在后面的模块中学习到 Vue 特有的语法。这里要指出的是，这段 Vue 代码看起来很像它所代表的用户界面，而原生 JavaScript 代码则不然。

幸亏有了 Vue，让我们不必编写自己的函数来构建用户界面；我们仅仅需要向 Vue 描述每个项目应该是什么样子，然后 Vue 会以很好的、高效的方式帮我们处理好。因此熟悉 Vue 的开发者可以轻松的加入我们的项目，快速地搞清楚项目是如何运作的。不仅仅是 Vue：使用其他框架也可以提高团队和个人的效率。

其实在原生 JavaScript 中也可以做*与此类似*的事情。模板字符串使得编写表示最终元素外观的 HTML 字符串变得容易起来。对于像创建上文示例的待办事项清单那样简单的应用来说，这可能是一种有用的方法，但它对于管理数千条数据记录，且可以在用户界面上渲染许多独特元素的大型应用程序来说，是难以维护的。

框架提供给我们的其他功能
------------

让我们看看使用框架还有哪些好处。正如我们之前提到的，虽然框架的优秀特性在原生 JavaScript 中也可以类似地实现，但是使用框架可以消除必须自己解决这些问题的认知负担。

### 工具

由于此模块中的每个框架都有一个庞大而活跃的社区，因此每个框架的体系都提供了工具来改善开发体验。这些工具让诸如添加测试（确保你的应用表现应有的行为）和 linting（确保你的代码没有错误且排版统一）变得更加容易。

**备注：** 如果你想了解有关 Web 工具概念的更多详细信息，请阅读客户端工具概述。

### 组件化

大多数框架都鼓励开发者将其用户界面的不同部分抽象为*组件（components）*——可维护、可重用且可以相互通信的代码块。与给定组件相关的所有代码都可以存在于一个文件（或几个特定文件）中，因此身为开发者的你就可以轻松地知道去哪里修改目标组件。在原生 JavaScript 中，你就必须花时间创建自己的一套高效、可扩展的编码约定来实现这一点。如果让许多 JavaScript 开发者使用他们自己的设备，最终可能会导致与用户界面某个部分相关的所有代码分布在一个文件中——或者完全分布在另一个文件中。

### 路由

Web 最重要的特点是它允许用户从一个页面导航到另一个页面——毕竟，它是一个相互链接的文档网络。当你点击网站上的链接时，你的浏览器会与目标服务器通信，获取新内容后将其显示给你。当浏览器这样做时，地址栏中的 URL 会发生变化。你可以记下来这个新 URL 并稍后返回该页面——或者把这个 URL 分享给其他人，以便他们轻松找到相同的页面。你的浏览器会记住你的导航历史记录，并允许你来回导航（译者注：其实就是浏览器的前进后退功能）。这被称作**服务端路由**。

现代 Web 应用程序通常不会获取和渲染新的 HTML 文件——它们加载单个 HTML shell，并不断更新其中的 DOM 而无需将用户导航到 Web 上的新地址。(这被称为**单页应用程序（single page app）** 或 **SPA**)。每个新的伪网页通常称为一个 *view*，且在默认情况下，不会做任何路由。

当 SPA 足够复杂并呈现足够多的独特视图时，将路由功能引入你的应用程序非常重要。人们习惯于能够链接到应用程序中的特定页面，在他们的浏览器中前进和后退等等，当这些标准的 Web 功能被破坏时，他们的体验会受到影响。当客户端应用程序以这种方式处理路由时，它被恰当地称为**客户端路由**。

*可以使用* JavaScript 和浏览器的原生功能来实现路由功能，但是流行的、活跃开发的框架具有配套库，使路由功能在开发过程中更直观。

使用框架的注意事项
---------

作为一名高效的 web 开发者，意味着使用最合适的工具来完成工作。JavaScript 框架使前端应用开发变得容易，但它们并不是解决所有问题的灵丹妙药。本节讨论在使用框架时你应该考虑的一些事项。请记住，你也许根本不需要框架——要注意不要为了使用框架而使用框架。

### 熟悉工具

就像原生 JavaScript 一样，框架需要时间来学习，也有它们自己的特性。在你决定为项目使用框架之前，确保你有足够的时间学习它的特性，使它发挥功效，而不是阻碍你，并确保你的团队成员也对它满意。

### 过度工程化

如果你的 web 开发项目是一个只有几个页面的个人作品集，并且这些页面几乎没有交互功能，那么可能根本不需要框架（及其所有的 JavaScript）。也就是说，框架并不是完全统一的，其中一些框架比其他框架更适合于小型项目。Sarah Drasner 在 Smashing 杂志的一篇文章中写道：作为一种使网页的一小部分具有交互性的工具 Vue 是如何取代 jQuery 的 。

### 大型代码库与抽象

通过在幕后处理 DOM 交互，框架允许你编写更多的声明式代码，有时总体上可以编写*更少*的代码。这种抽象对于你作为开发者的体验非常好，但它也是有代价的。为了将你编写的内容转换为 DOM 的变更，框架必须运行自己的代码，这反过来又会使你最终的软件变得更大，并且花费更高的运行成本。

一些额外的代码是必不可少的，且一个支持 tree-shaking 的框架（在构建过程中删除应用程序实际未使用的代码）将使你保持应用程序的小型化；但这些额外的代码仍然是你在考虑应用程序性能时需要记住的一个因素，尤其是在网络或存储限制更严格的设备上，比如手机。

框架的抽象不仅影响 JavaScript，还影响你与 web 的关系。无论你如何构建 web，最终，用户都是与 HTML 交互。用 JavaScript 编写整个应用可能会使你忽略 HTML 及其各种标签的用途，并导致产出一个无语义且难以使用的 HTML 文档。实际上，完全依赖 JavaScript 有可能写出脆弱的应用，且没有 JavaScript 就无法运行。

框架不是问题的根源。错误的优先级可能使得*任何*应用变得脆弱、臃肿和难以使用。框架对开发者很友好，如果你的首要任务是制作一个复杂的 web 应用程序，那么很容易做到。然而，如果你没有考虑性能和无障碍，那么框架将放大这种脆弱、臃肿和难以使用。现代开发者的这种重心放在了框架上，已经在许多地方颠倒了 web 的结构。现代 web 通常把 JavaScript 放在首位，用户体验放在最后，而不是一个健壮的、内容优先的文档网络。

框架驱动页面的无障碍
----------

让我们在上一节中所说的基础上继续，并更多地谈谈无障碍。使用户界面无障碍总是需要一些思考和努力，而框架可能会使这个过程复杂化。你通常必须使用高级框架 API 来访问本地浏览器的功能，如 ARIA live regions 或专注于对它们的管理。

在某些情况下，框架应用程序会造成传统网站不存在的无障碍问题。这方面的典型例子是前面提到的客户端路由。

使用传统的（服务器端）路由，导航 Web 具有可预测的结果。浏览器知道将焦点设置到页面顶部，辅助技术将宣读页面的标题。每次导航到新页面时，这些事情都会发生。

使用客户端路由，你的浏览器不会加载新的网页，因此它不知道它应该自动调整焦点还是宣读新的页面标题。框架作者投入了大量的时间和精力来编写 JavaScript 来重新创建这些特性，即使这样，也没有一个框架能做到非常完美。

结果是，你应该从*每个* Web 项目的一开始就考虑无障碍，但请记住，如果你不这样做，使用框架的抽象代码库更有可能遇到的无障碍问题。

如何选择一个框架
--------

本模块中讨论的每个框架都采用不同的 Web 应用程序开发方法。每一个都在定期改进或变化，各有利弊。选择合适的框架是一个依赖于团队和项目的过程，你应该自己做研究，发现什么适合你的需求。尽管如此，我们确定了你可能需要考虑的几个问题，以便更有效地研究你的选择：

1. 框架支持哪些浏览器？
2. 框架使用哪些领域特定语言？
3. 该框架是否有强大的社区和良好的文档（和其他支持）可用？

 本节中的表提供了每个框架提供的浏览器支持以及它的 **DSL 语言** 的简要描述。
 广义上，领域专用语言（**DSL**）是与软件开发的特定领域相关的编程语言。在框架的环境中，DSL 是 JavaScript 或 HTML 的变体，使让使用该框架开发变得更容易。至关重要的是，没有一个框架要求开发者使用特定的 DSL，但它们几乎都是在设计时考虑特定的 DSL。选择不使用框架首选的 DSL 将意味着你错过了改善开发者体验的功能。

在为任何新项目做出选择时，你应该认真考虑框架的模型和 DSL。不匹配的浏览器支持可能会成为用户的障碍；不匹配的 DSL 支持可能会成为你和你的团队成员的障碍。

| 框架 | 浏览器支持 | DSL 偏好 | 支持的 DSL |
| --- | --- | --- | --- |
| Angular | IE9+ | TypeScript | 基于 HTML、TypeScript |
| React | 现代的浏览器（IE9+ with Polyfills） | JSX | JSX、TypeScript |
| Vue | IE9+ | 基于 HTML | 基于 HTML、JSX、Pug |
| Ember | 现代的浏览器（IE9+ in Ember version 2.18） | Handlebars | Handlebars、TypeScript |

**备注：** 我们描述为“基于 HTML”的 DSL 没有官方名称。它们并不是真正的 DSL，但它们是非标准的 HTML，所以我们认为它们值得强调。

参考来源：

* React 浏览器支持：官方文档
* Ember 浏览器支持：Ember 3.0 发布
* Ember 模板语言（官方文档）
### 框架是否有强大的社区？

这也许是最难衡量的指标，因为社区规模与易于看见的数字并不直接相关。你可以查看一个项目的 GitHub star 数或每周 npm 下载量，以了解它的受欢迎程度，但有时最好的做法是搜索一些论坛或与其他开发者交谈。这不仅仅是社区的规模，还包括社区的欢迎和包容性，以及文档的好坏。

### 在互联网上的意见

不要只关注我们在这件事上的话——网上到处都有讨论。维基媒体基金会最近选择使用 Vue 作为其前端，并发布了关于框架采用方面的 RFC。RFC 的作者 Eric Gardner 概述了维基媒体项目的需求，以及为什么这个框架是团队不错选择。这个 RFC 是一个很好的例子，说明在计划使用前端框架时，你应该做一点调查和研究。

JavaScript 状况调查包含了来自 JavaScript 开发者的有效反馈。它涵盖了许多与 JavaScript 相关的主题，包括关于框架的使用和开发者对框架的好恶的数据。目前，好几年的数据可以让你感受到一个框架的受欢迎程度。
 Vue 团队详尽地将 Vue 与其他流行框架进行了比较。在这种比较中可能有一些偏见（他们注意到了这一点），但尽管如此，它还是一个宝贵的资源。

客户端框架的替代方案
----------

如果你正在寻找工具来加快 Web 开发过程，并且你知道你的项目不需要很多客户端 JavaScript，你可以寻求其他几种构建 Web 的解决方案：

* 内容管理系统
* 服务端渲染
* 静态站点生成器
### 内容管理系统

**内容管理系统**（**CMS**）是任何允许用户在不直接编写代码的情况下创建网页内容的工具。对于大型项目来说，它们是一种很好的解决方案，尤其是那些需要由编码能力有限的内容编写者来编写的项目，或者希望节省时间的程序员。然而，它们确实需要大量的时间来设置，使用 CMS 意味着你至少放弃了对网站最终输出的一些控制。例如：如果你选择的 CMS 默认不编写无障碍的内容，通常很难改进这一点。

受欢迎的 CMS 的一些例子：Wordpress、Joomla 和 Drupal。

### 服务端渲染

**服务端渲染**（**SSR**）是一种应用程序体系结构，在该体系结构中，呈现单页应用程序是服务器的工作。这与客户端渲染相反，服务端渲染是构建 JavaScript 应用程序最常见、最直接的方式。服务端渲染对于客户端的设备压力更小，因为你只发送渲染过的 HTML 文件，但它比客户端渲染更加难以构建。

本模块中介绍的所有框架都支持服务端渲染和客户端渲染。给 React 的 Next.js，给 Vue 的 Nuxt.js（是的，这很令人困惑，它们没有关系），给 Ember 的 FastBoot 还有给 Angular 的 Angular Universal 。

**备注：** 有些 SSR 解决方案是由社区编写和维护的，而有些是由框架的维护者提供的“官方”解决方案。

### 静态站点生成器

静态网站生成器是动态生成多页面网站的所有网页的程序——包括任何相关的 CSS 或 JavaScript——这样它们就可以在任何地方发布。发布主机可以是一个 GitHub Page，一个 Netlify 实例，或者任何你选择的私有服务器。这种方法有很多优点，主要是在性能方面（用户的设备不使用 JavaScript 构建页面，它已经完成了）和安全性（对静态页面的攻击更少）。这些网站仍然可以使用 JavaScript，它们需要它，但并不依赖于它。静态网站生成器需要时间来学习，就像任何其他工具，它可能是你开发过程中的障碍。

静态网站可以可以生成你想要独特的页面。正如框架使你能够快速编写客户端 JavaScript 应用程序一样，静态站点生成器使你能够快速创建本来需要单独编写的 HTML 文件。与框架一样，静态站点生成器允许开发者编写定义网页公共部分的组件，并将这些组件组合在一起创建最终页面。在静态站点生成器的相关术语中，这些组件称为**模板**。由静态站点生成器构建的 Web 页面甚至可以成为框架应用程序的主页：例如，如果你想让静态生成的网站的一个特定页面在用户访问 React 应用程序时启动它，你可以这样做。

静态网站生成器已经存在了相当长的时间，它们在 web 近来的历史上看到了一线生机。现在有一些非常好的选择，例如 Hugo、Jekyll, Eleventy 和 Gatsby。

如果你想了解更多关于静态网站生成器的信息，去看一看 Tatiana Mac 的 Eleventy 新手指南。在该系列的第一篇文章中，她解释了什么是静态网站生成器，以及它如何与发布 Web 内容的其他方法联系起来。

总结
--

我们对框架的介绍到此结束——我们还没有教给你任何代码，但希望我们已经给你一个有用的背景，说明为什么要使用框架，以及如何选择框架，让你兴奋地了解更多信息，并参与进来！

我们的下一篇文章将深入到较低的层次，看看框架倾向于提供的特定类型的功能，以及它们的工作原理。

* Overview: Client-side JavaScript frameworks
* 下一页


创建第一个 Vue 组件 - 学习 Web 开发

创建第一个 Vue 组件
============

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

现在是时候深入了解 Vue，并创建我们自己的自定义组件了--我们将从创建一个组件来表示待办事项列表中的每个项目开始。在这一过程中，我们将学习一些重要的概念，例如在其他组件中调用组件，通过道具向它们传递数据，以及保存数据状态。

**备注：** 如果你需要根据我们的版本检查你的代码，你可以在我们的 todo-vue 仓库找到中找到示例 Vue 程序代码的完成版本。有关运行中的实时版本，请参见 https://mdn.github.io/todo-vue/。

|  |  |
| --- | --- |
| 前提： | 熟悉HTML、CSS、JavaScript核心语言，了解终端或命令行。

 Vue 组件是由管理应用程序数据的 JavaScript 对象和映射到基础 DOM
 结构的基于 HTML 的模板语法组成的。对于安装，以及使用 Vue
 的一些更高级的功能（如单文件组件或渲染函数），你需要一个安装了 node 和
 npm 的终端。
  |
| 目标： | 学习如何创建一个 Vue 组件，将其渲染到另一个组件中，使用props将数据传递到组件中，并保存其状态。 |

创建一个 ToDoItem 组件
----------------

让我们创建第一个组件，它将显示一个单一的待办事项。我们将用它来建立我们的待办事项列表。

1. 在你的`moz-todo-vue/src/components`目录下，创建一个`ToDoItem.vue`的新文件。在你的代码编辑器中打开该文件。
2. 通过在文件顶部添加`<template></template>`来创建组件的模板部分。
3. 在你的模板部分下面创建一个`<script></script>`部分。在`<script>`标签内，添加一个默认导出对象`export default {}`，这是你的组件对象。

你的文件现在应该是这样的：

```html
<template> </template>
<script>
 export default {};
</script>
```

现在我们可以开始为`ToDoItem`添加实际内容了。Vue 模板目前只允许一个根元素--一个元素需要包裹模板内的所有内容（Vue 3 发布后会改变这种情况）。我们将为该根元素使用一个`<div>`。

1. 现在在你的组件模板中添加一个空的`<div>`。
2. 在那个 `<div>` 里面，让我们添加一个 `checkbox` 和一个对应的 `label`。给复选框添加一个 `id`，并添加一个 `for` 属性，将复选框映射到标签上，如下所示：

```html
<template>
  <div>
    <input type="checkbox" id="todo-item" checked="false" />
    <label for="todo-item">My Todo Item</label>
  </div>
</template>
```

### 在应用程序中使用 TodoItem 组件

这一切都很顺利，但我们还没有将组件添加到我们的应用程序中，所以没有办法测试它，看看一切是否正常。我们现在就把它添加进去吧。

1. 再次打开`App.vue`文件。
2. 在`<script>`标签的顶部，添加以下内容来引入`ToDoItem`组件：

```js
import ToDoItem from "./components/ToDoItem.vue";
```

3. 在你的组件对象里面，添加 `components` 属性，然后在它里面添加你的 ToDoItem 组件进行注册。

你的`<script>`内容现在应该是这样的：

```js
import ToDoItem from "./components/ToDoItem.vue";

export default {
  name: "app",
  components: {
    ToDoItem,
  },
};
```

这和之前 Vue CLI 注册`HelloWorld`组件的方式是一样的。

要在应用程序中实际展示 `ToDoItem` 组件，你需要在 `<template>` 模板内添加一个 `<to-do-item></to-do-item>` 元素。请注意，组件文件名及其在 JavaScript 中的表示方式总是用大写驼色（例如 `ToDoList`），而等价的自定义元素总是用连字符小写（例如 `<to-do-list>`）。

1. 在`<h1>`下面，创建一个无序列表 (`<ul>`)，其中包含一个列表项 (`<li>`)。
2. 在列表项 (<li>) 里面添加`<to-do-item></to-do-item>`.

你的`App.vue`的`<template>`内容现在应该是这样的：

```html
<div id="app">
  <h1>To-Do List</h1>
  <ul>
    <li>
      <to-do-item></to-do-item>
    </li>
  </ul>
</div>
```

如果你再次查看你的应用程序的渲染情况，你现在应该看的到渲染的`ToDoItem`组件，由一个复选框和一个标签组成。

![The current rendering state of the app, which includes a title of To-Do List, and a single checkbox and label](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Vue_first_component/rendered-todoitem.png)

使用 props 让组件动态化
---------------

我们的 `ToDoItem` 组件仍然不太可用，因为我们只能在页面上包含它一次（ID 必须唯一），而且我们没有办法设置 label 标签的文本。这一切都不是动态的。

我们需要的是一些组件状态。这可以通过在组件中添加 props 来实现。你可以认为 props 与函数中的输入类似。prop 的值给予了组件影响其显示的初始状态。

### 注册 props

在 Vue 中，注册 props 的方式有两种：

* 第一种方式是，以字符串数组的方式列出 props，数组中的每个实体对应一个 prop 名称。
* 第二种方法是将 props 定义为一个对象，每个 key 对应于 prop 名称。将 props 列为对象允许你指定默认值，将 props 标记为 required，执行基本的对象类型 (特别是 JavaScript 基本类型) ，并执行简单的 prop 校验。

**备注：** 注意：prop 验证只能在 development 模式下进行，所以你不能在生产环境中严格依赖它。此外，prop 验证函数在组件实例创建之前被调用，因此它们不能访问组件状态 (或其他 props)。

针对 ToDoItem 组件，我们将使用对象注册法。

1. 回到 `ToDoItem.vue` 文件。
2. 在默认导出的 `default {}` 对象中添加一个 `props` 属性，该 props 属性含有一个空对象。
3. 在这个对象里，添加两个 key 为 `label` 和 `done` 属性。
4. `label` 的值应该是一个带有两个属性的对象（或者是 **props**，因为它们被调用在可找到的组件的 context）
    1. 第一个 `required` 属性，它的值是 `true`. 这将会告诉 Vue 说，我们希望每个该组件的实例都必须有个 label 字段。如果 `ToDoItem` 组件没有 label 字段的话，Vue 会提示警告。
    2. 第二是添加一个 `type` 属性。这个属性的值设为 JavaScript 的 `String` 类型。这等于告诉 Vue，我们希望 type 属性的值是 String 类型的。
5. 现在转向 `done` prop.
    1. 首先添加一个 `default` 属性，它的值是 `false`。这意味着当没有 `done` prop 被传递给 `ToDoItem` 组件时， `done` prop 的值会是 false（注意 default 属性不是必需的————我们只在非 required props 里才需要 `default` ）
    2. 接着，添加一个 `type` 属性，值为 `Boolean`。这将告诉 Vue，我们希望这个 prop 的值是 JavaScript 的 Boolean 类型。

你的组件对象现在看起来应该像是这样：

```js
<script>
  export default {
    props: {
      label: { required: true, type: String },
      done: { default: false, type: Boolean }
    }
  };
</script>
```

### 使用已注册的 props

随着组件对象中这些 props 的定义，我们可以在 template 里使用这些变量值。让我们开始向组件模版中添加 `label` prop。

在你的 `<template>` 中，将 `<label>` 标签的 contents 内容修改为 `{{label}}`。

`{{}}` 是 Vue 中的一个特殊的模版语法，它能在模版内打印类中定义的 JavaScript 表达式的结果，包括值和方法。重要的是，`{{}}` 里的内容是作为文本显示，而非 HTML。在此例中，我们打印的是 `label` 的值。

现在，你组件的 template 部分应该是像这样：

```html
<template>
  <div>
    <input type="checkbox" id="todo-item" checked="false" />
    <label for="todo-item">{{label}}</label>
  </div>
</template>
```

回到网页浏览器中，你将会看到 todo item 与之前相同，但是没有 label。转到浏览器的开发者工具，你会在控制台中看到这样的警告：
```

[Vue warn]: Missing required prop: "label"

found in

---> <ToDoItem> at src/components/ToDoItem.vue
       <App> at src/App.vue
         <Root>
```

这是因为我们将 `label` prop 标记为 required, 但我们从未给组件这个 prop————我们已经在 template 内定义了希望使用这个 prop 值的位置，但在调用时我们没有把它传递进组件。让我们修复这个问题。

在我们的 `App.vue` 文件中，像常规 HTML 属性那样，在 `<to-do-item></to-do-item>` 里添加一个 `label` 属性：

```html
<to-do-item label="My ToDo Item"></to-do-item>
```

现在你会在页面上看到 label 了，并且 console 里不会再出现警告提示。

这就是一个简单的 props。接下来我们将讨论 Vue 如何持久化数据状态

Vue 的数据对象
---------

如果你改变 传递到在你的 App 组件中访问的 `<to-do-item></to-do-item>` 的 `label` prop 值，你应该会看到该值被更新。这很 ok。我们现在有了一个 checkbox，一个可更新的 label 标签。然而，我们目前没有对 "done" prop 做任何事情——我们可以在 UI 中选中 checkbox，但在应用程序中，我们没有实际记录 todo item 是否已完成。

为了实现它，我们将组件的 `done` prop 与 `<input>` 标签的 `checked` 属性作绑定。这样就可以记录复选框是否被选中。但是，props 是一种单向数据绑定，组件永远不该改变自己的 props 的值，这点很重要。坚守这点的原因有很多。其一是，组件修改 props 会造成程序调试困难。如果一个值被传递进多个子组件，将很难定位这个值是在哪里被修改的。此外，修改 props 会造成组件重新渲染。所以，一个组件里突变的 props 会触发组件重新渲染，这可能会反过来再次触发 props 突变。

要变通解决此问题，我们可以使用 Vue 的`data` 属性来管理 `done` 的状态。`data` 属性是你可以用来在组件中管理本地状态，它与 `props` 属性一同位于组件对象中并具有以下结构：

```js
data() {
  return {
    key: value
  }
}
```

你将注意到 `data` 属性是一个函数。这是为了在运行时保持组件的每个实例的数据值是唯一的——为每个组件实例单独调用函数。如果将数据声明为仅一个对象，则该组件的所有实例将共享相同的值。这是 Vue 注册组件方式的副作用，也是你不想要的。

你可以使用 `this` 从内部数据访问组件的 props 和其他属性，如所你料。我们很快就会看到一个示例。

**备注：** 由于 `this` 在箭头函数中的工作方式（绑定到父级的上下文），如果使用箭头函数，你将无法从内部 `data` 访问任何必要的属性。因此，不要对 `data` 属性使用箭头函数。

因此，让我们向 `ToDoItem` 组件添加一个 `data` 属性。这将返回一个包含单个属性的对象，即我们将调用 `isDone`，其值为 `this.done`。

按如下方式更新组件对象：

```js
export default {
  props: {
    label: { required: true, type: String },
    done: { default: false, type: Boolean },
  },
  data() {
    return {
      isDone: this.done,
    };
  },
};
```

Vue 在这里做了一点魔术 — 它把你所有的 props 直接绑定到组件实例上，所以我们不必调用 `this.props.done`。它还将其他属性（你已经看到 `data`，以及其他 `methods`，`computed` 等）直接绑该定到实例。这部分是为了使它们可用于的你模板。这样做的缺点是，你需要在这些属性中保持键的唯一性。这就是为什么我们将 `data` 属性为 `isDone` 而不是 `done`。

因此，现在我们需要将 `isDone` 属性附加到我们的组件。与 Vue 使用 `{{}}` 表达式在模板中显示 JavaScript 表达式的方式类似，Vue 有一种特殊的语法将 JavaScript 表达式绑定到 HTML 元素和组件：**`v-bind`**。`v-bind` 表达式如下所示：
```

v-bind:attribute="expression"
```

换句话说，你可以在要绑定到的任何 attribute/prop 前面加上 `v-bind:`。在大多数情况下，你可以对 `v-bind:` 属性使用速记，即只在 attribute/prop 前面加上冒号。所以 `:attribute="expression"` 的工作方式与 `v-bind:attribute="expression"` 相同。

因此，对于 `ToDoItem` 组件中的复选框，我们可以使用 `v-bind` 将 `isDone` 属性映射到 `<input>` 元素上的 `checked` 属性。以下两项是等效的：

```html
<input type="checkbox" id="todo-item" v-bind:checked="isDone" />

<input type="checkbox" id="todo-item" :checked="isDone" />
```

你可以自由使用任何你想要的模式。不过，最好保持一致。由于速记语法更常用，因此本教程将坚持该模式。

所以让我们这样做。立即更新你的 `<input>` 元素，将 `checked="false"` 替换为 `:checked="isDone"`。

通过将 `:done="true"` 传递给 `App.vue` 中的 `ToDoItem` 调用来测试你的组件。请注意，你需要使用 `v-bind` 语法，否则 `true` 将作为字符串传递。显示的复选框应该被选中。

```js
<template>
  <div id="app">
    <h1>My To-Do List</h1>
    <ul>
      <li>
        <to-do-item label="My ToDo Item" :done="true"></to-do-item>
      </li>
    </ul>
  </div>
</template>
```

尝试将 `true` 更改为 `false`，然后再返回，在两者之间重新加载应用以查看状态如何更改。

给 Todos 一个唯一的 ID
----------------

很好！我们现在有一个工作着的复选框，我们可以在其中以编程方式设置状态。但是，我们目前只能向页面添加一个 `ToDoList` 组件，因为 `id` 是硬编码的。这将导致辅助技术出错，因为需要 `id` 才能将标签正确映射到其复选框。为了解决这个问题，我们可以以编程方式在组件数据中设置 `id`。

我们可以使用 lodash 包的 `uniqueid()` 方法来帮助保持索引的唯一性。此包导出一个函数，该函数接收字符串并将唯一整数追加到前缀的末尾。这足以保持组件 `id` 的唯一性。

让我们使用 npm 将包添加到我们的项目中;停止服务器并在终端中输入以下命令：

```bash
npm install --save lodash.uniqueid
```

**备注：** 如果你更喜欢 yarn，则可以改用 `yarn add lodash.uniqueid`。

现在，我们可以将此包导入我们的 `ToDoItem` 组件中。在 `ToDoItem.vue` 的 `<script>` 元素的顶部添加以下行：

```js
import uniqueId from "lodash.uniqueid";
```

接下来，将字段 `id` 添加到我们的数据属性中，这样组件对象最终看起来就是这样（`uniqueId()` 返回指定的前缀——`todo`——并附加一个唯一的字符串）：

```js
import uniqueId from "lodash.uniqueid";

export default {
  props: {
    label: { required: true, type: String },
    done: { default: false, type: Boolean },
  },
  data() {
    return {
      isDone: this.done,
      id: uniqueId("todo-"),
    };
  },
};
```

接下来，将 `id` 绑定到复选框的 `id` 属性和标签的 `for` 属性，更新现有 `id` 和 `for` 属性，如下所示：

```html
<template>
  <div>
    <input type="checkbox" :id="id" :checked="isDone" />
    <label :for="id">{{label}}</label>
  </div>
</template>
```

总结
--

这将对本文有所帮助。此时，我们有一个工作良好的 `ToDoItem` 组件，可以传递一个标签来显示，将存储其检查状态，并在每次调用时使用唯一的 id 呈现。你可以通过在 `App.vue` 中临时添加更多 `<to-do-item></to-do-item>` 去调用，然后使用浏览器的 DevTools 检查其呈现的输出，从而检查唯一 `id` 是否正常工作。

现在，我们已准备好将多个 `ToDoItem` 组件添加到我们的应用中。在下一篇文章中，我们将介绍如何将一组待办事项数据添加到 `App.vue` 组件中，然后使用 `v-for` 指令在 `ToDoItem` 组件中循环显示这些数据。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


Svelte 中的动态行为：变量和属性 - 学习 Web 开发

Svelte 中的动态行为：变量和属性
===================

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

现在我们已经准备好了标记和样式，我们可以开始为 Svelte 待办清单应用程序开发所需的功能。在本篇文章中，我们将使用变量和属性使我们的应用程序动态化，允许我们新增和删除待办事项，标记它们为完成并通过状态过滤它们。

|  |  |
| --- | --- |
| 前提： |
 推荐你至少熟悉基本的
 HTML、CSS 和
 JavaScript
 等编程语言，并具备终端/命令行环境的基本知识。

你将需要一个已安装了 Node.js 和 npm 的终端来编译和构建你的应用程序。 |
| 目标： | 学习并实践一些基本的 Svelte 概念，如创建组件、使用属性传递数据、将 JavaScript 表达式渲染到我们的标记、修改组件的状态以及遍历列表。 |

与我们一起编写代码
---------

### Git

克隆 Github 仓库（如果你还没有完成）如下：

```bash
git clone https://github.com/opensas/mdn-svelte-tutorial.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.dev/repl/c862d964d48d473ca63ab91709a0a5a0?version=3.23.2

处理待办事项
------

目前我们的 `Todos.svelte` 组件只显示静态标记，让我们开始让它变得更加动态。我们将从标记语言中获取任务信息并将其存储在 `todos` 数组中。另外我们将创建两个变量来持续追踪任务总数和已完成的任务。

我们组件的状态将由这三个顶层变量代表。

1. 在 `src/components/Todos.svelte` 的顶部创建一个 `<script>` 区块并填充一些内容，如下所示：

```svelte
<script>
 let todos = [
 { id: 1, name: "Create a Svelte starter app", completed: true },
 { id: 2, name: "Create your first component", completed: true },
 { id: 3, name: "Complete the rest of the tutorial", completed: false },
 ];
 let totalTodos = todos.length;
 let completedTodos = todos.filter((todo) => todo.completed).length;
</script>
```

现在让我们用这些信息做点什么。
2. 让我们从显示状态信息开始。找到 `id` 为 `list-heading` 的 `<h2>` 标题并用动态表达式替换已经硬编码的当前任务及已完成任务数量：

```svelte
<h2 id="list-heading">{completedTodos} out of {totalTodos} items completed</h2>
```

3. 回到应用程序，你应该会看到和以前一样的“2 out of 3 items completed”信息，但这次的信息来自 `todos` 数组。
4. 为了证明这一点，回到数组并尝试改变一些待办事项对象的已完成属性值，甚至是新增一个新的待办事项对象。观察信息中的数字是如何被恰当地更新的。
从数据动态地生成待办事项
------------

目前我们显示的待办事项都是静态的。而我们想要遍历 `todos` 数组中的每个项目并为每个任务渲染其标记，让我们马上动手吧。

HTML 没有表达逻辑的方式（如条件和循环），但 Svelte 可以做到。在这种情况下，我们使用 `{#each}` 指令来迭代 `todos` 数组。假如提供了第二个参数，将包含当前项目的索引。此外，可以提供一个键表达式，它将唯一识别每个项目。当数据发生变化时，Svelte 将使用它来更改列表，而不是在末尾添加或删除项目，所以始终指定键表达式是一种好的做法。最后，可以提供 `:else` 区块，它将在列表为空时渲染如此。

试一试吧。

1. 将现有的 `<ul>` 元素替换为以下的简化版本，以了解其工作原理：

```svelte
<ul>
{#each todos as todo, index (todo.id)}
  <li>
    <input type="checkbox" checked={todo.completed}/> {index}. {todo.name} (id: {todo.id})
  </li>
{:else}
  Nothing to do here!
{/each}
</ul>
```

2. 回到应用程序，你会看到这样的画面：

 ![使用 each 块创建非常简单的待办事项输出](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_variables_props/01-each-block.png)
3. 现在我们已经看到这是有效的，接下来让我们使用 `{#each}` 指令的每个循环生成一个完整的待办事项并在其中嵌入来自 `todos` 数组中的信息：`id`、`name` 和 `completed`。将现有的 `<ul>` 区块替换为以下内容：

```svelte
<!-- To-dos -->
<ul role="list" class="todo-list stack-large" aria-labelledby="list-heading">
  {#each todos as todo (todo.id)}
  <li class="todo">
    <div class="stack-small">
      <div class="c-cb">
        <input
 type="checkbox"
 id="todo-{todo.id}"
 checked={todo.completed} />
        <label for="todo-{todo.id}" class="todo-label"> {todo.name} </label>
      </div>
      <div class="btn-group">
        <button type="button" class="btn">
          Edit <span class="visually-hidden">{todo.name}</span>
        </button>
        <button type="button" class="btn btn\_\_danger">
          Delete <span class="visually-hidden">{todo.name}</span>
        </button>
      </div>
    </div>
  </li>
  {:else}
  <li>Nothing to do here!</li>
  {/each}
</ul>
```

请注意我们如何使用大括号将 JavaScript 表达式嵌入 HTML 属性中，就像我们对勾选框的 `checked` 和 `id` 属性所做的那样。

我们已经将静态标记转为动态模板并从组件的状态来显示这些任务。做得好！

处理属性
----

由于待办事项清单是硬编码的，我们的 `Todos` 组件不是那么有用。为了将我们的组件变为一个通用的待办事项编辑器，我们应该允许它的上层组件可以传递待办事项清单以进行编辑。这将允许我们将它们存储至网络服务或本地存储区，以便之后将它们取回进行更新。所以让我们将数组变为一个 `prop`。

1. 在 `Todos.svelte` 中，将现有的 `let todos = …` 区块替换为 `export let todos = []`。

```js
export let todos = [];
```

起初这可能会让你觉得有点奇怪。这不像在 JavaScript 模块中使用 `export` 的常见方式！但这就是 Svelte 通过采用赋予有效语法其新用途来“扩展”JavaScript 的做法。在这种情况下，Svelte 使用 `export` 关键字将变量声明标记为属性，这意味着组件的消费者可以访问它。
 你也可以为属性指定默认初始值。如果组件的消费者没有在组件上指定属性，或者在实例化组件时属性的初始值未定义，则会使用该初始值。
 因此，通过 `export let todos = []` 我们告诉 Svelte 我们的 `Todos.svelte` 组件将可接受 `todos` 属性，而当省略该属性时，它将被初始化为一个空数组。
2. 回到应用程序，你将会看到“Nothing to do here!”信息。这是因为我们目前没有从 `App.svelte` 向它传递任何值，所以它使用默认值。
3. 现在让我们将待办事项移动到 `App.svelte` 并将它们作为属性传递给 `Todos.svelte` 组件。更新 `src/App.svelte` 如下：

```svelte
<script>
 import Todos from "./components/Todos.svelte";

 let todos = [
 { id: 1, name: "Create a Svelte starter app", completed: true },
 { id: 2, name: "Create your first component", completed: true },
 { id: 3, name: "Complete the rest of the tutorial", completed: false }
 ];
</script>

<Todos todos={todos} />
```

4. 当属性和变量具有相同名称时，Svelte 允许你仅指定变量作为捷径，因此我们可以像这样重写最后一行。现在试试吧。

```svelte
<Todos {todos} />
```

此时，你的待办事项渲染应该与以前一样，除了现在我们是从 `App.svelte` 组件来传递它们之外。

勾选和移除待办事项
---------

让我们添加一些功能来勾选任务状态。Svelte 具有用于监听 DOM 事件的 `on:事件名称` 指令。让我们为勾选框的 `on:click` 事件添加一个处理器来切换已完成的状态。

1. 在 `src/components/Todos.svelte` 中，更新 `<input type="checkbox">` 元素如下：

```svelte
<input type="checkbox" id="todo-{todo.id}"
 on:click={() => todo.completed = !todo.completed}
 checked={todo.completed}
/>
```

2. 接下来，我们将添加一个函数来从我们的 `todos` 数组中移除一个待办事项。在 `Todos.svelte` 的 `<script>` 区块底部，添加 `removeTodo()` 函数，如下：

```js
function removeTodo(todo) {
  todos = todos.filter((t) => t.id !== todo.id);
}
```

3. 我们将通过*Delete*按钮来调用它。更新 `click` 事件，如下：

```svelte
<button type="button" class="btn btn\_\_danger"
 on:click={() => removeTodo(todo)}
>
  Delete <span class="visually-hidden">{todo.name}</span>
</button>
```

在 Svelte 中处理器的一个常见错误是将执行函数的结果作为处理器传递，而不是传递函数。例如，如果你指定 `on:click={removeTodo(todo)}`，它将执行 `removeTodo(todo)` 并将结果作为处理器传递，但这不是我们想要的。
 在这种情况下，你必须指定 `on:click={() => removeTodo(todo)}` 作为处理器。如果 `removeTodo()` 没有接收参数，你可以使用 `on:event={removeTodo}`，但不能使用 `on:event={removeTodo()}`。这不是 Svelte 中的特殊语法——在这里我们只是使用常规的 JavaScript 箭头函数。

这是不错的进展——现在我们可以删除任务了。当待办事项的*Delete*按钮被按下时，相关的待办事项将从 `todos` 数组中移除，并更新用户界面以不再显示它。此外，我们现在可以勾选复选框，相关待办事项的已完成状态将在 `todos` 数组中被更新。

然而，“x out of y items completed”标题并未被更新。请继续阅读，了解出现这种情况的原因和解决方法。

待办事项响应性
-------

正如我们所看到的，每当组件顶层变量的值被修改时，Svelte 都知道如何更新用户界面。在我们的应用程序中，每当勾选或删除待办事项时，`todos` 数组的值会直接更新，所以 Svelte 会自动更新 DOM。

然而，对于 `totalTodos` 和 `completedTodos` 来说情况并非如此。在下面的代码中，当组件被实例化且脚本被执行时，它们会被指定一个值，但是之后它们的值不会改变：

```js
let totalTodos = todos.length;
let completedTodos = todos.filter((todo) => todo.completed).length;
```

我们可以在勾选和移除待办事项后重新计算它们，但有一种更简单的方式可以做到。

可以通过在前面加上 `$:` 前缀，告诉 Svelte 我们希望 `totalTodos` 和 `completedTodos` 等变量具有响应性。Svelte 将生成代码以在它们依赖的数据发生变化时自动更新它们。

**备注：** Svelte 使用 `$:` JavaScript 标记语句语法来标记响应性语句。就像用于声明属性的 `export` 关键字一样，这可能看起来有点陌生。但这是 Svelte 利用有效的 JavaScript 语法并赋予其新用途的另一个例子——在这种情况下，意味着“每当任何引用的值发生变化时，就会重新执行此代码”。一旦习惯了，你就无法回头了。

更新 `src/components/Todos.svelte` 中的 `totalTodos` 和 `completedTodos` 变量定义，如下：

```js
$: totalTodos = todos.length;
$: completedTodos = todos.filter((todo) => todo.completed).length;
```

如果你现在检查你的应用程序，当待办事项完成或被删除时，你将会看到标题的数字被更新。做得好！

在幕后，Svelte 编译器会解析和分析我们的代码，生成依赖关系树，然后生成 JavaScript 代码，以便在依赖关系更新时重新评估每个响应式语句。Svelte 中的响应性是以非常轻量级和高性能的方式实现的，无需使用监听器（listener）、设置器（setter）、获取器（getter）或任何其他复杂机制。

添加新的待办事项
--------

当前文章的下一个主要任务是添加一些用来添加新的待办事项的功能。

1. 首先，我们将创建一个变量来保存新待办事项的文字。将此声明添加到 `Todos.svelte` 文件的 `<script>` 区块中：

```js
let newTodoName = "";
```

2. 现在，我们将在 `<input>` 中使用此值来添加新任务。为此，我们需要将 `newTodoName` 变量绑定到 `todo-0` 输入框，以使 `newTodoName` 变量的值与输入框的 `value` 属性保持同步。我们可以这样做：

```svelte
<input value={newTodoName} on:keydown={(e) => newTodoName = e.target.value} />
```

每当 `newTodoName` 变量的值发生变化时，它将反映在 `value` 输入框的属性中，并且每当按下输入框的某个键时，我们将更新 `newTodoName` 变量的内容。
 这是对输入框双向数据绑定的手动实现。但是我们不需要这样做——Svelte 提供了一种更简单的方式来将任何属性绑定到变量，使用 `bind:property` 指令：

```svelte
<input bind:value={newTodoName} />
```

所以，让我们来实现它。更新 `todo-0` 输入框如下：

```svelte
<input
 bind:value={newTodoName}
 type="text"
 id="todo-0"
 autocomplete="off"
 class="input input\_\_lg" />
```

3. 测试是否有效的一个简单方式是添加一个响应式语句来记录 `newTodoName` 的内容。在 `<script>` 区块的末尾添加以下代码片段：

```js
$: console.log("newTodoName: ", newTodoName);
```

**备注：** 你可能已经注意到，响应式语句不仅限于变量声明。你可以在 `$:` 符号后面放上*任何* JavaScript 语句。
4. 现在试着回到 `localhost:5042`，按下 `Ctrl` + `Shift` + `K` 打开浏览器控制台，并在输入框中输入一些内容。你应该能看到你的输入被记录下来。此时，你可以根据需要删除响应式 `console.log()`。
5. 接下来，我们将创建一个函数来添加新的待办事项——`addTodo()`——它会将一个新的 `todo` 对象添加到 `todos` 数组中。将此函数添加到 `src/components/Todos.svelte` 的 `<script>` 区块的底部：

```js
function addTodo() {
  todos.push({ id: 999, name: newTodoName, completed: false });
  newTodoName = "";
}
```

**备注：** 目前我们为每个待办事项都指定相同的 `id`，不用担心，我们会尽快解决这个问题。
6. 现在我们要更新我们的 HTML，以便在表单提交时调用 `addTodo()`。更新添加 NewTodo 的起始标签如下：

```svelte
<form on:submit|preventDefault={addTodo}>
```

`on:事件名称`指令支持使用 `|` 字符向 DOM 事件添加修饰词（modifier）。在这种情况下，`preventDefault` 修饰词告诉 Svelte 在运行处理器之前生成调用 `event.preventDefault()` 的代码。浏览前一个链接以查看其他可用的修饰词有哪些。
7. 假设你现在尝试添加新的待办事项，新的待办事项将被添加到待办事项数组中，但我们的用户界面不会更新。在 Svelte 中，响应性是由指定操作来触发的。这意味着尽管 `addTodo()` 函数被执行，元素被新增到 `todos` 数组中，但 Svelte 不会检测到 push 方法修改了数组，所以它也不会刷新任务 `<ul>`。
 要解决这个问题，只需要将 `todos = todos` 添加到 `addTodo()` 函数的末尾，但在函数末尾包含它似乎很奇怪。相反，我们将移除 `push()` 方法并使用展开语法来达到相同的结果：我们将为 `todos` 数组指定一个等于 `todos` 数组加上新对象的数值。

**备注：** 数组（`Array`）有几个修改操作：`push()`、`pop()`、`splice()`、`shift()`、`unshift()`、`reverse()` 和 `sort()`。使用它们常常会导致难以追踪的副作用和错误。通过使用展开语法而不是 `push()`，我们可以避免改变数组本身，这被认为是一种良好的做法。

更新你的 `addTodo()` 函数如下：

```js
function addTodo() {
  todos = [...todos, { id: 999, name: newTodoName, completed: false }];
  newTodoName = "";
}
```

给每个待办事项唯一的 ID
-------------

如果现在尝试在你的应用程序中添加新的待办事项，你将可以添加新的待办事项并让它出现在用户界面中——一次。但如果你尝试第二次，它将没有效果，并且你将收到一个控制台消息“Error: Cannot have duplicate keys in a keyed each”（错误：不能有重复的键）。我们的待办事项需要唯一 ID。

1. 让我们声明一个由待办事项数量加 1 计算得出的 `newTodoId` 变量并使其具有响应性。将以下代码片段添加到 `<script>` 区块：

```js
let newTodoId;
$: {
  if (totalTodos === 0) {
    newTodoId = 1;
  } else {
    newTodoId = Math.max(...todos.map((t) => t.id)) + 1;
  }
}
```

**备注：** 如你所见，响应式陈述不仅限于单行。以下代码也可以生效，但可读性较差：`$: newTodoId = totalTodos ? Math.max(...todos.map((t) => t.id)) + 1 : 1`
2. Svelte 是如何做到这一点的？编译器会解析整个响应式语句并检测到它依赖于 `totalTodos` 变量和 `todos` 数组。所以每当它们中任何一个被修改时，都会重新评估代码并相应地更新 `newTodoId`。
 让我们在 `addTodo()` 函数中使用它。更新它如下：

```js
function addTodo() {
  todos = [...todos, { id: newTodoId, name: newTodoName, completed: false }];
  newTodoName = "";
}
```

通过状态筛选待办事项
----------

在本文的最后，让我们实现通过状态筛选待办事项的功能。我们将创建一个变量来保存当前的筛选器，并且编写一个辅助函数来返回经过筛选的待办事项。

1. 在我们的 `<script>` 区块底部新增以下内容：

```js
let filter = "all";
const filterTodos = (filter, todos) =>
  filter === "active"
    ? todos.filter((t) => !t.completed)
    : filter === "completed"
      ? todos.filter((t) => t.completed)
      : todos;
```

我们使用 `filter` 变量来控制当前的筛选器：*all*、*active*、*completed*。只需将其中一个值赋给筛选器变量，即可使用该筛选器并更新待办事项列表。让我们看看它是如何实现的。
 `filterTodos()` 函数接收当前的筛选器和待办事项列表，并返回经过筛选的新待办事项数组。
2. 让我们更新筛选器按钮的标记以使其动态化，当用户点击其中一个筛选器按钮时，更新当前的筛选器。更新它如下：

```svelte
<div class="filters btn-group stack-exception">
  <button class="btn toggle-btn" class:btn\_\_primary={filter === 'all'} aria-pressed={filter === 'all'} on:click={() => filter = 'all'} >
    <span class="visually-hidden">Show</span>
    <span>All</span>
    <span class="visually-hidden">tasks</span>
  </button>
  <button class="btn toggle-btn" class:btn\_\_primary={filter === 'active'} aria-pressed={filter === 'active'} on:click={() => filter = 'active'} >
    <span class="visually-hidden">Show</span>
    <span>Active</span>
    <span class="visually-hidden">tasks</span>
  </button>
  <button class="btn toggle-btn" class:btn\_\_primary={filter === 'completed'} aria-pressed={filter === 'completed'} on:click={() => filter = 'completed'} >
    <span class="visually-hidden">Show</span>
    <span>Completed</span>
    <span class="visually-hidden">tasks</span>
  </button>
</div>
```

这段标记中发生了几件事情。
 我们通过将 `btn__primary` 类应用于筛选器按钮来显示当前使用的筛选器。为了有条件地将样式类应用于元素上，我们使用 `class:name={value}` 指令。如果值表达式的计算结果为真，那么类名将被应用。你可以将许多不同条件的指令放在同一个元素上。所以当我们写为 `class:btn__primary={filter === 'all'}` 时，如果筛选器（filter）变量的值等于 `'all'`，Svelte 将会应用 `btn__primary` 类。

**备注：** Svelte 提供了一个便利的快捷方式，允许我们在类与变量名称相同时，可以将 `<div class:active={active}>` 缩短为 `<div class:active>`。

`aria-pressed={filter === 'all'}` 也是类似的情况：当在大括号之间传入的 JavaScript 表达式的计算结果为真值时，`aria-pressed` 属性将被加入到按钮上。
 每当我们点击按钮时，我们通过触发 `on:click={() => filter = 'all'}` 来更新筛选器变量。继续阅读以了解 Svelte 响应式将如何处理其余部分。
3. 现在我们只需要在 `{#each}` 循环中使用辅助函数；像这样更新它：

```svelte
…
  <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading">
  {#each filterTodos(filter, todos) as todo (todo.id)}
…
```

在分析我们的代码后，Svelte 检测到我们的 `filterTodos()` 函数依赖于变量 `filter` 和 `todos`。而就像嵌入在标记中的任何其他动态表达式一样，每当这些依赖发生变化时，DOM 都会相应地更新。所以每当 `filter` 或 `todos` 发生变化时，`filterTodos()` 函数将会重新评估并更新循环内的项目。

**备注：** 响应式有时会很棘手。Svelte 将 `filter` 识别为依赖，是因为我们在 `filterTodos(filter, todo)` 表达式中引用它。而 `filter` 是一个顶层变量，所以我们可能会想把它从辅助函数参数（params）中删除，然后像这样调用它：`filterTodos(todo)`。这虽然能够工作，但现在 Svelte 将无法发现 `{#each filterTodos(todos) }` 依赖于 `filter` 并且当过滤器变更时，过滤后的待办事项列表不会再被更新。切记 Svelte 会分析我们的代码以找出依赖关系，所以最好明确解释它，而不是依赖于顶层变量的可见性。此外，让我们的代码清晰并明确地解释它正在使用的信息是一个很好的做法。

到目前为止的代码
--------

### Git

如果想要查看本文结束后代码所呈现的最终结果，你可以按照以下方式访问你对我们的仓库的克隆：

```bash
cd mdn-svelte-tutorial/04-componentizing-our-app
```

或直接下载文件夹内容：

```bash
npx degit opensas/mdn-svelte-tutorial/04-componentizing-our-app
```

记得执行 `npm install && npm run dev` 以开发模式来运行你的应用程序。

### REPL

如果想在 REPL 上看到当前代码状态，请点击以下链接：

https://svelte.dev/repl/99b9eb228b404a2f8c8959b22c0a40d3?version=3.23.2

总结
--

截至目前为止做得好！在本篇文章中，我们已经实现了大部分想要的功能。我们的应用程序可以显示、新增和删除待办事项，切换其完成状态、显示已完成的数量和使用过滤器。

回顾一下，我们涵盖了以下主题：

* 建立和使用组件
* 转换静态标记为动态模板
* 在标记语言嵌入 JavaScript 表达式
* 使用 `{#each}` 指令遍历列表
* 通过属性在组件之间传递信息
* 监听 DOM 事件
* 声明响应式语句
* 使用 `console.log()` 和响应式语句进行基本调试
* 使用 `bind:property` 指令绑定 HTML 属性
* 通过指定操作触发响应式
* 使用响应式表达式过滤数据
* 显式定义我们的响应依赖关系

在下一篇文章中，我们将新增更多功能，允许用户编辑待办事项。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


Svelte 进阶：响应式、生命周期以及无障碍 - 学习 Web 开发

Svelte 进阶：响应式、生命周期以及无障碍
=======================

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

在上一篇文章中，我们为待办事项列表添加了更多功能，并开始将应用程序组织成组件。在本文中，我们将添加应用程序的最终功能，并进一步组件化我们的应用程序。我们将学习处理与更新对象和数组相关的响应式问题。为了避免常见的问题，我们需要深入了解 Svelte 的响应式系统。我们还将解决一些无障碍聚焦问题，以及其他一些问题。

|  |  |
| --- | --- |
| 前提： |
 至少，建议你熟悉核心
 HTML、CSS
 和 JavaScript
 语言，并且了解终端/命令行的使用。

你需要安装了 Node.js 和 npm 的终端来编译和构建应用程序。 |
| 目标： | 学习一些高级的 Svelte 技巧，包括解决响应性问题、与组件生命周期相关的键盘无障碍问题等等。 |

我们将重点关注涉及焦点管理的一些无障碍问题。为此，我们将利用一些访问 DOM 节点和执行 `focus()` (en-US) 和 `select()` 等方法的技术。我们还将学习如何声明以及清除 DOM 元素的事件监听器。

我们也需要学习一些组件生命周期的知识，以了解这些 DOM 节点何时在 DOM 中挂载（mount）和卸载（unmount），以及我们如何访问它们。我们还将学习 `action` 指令，它将允许我们以可重用和声明性的方式扩展 HTML 元素的功能。

最后，我们将进一步了解组件。到目前为止，我们已经看到组件如何使用属性共享数据，并使用事件和双向数据绑定与其父组件通信。现在，我们将看到组件如何暴露方法和变量。

本文将在课程中开发以下新组件：

* `MoreActions`：显示 *Check All*（全选）和 *Remove Completed*（移除已完成）按钮，并发出处理其功能所需的相应事件。
* `NewTodo`：显示用于添加新待办事项的 `<input>` 字段和 *Add*（添加）按钮。
* `TodosStatus`：显示“x out of y items completed”（“x 个已完成，共 y 个项目”）的状态标题。
与我们一起编写代码
---------

### Git

使用以下命令克隆 GitHub 仓库（如果尚未完成）：

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

然后运行以下命令以进入当前应用状态：

```bash
cd mdn-svelte-tutorial/05-advanced-concepts
```

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

```bash
npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts
```

记得运行 `npm install && npm run dev` 以在开发模式下启动应用。

### REPL

要使用 REPL 与我们一起编码，请访问以下链接：

https://svelte.dev/repl/76cc90c43a37452e8c7f70521f88b698?version=3.23.2

处理 MoreActions 组件
-----------------

现在我们来处理*全选*和*移除已完成*按钮。让我们创建一个组件，这个组件负责显示这些按钮并发出相应的事件。

1. 创建新文件，`components/MoreActions.svelte`。
2. 当点击第一个按钮时，我们将发出 `checkAll` 事件，表示所有待办事项应该被全选或取消全选。当点击第二个按钮时，我们将发出 `removeCompleted` 事件，表示所有已完成的待办事项应该被移除。将以下内容放入你的 `MoreActions.svelte` 文件中：

```svelte
<script>
 import { createEventDispatcher } from "svelte";
 const dispatch = createEventDispatcher();

 let completed = true;

 const checkAll = () => {
 dispatch("checkAll", completed);
 completed = !completed;
 };

 const removeCompleted = () => dispatch("removeCompleted");
</script>

<div class="btn-group">
  <button type="button" class="btn btn\_\_primary" on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button>
  <button type="button" class="btn btn\_\_primary" on:click={removeCompleted}>Remove completed</button>
</div>
```

我们还包含了 `completed` 变量，用于在全选和取消全选之间进行切换。
3. 回到 `Todos.svelte`，我们要导入我们的 `MoreActions` 组件，并创建两个函数来处理 `MoreActions` 组件发出的事件。
 在现有的导入语句下面添加以下导入语句：

```js
import MoreActions from "./MoreActions.svelte";
```

4. 然后在 `<script>` 部分的末尾添加下面描述的函数：

```js
const checkAllTodos = (completed) =>
  todos.forEach((t) => (t.completed = completed));

const removeCompletedTodos = () =>
  (todos = todos.filter((t) => !t.completed));
```

5. 现在转到 `Todos.svelte` 标记部分的底部，用 `MoreActions` 组件调用替换我们从 `MoreActions.svelte` 复制的 `<div class="btn-group">` 元素，如下所示：

```svelte
<!-- MoreActions -->
<MoreActions
 on:checkAll={(e) => checkAllTodos(e.detail)}
 on:removeCompleted={removeCompletedTodos}
/>
```

6. 好的，让我们回到应用程序中尝试一下。你会发现*移除已完成*按钮正常工作，但*全选*/*全部取消*按钮却没有任何反应。

要明白这里发生了什么，我们需要深入了解 Svelte 的响应式。

响应式问题：更新对象和数组
-------------

为了查看发生了什么，我们可以在 `checkAllTodos()` 函数中将 `todos` 数组记录到控制台中。

1. 将现有的 `checkAllTodos()` 函数更新如下：

```js
const checkAllTodos = (completed) => {
  todos.forEach((t) => (t.completed = completed));
  console.log("todos", todos);
};
```

2. 返回浏览器，打开开发者工具控制台，然后点击*全选*/*全部取消*几次。

你会注意到，每次按下按钮时，数组都会成功更新（`todo` 对象的 `completed` 属性在 `true` 和 `false` 之间切换），但是 Svelte 无法意识到这一点。这也意味着，在这种情况下，像 `$: console.log('todos', todos)` 这样的响应式语句并没有太大用处。

为了弄清楚发生这种情况的原因，我们需要了解在更新数组和对象时 Svelte 中的响应式是如何工作的。

许多 Web 框架使用虚拟 DOM 技术来更新页面。基本上，虚拟 DOM 是网页内容的内存副本。框架更新这个虚拟表示，然后将其与“真实”DOM 同步。这比直接更新 DOM 要快得多，并允许框架应用许多优化技术。

这些框架通常会在每次对虚拟 DOM 更改后重新运行我们的 JavaScript 代码，并应用不同的方法来缓存资源消耗巨大的计算和优化执行。它们几乎不会尝试理解我们的 JavaScript 代码在做什么。

Svelte 不使用虚拟 DOM 表示。相反，它解析并分析我们的代码，创建依赖树，然后生成所需的 JavaScript 代码，这段生成的代码仅更新需要更新的 DOM 部分。这种方法通常会生成具有最小开销的优化 JavaScript 代码，但它也有其局限性。

有时候 Svelte 无法检测到正在监视的变量的更改。请记住，要告诉 Svelte 某个变量已经改变，你必须为它分配新值。一个简单的规则是**已更新的变量的名称必须出现在赋值语句的左侧**。

例如，在下面的代码片段中：

```js
const foo = obj.foo;
foo.bar = "baz";
```

除非你紧接着使用 `obj = obj`，否则 Svelte 不会更新对 `obj.foo.bar` 的引用。这是因为 Svelte 无法跟踪对象引用，所以我们必须通过执行赋值操作，来明确告诉它 `obj` 已经改变了。

**备注：** 如果 `foo` 是顶级变量，你可以使用以下反应语句轻松地告诉 Svelte 在 `foo` 更改时更新 `obj`：`$: foo, obj = obj`。通过这样做，我们将 `foo` 定义为依赖项，每当它更改时，Svelte 将运行 `obj = obj`。

在我们的 `checkAllTodos()` 函数中，当我们运行：

```js
todos.forEach((t) => (t.completed = completed));
```

Svelte 不会将 `todos` 标记为已更改，因为它不知道当我们在 `forEach()` 方法内部更新我们的 `t` 变量时，我们也正在修改 `todos` 数组。这是有道理的，因为否则 Svelte 将需要了解 `forEach()` 方法的内部工作方式；同样道理也适用于附属于对象或数组的任何方法。

尽管如此，我们可以应用多种技术来解决这个问题，所有这些技术都涉及对正在监视的变量进行新赋值。

正如我们已经看到的，我们可以通过自赋值来通知 Svelte 变量的更新，像这样：

```js
const checkAllTodos = (completed) => {
  todos.forEach((t) => (t.completed = completed));
  todos = todos;
};
```

这将解决问题。在内部，Svelte 将标记 `todos` 为已更改，并移除显然多余的自赋值。除了看起来有点奇怪之外，这种技术是完全可以的，有时它也是最简洁的方式。

我们还可以通过索引访问 `todos` 数组，像这样：

```js
const checkAllTodos = (completed) => {
  todos.forEach((t, i) => (todos[i].completed = completed));
};
```

对数组和对象的属性赋值，例如 `obj.foo += 1` 或 `array[i] = x`，与对值本身的赋值方式相同。当 Svelte 分析此代码时，它可以检测到 `todos` 数组正在被修改。

另一种解决方案是创建新数组，新数组中包含所有待办事项的副本，并在其中更新 `completed` 属性，然后将新数组分配给 `todos`，像这样：

```js
const checkAllTodos = (completed) => {
  todos = todos.map((t) => ({ ...t, completed }));
};
```

在这种情况下，我们使用 `map()` 方法，它返回的结果是通过对数组中每个项执行给定的函数而生成的新数组。该函数使用展开语法返回每个待办事项的副本，并相应地更新了每个副本的 `completed` 属性。这个解决方案的额外好处是返回了具有新对象的新数组，从而完全避免了对原始 `todos` 数组的修改。

**备注：** Svelte 允许我们指定不同的选项来影响编译器的工作方式。 `<svelte:options immutable={true}/>` 选项告诉编译器你承诺不会修改任何对象。这允许它以更加激进的方式检查值是否已更改，并生成更简单和更高性能的代码。有关 `<svelte:options>` 的更多信息，请参阅 Svelte 选项文档。

所有这些解决方案都涉及赋值操作，其中更新的变量位于等式的左侧。这些技术中的任何一种都将使 Svelte 注意到我们的 `todos` 数组已被修改。

**选择一种方法，并根据需要更新你的 `checkAllTodos()` 函数。现在你应该能够同时选中或取消选中所有待办事项。试试看吧！**

完善我们的 MoreActions 组件
--------------------

我们将为我们的组件增加一些实用的细节。在没有任务要处理时，我们会禁用按钮。我们将通过接收 `todos` 数组作为属性，并相应地设置每个按钮的 `disabled` 属性来实现此功能。

1. 将你的 `MoreActions.svelte` 组件更新如下：

```svelte
<script>
 import { createEventDispatcher } from 'svelte';
 const dispatch = createEventDispatcher();

 export let todos;

 let completed = true;

 const checkAll = () => {
 dispatch('checkAll', completed);
 completed = !completed;
 }

 const removeCompleted = () => dispatch('removeCompleted');

 $: completedTodos = todos.filter((t) => t.completed).length;
</script>

<div class="btn-group">
  <button type="button" class="btn btn\_\_primary"
 disabled={todos.length === 0} on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button>
  <button type="button" class="btn btn\_\_primary"
 disabled={completedTodos === 0} on:click={removeCompleted}>Remove completed</button>
</div>
```

我们还声明了响应式的 `completedTodos` 变量，用于启用或禁用*删除已完成*按钮。
2. 不要忘记从 `Todos.svelte` 中调用组件的位置将属性传递给 `MoreActions`：

```svelte
<MoreActions {todos}
 on:checkAll={(e) => checkAllTodos(e.detail)}
 on:removeCompleted={removeCompletedTodos}
 />
```

使用 DOM：关注细节
-----------

现在我们已经完成了应用程序的所有必需功能，接下来我们将专注于一些无障碍特性，以改善我们的应用程序对仅使用键盘或屏幕阅读器的用户的可用性。

在当前状态下，我们的应用程序存在一些键盘无障碍问题，比如说在焦点管理方面。让我们了解一下这些问题。

探索我们的待办事项应用中的键盘无障碍问题
--------------------

目前，对于使用键盘的用户来说，我们应用的焦点流动不够可预测或连贯。

如果你点击应用顶部的输入框，该输入框周围会出现粗实线框。这个实线框是你的视觉指示器，表示浏览器当前聚焦在这个元素上。

如果你是使用鼠标的用户，可能会忽略这个视觉提示。但是，如果你完全使用键盘工作，知道哪个控件有焦点非常重要。它告诉我们哪个控件将接收我们的按键输入。

如果你反复按下 `Tab` 键，你会看到实线焦点指示器在页面上的所有可以获得焦点的元素之间循环。如果将焦点移到*编辑*按钮上并按下 `Enter` 键，突然焦点就消失了，我们无法再知道哪个控件将接收我们的按键输入。

此外，如果按下 `Escape` 或 `Enter` 键，什么都不会发生。如果点击*取消*或*保存*，焦点再次消失。对于使用键盘的用户来说，这很令人困惑。

我们还希望添加一些实用特性，例如在必填字段为空时禁用*保存*按钮，在文本输入获得焦点时给予某些 HTML 元素焦点或自动选择内容。

要实现所有这些特性，我们需要以编程方式访问 DOM 节点，以运行诸如 `focus()` (en-US) 和 `select()` 等函数。我们还必须使用 `addEventListener()` 和 `removeEventListener()` 以在控件获得焦点时运行特定的任务。

问题是，所有这些 DOM 节点是由 Svelte 在运行时动态创建的。因此，我们必须等待直到它们被创建并添加到 DOM 中，才能使用它们。为此，我们需要学习关于组件生命周期的知识，以了解何时可以访问它们（稍后再详细讨论）。

创建 NewTodo 组件
-------------

让我们首先将我们的创建待办事项的表单提取到独立的组件中。根据我们目前所了解的知识，我们可以创建新的组件文件，并调整代码以发出 `addTodo` 事件，来传递新待办事项的名称和其他详细信息。

1. 创建名为 `components/NewTodo.svelte` 的新文件。
2. 将以下内容放入其中：

```svelte
<script>
 import { createEventDispatcher } from 'svelte';
 const dispatch = createEventDispatcher();

 let name = '';

 const addTodo = () => {
 dispatch('addTodo', name);
 name = '';
 }

 const onCancel = () => name = '';

</script>

<form on:submit|preventDefault={addTodo} on:keydown={(e) => e.key === 'Escape' && onCancel()}>
  <h2 class="label-wrapper">
    <label for="todo-0" class="label\_\_lg">What needs to be done?</label>
  </h2>
  <input bind:value={name} type="text" id="todo-0" autoComplete="off" class="input input\_\_lg" />
  <button type="submit" disabled={!name} class="btn btn\_\_primary btn\_\_lg">Add</button>
</form>
```

这里我们使用 `bind:value={name}` 将 `<input>` 绑定到 `name` 变量，并使用 `disabled={!name}` 来在输入为空（即没有文本内容）时禁用*添加*按钮。我们还使用 `on:keydown={(e) => e.key === 'Escape' && onCancel()}` 处理了 `Escape` 键。当按下 `Escape` 键时，我们运行 `onCancel()`，这个函数会清空 `name` 变量。
3. 现在我们需要从 `Todos.svelte` 中导入并使用它，并更新 `addTodo()` 函数，使其接收新待办事项的名称作为实参。
 在 `Todos.svelte` 中的其他 `import` 语句下面添加以下 `import` 语句：

```js
import NewTodo from "./NewTodo.svelte";
```

4. 并将 `addTodo()` 函数更新为以下内容：

```js
function addTodo(name) {
  todos = [...todos, { id: newTodoId, name, completed: false }];
}
```

`addTodo()` 现在直接接收新待办事项的名称，因此我们不再需要 `newTodoName` 变量来提供值。这部分由我们的 `NewTodo` 组件处理。

**备注：** `{ name }` 的语法只是 `{ name: name }` 的简写形式。它来自 JavaScript 本身，与 Svelte 无关，只是为 Svelte 的缩写语法提供了一些灵感。
5. 最后，在此部分中，将 NewTodo 表单标记替换为对 `NewTodo` 组件的调用，如下所示：

```svelte
<!-- NewTodo -->
<NewTodo on:addTodo={(e) => addTodo(e.detail)} />
```

使用 `bind:this={dom_node}` 指令处理 DOM 节点
-------------------------------------

现在，我们希望在每次按下*添加*按钮后，`NewTodo` 组件的 `<input>` 元素重新获得焦点。为此，我们需要对输入框的 DOM 节点的引用。Svelte 提供了一个方法来实现这一点，那就是使用 `bind:this={dom_node}` 指令。当指定了该指令后，一旦组件被挂载并且其 DOM 节点创建完成，Svelte 就会将对该 DOM 节点的引用分配给指定的变量。

我们将创建 `nameEl` 变量，并使用 `bind:this={nameEl}` 将其绑定到输入框上。然后在 `addTodo()` 中，在添加新待办事项后，我们将调用 `nameEl.focus()` 来重新将焦点设置到 `<input>` 上。当用户按下 `Escape` 键时，在 `onCancel()` 函数中，我们也将执行相同的操作。

请将 `NewTodo.svelte` 的内容更新如下：

```svelte
<script>
 import { createEventDispatcher } from 'svelte';
 const dispatch = createEventDispatcher();

 let name = '';
 let nameEl; // 对 name 输入框 DOM 节点的引用

 const addTodo = () => {
 dispatch('addTodo', name);
 name = '';
 nameEl.focus(); // 聚焦 name 输入框
 }

 const onCancel = () => {
 name = '';
 nameEl.focus(); // 聚焦 name 输入框
 }
</script>

<form on:submit|preventDefault={addTodo} on:keydown={(e) => e.key === 'Escape' && onCancel()}>
  <h2 class="label-wrapper">
    <label for="todo-0" class="label\_\_lg">What needs to be done?</label>
  </h2>
  <input bind:value={name} bind:this={nameEl} type="text" id="todo-0" autoComplete="off" class="input input\_\_lg" />
  <button type="submit" disabled={!name} class="btn btn\_\_primary btn\_\_lg">Add</button>
</form>
```

尝试一下应用程序：在 `<input>` 字段中输入新的待办事项名称，按下 `Tab` 以将焦点转移到*添加*按钮，然后按下 `Enter` 或 `Escape` 键，可以看到输入框重新获得焦点。

### 为输入框设置自动聚焦

接下来，我们将为 `NewTodo` 组件添加 `autofocus` 属性，以指定 `<input>` 字段在页面加载时获得焦点。

1. 我们首先尝试的方法如下：尝试添加 `autofocus` 属性，并在 `<script>` 块中调用 `nameEl.focus()`。将 `NewTodo.svelte` 中的 `<script>` 部分的前四行更新如下：

```svelte
<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  export let autofocus = false;

  let name = '';
  let nameEl; // 引用名称输入框的 DOM 节点

  if (autofocus) nameEl.focus();
```

2. 现在返回到 `Todos` 组件，并将 `autofocus` 属性传递给 `<NewTodo>` 组件调用，如下所示：

```svelte
<!-- NewTodo -->
<NewTodo autofocus on:addTodo={(e) => addTodo(e.detail)} />
```

3. 如果现在尝试运行应用程序，你会发现页面现在是空白的，并且在开发者工具的控制台中会显示类似于 `TypeError: nameEl is undefined` 的错误。

要理解这里发生了什么，让我们更详细地讨论一下我们之前提到的组件生命周期。

组件生命周期和 `onMount()` 函数
----------------------

当某个组件被实例化时，Svelte 会运行其初始化代码（即该组件的 `<script>` 部分）。但是在那个时刻，组件的所有节点都还没有附加到 DOM 中，事实上，它们甚至还不存在。

那么，你如何知道组件何时已经被创建并挂载到 DOM 上？答案是每个组件都有一个生命周期，生命周期从组件创建开始，到组件销毁结束。有一些函数可以让你在生命周期的关键时刻运行代码。

`onMount()` 是最常使用的函数之一，它允许我们在组件被挂载到 DOM 上后立即运行回调函数。让我们尝试一下，看看 `nameEl` 变量会发生什么变化。

1. 首先，在 `NewTodo.svelte` 的 `<script>` 部分的开头添加以下行：

```js
import { onMount } from "svelte";
```

2. 然后在该部分的末尾添加以下行：

```js
console.log("initializing:", nameEl);
onMount(() => {
  console.log("mounted:", nameEl);
});
```

3. 现在，移除 `if (autofocus) nameEl.focus()` 这行代码，以避免之前出现的错误。
4. 应用程序现在将再次正常工作，并且你将在控制台中看到以下内容：
```

initializing: undefined
mounted: <input id="todo-0" class="input input__lg" type="text" autocomplete="off">
```

如你所见，在组件初始化期间，`nameEl` 是 `undefined`，这是有道理的，因为 `<input>` 节点甚至还不存在。在组件被挂载后，Svelte 会将对 `<input>` DOM 节点的引用赋值给 `nameEl` 变量，这是由 `bind:this={nameEl}` 指令完成的。
5. 为了使自动聚焦功能正常工作，将之前添加的 `console.log()`/`onMount()` 代码块替换为以下内容：

```js
onMount(() => autofocus && nameEl.focus()); // 如果 autofocus 为 true，则运行 nameEl.focus()
```

6. 再次打开你的应用程序，你将看到 `<input>` 字段在页面加载时获得焦点。

**备注：** 你可以查看 Svelte 文档中的其他生命周期函数，并在交互式教程中看到它们的实际应用。

使用 `tick()` 函数等待 DOM 更新
-----------------------

现在我们将解决 `Todo` 组件的焦点管理细节问题。首先，当我们按下*编辑*按钮进入编辑模式时，我们希望 `Todo` 组件的编辑框 `<input>` 获得焦点。就像之前一样，我们将在 `Todo.svelte` 中创建一个 `nameEl` 变量，并在将 `editing` 变量设置为 `true` 后调用 `nameEl.focus()`。

1. 打开 `components/Todo.svelte` 文件，在 editing 和 name 变量的下面添加 `nameEl` 变量声明：

```js
let nameEl; // 对 name 输入框 DOM 节点的引用
```

2. 现在更新你的 `onEdit()` 函数如下：

```js
function onEdit() {
  editing = true; // 进入编辑模式
  nameEl.focus(); // 将焦点设置到 name 输入框
}
```

3. 最后，通过以下方式将 `nameEl` 绑定到 `<input>` 输入框：

```svelte
<input
 bind:value={name}
 bind:this={nameEl}
 type="text"
 id="todo-{todo.id}"
 autocomplete="off"
 class="todo-text" />
```

4. 然而，如果你尝试更新应用程序，当你按下待办事项的*编辑*按钮时，控制台会出现类似“TypeError: nameEl is undefined”的错误。

那么这里发生了什么？当你在 Svelte 中更新组件的状态时，它不会立即更新 DOM。相反，它会等到下一个微任务来检查是否有其他需要应用的更改，包括其他组件。这样做可以避免不必要的工作，并允许浏览器更有效地批处理操作。

在这种情况下，当 `editing` 是 `false` 时，编辑 `<input>` 不可见，因为它不存在于 DOM 中。在 `onEdit()` 函数中，我们将 `editing` 设置为 `true`，然后立即尝试访问 `nameEl` 变量并执行 `nameEl.focus()`。问题在于，Svelte 还没有更新 DOM。

解决这个问题的一种方法是使用 `setTimeout()` 函数，延迟调用 `nameEl.focus()`，直到下一个事件循环，并给 Svelte 更新 DOM 的机会。

现在尝试一下这个解决方案：

```js
function onEdit() {
  editing = true; // 进入编辑模式
  setTimeout(() => nameEl.focus(), 0); // 异步调用，将焦点设置到 name 输入框
}
```

上面的解决方案是可行的，但不够优雅。Svelte 提供了更好的处理方式。`tick()` 函数 返回一个 Promise，在任何待处理的状态更改应用到 DOM 后解析（如果没有待处理的状态更改，立即解析）。让我们尝试一下。

1. 首先，在 `<script>` 部分的开头与现有的导入一起导入 `tick`：

```js
import { tick } from "svelte";
```

2. 接下来，在异步函数中使用 `await` 调用 `tick()`；更新 `onEdit()` 如下：

```js
async function onEdit() {
  editing = true; // 进入编辑模式
  await tick();
  nameEl.focus();
}
```

3. 如果现在尝试，你将看到一切都按预期工作。

**备注：** 要查看另一个使用 `tick()` 的示例，请访问 Svelte 教程。

使用 `use:action` 指令为 HTML 元素添加功能
-------------------------------

接下来，我们希望在 `<input>` 元素获得焦点时自动选择所有文本。此外，我们希望使其能够轻松地在任何 HTML `<input>` 上重用且是以声明性的方式应用。我们将借助解决这个需求的过程来展示 Svelte 的一个非常强大的特性：action。我们利用这个特性向常规 HTML 元素添加功能。

要选择 DOM 输入节点的文本，我们需要调用 `select()` 方法。为了在节点获得焦点时调用此函数，我们需要一个类似于以下的事件监听器：

```js
node.addEventListener("focus", (event) => node.select());
```

为了避免内存泄漏，我们还应该在节点销毁时调用 `removeEventListener()` 函数。

**备注：** 所有这些只是标准的 WebAPI 功能；这里没有任何内容是特定于 Svelte 的。

我们可以在我们的 `Todo` 组件中实现所有这些，每当我们将 `<input>` 添加或从 DOM 中移除时都必须执行这些操作。但是，我们必须非常小心，应当在节点添加到 DOM 后添加事件监听器，并在节点从 DOM 中移除之前删除监听器。此外，我们的解决方案不太可重用。

这就是 Svelte action 发挥作用的地方。基本上，它们允许我们在元素添加到 DOM 后以及从 DOM 中移除后运行函数。

在我们的当前用例中，我们将定义名为 `selectOnFocus()` 的函数，它将接收一个节点作为参数。该函数将向该节点添加一个事件监听器，以便在节点获得焦点时选择文本。然后，它将返回一个带有 `destroy` 属性的对象。`destroy` 属性是 Svelte 在从 DOM 中移除节点后执行的函数。在这里，我们将移除监听器，以确保不留下任何内存泄漏。

1. 让我们创建 `selectOnFocus()` 函数。将以下代码添加到 `Todo.svelte` 的 `<script>` 部分的底部：

```js
function selectOnFocus(node) {
  if (node && typeof node.select === "function") {
    // 确保 node 已定义且具有 select() 方法
    const onFocus = (event) => node.select(); // 事件处理器
    node.addEventListener("focus", onFocus); // 当节点获得焦点时调用 onFocus()
    return {
      destroy: () => node.removeEventListener("focus", onFocus), // 当节点从 DOM 中移除时执行
    };
  }
}
```

2. 现在，我们需要告诉 `<input>` 使用该函数，使用 `use:action` 指令：

```svelte
<input use:selectOnFocus />
```

通过这个指令，我们告诉 Svelte 在组件挂载到 DOM 时立即运行此函数，并将 `<input>` 的 DOM 节点作为参数传递。它还负责在组件从 DOM 中删除时执行 `destroy` 函数。因此，通过 `use` 指令，Svelte 为我们处理了组件的生命周期。
 在我们的例子中，我们的 `<input>` 最终如下所示：更新组件的第一个标签/输入对（在编辑模板内）如下：

```svelte
<label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label>
<input
 bind:value={name}
 bind:this={nameEl}
 use:selectOnFocus
 type="text"
 id="todo-{todo.id}"
 autocomplete="off"
 class="todo-text" />
```

3. 让我们试试看。前往你的应用程序，点击一个待办事项的*编辑*按钮，然后使用 `Tab` 键将焦点移出 `<input>`。现在点击 `<input>`，你会看到整个输入文本被选中。
### 使动作可重用

现在让我们将这个函数真正地做成可在组件间重用的。`selectOnFocus()` 只是一个函数，其并没有依赖于 `Todo.svelte` 组件，因此我们可以将它提取到一个文件中并从那里使用它。

1. 在 `src` 文件夹中创建名为 `actions.js` 的新文件。
2. 给它添加以下内容：

```js
export function selectOnFocus(node) {
  if (node && typeof node.select === "function") {
    // 确保 node 已定义并具有 select() 方法
    const onFocus = (event) => node.select(); // 事件处理器
    node.addEventListener("focus", onFocus); // 当 node 获得焦点时调用 onFocus()
    return {
      destroy: () => node.removeEventListener("focus", onFocus), // 当节点从 DOM 中移除时执行此操作
    };
  }
}
```

3. 现在在 `Todo.svelte` 中导入它；在其他导入语句下面添加以下导入语句：

```js
import { selectOnFocus } from "../actions.js";
```

4. 并且从 `Todo.svelte` 中删除 `selectOnFocus()` 定义，因为我们不再需要它。
### 重用我们的 action

为了展示我们的 action 的可重用性，让我们在 `NewTodo.svelte` 中使用它。

1. 也在这个文件中导入 `actions.js` 中的 `selectOnFocus()`，如之前所述：

```js
import { selectOnFocus } from "../actions.js";
```

2. 在 `<input>` 元素中添加 `use:selectOnFocus` 指令，像这样：

```svelte
<input
 bind:value={name}
 bind:this={nameEl}
 use:selectOnFocus
 type="text"
 id="todo-0"
 autocomplete="off"
 class="input input\_\_lg" />
```

只需几行代码，我们就可以以高度可重用和声明性的方式为常规 HTML 元素添加特性。只需要一个 `import` 语句和一个类似 `use:selectOnFocus` 这样的清晰描述其目的的短指令，就可以实现这一点，而无需创建自定义包装元素，如 `TextInput`、`MyInput` 或类似的元素。此外，你可以为一个元素添加任意多个 `use:action` 指令。

此外，我们不需要费力处理 `onMount()`、`onDestroy()` 或 `tick()`——`use` 指令会为我们处理组件的生命周期。

### 其他操作改进

在之前的部分中，当使用 `Todo` 组件时，我们不得不处理 `bind:this`、`tick()` 和 `async` 函数，只是为了在 `<input>` 添加到 DOM 后立即将焦点移在它上面。

1. 以下是我们使用 action 来实现它的方式：

```js
const focusOnInit = (node) =>
  node && typeof node.focus === "function" && node.focus();
```

2. 然后在我们的标记中，我们只需要添加另一个 `use:` 指令：

```svelte
<input bind:value={name} use:selectOnFocus use:focusOnInit />
```

3. 现在我们的 `onEdit()` 函数可以简单得多：

```js
function onEdit() {
  editing = true; // 进入编辑模式
}
```

在我们继续之前，让我们回到我们的 `Todo.svelte` 组件，在用户按下*保存*或*取消*后，我们希望将焦点放在*编辑*按钮上。

我们可以尝试再次重用我们的 `focusOnInit` action，将 `use:focusOnInit` 添加到*编辑*按钮上。但是这样会引入一个细微的 bug。当你添加一个新的待办事项时，焦点将移到最近添加的待办事项的*编辑*按钮上。这是因为 `focusOnInit` action 在组件创建时运行。

这不是我们想要的——我们希望*编辑*按钮只在用户按下*保存*或*取消*后接收焦点。

1. 回到你的 `Todo.svelte` 文件。
2. 首先，我们将创建名为 `editButtonPressed` 的标志，并将其初始化为 `false`。在其他变量定义的下方添加以下内容：

```js
let editButtonPressed = false; // 跟踪编辑按钮是否已按下，以便在取消或保存后将焦点放在它上面
```

3. 接下来，我们将修改*编辑*按钮的特性来保存这个标志，并创建一个 action。像这样更新 `onEdit()` 函数：

```js
function onEdit() {
  editButtonPressed = true; // 用户按下了编辑按钮，焦点将返回到编辑按钮
  editing = true; // 进入编辑模式
}
```

4. 在其下方，添加以下 `focusEditButton()` 的定义：

```js
const focusEditButton = (node) => editButtonPressed && node.focus();
```

5. 最后，我们在*编辑*按钮上使用 `focusEditButton` action，像这样：

```svelte
<button type="button" class="btn" on:click={onEdit} use:focusEditButton>
  Edit<span class="visually-hidden"> {todo.name}</span>
</button>
```

6. 回去并再次尝试你的应用程序。此时，每当*编辑*按钮添加到 DOM 中时，`focusEditButton` action 就会执行，但只有当 `editButtonPressed` 标志为 `true` 时，它才会将焦点移到按钮上。

**备注：** 在这里，我们只是初步介绍了 action 的功能。action 还可以具有响应式参数，并且 Svelte 允许我们检测这些参数的任何更改。因此，我们可以添加与 Svelte 响应式系统良好集成的功能。如果想更详细地了解 action，请考虑查看 Svelte 互动教程或 Svelte `use:action` 文档。

组件绑定：使用 `bind:this={component}` 指令暴露组件方法和变量
-------------------------------------------

还有一个无障碍问题需要解决。当用户点击*删除*按钮时，焦点消失了。

本文中要介绍的最后一个特性是在删除待办事项后将焦点设置在状态标题上。

为什么是状态标题？在这种情况下，具有焦点的元素已被删除，因此没有明确的候选项可以接收焦点。我们选择了状态标题，因为它靠近待办事项列表，并且它是向视觉用户提供任务删除的反馈的一种方式，同时还向屏幕阅读器用户指示发生了什么。

首先，我们将把状态标题提取为单独的组件。

1. 创建一个新文件，`components/TodosStatus.svelte`。
2. 将以下内容添加到文件中：

```svelte
<script>
 export let todos;

 $: totalTodos = todos.length;
 $: completedTodos = todos.filter((todo) => todo.completed).length;
</script>

<h2 id="list-heading">
  {completedTodos} out of {totalTodos} items completed
</h2>
```

3. 在 `Todos.svelte` 的开头导入该文件，在其他导入语句下面添加以下 `import` 语句：

```js
import TodosStatus from "./TodosStatus.svelte";
```

4. 将 `Todos.svelte` 中的 `<h2>` 状态标题替换为对 `TodosStatus` 组件的调用，将 `todos` 作为属性传递给它，如下所示：

```svelte
<TodosStatus {todos} />
```

5. 进行一些清理工作，从 `Todos.svelte` 中移除 `totalTodos` 和 `completedTodos` 变量。只需移除 `$：totalTodos = …` 和 `$：completedTodos = …` 行，还要在计算 `newTodoId` 时移除对 `totalTodos` 的引用，转而使用 `todos.length`。要做到这一点，请使用以下内容替换以 `let newTodoId` 开头的块：

```js
$: newTodoId = todos.length ? Math.max(...todos.map((t) => t.id)) + 1 : 1;
```

6. 一切都按预期工作——我们刚刚将最后一部分标记提取到了独立的组件中。

现在，我们需要找到一种方法，在删除待办事项后将焦点设置在 `<h2>` 状态标题上。

到目前为止，我们已经看到如何通过属性将信息传送到组件，并且组件可以通过发出事件或使用双向数据绑定与其父组件进行通信。子组件可以使用 `bind:this={dom_node}` 来获取对 `<h2>` 节点的引用，并使用双向数据绑定将其暴露给外部。但是，这样做将破坏组件的封装性；将焦点设置在组件上应该是组件自己的责任。

因此，我们需要 `TodosStatus` 组件公开一个方法，供其父组件调用以将焦点放在 `<h2>` 标题上。组件需要公开一些行为或信息以供使用者使用，这是一种非常常见的情况；让我们看看如何在 Svelte 中实现这一点。

我们已经了解 Svelte 使用 `export let varname = …` 来声明属性。但是，如果你不使用 `let` 导出 `const`、`class` 或 `function`，那么它们在组件外部是只读的。然而，函数表达式是有效的属性。在下面的示例中，前三个声明是属性，其余的是导出的值：

```svelte
<script>
 export let bar = "optional default initial value"; // 属性
 export let baz = undefined; // 属性
 export let format = (n) => n.toFixed(2); // 属性

 // 这些是只读的
 export const thisIs = "readonly"; // 只读导出

 export function greet(name) {
 // 只读导出
 alert(`Hello, ${name}!`);
 }

 export const greet = (name) => alert(`Hello, ${name}!`); // 只读导出
</script>
```

有了这个理解，我们回到我们的用例。我们将创建一个名为 `focus()` 的函数，它将焦点放在 `<h2>` 标题上。为此，我们需要 `headingEl` 变量来保存对 DOM 节点的引用，并使用 `bind:this={headingEl}` 将其绑定到 `<h2>` 元素上。我们的聚焦方法只需运行 `headingEl.focus()`。

1. 更新 `TodosStatus.svelte` 的内容如下：

```svelte
<script>
 export let todos;

 $: totalTodos = todos.length;
 $: completedTodos = todos.filter((todo) => todo.completed).length;

 let headingEl;

 export function focus() {
 // 简写版本: export const focus = () => headingEl.focus()
 headingEl.focus();
 }
</script>

<h2 id="list-heading" bind:this={headingEl} tabindex="-1">
  {completedTodos} out of {totalTodos} items completed
</h2>
```

注意，我们在 `<h2>` 上添加了 `tabindex` 属性，以便允许元素通过编程方式接收焦点。
 正如我们之前所看到的，使用 `bind:this={headingEl}` 指令可以将 DOM 节点的引用存储在变量 `headingEl` 中。然后，我们使用 `export function focus()` 暴露一个函数，该函数将焦点放在 `<h2>` 标题上。
 我们如何从父组件访问这些导出的值呢？正如你可以使用 `bind:this={dom_node}` 指令绑定到 DOM 元素一样，你也可以使用 `bind:this={component}` 指令绑定到组件实例本身。因此，当你在 HTML 元素上使用 `bind:this` 时，你会得到对 DOM 节点的引用，当你在 Svelte 组件上使用它时，你会得到对该组件实例的引用。
2. 要绑定到 `TodosStatus` 实例，我们首先在 `Todos.svelte` 中创建 `todosStatus` 变量。在 `import` 语句下面添加以下行：

```js
let todosStatus; // 对 TodosStatus 实例的引用
```

3. 接下来，在调用中添加 `bind:this={todosStatus}` 指令，如下所示：

```svelte
<!-- TodosStatus -->
<TodosStatus bind:this={todosStatus} {todos} />
```

4. 现在，我们可以从 `removeTodo()` 函数中调用 `exported focus()` 方法：

```js
function removeTodo(todo) {
  todos = todos.filter((t) => t.id !== todo.id);
  todosStatus.focus(); // 将焦点放在状态标题上
}
```

5. 回到你的应用程序。现在，如果删除任何待办事项，状态标题将获得焦点。突出显示待办事项数量的变化，无论是对于视觉用户还是对于屏幕阅读器用户都很有用。

**备注：** 你可能想知道为什么需要为组件绑定声明一个新变量。为什么不能直接调用 `TodosStatus.focus()`？这是因为应用可能同时有多个 `TodosStatus` 实例，因此需要引用特定实例的方法。这就是为什么需要指定变量来绑定特定实例。

到目前为止的代码
--------

### Git

如果想要查看本文结束后代码所呈现的最终结果，你可以按照以下方式访问你对我们的仓库的克隆：

```bash
cd mdn-svelte-tutorial/06-stores
```

或直接下载文件夹内容：

```bash
npx degit opensas/mdn-svelte-tutorial/06-stores
```

记得执行 `npm install && npm run dev` 以开发模式来运行你的应用程序。

### REPL

如果想在 REPL 上看到当前代码状态，请点击以下链接：

https://svelte.dev/repl/d1fa84a5a4494366b179c87395940039?version=3.23.2

总结
--

在本文中，我们已经向应用程序添加所有所需的功能，同时我们还解决了一些无障碍和可用性问题。我们还将应用程序拆分为可管理的组件，每个组件都有独特的任务。

与此同时，我们学习了一些进阶的 Svelte 技术，包括：

* 在更新对象和数组时处理响应式的注意事项
* 使用 `bind:this={dom_node}`（绑定 DOM 元素）来处理 DOM 节点
* 使用组件生命周期函数 `onMount()`
* 使用 `tick()` 函数强制 Svelte 解决待处理的状态更改
* 使用 `use:action` 指令以可重用且声明性的方式为 HTML 元素添加功能
* 使用 `bind:this={component}`（绑定组件）访问组件方法

在下一篇文章中，我们将学习如何使用 store 在组件之间进行通信，并为我们的组件添加动画效果。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


React 入门 - 学习 Web 开发

React 入门
========

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

本文会引导我们进入一段 React 学习之旅。我们将逐步了解有关它的背景和用例的一些细节，在自己的电脑上建起基本的 React 工具链，创建并使用一个简单的入门应用程序，以学习一些关于 React 在此过程中如何工作的知识。

|  |  |
| --- | --- |
| 前提： |
 熟悉核心 HTML，CSS
 和 JavaScript 语言，了解终端/命令行。

 React 使用称为 JSX（JavaScript 和 XML）的 HTML-in-JavaScript
 语法。熟悉 HTML 和 JavaScript 可以帮助你学习
 JSX，并更好地确定应用程序中的错误是与 JavaScript 还是与 React
 的更特定领域相关。
  |
| 目标： | 要设置本地 React 开发环境，创建一个启动应用程序，并了解其工作原理 |

你好 React
--------

如其官方口号所示，React 是一个用于构建用户界面的库。React 不是一个框架——它的应用甚至不局限于 Web 开发，它可以与其他库一起使用以渲染到特定环境。例如，React Native 可用于构建移动应用程序；React 360 可用于构建虚拟现实应用程序……

为了构建 Web 应用，开发人员将 React 与 ReactDOM 结合使用。React 和 ReactDOM 通常被与其他真正的 Web 开发框架相提并论，并用于解决相同的问题。当我们将 React 称为“框架”时，就是在进行口语化的理解。

React 的主要目标是最大程度地减少开发人员构建 UI 时发生的错误。它通过使用组件——描述部分用户界面的、自包含的逻辑代码段——来实现此目的。这些组件可以组合在一起以创建完整的 UI，React 将许多渲染工作进行抽象化，使你可以专注于 UI 设计 (译者注：显而易见，此设计不等于视觉稿的设计)。

用例
--

与本模块中涵盖的其他框架不同，React 不会对代码约定或文件组织实施严格的规则。这使团队可以设置最适合自己的约定，并以他们希望的任何方式采用 React。React 可以处理一个按钮，一个界面的几个部分或应用程序的整个用户界面。

尽管 React 可以用于界面的小片段中，但要和 jQuery 这样的库，甚至是像 Vue 这样的框架那样“引入”应用程序并不容易——当你使用 React 构建整个应用程序时更容易上手。

另外，许多开发人员的经验对于 React 应用程序也是有用处的，例如使用 JSX 编写界面是需要编译过程的。在网站上添加类似于 Babel 的编译器会让网站上代码的运行速度变慢，因此开发人员通常会在构建项目的时候设置这样的工具。React 对于工具的要求可以说是很高的，但这是能够学习解决的。

本文将重点介绍使用 React 通过 Facebook 的 create-react-app 内的工具渲染应用程序中整个用户界面的用例。

React 如何使用 JavaScript?
----------------------

React 中的许多模式都使用了现代 JavaScript 的功能。React 与 JavaScript 的最大区别在于 JSX 语法的使用上。JSX 是在 JavaScript 语法上的拓展，因此类似于 HTML 的代码可以和 JSX 共存。例如：

```js
const heading = <h1>Mozilla Developer Network</h1>;
```

该 heading 常量称为 **JSX 表达式**。React 可以使用它在我们的应用程序中渲染 `<h1>` 标签。

假设出于语义原因，我们想将 heading 包装 `<header>` 在标记中？JSX 方法允许我们将元素彼此嵌套，就像使用 HTML 一样：

```js
const header = (
  <header>
    <h1>Mozilla Developer Network</h1>
  </header>
);
```

**备注：** 上一个代码段中的括号并非 JSX 的一部分，它对你的应用程序没有任何影响，括号只是用来向你（和你的计算机）表明其中的多行代码属于同一个表达式 (译者注：原文表述实在有点啰嗦)。因此上面的代码等同于：

```jsx
const header = <header>
 <h1>Mozilla Developer Network</h1>
</header>;
```

这看起来多少有点不适感，因为表达式前面的 `<header>` 标记没有缩进与其对应的结束标记相同的位置。

浏览器是无法读取直接解析 JSX 的。我们的 header 表达式经过（ Babel 或 Parcel 之类的工具）编译之后是这样的：

```js
const header = React.createElement(
  "header",
  null,
  React.createElement("h1", null, "Mozilla Developer Network"),
);
```

*可以*跳过编译步骤，并使用 `React.createElement()` 自己编写 UI。但是，这样做会失去 JSX 的声明性优势，并且代码变得更难以阅读。编译是开发过程中的一个额外步骤，但是 React 社区中的许多开发人员都认为 JSX 的可读性值得。另外，流行的工具使 JSX-to-JavaScript 编译成为其设置过程的一部分。除非你愿意，否则不必自己配置编译。

由于 JSX 是 HTML 和 JavaScript 的结合，因此一些开发人员认为它很直观。其他人则说它的混合特性使它变得混乱。但是，一旦熟悉了它，它将使你能够更快，更直观地构建用户界面，并使其他人一眼就能更好地理解你的代码库。

要阅读有关 JSX 的更多信息，请查看 React 团队的 JSX In Depth 文章。

设置你的第一个 React 应用
----------------

有很多使用 React 的方法，但是我们将使用命令行界面（CLI）工具 create-react-app，如前所述，该方法通过安装一些软件包并创建一些软件包来加快开发 React 应用程序的过程。文件供你处理上述工具。

通过将一些 `<script>` 元素复制到 HTML 文件中，可以在没有 create-react-app 的情况下将 React 添加到网站，但是 create-react-app CLI 是 React 应用程序的常见起点。使用它可以让你花费更多的时间来构建应用，而花更少的时间进行设置。

### 要求

为了使用 create-react-app，你需要安装 Node.js。建议你使用长期支持（LTS）版本。Node 包括 npm（Node 程序包管理器）和 npx（Node 程序包运行器）

你也可以使用 Yarn 软件包管理器作为替代方案，但是我们假设在这套教程中使用 npm。有关 npm 和 yarn 的更多信息，请参见程序包管理基础。

如果你使用的是 Windows，则需要安装一些软件以与 Unix/macOS 终端保持同等地位，才能使用本教程中提到的终端命令。**Gitbash**（作为 git Windows 工具集的一部分提供）或\*\*适用于 Linux 的 Windows 子系统\*\*（**WSL**）均适用。有关这些以及一般终端命令的更多信息，请参见命令行速成课程。

还请记住，React 和 ReactDOM 生成的应用程序只能在相当现代的一组浏览器上运行——通过某些 polyfill 可以使用 IE9+。在阅读这些教程时，建议你使用 Firefox，Safari 或 Chrome 等现代浏览器。

另外，有关更多信息，请参见以下内容：

* "What is npm" on nodejs.org
* "Introducing npx" on the npm blog
* The create-react-app documentation
### 初始化你的应用

`create-react-app` ，该命令接受一个参数：即你想给自己的应用所起的名字。`create-react-app` 将为此应用创建一个同名的文件夹，并在其中创建所需文件。在你打算放置你的应用程序的文件夹下打开你的命令终端，并键入命令：

```bash
npx create-react-app moz-todo-react
```

这句命令创建了一个名为 `moz-todo-react` 的文件夹，并在此文件夹里做了如下工作：

* 为你的应用程序安装了一些 npm 包；
* 写入 react 应用启动所需要的脚本文件；
* 创建一系列结构化的子文件夹和文件，奠定应用程序的基础架构；
* 如果你的电脑上安装了 git 的话，顺便帮你把 git 仓库也建好。

**备注：** 如果你的电脑上安装了 yarn 的话，create-react-app 会默认使用 yarn 而非 npm。如果你同时安装了 yarn 和 npm，但你希望使用 npm 的话，在 create-react-app 的时候需要输入 `--use-npm` **:**

```bash
npx create-react-app moz-todo-react --use-npm
```

`create-react-app` 运行的时候会在终端上显示一些与其状态相关的信息，通常情况下无需为此担心。运行需要一点时间，在此期间你可以适度放松一下。

处理完成之后，你可以 `cd` 到 `moz-todo-react` 文件夹下，然后键入 `npm start` 命令并回车，先前由 create-react-app 创建的脚本会启动一个地服务 localhost:3000，并打开你的默认浏览器来访问这个服务。成功启动浏览器的话，你的浏览器上会显示如下画面：

![Firefox MacOS 的屏幕截图，打开到 localhost：3000，显示了默认的 create-react-app 应用程序](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_getting_started/default-create-react-app.png)

### 应用结构

create-react-app 提供了开发 React 应用所需的工具。它的初始文件结构如下：
```

moz-todo-react
├── README.md
├── node_modules
├── package.json
├── package-lock.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── serviceWorker.js
```

目录 **`src`** 是我们花费时间最多的地方，因为它是我们 React 应用源码存放的目录。

目录 **`public`** 包含了开发应用时浏览器会读取的文件，其中最重要的就是 `index.html`。React 将目录 **`src`** 中的代码嵌入这个文件，从而浏览器才能运行此文件。 `index.html`中的有些内容关乎 create-react-app 的运作，因此除非你知道自己在做什么样的修改，否则不建议编辑这个文件。当然，你可以修改`index.html`中的 `<title>` 元素的内容来表现此应用程序通俗易懂的名称。

目录 `public` 会在建立并部署此应用的时候更新。此教程不涉及部署，你可以参考 Deploying our app (en-US) 这一篇教程。

文件 `package.json` 包含了 Node.js/npm 为了建立该应用程序所管理着的文件信息。这个文件不是 React 应用独有的。你无需理解这个文件也能看懂这篇教程。不过，如果你想了解更多，你可以阅读 What is the file `package.json`? on NodeJS.org 和 Package management basics。

探索第一个 React 组件 — `<App/>`
-------------------------

在 React 中，组件是组成应用程序的可重复利用的模块。组件可大可小，但它们都只有单一的、明确的功能。

打开 `src/App.js`，之前打开的页面也提示我们对这个文件进行编辑。这个文件包含了我们第一个组件 `App`，内容如下：

```js
import React from "react";
import logo from "./logo.svg";
import "./App.css";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="\_blank"
          rel="noopener noreferrer">
          Learn React
        </a>
      </header>
    </div>
  );
}
export default App;
```

文件 `App.js` 主要由三部分组成：顶部的 `import` 语句，中间的 `App` 组件，以及底部的 `export` 语句。大多数 React 组件都遵循这个模式。

### import 语句

脚本开头的 `import` 语句允许在此脚本中使用其他文件中的代码，让我们更进一步地了解这些语句。

```js
import React from "react";
import logo from "./logo.svg";
import "./App.css";
```

第一句代码引入了 React 库，这是为了将代码中的 JSX 语句转为`React.createElement()`，所有的 React 模块都应该引入 React 模块，否则会抛错。

第二句代码引入了 `'./logo.svg'`。注意文件路径以 `./` 开头、由 `.svg` 尾——表明这是一个本地文件，并且它不是 JavaScript 文件。

**备注：** 我们没有指定 React 模块的路径——表明它并非来自本地文件，而是在 `package.json` 文件中列为依赖项。在整个学习过程中，请务必留心这两种引入方式的不同之处。

第三行引入了我们的组件所需的 CSS 文件。与上面两句不同，这里没有将引入的内容赋给任何变量、也没有用到 `from` 指令。请注意这种特殊的语法并非原生 JS 的语法——它源自前端资源打包工具 webpack，而 create-react-app 正是基于 webpack 配置而来的。

**备注：** 译者补充：webpack 可用于打包 JS 和非 JS 的内容 (当然，非 JS 的内容需要一些插件或加载器来处理)，但是 JavaScript 标准只有关于 JS 的内容，所以 webpack 社区使用这种特殊的 `import` 语句来声明对非 JS 内容的引用。

详情参见 webpack 官方和社区，截止目前 (2020 年下旬)，webpack 仍是现代前端工作中必不可少的技能之一。

### `App` 组件

在 import 所需资源之后，我们定义了一个名为 App 的函数，尽管大部分 JavaScript 社区推崇使用驼峰式命名法，如：“helloWorld”。但 React 组件使用帕斯卡命名法，如“HelloWorld”，来帮助用户辨认一个 JSX 元素是 React 组件而非普通的 HTML 标签。如果你将函数名“App”改为“app”，你的浏览器会显示错误。

让我们进一步看下 App 方法。

```js
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="\_blank"
          rel="noopener noreferrer">
          Learn React
        </a>
      </header>
    </div>
  );
}
```

App 方法返回一个 JSX 表达式，这个表达式定义了浏览器最终要渲染的 DOM。

表达式中的元素就像以前写的 HTML 一样，都拥有属性，并且遵循 `attribute="value"` 的模式。在第三行，开始标签 `<div>` 有着 `className` 属性。这个属性与在 HTML 中的 `class` 属性相同，但是由于 JSX 就是 JavaScript, 我们不能使用 `class` 属性 - 这个是关键字，意味着 JavaScript 已经用它执行其他任务，使用 `class` 属性将会在我们的代码中产生冲突。由于同样的原因，一些其他的 HTML 属性在 JSX 中也有着不同的书写方式，当我们碰到它们时，我们将会详述。

把第 6 行的 `<p>` 标签内容改为 "Hello, world!" 并保存文件。你会发现这个改变也会立即在浏览器的`http://localhost:3000` 中同步渲染。现在删掉 `<a>` 标签并保存，"Learn React"链接也会同样被删除。

你的 `App` 组件应该如下所示：

```js
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello, World!</p>
      </header>
    </div>
  );
}
```

### Export 语句

在 `App.js` 文件的最底部， `export default App` 语句使得 `App` 组件能被其他模块使用。

Interrogating the index
-----------------------

现在让我们打开 `src/index.js`, 因为这也是 `App` 组件被用到的地方。这个文件是我们 app 的入口点，在一开始它如下所示

```js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import \* as serviceWorker from "./serviceWorker";

ReactDOM.render(<App />, document.getElementById("root"));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
```

就像 `App.js` 一样，这个文件一开始 import 了所有的 JS 模块和其他运行所需要的资源。`src/index.css`定义了运用于整个 app 的 global style。我们可以看到我们的 `App` 组件也被 imported 了，这是在 `App.js` 底部的语句让 import `App` 变得可行。

第七行调用 React 的 `ReactDOM.render()` 函数，并传入两个参数：

* 我们想要渲染的组件，在这个例子中是 `<App />` .
* 我们想要渲染组件所在的 DOM 元素，在这个例子中是带着 `root` 标签的元素。让我们看一下 `public/index.html` 的代码，可以看到这有一个 `<div>` 元素 在 `<body>` 里。

上述所有都告诉 React 我们想把 `App` 组件作为 root 或者第一个组件来渲染我们的 React App。

**备注：** 在 JSX 中，React 组件和 HTML 元素必须有关闭斜杠（`/`），如 `<App />`，如果我们写 `<App>` 或者 `<img>` 将会报错。

Service workers 能让我们的 App 离线运行，但它不在本篇文章的范围中，你可以删除第 5 行和第 9 到 12 行。

你最终的 `index.js` 文件应该如下所示：

```js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));
```

Variables and props
-------------------

接下来，我们将使用一些 JavaScript 的技巧来让我们在 React 中编辑组件以及处理数据更加顺手。我们将讨论如何在 JSX 中 使用 variables，并且介绍 props，props 是我们用来往组件里传入数据的一种方法，传入之后我们可以用 variable 访问传入的变量。

### Variables in JSX

回到 `App.js`, 让我们看一下第 9 行：

```js
<img src={logo} className="App-logo" alt="logo" />
```

可以看到，`<img />` 标签的 `src` 属性值是在大括号中的——`{logo}`。这是 JSX 识别变量的方式。React 将会识别 `{logo}`，知道你在我们 app 第二行引入的 logo，然后 React 会读取这个文件它并渲染。

让我们试着设置一个我们自己的变量，在 `App` return 之前，添加 `const subject = 'React';`。你的代码现在应该如下所示：

```js
function App() {
  const subject = "React";
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello, World!</p>
      </header>
    </div>
  );
}
```

把第 8 行的 "world" 替换成我们自己的变量 `subject` ，如下所示：

```js
function App() {
  const subject = "React";
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello, {subject}!</p>
      </header>
    </div>
  );
}
```

当我们保存时，浏览器将会显示 "Hello, React!"，而不是 "Hello, world!"

变量十分方便，但是我们没有利用到 React 的特性，接下来我们将介绍 Props。

### Component props

**prop** 是任何传入 React 组件的数据。Props 写在组件中，并且像 HTML 属性一样写成 `prop="value"`。让我们打开 `index.js` 并且为我们的 `<App/>` 添加第一个 prop。

为 `<App/>` 组件添加一个叫 `subject` 并有着 `Clarice` 值的 prop。当完成之后，你的代码应如下所示：

```js
ReactDOM.render(<App subject="Clarice" />, document.getElementById("root"));
```

回到 `App.js`，代码应该如下所示（return 中的内容省略了）

```js
function App() {
  const subject = "React";
  return (
    // return statement
  );
}
```

改变 `App` 的函数签名，让它接收 `props` 作为一个参数。就像其他参数一样，你可以把 `props` 放在 `console.log()` 中，让其在浏览器打印出来。把它放在你的 `subject` 之后，以及 `return` 之前，你的代码应如下所示：

```js
function App(props) {
  const subject = "React";
  console.log(props);
  return (
    // return statement
  );
}
```

保存你的文件并检查你浏览器中的 JavaScript Console，你将会发现如下所示的语句：

```js
Object { subject: "Clarice" }
```

对象的 `subject` 属性与我们放入 `App` 函数签名的 prop 相对应，并且 `Clarice` 字符串与它的值相对应，在 React 中的组件 props 总是用这种方式传入 object 中。

现在 `subject` 是我们的 props 之一了，让我们在 `App.js` 中使用它。用 `props.subject` 替代原本的 `React` 字符串，如果你想的话，也可以删除 `console.log()`，你的代码将如下所示：

```js
function App(props) {
  const subject = props.subject;
  return (
    // return statement
  );
}
```

当你保存之后，app 应该会输出 "Hello, Clarice!"。如果你回到 `index.js`，并且修改 `subject` 的值并保存，你输出的字也会随之改变。

总结
--

以上就是我们对 React 的初步认识，包括如何在本地下载它，创建一个初始 app，以及一些基本的操作。在下篇文章，我们将会开始创建我们的第一个 app——一个任务清单。在我们开始下篇文章之前，让我们先复习下我们现在所学的。

在 React 中：

* 组件可以 import 它们需要的模块，并且在文件底部将自身 export，以让其他组件使用。
* 组件是用 `PascalCase` 也就是帕斯卡命名法命名的。
* 通过把变量放在大括号中，你可以读取 JSX 的变量，如`{so}`
* 一些 JSX 属性与 HTML 属性不相同，这样就不会与 JavaScript 的保留字相冲突，比如说，在 HTML 中的 `class` 会在 JSX 中转译为 `className`。注意这些属性都是驼峰式命名的。
* Props 就像属性一样写在组件里，并且传入组件。
* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


Svelte 入门 - 学习 Web 开发

Svelte 入门
=========

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

此篇文章我们将简要介绍 Svelte 框架。我们将会学习 Svelte 如何运作，以及它与其它框架和工具的不同之处。接着我们将学习如何设置我们的开发环境并建立一个示例应用程序，了解其项目结构及如何在本地运行，最后可以将其构建于生产环境。

|  |  |
| --- | --- |
| 前提： |
 推荐你至少需熟悉基本的
 HTML、CSS 与
 JavaScript
 等程序语言且具备终端/命令行基本知识。

Svelte 为一个编译程序，从我们的源代码去产生出最小且高度优化的 JavaScript 程序代码；你需要一个安装了 node 和 npm 的终端来编译和构建你的应用程序。 |
| 目标： | 设置 Svelte 本地开发环境，建立初始应用程序并了解 Svelte 基本运作方式。 |

Svelte：建构丰富用户界面的新方式
-------------------

与此模块中介绍的其它框架相比，Svelte 提供了建构 web 应用程序的不同的方式。应用程序运行时，像 React 和 Vue 等框架会在使用者的浏览器做一大堆工作，而 Svelte 则是将做的这些事移到编译阶段处理，因此那些要做的事情只会发生在你构建应用程序的时候，进而能产生高度优化的原生 JavaScript 程序代码。

这种方式产生的结果并不只是将应用程序的包最小化及取得最佳性能。对于现代的生态环境拥有较少技术经验的人，也可以通过 Svelte 取得更好的开发体验。

Svelte 更贴近传统的 Web 开发模型（HTML、CSS、JS），它只是对 HTML 和 JavaScript 增加了一点扩展。与其它框架相比，可以说只有一点点概念和工具需要学习。

现阶段的缺点是它目前仍在发展中——和其它成熟框架相比，它的生态环境较受到了工具、支持、插件、使用模式等等限制，工作机会也比较少。但它的优点应该足够让你感兴趣并去探索它。

**备注：** Svelte 具有官方的 TypeScript 支持。我们将在之后的教学系列中讲到它。

我们鼓励你完成 Svelte 教程来快速了解基本的概念，然后再回到此教学系列来学习如何建构一些更深入的东西。

使用案例
----

Svelte 可以被用来开发一小块界面或整个应用程序。你也可以从头开始来让 Svelte 驱动你的用户界面或将它渐进地整合至已存在的应用程序。

不过 Svelte 特别适合处理下列几种情况：

* 让 web 应用程序更适用于低功耗设备：使用 Svelte 建构的应用程序将拥有较小的捆绑包（bundle）大小，特别适用于网络连接缓慢和处理能力有限的设备。因为程序代码较少，所以需要下载、解析、执行并保存于内存中的字节数也较少。
* 可应付需频繁交互的页面或复杂的可视化需求：比如你建构一个数据可视化应用程序，你可能会需要显示大量的 DOM 元素，从该框架就能获得较好的性能增益，因为它不会有运行时的开销，可以确保使用者的互动更快速与实时。
* 具备基本的 Web 开发知识即可：Svelte 学习曲线不高。Web 开发人员具备基本的 HTML、CSS、JavaScript 知识就能在短时间简单地掌握 Svelte 的要点并开始构建 Web 应用程序。

Svelte 团队推出了 SvelteKit，这是一个使用 Svelte 构建 Web 应用程序的框架。它包含了现代 Web 框架的特性，如基于文件系统的路由、服务器端渲染（SSR）、页面特定的渲染模式、离线支持等。关于 SvelteKit 的更多信息，可以参考官方教程和文档。

**备注：** 作为构建 Web 应用的推荐 Svelte 框架，SvelteKit 旨在取代 Sapper。

Svelte 也可以通过 Svelte Native 用于移动开发。

Svelte 如何工作？
------------

作为一个编译程序，Svelte 能扩展 HTML、CSS、JavaScript，产生最佳的 JavaScript 程序代码而不会有任何运行时开销。为了达成这个目的，Svelte 通过下列方法来扩展其原生 web 技术：

* 扩展 HTML：允许在标记中使用 JavaScript 表达式并提供了一些指令来使用条件和循环，做法和 handlebars 相似。
* 扩展 CSS：添加作用域机制，并允许每个组件定义自己的样式（不会因为其它组件的样式改变而被影响）。
* 扩展 JavaScript：重新解释一些特定指令来达到真实的响应式并减轻组件状态管理的复杂度。

只有在非常特殊的情况和 Svelte 组件的上下文中编译程序才会介入。其中对 JavaScript 的扩展也相对比较少且谨慎，就是为了不破坏 JavaScript 的语法，而使开发人员觉得困惑。事实上，你大部分时间还是会用到原生 JavaScript 来开发。

Svelte 第一步
----------

既然 Svelte 是一个编译程序，你不能只是把 `<script src="svelte.js">` 标签加入到你的页面并加载它到你的应用程序。你必须设置你的开发环境，为的是能让编译程序可以做它要做的事情。

### 条件

要想使用 Svelte，你需要安装 Node.js。推荐使用长期支持版本（LTS）。Node 包含 npm（node 包管理器）和 npx（node 包运行器）。另外你也可以使用 Yarn 包管理工具来代替 npm，但我们先假定你在教学系列中使用 npm。若想知道更多 npm 和 yarn 的相关信息，请参见软件包管理基础。

如果你使用的是 Windows 的话，则需要安装一些软件来使其命令行与 Unix/macOS 的终端的行为一致，以便使用本教程中提到的终端命令。Gitbash（适用于 Windows 的 git 工具集中的一部分功能）或使用适用于 Linux 的 Windows 子系统（WSL），这些都是合适的解决方案。若想了解有关命令行的更多信息，请参见命令行课程。

若想知道更多相关信息也可以阅读下列资料：

* npm 文档：“关于 npm”
* npm 博客：“npx 介绍”
### 建构你的第一个 Svelte 应用程序

最简单的方式就是使用入门模板来建构初始应用程序。你可以访问 GitHub 上的 sveltejs/template 下载并解压起始模板或者使用 degit 来做这件事。

要构建你第一个起始模板应用程序，请先执行下列终端指令：

```bash
npx degit sveltejs/template moz-todo-svelte
cd moz-todo-svelte
npm install
npm run dev
```

**备注：** 不用太惊讶 degit 做了这么多事情——它其实就只是让你可以从 git 仓库下载和解压缩最新版本的内容。使用它比起使用 `git clone` 要快得多，因为它并不会下载整个仓库的历史记录，也不会创建完整的本地克隆。

执行 `npm run dev` 之后，Svelte 将会编译和建构你的应用程序。它会在 `localhost:8080` 上运行本地服务器。当你对源代码文件做了变动，Svelte 将会侦测到文件更新并自动地重新编译和刷新应用程序。你的浏览器将显示如下内容：

![一个简单的起始页，上面写着 hello world，并提供指向官方 svelte 教程的链接](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_getting_started/01-svelte-starter-app.png)

### 应用程序结构

入门模板有下列结构：
```

moz-todo-svelte
├── README.md
├── package.json
├── package-lock.json
├── rollup.config.js
├── .gitignore
├── node_modules
├── public
│   ├── favicon.png
│   ├── index.html
│   ├── global.css
│   └── build
│       ├── bundle.css
│       ├── bundle.js
│       └── bundle.js.map
├── scripts
│   └── setupTypeScript.js
└── src
    ├── App.svelte
    └── main.js
```

内容解释如下：

* `package.json` 和 `package-lock.json`：包含 Node.js/npm 用于组织化管理你的项目的信息。在这个教程中，你完全不需要了解这个文件，但如果你想要学习更多内容，你可以阅读 npmjs.com 上的 `package.json` 处理；我们在软件包管理基础教程也有谈到。
* `node_modules`：这里是 node 存放项目依赖的地方。这些依赖在生产环境中不会用到，只有开发时才会被使用到。
* `.gitignore`：告诉 git 有哪些文件或文件夹不要纳入项目版本控制——如果你决定将应用程序包含在 git 仓库中，它还蛮实用的！
* `rollup.config.js`：Svelte 使用 rollup.js 作为模块打包工具。这个配置文件告诉 rollup 如何编译和构建你的应用程序。假如你偏好使用 webpack，你可以改为执行 `npx degit sveltejs/template-webpack svelte-app` 来创建你的入门项目。
* `scripts`：包含所需的设置脚本。现在应该只有 `setupTypeScript.js`。
    + `setupTypeScript.js`：此脚本用来支持 Svelte 使用 TypeScript。我们将在之后的文章详细讨论这一点。
* `src`：这个目录主要放置应用程序的源代码——你创建的程序代码基本都会放在这里。
    + `App.svelte`：在你的应用程序中它是顶层组件。不过它现在只会渲染信息“Hello World!”。
    + `main.js`：我们应用程序的入口点。它会实例化 `App` 组件并将其绑定至我们 HTML 页面的 body 上。
* `public`：这个目录包含我们会在生产环境中发布的所有文件。
    + `favicon.png`：这个是你应用程序的图标。现在应该会是 Svelte 的标志。
    + `index.html`：这个是你应用程序的主页面。最初为一个空的，会载入由 Svelte 产生的 CSS 文件和 js 捆绑包的 HTML 页面。
    + `global.css`：这个文件包含没有作用域限制的样式。这是一个常规的，会被应用到整个应用程序的 CSS 文件。
    + `build`：这个文件夹会包含那些编译生成的 CSS 和 JavaScript 源代码。
        - `bundle.css`：Svelte 根据每一个组件所定义的样式生产的 CSS 文件。
        - `bundle.js`：从所有的 JavaScript 源代码编译而来的 JavaScript 文件。
查看第一个 Svelte 组件
---------------

组件是 Svelte 应用程序建构的基础。组件位于 `.svelte` 文件中，可以使用 HTML 的超集来编写。

总共有三个部分——`<script>`、`<styles>` 和标记（markup）区块——它们都是可选的，而且可以按你喜欢的顺序排列。

```html
<script>
 // 逻辑放这
</script>

<style>
 /\* 样式放这 \*/
</style>

<!-- 标记（零或多个 HTML 元素）放这 -->
```

**备注：** 有关组件格式的更多信息，请查看 Svelte 组件文档。

接着我们来看入门模板中的 `src/App.svelte` 文件，你应该会看到以下内容：

```html
<script>
 export let name;
</script>

<main>
  <h1>Hello {name}!</h1>
  <p>
    Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn
    how to build Svelte apps.
  </p>
</main>

<style>
 main {
 text-align: center;
 padding: 1em;
 max-width: 240px;
 margin: 0 auto;
 }

 h1 {
 color: #ff3e00;
 text-transform: uppercase;
 font-size: 4em;
 font-weight: 100;
 }

 @media (min-width: 640px) {
 main {
 max-width: none;
 }
 }
</style>
```

### `<script>` 部分

`<script>` 区块包含组件实例被创建时运行的 JavaScript。顶层声明（或导入）的变量对于组件标记是“可见”的。顶层变量是 Svelte 用来处理组件状态的方式，它们默认是响应式的。我们之后会详细解释这意味着什么。

```html
<script>
 export let name;
</script>
```

Svelte 使用 `export` 关键字将变量声明为属性，代表这个属性可以被组件的使用者（例如，其它组件）访问。这是 Svelte 扩展 JavaScript 语法使其更有用且使用上如同以往的其中一个例子。

### 标记部分

在标记部分，你可以插入任何 HTML，另外你也可以在一对大括号（`{}`）中插入有效的 JavaScript 表达式。此例中我们在 `Hello` 文本之后嵌入 `name` 属性值。

```html
<main>
  <h1>Hello {name}!</h1>
  <p>
    Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn
    how to build Svelte apps.
  </p>
</main>
```

Svelte 也支持一些标签，像是 `{#if...}`、`{#each...}` 和 `{#await...}`——分别允许你可以条件式地渲染标记中的一部分、迭代列表元素和处理异步值。

### `<style>` 部分

假如你有写过一些 CSS，下面这段应该不会太陌生：

```html
<style>
 main {
 text-align: center;
 padding: 1em;
 max-width: 240px;
 margin: 0 auto;
 }

 h1 {
 color: #ff3e00;
 text-transform: uppercase;
 font-size: 4em;
 font-weight: 100;
 }

 @media (min-width: 640px) {
 main {
 max-width: none;
 }
 }
</style>
```

我们现在对 `<h1>` 元素应用了一些样式。这对其它组件中的 `<h1>` 元素们将会有什么影响呢？

在 Svelte 中，组件 `<style>` 区块中的 CSS 会被局限于该组件范围之内。这是通过对所选的元素加上一个类来实现，而这个类名则会是基于组件样式的哈希值。

你可以观察到这个现象。在浏览器打开一个新标签，进入 `localhost:8080`，对准 *HELLO WORLD!* 标签鼠标右键/按 `Ctrl` 键同时点击该标签，之后选择*检查*：

![打开开发工具的 Svelte 入门应用程序，显示范围样式的类](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_getting_started/02-svelte-component-scoped-styles.png)

编译应用程序时，Svelte 会将 `h1` 的样式定义改变为 `h1.svelte-1tky8bj` 并更改每一个在此组件中的 `<h1>` 元素为 `<h1 class="svelte-1tky8bj">`，如此就能让该组件取得所需的样式。

**备注：** 你也可以使用 `:global()` 修饰符覆盖此行为并且应用全局选择器的样式（请参见 Svelte `<style>` 文档以了解更多信息）。

做一些小改变
------

现在我们已经大致了解了内容是怎么整合在一起的，接着可以来做一些小改变。你可以尝试更新你的 `App.svelte` 组件——例如将 `App.svelte` 第 6 行位置的 `<h1>` 元素内容变更如下：

```html
<h1>Hello {name} from MDN!</h1>
```

保存你的变更之后，运行在 `localhost:8080` 上的应用程序将会自动更新。

### 初见 Svelte 响应式

在用户界面框架当中，响应式意味着当任何组件的状态被改变时，框架能够自动地更新 DOM。

在 Svelte 中，可以通过为组件中的任何顶层变量分配新值来触发响应。例如，我们可以在 `App` 组件中写一个 `toggleName()` 方法并通过一个按钮来运行它。

尝试更新你的 `<script>` 和标记部分，如下：

```html
<script>
 export let name;

 function toggleName() {
 if (name === "world") {
 name = "svelte";
 } else {
 name = "world";
 }
 }
</script>

<main>
  <h1>Hello {name}!</h1>
  <button on:click="{toggleName}">Toggle name</button>
  <p>
    Visit the <a href="https://learn.svelte.dev/">Svelte tutorial</a> to learn
    how to build Svelte apps.
  </p>
</main>
```

每当按钮被点击，Svelte 会执行 `toggleName()` 函数并更新 `name` 变量的值。

你可以看到 `<h1>` 标签已经自动地被更新。Svelte 在背后创建了一些 JavaScript 程序代码，每当 name 变量数值改变时，就会更新 DOM，而没有使用到任何虚拟 DOM 或复杂的协调机制。

注意在 `on:click` 中使用的 `:`。这是 Svelte 用于 DOM 事件监听的语法。

检视 main.js：我们应用程序的入口点
---------------------

让我们打开 `src/main.js`，`App` 组件正是在这里被加载并使用的。这个文件是我们应用程序的入口点，初始内容如下：

```js
import App from "./App.svelte";

const app = new App({
  target: document.body,
  props: {
    name: "world",
  },
});

export default app;
```

`main.js` 一开始就会导入要使用的 Svelte 组件。然后在第 3 行的位置传入具有如下属性的选项对象并实例化这个组件：

* `target`：决定我们想要把组件渲染在哪一个 DOM 元素上，此例为 `<body>` 元素。
* `props`：指定 `App` 组件中的属性值。
葫芦里卖什么药？
--------

Svelte 是如何很好地让所有文件协同工作？

Svelte 编译程序处理每一个组件的 `<style>` 部分并将它们编译到 `public/build/bundle.css` 文件中。

它也编译每一个组件的标记和 `<script>` 部分并把结果储存至 `public/build/bundle.js`。它也会添加一些程序代码到 `src/main.js` 以引用每一个组件的功能。

最后 `public/index.html` 文件会引入那些生成的 `bundle.css` 和 `bundle.js` 文件：

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <title>Svelte app</title>

    <link rel="icon" type="image/png" href="/favicon.png" />
    <link rel="stylesheet" href="/global.css" />
    <link rel="stylesheet" href="/build/bundle.css" />

    <script defer src="/build/bundle.js"></script>
  </head>

  <body></body>
</html>
```

`bundle.js` 的压缩版本容量大概 3KB 多一点，其中包含“Svelte 运行时”（约 300 行 JavaScript 程序代码）和被编译后的 `App.svelte` 组件。可以看出，`bundle.js` 是唯一一个被 `index.html` 引用的 JavaScript 文件。网页没有载入其他库。

比起其它框架编译后的捆绑包，这要小的多。考虑到代码捆绑包这件事，问题不仅仅是需下载文件的大小，还包括将可执行代码解析、执行并保存于内存中。所以这很重要，特别是对于低功耗设备或 CPU 密集型应用程序而言。

跟随这个教程
------

在这个教学系列你将建构一个完整的 web 应用程序。我们将学习有关 Svelte 的所有基础，以及一些进阶内容。

你只需阅读内容就可充分了解 Svelte 的一些特性，但如果你跟着我们一起编写应用程序，将可以从本教程获得最大的收益。为了让你可以更轻松地阅读每一篇文章，我们提供了一个 GitHub 仓库并以文件夹分隔出每一个教程开头需要的应用程序源代码。

Svelte 也提供了一个在线 REPL，它是在 web 上用于实时编写 Svelte 应用程序的一个游乐场，而无需在计算机上安装任何东西。我们在每一篇文章都有提供 REPL，所以你可以马上开始编写代码。接着谈一谈如何使用这些工具。

### 使用 Git

最受欢迎的版本控制系统是 Git 以及 GitHub，Github 网站提供了你的仓库托管和多种与它协作的工具。

我们将会使用 GitHub，所以你可以在每一篇文章轻易地下载到源代码。不用担心你错过了什么，因为你将能够取得这些于每一篇文章欲完成的程序代码内容。

安装 git 之后，为了克隆仓库你应该执行：

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

接着在开始阅读每一篇文章前，你可以 `cd` 至对应文件夹并将应用程序以开发（dev）模式运行确认其当前状态为何，如下所示：

```bash
cd 02-starting-our-todo-app
npm install
npm run dev
```

假如你想要学习有关 git 和 GitHub 的更多信息，我们已经整理了一些有帮助的指南的链接列表——请参阅 Git 和 GitHub。

**备注：** 假如你只是想要下载文件而不是克隆 git 仓库，你可以使用 degit 工具，像这样——`npx degit opensas/mdn-svelte-tutorial`。你也可以用 `npx degit opensas/mdn-svelte-tutorial/01-getting-started` 来下载特定文件夹。Degit 不会创建本地 git 仓库，它只下载特定文件夹下的文件。

### 使用 Svelte REPL

REPL（读取—求值—输出循环）是一个交互式环境，允许你输入一些指令并能立即看到结果——大部分的程序语言都有提供 REPL。

Svelte 的 REPL 功能却不只如此。它是一个在线工具，允许你建构一个完整的应用程序并储存它们于线上，也能分享给其他人。

它是在任何机器上开始体验 Svelte 的最简单的方式，无需安装任何东西。它也广泛地被 Svelte 社区所使用。假如你想要分享一些想法、寻求帮助或报告问题，创建一个 REPL 来演示问题总是有帮助的。

让我们快速看一下 Svelte REPL 和你应该怎么去使用它。它的界面是这样的：

![运行中的 svelte repl，左侧显示组件代码，右侧显示输出结果](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_getting_started/03-svelte-repl-in-action.png)

要开始使用 REPL，打开你的浏览器并导航至 https://svelte.dev/repl。

* 屏幕左边你将可以看到你组件的代码，屏幕右边你可以看到你应用程序的执行输出。
* 程序代码上方的栏让你可以创建 `.svelte` 和 `.js` 文件，也能重新排列它们。要在文件夹中创建文件，你只要输入完整的路径名称——`components/MyComponent.svelte`。文件夹将被自动创建。
* 更上方的栏是 REPL 的标题。点击可以编辑它。
* 右边有三个标签：
    + *Result* 标签内会显示你应用程序的输出，底下也有提供控制台。
    + *JS output* 标签内可以让你检查 Svelte 产生的 JavaScript 代码且可以设定编译选项。
    + *CSS output* 标签内会显示 Svelte 产生的 CSS。
* 这些标签的上方，你将会找到一个工具栏，让你可以进入全屏幕模式和下载你的应用程序。假如你有登入 GitHub 账号，你将能够复刻（fork）和保存应用程序。通过点击你 GitHub 的用户名个人资料并选择*你已保存的应用程序*（Your saved apps），将能够看到你已保存的所有 REPL。

每当你在 REPL 中改变任何文件，Svelte 将会重新编译应用程序并更新结果（Result）标签。要分享你的应用程序，请直接分享其网址。例如，这是运行我们完整应用程序的 REPL 链接：https://svelte.dev/repl/378dd79e0dfe4486a8f10823f3813190?version=3.23.2。

**备注：** 请注意在链接上标注 Svelte 版本的方式。当你在报告 Svelte 特定版本的问题时，这会非常有用。

我们将会在每一篇文章的开头与结尾提供 REPL，所以你就能立刻和我们一起编写代码。

**备注：** 目前 REPL 无法正确处理文件夹名称。如果你按照 REPL 上的教程进行操作，只需在根文件夹中创建所有组件即可。然后在你看到代码中的路径时，例如 `import Todos from './components/Todos.svelte'`，只需要将它替换成类似 `import Todos from './Todos.svelte'` 这样的即可。

到目前为止的代码
--------

### Git

克隆 Github 仓库（假如你还没完成）如下：

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

然后获得当前应用程序状态，运行

```bash
cd mdn-svelte-tutorial/01-getting-started
```

或直接下载文件夹内容：

```bash
npx degit opensas/mdn-svelte-tutorial/01-getting-started
```

记得执行 `npm install && npm run dev` 以开发模式来运行你的应用程序。

### REPL

要使用 REPL 和我们一起编写代码，请点击

https://svelte.dev/repl/fc68b4f059d34b9c84fa042d1cce586c?version=3.23.2

总结
--

这篇文章带我们初步了解了 Svelte，包含如何在本地安装、创建起始应用程序以及它的基本工作方式。在下一篇文章中，我们将开始构建第一个应用程序——一个待办事项列表。在我们开始之前，让我们先回顾一下已经学到的东西。

在 Svelte 当中：

* 我们在每个组件（单个 `.svelte` 文件）中定义脚本、样式、标记。
* 通过 `export` 关键字来声明组件属性。
* 可以通过加载对应的 `.svelte` 文件来使用这些 Svelte 组件。
* 组件样式有范围限制，以防止它们之间有冲突。
* 在标记部分，你可以通过将 JavaScript 表达式放在大括号之间来包含它们。
* 组件中的顶层变量会构成其状态。
* 当为顶层变量赋新值时，将触发其响应。
* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


介绍完整的工具链 - 学习 Web 开发

介绍完整的工具链
========

* 上一页
* Overview: Understanding client-side tools
* 下一页  (en-US)

在本系列的最后几篇文章中，我们将通过演示如何构建一个示例案例工具链来巩固你的工具知识。我们将从设置合理的开发环境和设置转换工具开始，一直到在 Netlify 上实际部署应用程序。在本文中，我们将介绍研究案例、设置开发环境和设置代码构建转换工具。

|  |  |
| --- | --- |
| 预备条件： |
 熟悉核心的 HTML、CSS 和
 JavaScript 语言。
  |
| 目标： | 通过完成完整的工具链案例研究来巩固我们已经学到的知识。 |

实际上，工具和使用方法的组合是无限的，本文和下一篇文章中介绍的只是使用特定工具的*一种*方式。

**备注：** 也值得重申的是，并非所有这些工具都需要在命令行上运行。许多当今的代码编辑器（如 VS Code 和 Atom）都通过插件支持*许多*工具的集成。

介绍我们的学习案例
---------

我们在本文中创建的工具链将用于构建和部署一个迷你网站，该网站列出了有关潜在危险的空间物体的数据（取自 NASA 开放 API 之一），这些物体威胁着我们在地球上的存在！它看起来像这样：

![示例网站“will it miss”的屏幕截图](/zh-CN/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Introducing_complete_toolchain/will-it-miss-screenshot.png)

你可以在 near-misses.netlify.com 上看到该网站的实时版本。

工具链中使用的工具
---------

在本文中，我们将使用以下工具和功能：

* JSX，一组与 React 相关的语法扩展，允许你在 JavaScript 中定义组件结构等。你不需要了解 React 就可以按照本教程进行操作，但我们已经包含了这一点，以便让你了解非原生 Web 语言如何集成到工具链中。
* 最新的 JavaScript 内置特性（在撰写本文时），例如 `import`。
* 有用的开发工具，例如用于格式化的 Prettier 和用于代码规范检测的 ESLint。
* PostCSS 提供 CSS 嵌套功能。
* Parcel 用于构建和压缩我们的代码，并自动为我们编写一堆配置文件内容。
* GitHub 用于管理我们的源代码。
* Netlify 用于自动化我们的部署过程。

你可能不熟悉上述所有功能和工具或它们的作用，但不要惊慌——随着我们在本文中的深入讨论，我们将解释每个部分。

工具链及其固有的复杂性
-----------

与任何链条一样，你的工具链中的环节越多，它就越复杂，潜在的脆弱性也就越大——例如，它可能更难配置，更容易出问题。相反，环节越少，工具链就越稳定可靠。

所有 Web 项目都是不同的，你需要考虑工具链的哪些部分是必要的，并仔细考虑剩余的每个部分。

最小的工具链是根本没有工具链。你可以手动编写 HTML，使用“vanilla JavaScript”（即没有框架或中介语言），并手动将其全部上传到服务器进行托管。

然而，更复杂的软件很可能会需要使用工具来帮助简化开发过程。此外，你应该在将软件部署到生产服务器之前包括测试，以确保你的软件按预期工作——这已经听起来像是必要的工具链。

对于我们的示例项目，我们将使用专门设计的工具链来帮助我们的软件开发并支持软件设计阶段所做的技术选择。但是，我们将避免使用任何多余的工具，以最小化复杂程度。

例如，我们*可以*在构建过程中包含一个工具，以缩小我们的 SVG 文件大小。但是，该项目只有 4 个 SVG 图像，这些图像在添加到项目之前已经使用 SVGO 进行了手动缩小。

一些先决条件
------

除了我们将安装的工具之外，我们在上面的工具列表中提到了两个 Web 服务。现在让我们确保我们已经准备好使用它们了。如果你希望完成本教程，你需要在 GitHub 和 Netlify 中创建帐户。

* 如前所述，GitHub 是一个源代码存储库服务，还包含了社区功能，例如问题跟踪、关注项目发布等等。在下一章中，我们将推送到 GitHub 代码库，这将导致级联效应——这（应该）将自动所有软件部署到 Web 上。
* Netlify 是一个用于静态网站（即完全由不实时更改的文件组成的网站）的托管服务，它允许我们每天部署多次，并免费托管各种静态站点。Netlify 就是提供上述“网络主页”的服务——为我们提供免费托管以部署我们的测试应用程序。

如果你已经注册了 GitHub（如果你还没有帐户，请单击主页上的“注册”链接并按照说明操作），你可以使用 GitHub 帐户进行身份验证 Netlify（单击“注册”，然后从“以下账户之一注册”列表中选择“GitHub”），因此从技术上讲，你只需要创建一个新帐户。

稍后，你需要将 Netlify 帐户连接到 GitHub 存储库，以部署此项目；我们将在下一章中看到如何执行此操作。

工具的三个阶段
-------

正如我们在第一章中所讨论的那样，工具链将分为以下阶段：

* **安全的网络**：使软件开发体验稳定且更高效。我们也可能将其称为开发环境。
* **编译与构建**：允许我们在开发过程中使用编程语言（例如 JavaScript）的最新特性或其他语言（例如 JSX 或 TypeScript），然后转译我们的代码，以便生产版本仍在各种浏览器上运行，无论是现代的和陈旧的。
* **开发后**：在完成开发主体后发挥作用的工具，以确保你的软件持续运行。在这个案例中，我们将研究如何向你的代码添加测试流程，并使用 Netlify 部署你的应用程序，以便所有人都可以访问。

让我们从开发环境开始。

创建开发环境
------

这部分工具链有时被视为耽搁实际工作的原因，因为它很容易使开发者陷入工具的“兔子洞”中，花费大量时间尝试使环境“恰到好处”。

但是你可以用以看待布置实际工作环境相同的方式来看待这一点。椅子需要舒适，并设置合适的位置以帮助你调整姿势。你需要电源、Wi-Fi 和 USB 接口！可能有重要的装饰或音乐可以帮助你改善精神状态——这些都是做好工作的重要因素，而且如果设置得当，它们也应该只需要设置一次。

同样，如果做得好，设置你的开发环境只需要做一次，并且应该可以在许多未来的项目中重复使用。你可能需要定期审查工具链的这一部分，并考虑是否应该引入任何升级或更改，但这不应该太经常需要。

你的工具链将取决于你自己的需求，但是对于这个（可能的）完整工具链的示例，将在开始阶段安装以下工具：

* 软件包安装工具——用于添加依赖项。
* 代码修订控制。
* 代码格式化工具——用于整理 JavaScript、CSS 和 HTML。
* 代码检查工具——用于检查我们的代码和规范。
### 软件包安装工具

我们将使用 npm 安装我们的工具，我们已经在第二章中介绍了它。你应该已经安装了 Node.js 和 npm，但如果没有，请参考该部分。

**备注：** 虽然不清楚具体的安装过程，但安装 npm 时还会安装一个名为 npx 的补充工具。我们稍后将使用 npx 来帮助运行作为项目本地依赖项安装的工具。

npm 将用于安装我们工具链的后续部分。接下来，我们将安装 git 来帮助进行修订控制和代码管理。

### 代码修订控制

你可能之前听说过“git”。Git 是目前开发人员可用的最流行的源代码修订控制工具——版本控制提供了许多优势，例如在远程位置备份你的工作，以及在同一项目上团队合作而不必担心互相覆盖彼此的代码。

对于一些人来说，这可能是显而易见的，但是需要重申：Git 不同于 GitHub。Git 是版本控制工具，而 GitHub 是 git 存储库的在线存储库（附带了许多有用的相关工具）。请注意，尽管我们在本章中使用 GitHub，但还有几个替代方案，包括 GitLab 和 Bitbucket，你甚至可以托管自己的 git 存储库。

在你的项目中使用修订控制并将其作为工具链的一部分将有助于管理代码的演变。它提供了一种方法，可以在开发过程中“提交”工作块，以及诸如“实现 X 新功能”或“由于 Y 更改，Bug Z 现在已修复”之类的注释。修订控制还可以允许你为项目代码创建*分支*，创建一个单独的版本，并在上面尝试新功能，而不会影响你的原始代码。

最后，如果你在代码中引入了错误并且难以修复它，则它可以帮助你撤消更改或将代码恢复到“它正常工作时”——这是所有开发者的保命技能！

Git 可以通过 git-scm 网站下载和安装——下载适用于你系统的相关安装程序，运行它，并按照屏幕上的提示操作。干就完事了。

你可以以多种不同的方式与 git 进行交互，从使用命令行发出命令，到使用 git GUI 应用程序通过按按钮发出相同的命令，甚至从直接在代码编辑器内部使用，如下面的 Visual Studio Code 示例所示：

![在 VS Code 中显示的 GitHub 集成](/zh-CN/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Introducing_complete_toolchain/vscode-git.png)

好了，现在安装好 git 就足够了。让我们继续。

### 代码格式化工具

我们将在本项目中使用 Prettier——我们在第二章中介绍了它——来格式化我们的代码。如果你按照安装 Prettier 部分中的说明进行操作，那么你可能已经安装了 Prettier。如果没有，我们将让你立即使用终端将 Prettier 安装为全局程序。

你可以使用以下命令检查是否已在全局安装它：

```bash
prettier -v
```

如果已安装，你将获得返回的版本号，例如 2.0.2；如果没有，它将返回类似于“找不到命令”的内容。如果是这种情况，请使用以下命令安装它：

```bash
npm install prettier -g
```

现在，我们可以使用 Prettier 来格式化我们的代码。在终端中，切换到项目的根目录，然后运行以下命令：

```bash
prettier --write ./src/index.html
```

**备注：** 在上面的命令中，我使用了 Prettier 的 `--write` 选项。这对 Prettier 意味着“如果我的代码格式有任何问题，请继续修复它们，然后保存我的文件”。这对于我们的开发过程来说很好，但是我们也可以只使用 `prettier` 而不使用选项，它将仅检查文件。检查文件（而不保存它）对于运行在发布之前的检查（例如“不要发布任何未经适当格式化的代码”）非常有用。

对每个文件运行初始命令可能很繁琐，有一个单一的命令来为我们执行此操作将非常有用（我们的代码检查工具也是如此）。

有许多解决此问题的方法；以下是其中的一些：

* 使用 npm 脚本在一次命令中从命令行运行多个命令，例如 `npm run tidy-code`。
* 使用特殊的“git 钩子”在提交之前测试代码是否已格式化。
* 使用代码编辑器插件在每次保存文件时运行 Prettier 命令。

**备注：** 什么是 git 钩子？Git（而不是 GitHub）提供了一种系统，可以让我们将预操作和后操作附加到我们使用 git 执行的任务（例如提交代码）。尽管 git 钩子可能有点过于复杂（在笔者看来），但是一旦它们配置就绪，就可以非常强大。如果你有兴趣使用钩子，Husky 是使用钩子的一个很方便的途径。

对于 VS Code，一个有用的扩展是 Esben Petersen 编写的 Prettier Code Formatter，它允许 VSCode 在保存时自动格式化代码。这意味着我们正在开发的项目中的任何文件都会得到合适的格式化，包括 HTML、CSS、JavaScript、JSON、markdown 等。编辑器所需的只是启用“保存时格式化”。

与许多比较新的工具一样，Prettier 带有“合理的默认值”。这意味着你将能够使用 Prettier 而无需配置任何内容（如果默认值满足你的要求）。这使你可以专注于重要的事情：创意工作。

### 代码检查工具

代码检查有助于提高代码质量，也是在开发过程中更早地捕获潜在错误的一种方法。它是良好工具链的关键组成部分，许多开发项目默认包含它。

Web 开发的代码规范检查工具主要用于 JavaScript 中（尽管有一些可用于 HTML 和 CSS）。这是因为，如果使用未知的 HTML 元素或无效的 CSS 属性，由于这两种语言的弹性特性，可能不会出现任何问题。JavaScript 则比较脆弱——例如错误地调用不存在的函数会导致 JavaScript 中断；因此，对于较大的项目，对 JavaScript 进行代码检查尤其重要。

JavaScript 代码检查的首选工具是 ESLint。它是一个非常强大的多功能工具，但是正确配置它可能会很棘手，你可能会花费很多时间来获得一个“完美的”配置！

默认情况下，ESLint 将报错——无法找到配置文件。配置文件支持多种格式，但是对于此项目，我们将使用 `.eslintrc.json`（前缀的 `.` 表示文件默认情况下是隐藏的）。

ESLint 通过 npm 安装，根据第二章中的讨论，你可以选择在本地或全局安装此工具。建议同时使用两者：

* 对于你打算共享的项目，你应始终将 ESLint 作为本地依赖项包含在内，以便任何拷贝自己的版本的人都可以遵循你应用于项目的规则。
* 你还应考虑全局安装 ESLint，以便你可以快速使用它来检查任何文件。

为简单起见，在本章中，我们不会探讨所有 ESLint 功能，但是我们将提供适用于我们特定项目及其要求的配置。但是，请记住，如果需要细化和强制执行有关代码规范（或验证）的规则，很可能可以通过使用正确的 ESLint 配置来实现这一需求。

稍后在本章中，我们将提供 ESLint 配置。一旦有了工作配置，我们将使用它来检查我们的代码。下面是一个 ESLint 输出示例：

```bash
./my-project/src/index.js
   2:8 error 'React' is defined but never used  no-unused-vars
 22:20 error 'body' is defined but never used   no-unused-vars
 96:19 error 'b' is defined but never used      no-unused-vars

✖ 3 problems (3 errors, 0 warnings)
```

**备注：** 我们将在下一节安装 ESLint；暂时不要担心这个。

与其他工具一样，代码编辑器集成支持通常对 ESLint 有好处，并且可能更有用，因为它可以在出现问题时向我们提供实时反馈：

![VS Code 中集成 ESLint 显示的错误](/zh-CN/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Introducing_complete_toolchain/eslint-error.png)

安装完成后，我们需要配置 Prettier，以便它可以与 ESLint 配合使用。我们将在下一节中进行此操作。

### 配置初始项目

使用这些工具，可以安全地建立一个新项目，因为许多“基本”问题将在早期被发现。

使用命令行，我们可以创建项目、安装初始工具并创建基本配置文件。再次重复此过程几次后，你将对默认设置有所了解。当然，这*只是一种*可能的配置。

### 初始设置

好的，让我们先完成初始项目设置。

1. 首先打开终端，切换到一个你能够轻松找到并访问的地方。例如桌面、主目录或文档文件夹。
2. 接下来，运行以下命令创建一个文件夹来保存你的项目，并进入该文件夹：

```bash
mkdir will-it-miss
cd will-it-miss
```

3. 现在，我们将为所有网站开发代码创建一个新目录，运行以下命令：

```bash
mkdir src
```

代码组织结构往往因团队而异。对于这个项目，源代码将位于 `src` 中。
4. 确保你在 `will-it-miss` 目录的根目录中，输入以下命令以启动 git 的源代码控制功能：

```bash
git init
```

这意味着你现在可以开始存储文件夹内容的修订版本，将其保存到远程存储库等。这部份内容我们稍后细讲！
5. 接下来，输入以下命令，将你的目录转换为 npm 包，这样我们就可以利用前一篇文章中讨论的优势：

```bash
npm init --force
```

这将创建一个默认的 `package.json` 文件，如果需要，我们可以稍后对其进行配置。`--force` 选项使命令立即创建一个默认的 `package.json` 文件，而不会询问你有关其内容的所有通常问题（如我们之前提到的）。现在我们只需要默认值，因此这可以节省我们一些时间。

#### 获取项目代码文件

此时，我们将获取项目的代码文件（HTML、CSS、JavaScript 等），并将它们放在我们的 `src` 目录中。我们不会教你它们的工作原理，因为这不是本章的重点。我们只是在这里运行它们，教你如何使用*它们*。

1. 要获取代码文件，请访问 https://github.com/remy/mdn-will-it-miss，下载并解压缩此仓库的内容到本地硬盘的某个位置。你可以通过选择 *Clone or download* > *Download ZIP* 下载整个项目的 zip 文件。

 ![GitHub 上托管的“will it miss”仓库](/zh-CN/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Introducing_complete_toolchain/github-will-it-miss.png)
2. 现在将项目的 `src` 目录的内容复制到你当前的空 `src` 目录中。

我们已经准备好了项目文件。这就是我们现在需要做的全部内容！

**备注：** 要在本地上设置项目，请转到解压缩文件夹的根目录，在该位置打开终端，并在终端中执行 `npm install` 命令。这将安装在 `package.json` 文件中提到的所有项目依赖项。

#### 安装我们的工具

现在是时候安装我们将在开发环境中使用的初始工具集了。从项目的根目录中运行以下命令：

```bash
npm install --save-dev eslint prettier babel-eslint
```

你刚刚运行的命令有两个重要的事情需要注意。第一是我们将依赖项安装到项目的本地位置——在特定项目中安装工具更好。本地安装（不包括 `--global` 选项）使我们能够轻松地在其他机器上重新创建此设置。

这个安装命令的第二个重要部分是 `--save-dev` 选项。这告诉 npm 工具这些特定的依赖项只用于开发（因此 npm 将它们列在 `package.json` 文件中的 `devDependencies` 下，而不是 `dependencies` 下）。这意味着如果这个项目在生产模式下安装，这些依赖项将不会被安装。一个 "典型" 的项目可以有许多开发依赖项，这些依赖项在实际运行代码时并不需要。将它们作为单独的依赖项保持分离，可以减少在部署到生产环境时的不必要工作（我们将在下一章中讨论）。

在开始实际应用程序代码的开发之前，需要进行一些配置，以使我们的工具正常工作。这不是 Web 开发的前提条件，但如果这些工具在开发过程中有助于捕捉错误，正确配置工具是非常有用的，特别是 ESLint。

### 配置我们的工具

在项目的根目录下（而不是 `src` 目录下），我们将添加配置文件来配置我们的一些工具，即 Prettier 和 ESLint。这是工具配置的一般实践——你往往会在项目根目录中找到配置文件，其中往往包含用 JSON 结构表达的配置选项（尽管我们的工具和许多其他工具也支持 YAML，如果这是你喜欢的配置文件格式，你可以切换到它）。

1. 首先，在 `will-it-miss` 目录的根目录下创建一个名为 `.prettierrc.json` 的文件。
2. 要配置 Prettier，请将 `.prettierrc.json` 内容设置为以下内容：

```json
{
  "singleQuote": true,
  "trailingComma": "es5"
}
```

使用这些设置，当 Prettier 为你格式化 JavaScript 代码时，它将对所有带引号的值使用单引号，并且不使用尾随逗号（这是 ECMAScript 的新特性，在旧版浏览器中这会导致错误）。你可以在 Prettier 文档中找到更多关于配置 Prettier 的信息。
3. 接下来，我们将配置 ESLint——在 `will-it-miss` 目录的根目录中创建另一个名为 `.eslintrc.json` 的文件，并将其设置为以下内容：

```json
{
  "env": {
    "es6": true,
    "browser": true
  },
  "extends": "eslint:recommended",
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module"
  },
  "rules": {
    "no-console": 0
  }
}
```

上述 ESLint 配置表示我们想使用"推荐的"的 ESLint 设置，我们将允许使用 ES6 特性（例如 `map()` 或 `Set()`），我们可以使用模块的 `import` 语句，并且允许使用 `console.log()`。
4. 然而，在项目的源文件中，我们使用了 React JSX 语法（对于你的实际项目，你可能会使用 React 或 Vue 或任何其他框架，或者不使用任何框架！）。
 在我们的 JavaScript 代码中使用 JSX 语法会导致 ESLint 很快报错，因此我们需要添加更多的配置以使其接受 JSX 特性。
 最终的配置文件应该看起来像这样——添加粗体字部分并保存：

```json
{
  "env": {
    "es6": true,
    "browser": true
  },
  "extends": ["eslint:recommended", "plugin:react/recommended"],
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "plugins": ["react"],
  "rules": {
    "semi": "error",
    "no-console": 0,
    "react/jsx-uses-vars": "error"
  }
}
```

由于配置现在使用了一个名为 "React" 的插件，因此还需要安装该插件作为开发依赖项，以便为运行该部分的代码检查过程提供代码。
5. 在项目文件夹的根目录下运行以下终端命令：

```bash
npm install --save-dev eslint-plugin-react
```

这里有一个完整的 ESLint 规则列表，你可以根据自己的需求进行调整和配置，许多公司和团队已发布了自己的 ESLint 配置，这对于选择一个适合自己标准的配置有时可能有用，可以帮助我们获得灵感。不过需要提醒的是：ESLint 配置是一个非常深的“兔子洞”问题！

到此为止，我们的开发环境设置完成了。现在，最后，我们（几乎）准备好编写代码了。

构建和转换工具
-------

如上所述，本项目将使用 React，这也意味着源代码将使用 JSX。该项目还将使用最新的 JavaScript 特性。

一个迫在眉睫的问题是，没有任何浏览器原生支持 JSX——它是一种中间语言，旨在被编译成浏览器可以理解的语言。

如果浏览器尝试运行源 JavaScript 代码，它将立即报错。因此这个项目需要一个构建工具来将源代码转换为浏览器可以兼容运行的代码。

转换工具有很多选择，尽管 WebPack 是一个特别流行的选择，但本项目将使用 Parcel——因为它只需要很少的配置。

Parcel 的工作原理是它会尝试根据你的开发需求进行动态配置。Parcel 会监视代码并在开发过程中运行实时重新加载的 Web 服务器。这也意味着 Parcel 会自动安装我们的软件依赖项，因为它们在源代码中被引用，就像我们在第三章中所看到的那样。

Parcel 将处理安装任何所需的转换工具和配置，而我们不需要干预（在大多数情况下）。

最后，Parcel 还可以捆绑和准备代码以进行生产部署，处理代码压缩和浏览器兼容性要求。

因此，我们还需要在项目中安装 parcel 的依赖项——在终端中运行以下命令：

```bash
npm install --save-dev parcel-bundler
```

### 使用未来的特性

我们的项目代码使用了一些新的 Web 功能，包括一些尚未完全标准化的特性。例如，本项目使用 W3C 提出的 CSS 嵌套，而不是像 Sass 这样的工具。CSS 嵌套允许我们将 CSS 选择器和属性嵌套在彼此内，从而创建更具体的选择器范围。Sass 是第一个支持嵌套的预处理器之一（如果不是第一个），但现在经过多年的发展，CSS 嵌套看起来很快就会标准化，这意味着我们将在不需要构建工具的情况下在浏览器中使用它。

在此之前，Parcel 将使用 PostCSS 的帮助在嵌套 CSS 和本地支持的 CSS 之间进行转换，Parcel 可以直接使用 PostCSS。由于我们明确决定本项目应使用 CSS 嵌套（而不是 Sass），因此该项目将需要包含一个 PostCSS 插件。

让我们使用 postcss-preset-env，它可以让我们“立即体验未来的 CSS”。要使用它，请按照以下步骤操作：

1. 在项目目录的根目录下添加一个名为 `.postcssrc` 的文件。
2. 将以下内容添加到新文件中，这将自动为我们提供最新的 CSS 功能的完全访问权限：

```json
{
  "plugins": {
    "postcss-preset-env": {
      "stage": 0
    }
  }
}
```

这就是我们需要做的全部——请记住，Parcel 默认会为我们安装依赖项！

尽管我们的工具链的这个阶段可能会非常痛苦，因为我们选择了一个旨在减少配置和复杂性的工具，但在开发阶段我们真的不需要做更多的事情。模块已正确导入，嵌套的 CSS 已正确转换为“常规 CSS”，我们的开发不受构建过程的影响。

现在我们的软件已准备好开始编写了！

构建和运行
-----

要开始处理我们的项目，我们将在命令行上运行 Parcel 服务器。在其默认模式下，它将监测你的代码中的更改并自动安装你的依赖项。这很好，因为我们不必在代码和命令行之间来回切换。

1. 要在后台启动 Parcel，请转到终端并运行以下命令：

```bash
npx parcel src/index.html
```

你应该会看到像这样的输出（如果安装了依赖项）：

```bash
Server running at http://localhost:1234
✨  Built in 129ms.
```

Parcel 还安装了我们将在代码中使用的依赖项，包括 react、react-dom、react-async-hook、date-fns 和 format-number。因此，这第一次运行将比 Parcel 的典型运行时间更长。

**备注：** 如果你在此项目上运行 Parcel 并遇到像 `Error: ENOENT: no such file or directory` 这样的错误，请使用 `Ctrl` + `C` 停止该进程，然后尝试重新运行它。

服务器现在正在输出的 URL 上运行（在本例中为 localhost:1234）。
2. 在浏览器中打开此 URL，你将看到示例应用正在运行！

Parcel 的另一个聪明技巧是，对源代码的任何更改现在都会触发浏览器中的更新。试试：

1. 在你喜欢的文本编辑器中打开文件 `src/components/App.js`。
2. 搜索文本“near misses”，并将其替换为一些搞怪的东西，比如“flying pigs”。
3. 保存文件，然后直接返回在浏览器中运行的应用程序。你会注意到浏览器已自动刷新，并且页面顶部的“<date> there will be <number> near misses”一行已经发生了更改！

你还可以尝试使用 ESLint 和 Prettier，尝试故意从其中一个文件中删除大量空格，然后运行 Prettier 以清理它，或者在其中一个 JavaScript 文件中引入语法错误，然后查看当你尝试使用 Parcel 再次构建它时，ESLint 给你什么错误提示。

总结
--

在本章中，我们已经走了很长的路，建立了一个相当不错的本地开发环境，用于创建应用程序。

在 Web 软件开发过程中的这一点上，你通常会为你打算构建的软件编写代码。由于本模块是关于学习 Web 开发周围的工具，而不是 Web 开发代码本身，因此我们不会教授任何实际的编码——你可以在 MDN 的其它部分中找到这些教程！

相反，我们为你编写了一个示例项目，供你使用你的工具。我们建议你使用我们的示例代码来完成本章的其余部分，然后你可以尝试更改 src 目录的内容以适应你自己的项目，并将其发布到 Netlify 上！事实上，将部署到 Netlify 将是下一章的最终目标！

* 上一页
* Overview: Understanding client-side tools
* 下一页  (en-US)