被称为“三大框架”替代方案,Svelte 如何简化 Web 开发工作(下)
Web 框架层出不穷,作为主流 Web 框架之一的 Svelte,有着独特的优势。它不仅可以构建完整的 Web 应用程序,还可以创建自定义元素,并在其他框架实现的已有 Web 应用程序中使用。本文将对 Svelte 进行详细的介绍,并带领读者了解使用 Svelte 从头开始构建 Web 应用程序所需的基础知识。您可点击这里回顾该文章上篇。
存储
存储(Stores)在所有组件外部保持应用程序状态。它们是使用 props 或上下文来使数据在组件中可用的替代方法。
对于应该对所有组件可用的存储,请在 src/stores.js 之类的文件中定义并导出它们,并在需要时从该文件导入它们。
对于应该仅对给定组件的后代可用的存储,请在这个组件中定义它们,然后使用 props 或上下文将它们传递给后代。
Svelte 提供三种存储。
- 可写存储 ——这是唯一可以由组件修改的存储。
- 可读存储 ——这些存储处理它们自己的数据。
- 派生存储 ——这些存储从其他存储的当前值派生数据。
这些存储都有一个 subscribe 方法,该方法返回一个可调用的函数来 unsubscribe。
也可以创建自定义存储。它们唯一的限制是成为具有正确实现的 subscribe 方法的对象。示例见 此 。
可写存储
要创建可写存储,请调用 svelte/store 包中定义的 writable 函数。然后传递初始值,还可以传递一个带有 set 函数的函数。如果传入了后者,它可以异步确定存储的值。例如,它可以调用 REST 服务,并将返回的值传递给 set。在第一个组件订阅存储之前不会调用此函数。
除了 subscribe 方法外,可写存储还有以下方法:
- set(newValue)
这将为商店设置一个新值。
- update(fn)
这将基于当前值更新存储值。fn 是一个传递当前值并返回新值的函数。
下面是仅使用初始值定义可写存储的示例。
复制代码
// 在 stores.js 内 import{writable}from'svelte/store'; // 初始值是空数组。 exportconstdogStore = writable([]);
这是一个使用函数确定值来定义可写存储的示例。
复制代码
// 在 stores.js 内 import{writable}from'svelte/store'; exportconstdogStore = writable(initialValue,asyncset=> { // 订阅计数由 0 到 1 时调用。 // 计算初始值并传递给 set 函数。 constres =awaitfetch('/dogs'); constdogs =awaitres.json(); set(dogs); return()=>{ // 订阅计数归零时调用。 }; });
可以将表单元素的值绑定到可写存储。当用户更改表单元素值时将更新存储。
复制代码
存储名称上的 $ 前缀接下来会解释。
可读存储
要创建可读存储,请调用 svelte/store 包中定义的 readable 函数。
与可写存储一样,这里要为它传递一个初始值,还可以传递一个带有 set 函数的函数。
例如:
复制代码
import{readable} from'svelte/store'; exportconstdogStore = readable( [],// 初始值。 set=> { constres =awaitfetch('/dogs'); constdogs =awaitres.json(); set(dogs); // 这里可以返回一个清理函数。 } );
set 函数可以使用 setInterval 来连续更改值。
使用存储
要开始使用存储,请选择下面一种方式来访问它:
- 作为 prop 接受它。
- 从上下文中获取它。
- 从.js 文件导入(适用于全局范围)。
有两种方法从存储中获取值:
- 在其上调用 subscribe 方法(有些冗长)。
- 使用自动订阅的捷径(通常是首选)。
下面是使用 subscribe 方法的示例。
复制代码
import{onDestroy}from'svelte'; import{dogStore}from'./stores.js'; letdogs; constunsubscribe = dogStore.subscribe(value=>(dogs = value)); onDestroy(unsubscribe);
下面是使用自动订阅的示例。
名称以 $ 开头的所有变量都必须放入存储。通过这种方法,组件在首次使用时会自动订阅存储,而被销毁时会自动取消订阅。
复制代码
import{dogStore}from'./stores.js';
下面是更改可写存储的示例。
订阅存储的组件将看到更改。
复制代码
import{dogStore}from'./stores.js'; importChildfrom'./Child.svelte'; constdog = $dogStore; functionchangeDog(){ // 方法 #1 - 创建新对象 //dogStore.set({age: 2, breed: 'GSP', name: 'Oscar'}); // 方法 #2 - 调整并复用对象 dog.age =2; dog.breed ='GSP'; dog.name ='Oscar'; dogStore.set(dog); }Store Demo
Change Dog
下面是一个使用 HTML 中的 $ 引用从存储中获取更改的示例。
复制代码
import{dogStore}from'./stores.js';{$dogStore.name}is a{$dogStore.breed}that is{$dogStore.age}years old.
下面的代码效果同上,但是使用 JavaScript 代码从存储中获取数据。
复制代码
import{dogStore}from'./stores.js'; // 这里需要 Parens 才能知道开放的大括号不是块的开头。 $: ({age, breed, name}= $dogStore);{name}is a{breed}that is{age}years old.
模块上下文(module context)
想要只在组件源文件中运行一次 JavaScript 代码,而不是为创建的每个组件实例都运行一次代码,请将代码包含在指定模块上下文的 script 标记中。
复制代码
...
如果 script 标记未指定其上下文,则它为 实例上下文 。
两种 script 标记(实例和 模块上下文 )都可以出现在组件源文件中。
两种上下文中都可以导出值。无法指定默认导出,因为组件本身会自动成为默认导出。
模块上下文可以声明变量并定义函数。这些可以在组件所有实例的实例上下文中访问,但它们不是响应式的。组件更改时不会重新渲染。这样就能在所有实例之间共享数据。
实例上下文变量和函数在模块上下文中不可访问。
请注意,不需要将不访问组件状态的函数移至模块上下文,因为(根据 Svelte API 文档)“ Svelte 将从组件定义中提升所有不依赖本地状态的函数。” 但将函数放在模块上下文中的一个目的是从外部导出和调用它们。
批处理 DOM 更新
可以更改顶级组件变量的值来使组件状态无效。
根据 Svelte 文档,“当你使 Svelte 中的组件状态无效时,它不会立即更新 DOM。相反,它会等到下一个微任务才查看是否需要应用其他任何更改(包括在其他组件中)。这样做避免了不必要的工作,并使浏览器可以更有效地对事物进行批处理。”
tick 函数“返回一个 promise,该 promise 将在任何未决状态更改应用于 DOM 时立即解析(如果没有未决状态更改,则立即解析)。”
应用 DOM 更新后,可以使用此方法进行其他状态更改。
复制代码
import{tick}from'svelte'; ... // 做一些状态更改。 // 下面的内容预防 tick 调用后的批量更新。 awaittick(); // DOM 更新后做更多状态更改。 ...
调用 await tick() 在测试中也很有用,可以在测试效果之前等待更改被处理。
动画
Svelte 提供了许多功能用来轻松将动画添加到元素。以下是它提供的一些函数和过渡效果值。
- svelte/animate 包提供了 flip 函数。
- svelte/motion 包提供 spring 和 tweened 函数。
- svelte/transition 包提供了 crossfade 函数及过渡值 draw(用于 SVG 元素)、fade、fly、scale 和 slide。
- 另请参见 svelte/easing 包,这个包提供了控制动画随时间变化的速率的缓动函数。
下面是一个基本的动画示例,其中有一个列表项在装载时淡入并在销毁时淡出。
复制代码
import{fade}from'svelte/transition';
可以创建自定义动画。示例见 此 。
组件可以侦听事件,以了解过渡何时开始和结束。需要侦听的事件有:
- introstart 和 introend
- outrostart 和 outroend
特殊元素
Svelte 支持几种特殊元素,其形式为 。总结如下。
复制代码
它将渲染 expression 指定的组件。如果 expression 是虚值则不渲染任何内容。可选的 props 会被传递到要渲染的组件。
复制代码
它允许组件渲染其自身的实例。它支持递归组件,这是必需的,因为组件无法导入自身。
复制代码
它将注册一个由 DOM window 对象调度给定事件时要调用的函数。resize 事件就是一个例子。
复制代码
它会将变量绑定到 window 属性。一个例子是 innerWidth。
复制代码
当 DOM body 元素调度给定事件时,此方法注册一个要调用的函数。例子包括 mouseEnter 和 mouseLeave。
复制代码
elements
它会将元素插入 DOM 文档的 head 元素中。例子包括插入 link 和 script 标记。
复制代码
它位于.svelte 文件的顶部,而不是 script 标记内部。它指定了编译器选项,包括:
复制代码
immutable
它意味着 props 将被视为不可变的,从而提供了优化。
默认值为 false。不可变意味着父组件将为对象 props 创建新对象,而不是修改现有对象的属性。这使 Svelte 可以通过对比对象引用(而不是对象属性)来确定 prop 是否已更改。
当此选项设置为 true 时,如果父组件修改了子组件的对象属性,则子组件将不会检测到更改并且不会重新渲染。
复制代码
accessors
它为组件 props 添加了 getter 和 setter 方法。默认为 false。将 Svelte 组件编译为非 Svelte 应用程序中使用的自定义元素时,这个方法很有用。
复制代码
namespace="value"
它指定了组件的命名空间。一种用途是为 SVG 组件指定命名空间 svg。
复制代码
tag="value"
它指定将 Svelte 组件编译为自定义元素时要使用的名称。它允许 Svelte 组件用作非 Svelte 应用程序中的自定义元素。
调试
当给定变量更改时,使用 @debug 中断,并在 devtools 控制台中输出它们的值。
将其放置在 HTML 部分的顶部,不要放在 script
标记内部。
例如:
复制代码
{@debugvar1, var2, var3}
被监视的变量可以具有任何类型的值,包括对象。
要在所有状态更改时中断,请省略变量名称。
复制代码
{@debug}
ESLINT
ESLint 称自己为“针对 JavaScript 和 JSX 的可插入式 linting 实用程序”。它可以报告许多语法错误和潜在的运行时错误。它还可以报告与指定编码准则的差异。
要在 Svelte 项目中安装 ESLint 所需的全部内容,请输入 npm install -D name,其中 name 为:
- eslint
- eslint-plugin-svelte3
创建具有以下内容的.eslintrc.json 文件:
复制代码
{ "env": { "browser":true, "es6":true, "jest":true }, "extends": ["eslint:recommended","plugin:import/recommended"], "globals": { "cy":"readonly" }, "overrides": [ { "files": ["**/*.svelte"], "processor": ["svelte3/svelte3"] } ], "parserOptions": { "ecmaVersion":2019, "sourceType":"module" }, "plugins": ["svelte3"], "rules": { "no-console":"off" } }
将以下 npm 脚本添加到 package.json:
复制代码
"lint":"eslint--fix--quietsrc--ext.js,.svelte",
要运行 ESLint,请输入 npm run lint。
有关针对 Svelte 的 ESLint 选项的更多信息,请参见 文档 。
Prettier
Prettier 称自己为“经过优化的 JavaScript 格式化程序”。它支持多种语言和语言功能,包括 ES2017、TypeScript、JSON、HTML、CSS、LESS、SCSS、JSX、Vue 和 Markdown。
要在 Svelte 项目中安装 Prettier 所需的全部内容,请输入 npm install -D name,其中 name 为:
- prettier
- prettier-plugin-svelte
Svelte ESLint 插件强制按 script
、 style
和 HTML 的顺序执行。
将以下 npm 脚本添加到 package.json:
复制代码
"format":"prettier --write '{public,src}/**/*.{css,html,js,svelte}'",
要运行 Prettier,请输入 npm run format。
ToDo 应用
下面来看一个简单的 Todo 应用程序的实现,正好过一遍最重要的那些 Svelte 概念。代码链接在 GitHub 上。
要添加新的待办事项时,需要在输入框中输入待办事项的文本,然后按“添加”按钮或 Enter 键。
要将待办事项在已完成和未完成的状态之间切换,需要单击其左侧的复选框。请注意,顶部附近的“remaining”文本显示当前未检查的待办事项数和待办事项总数。
要删除待办事项,需要单击其右侧的“Delete”按钮。
要存档所有已检查的待办事项,需要单击“Archive Completed”按钮。但这个版本的应用并不会真的存储它们,其实它们都被删掉了。
下面是文件 src/main.js,它在文档主体中渲染 TodoList 组件来启动应用程序。
复制代码
importTodoListfrom'./TodoList.svelte'; constapp =newTodoList({target:document.body}); exportdefaultapp;
下面是文件 src/Todo.svelte 中 Todo 组件的代码。
它是一个包含以下内容的列表项:
- 待办事项文本
- 一个复选框
- 一个“Delete”按钮
它需要一个名为“todo”的 prop 来保存待办事项的文本。
切换复选框后,它将调度一个“toggleDone”事件。
按下“Delete”按钮时,它将调度一个“删除”事件。
复制代码
import{createEventDispatcher}from'svelte'; constdispatch = createEventDispatcher(); exportlettodo;// 唯一的 prop /* 在 todo 的文本上画一条线标记其完成状态。 */ .done-true{ color: gray; text-decoration: line-through; } li{ margin-top:5px; }
下面是文件 src/TodoList.svelte 中 TodoList 组件的代码。
看到这里,关于 Svelte 你已经很熟悉了,所以这些代码应该很容易看懂。
复制代码
importTodofrom'./Todo.svelte'; letlastId =0; // 创建一个 todo 对象。 constcreateTodo =(text, done =false) =>({id: ++lastId, text, done}); lettodoText =''; // 应用程序初始有两个 todo 项目。 lettodos = [ createTodo('learn Svelte',true), createTodo('build a Svelte app') ]; letuncompletedCount =0; // 这是 " 响应式声明 "。 // 它保证未完成的代码在 todo 数组修改时被更新。 $: uncompletedCount = todos.filter(t=>!t.done).length; // 这是另一个 " 响应式声明 "。 // 保证当 uncompletedCount 或 todo 数组更改时状态随时更新 $: status =`${uncompletedCount}of${todos.length}remaining`; // 创建并添加一个新的 todo. functionaddTodo(){ // 回想这里为何必须使用 concat 代替 push。 todos = todos.concat(createTodo(todoText)); todoText ='';// 清空 input } // 删除全部标记为完成的 todo。 constarchiveCompleted =()=>(todos = todos.filter(t=>!t.done)); // 删除特定 todo。 constdeleteTodo =todoId=>(todos = todos.filter(t=>t.id !== todoId)); // 改变给定 todo 的状态。 functiontoggleDone(todo){ const{id} = todo; todos = todos.map(t=>(t.id === id ? {...t,done: !t.done} : t)); } button{ margin-left:10px; } /* 从加点(·)列表中移除点。 */ ul.unstyled{ list-style: none; margin-left:0; padding-left:0; }To Do List
{status} Archive Completed
Add {#each todos as todo} deleteTodo(todo.id)} on:toggleDone={() => toggleDone(todo)} /> {/each}
单元测试
Svelte 组件的单元测试可以使用 Jest 实现。另外建议使用“Svelte 测试库”。它与 Jest 协作,可以简化 Svelte 组件的单元测试编写。
本文不会深入探究这些测试工具的细节,但下面提供了测试代码示例。要了解这些工具的更多信息,请访问 https://jestjs.io/ 和 https://testing-library.com/ 。
要安装所需的全部内容,请输入 npm install -D name,其中 name 为:
- @babel/core
- @babel/preset-env
- @testing-library/svelte
- babel-jest
- jest
- jest-transform-svelte
使用以下内容创建文件 babel.config.js:
复制代码
module.exports = { presets: [ [ '@babel/preset-env', { targets: { node:'current' } } ] ] };
如果未按上面所示设置 targets.node,则在运行测试时将显示错误消息“regenerator-runtime not found”。
使用以下内容创建文件 jest.config.js:
复制代码
module.exports= { transform: { '^.+ .js$':'babel-jest', '^.+ .svelte$':'jest-transform-svelte' }, moduleFileExtensions: ['js','svelte'], bail:false, verbose:true };
将 bail 设置为 false 意味着 Jest 在某个测试失败时不应退出测试套件。
将 verbose 设置为 true 会使 Jest 显示每个测试的结果,而不只是各个测试套件的结果摘要。
将以下 npm 脚本添加到 package.json:
复制代码
"test":"jest --watch src",
要运行单元测试,请输入 npm test。
以下是用于测试文件 src/Todo.spec.js 中 Todo 组件的代码:
复制代码
import{cleanup, render}from'@testing-library/svelte'; importTodofrom'./Todo.svelte'; describe('Todo',()=>{ consttext ='buy milk'; consttodo = {text}; // 卸载之前测试中挂载的所有组件。 afterEach(cleanup); test('should render',()=>{ const{getByText} = render(Todo, {props: {todo}}); constcheckbox =document.querySelector('input[type="checkbox"]'); expect(checkbox).not.toBeNull();// 找到复选框 expect(getByText(text));// 找到 todo 文本 expect(getByText('Delete'));// 找到 Delete 按钮 }); // 测试事件在 checkbox 状态更改或 "Delete" 按钮按下时是否 fired 没有捷径。 // 它们由 TodoList.spec.js 的测试覆盖。 });
以下是用于测试文件 src/TodoList.spec.js 中 TodoList 组件的代码:
复制代码
import{tick} from'svelte'; import{cleanup, fireEvent, render, wait} from'@testing-library/svelte'; importTodoList from'./TodoList.svelte'; describe('TodoList', () => { constPREDEFINED_TODOS =2; afterEach(cleanup); // 它被下面的很多测试函数使用。 function expectTodoCount(count) { returnwait(() => { // 每个 todo 有一个
端到端测试
可以使用 Cypress 来实现 Svelte 应用程序的端到端测试。本文不会深入探究 Cypress 的细节,但是下面提供了测试代码示例。
要了解有关 Cypress 的更多信息,请访问 https://www.cypress.io/ 。
要安装 Cypress,请输入 npm install -D cypress。
将以下 npm 脚本添加到 package.json:
复制代码
"cy:open":"cypress open", "cy:run":"cypress run",
要以交互方式启动 Cypress 测试工具,请输入 npm run cy:open。如果尚不存在 cypress 目录,它还会创建一个带有以下子目录的目录:
复制代码
fixtures
这个目录可以保存测试使用的数据。数据通常在导入到测试的.json 文件中。
复制代码
integration
你的测试文件在此目录的顶部或子目录中。
复制代码
plugins
此目录扩展了 Cypress 的功能。示例请参见 https://github.com/bahmutov/cypress-svelte-unit-test 。在运行每个规范文件之前,Cypress 会自动在该目录的 index.js 文件中运行代码。
复制代码
screenshots
这个目录存放屏幕截图,截图通过调用 cy.screenshot() 生成。这在调试测试时很有用。
复制代码
support
此处的文件会添加自定义的 Cypress 命令,使它们在测试中可用。在运行每个规范文件之前,Cypress 会自动在该目录的 index.js 文件中运行代码。
这些目录中装有示例文件,所有示例文件都可以删除。
在 cypress/integration 目录下创建带有.spec.js 扩展名的测试文件。
要运行端到端测试:
- 使用 npm run dev 启动应用程序服务器
- 输入 npm run cy:open
- 按下 Cypress 工具右上角的“Run all specs”按钮
这将打开一个浏览器窗口,在其中运行所有测试。
完成测试后,关闭此浏览器窗口和 Cypress 工具。
以下是文件 cypress/integration/TodoList.spec.js 中 Todo 应用程序的端到端测试代码。
复制代码
constbaseUrl ='http://localhost:5000/'; describe('Todo app', () => { it('should add todo', () => { cy.visit(baseUrl); cy.contains('1 of 2 remaining'); // "Add" 按钮应被禁用,直到文本输入后才解除。 cy.contains('Add') .as('addBtn') .should('be.disabled'); // 输入 todo 文本。 consttodoText ='buy milk'; cy.get('[data-testid=todo-input]') .as('todoInput') .type(todoText); cy.get('@addBtn').should('not.be.disabled'); cy.get('@addBtn').click(); cy.get('@todoInput').should('have.value',''); cy.get('@addBtn').should('be.disabled'); cy.contains(todoText); cy.contains('2 of 3 remaining'); }); it('should toggle done', () => { cy.visit(baseUrl); cy.contains('1 of 2 remaining'); // 找到第一个 checkbox 并打勾。 cy.get('input[type=checkbox]') .first() .as('cb1') .click(); cy.contains('2 of 2 remaining'); // 转换同一个 checkbox。 cy.get('@cb1').check(); cy.contains('1 of 2 remaining'); }); it('should delete todo', () => { cy.visit(baseUrl); cy.contains('1 of 2 remaining'); consttodoText ='learn Svelte';// 第一个 todo cy.contains('ul', todoText); // 点击第一个 "Delete" 按钮。 cy.contains('Delete').click(); cy.contains('ul', todoText).should('not.exist'); cy.contains('1 of 1 remaining'); }); it('should archive completed', () => { cy.visit(baseUrl); consttodoText ='learn Svelte';// 第一个 todo cy.contains('ul', todoText); // 点击 "Archive Completed" 按钮。 cy.contains('Archive Completed').click(); cy.contains('ul', todoText).should('not.exist'); cy.contains('1 of 1 remaining'); }); });
要重新运行测试,请单击浏览器窗口顶部附近的圆形箭头按钮。
为了更好地调试,请在应用程序代码中添加 console.log 调用,然后在运行测试的浏览器窗口中打开 devtools 控制台。
将更改保存到应用程序源文件或测试文件时,测试将自动重新运行。
要以命令行模式启动 Cypress 测试工具,请输入 npm run cy:run。这将在终端窗口中输出测试结果,记录测试运行的视频,并输出视频的文件路径。双击视频文件即可观看。
相关工具
以下是推荐读者研究的 Svelte 相关工具。
- Svelte VS Code 扩展
- Sapper
这是“由 Svelte 支持的应用程序框架”。Sapper 是一名士兵,负责诸如修建和维修道路和桥梁,铺设和清除地雷等任务。Sapper 类似 Next 和 Gatsby。它提供路由、服务端渲染和代码拆分功能。
这是一个社区推动的项目,支持使用 Svelte 实现原生移动应用程序。它基于 nativescript-vue。
这是 Three.js 3D 图形库的 Svelte 版本,正在开发中。
-
Storybook 是用于演示和试验 Web UI 组件的工具。
总结
到这里就结束了!Svelte 是当前流行的 React、Vue 和 Angular 框架的很好的替代品。它有许多好处,包括较小的包体积、简单的组件定义、方便的状态管理以及无需虚拟 DOM 的反应性。
非常感谢 Charles Sharp 和 Kristin Kroeger 审阅本文!