点击上方“开发者技术前线”,选择“星标”
13:21 在看 真爱
Svelte 是 React、Vue 和 Angular 等 Web 框架的替代方案。与其同类产品一样,Svelte 可用于构建完整的 Web 应用程序。它还能用来创建自定义元素,这些自定义元素可以在使用其他框架实现的已有 Web 应用程序中使用。
Svelte 由 Rich Harris 开发,Rich Harris 曾在《卫报》工作,目前任职于《纽约时报》。Harris 先前创建的 Ractive Web 框架被《卫报》采用,并成为 Vue 的部分功能的灵感来源。Harris 还创建了 Rollup 模块打包器,它是 Webpack 和 Parcel 的替代品。
Svelte 尚未得到应有的重视。人们提到它时往往更关注它生成打包代码的能力,它打包出来的代码明显比竞争对手更小。但除此之外 Svelte 还简化了许多工作,包括定义组件、管理组件状态、管理应用程序状态以及添加动画等。
本文对 Svelte 进行了详尽的介绍,并提供了使用它从头开始构建 Web 应用程序所需的基础知识。
与其他 Web 框架创建的应用程序相比,Svelte 应用程序的包体积较小。它将应用程序代码编译到一个只包含少量框架代码的优化过的 JavaScript 文件来做到了这一点。
Svelte 是用 TypeScript 实现的 Web 应用程序编译器。它不是运行时库。例如,稍后介绍的 Todo 应用程序的包体积只有等效 React 应用程序的 13%。这两款应用程序的链接如下:
https://github.com/mvolkmann/svelte-todo
https://github.com/mvolkmann/react-todo
-
Angular + ngrx:134
-
React + Redux:193
-
Vue:41.8
Svelte:9.7
https://www.freecodecamp.org/news/a-realworld-comparison-of-front-end-frameworks-with-benchmarks-2019-update-4be0d3c78075/
某些 Web 框架(包括 React 和 Vue)使用虚拟 DOM 来优化渲染更改。重新渲染组件时,框架会在内存中构建 DOM 的新版本,然后将其与以前的版本做对比,不一样的部分才会被应用到实际的 DOM 上。
尽管这比更新实际 DOM 中的所有内容要快,但构建虚拟 DOM 并将其与前一个 DOM 进行比较是需要花时间的。
Svelte 无需使用虚拟 DOM 就可以提供反应性。为了做到这一点,它会跟踪影响各个组件渲染的顶级组件变量的更改,并仅在检测到更改时才重新渲染 DOM 的这些部分。这样就能获得良好的性能表现。
Svelte 大大简化了组件和应用程序状态管理。相关功能包括上下文、存储和模块上下文,稍后将逐一详细介绍。
Svelte 为可访问性问题提供了运行时警告。例如,没有 alt 属性的<img>
元素会被标记出来。Svelte 当前不支持 TypeScript,但正在推进相关工作。Svelte Native 支持开发移动应用程序。它基于 NativeScript。
有人说一旦应用程序构建完毕,Svelte 就会消失。
Svelte 库主要由 node_modules/svelte 目录中的.js 文件定义。主要函数在 internal.js 中定义,目前大约有 1400 行代码。
-
easing.js
-
motion.js
-
register.js
-
store.js
transition.js
输入 npm run build 会在 public 目录中生成文件,包括 bundle.js。应用程序使用的 Svelte 库函数将复制到 bundle.js 的顶部。后文展示的 Todo 应用程序中,这里大约是 500 行代码。
因此 Svelte 库代码不会消失,只是它与其他 Web 框架相比体积很小。
重新思考反应性:https://svelte.dev/blog/svelte-3-rethinking-reactivity
-
Svelte 主页:https://svelte.dev
-
Svelte 教程:https://svelte.dev/tutorial
-
Svelte API:https://svelte.dev/docs
-
Svelte 示例:https://svelte.dev/examples
在线 Svelte REPL:https://svelte.dev/repl
-
Svelte 博客:https://svelte.dev/blog
-
Discord 聊天室:https://discordapp.com/invite/yy75DKs
Svelte GitHub 存储库:https://github.com/sveltejs/svelte
从 https://nodejs.org 安装 Node.js。
npx degit sveltejs/template app-name
-
cd app-name
-
npm install
npm run dev
浏览
localhost:5000
这一步会输出紫色的“Hello world!”。
现在你可以开始修改应用程序了。
初探package.json
文件会发现两件事。首先是 Svelte 默认使用 Rollup 来打包模块。需要的话,可以将其更改为使用 Webpack 或 Parcel。其次是 Svelte 应用程序没有必需的运行时依赖项,只有 devDependencies。
public/index.html
src/main.js
src/App.svelte
文件
public/index.html
包含以下内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width" />
<title>Svelte app</title>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="global.css" />
<link rel="stylesheet" href="bundle.css" />
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>
-
global.css
包含可以影响任何组件的 CSS。 -
bundle.css
由每个组件中的 CSS 生成。 bundle.js
是由每个组件中的 JavaScript 和 HTML,以及应用程序中其他所有的 JavaScript 生成的。
文件
src/main.js
包含以下内容:
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
name: 'world'
}
});
export default app;
文件
src/App.svelte
包含以下内容:
<script>
export let name;
</script>
<style>
h1 {
color: purple;
}
</style>
<h1> Hello {name}! </h1>
可以在使用该组件的文件中将导出的变量设置为 props。
大括号用于输出 JavaScript 表达式的值。这里称为 插值。稍后我们将看到,大括号也用于动态属性值。
-
Angular 使用类。
-
React 使用函数或类。
Vue 使用对象字面量。
Svelte 不使用任何 JavaScript 容器。
Svelte 组件由包含 JavaScript 代码、CSS 和 HTML 的.svelte 文件定义。它们组合在一起形成组件定义,该定义将自动成为默认导出。
.svelte 文件可以在 src 目录下的任何位置。它们包含以下三部分,这三部分都是可选的。
<script>
// 确定范围的 JavaScript 在这里。
</script>
<style>
/* 确定范围的 CSS 规则在这里。*/
</style>
<!-- 要渲染的 HTML 在这里。-->
请注意,每个部分都可以使用不同的注释语法。
Svelte 组件定义未指定组件名称。其他框架中组件名称是在源文件中由类名称、函数名称或属性值提供的,这里不是这样,导入.svelte 文件时组件名称会被关联,并且必须以大写字母开头。小写名称保留给预定义元素,例如 HTML 和 SVG 提供的元素。
// 有些令人困惑
import AnyNameIWant from './some-name.svelte';
// 较清晰的写法
import SameName from './SameName.svelte';
在 Svelte 组件之间共享数据有四种方法。
它们将数据从父组件传递到子组件。
祖先组件可以用它们来使数据用于后代组件。
它们将数据存储在所有组件外,并使其对所有组件可用。
它们将数据存储在组件模块中,并使数据可用于组件的所有实例。
这些内置方法非常有用,实际上你都不需要状态管理库。
组件可以通过 props 接受输入。它们被指定为父组件渲染的组件元素上的属性。例如,父组件可以执行以下操作:
<script>
import Hello from './Hello.svelte';
</script>
<Hello name="Mark" />
这里 name prop 的值是字面量字符串。
作为 JavaScript 表达式或非字符串字面量的 prop 值必须用大括号括起来,不能用引号。
src/Hello.svelte 中定义的子组件可以这样做:
<script>
export let name = 'World';
</script>
<div>
Hello, {name}!
</div>
这里使用 export 关键字在组件的<script>
部分中声明 props。这里用 Svelte 特有的方式使用了有效的 JavaScript 语法。
由于父元素可以更改值,因此必须使用 let 关键字而不是 const。为 props 分配默认值是可选的。
目前 Svelte 没有像 React、Vue 和 Angular 那样(通过 TypeScript)进行 prop 类型检查的功能。
元素的属性值可以用 JavaScript 表达式提供。其语法为:
<element-name attribute-name =“ expression” />
表达式也可以嵌入到字符串值中。例如:
<Person fullName="{firstName} {middleInitial}. {lastName}" />
当属性值位于与该属性同名的变量中时,可以使用简写语法。例如:
<Person {fullName} />
如果多个属性位于一个对象中,则可以使用散布运算符插入多个属性,其中键是属性名称,值是它们的值。例如:
<script>
let score = 0;
const inputAttrs = {
type: 'number',
max: 10,
min: 0,
value: score
};
</script>
<input {...inputAttrs} bind:value={score} />
上面的示例使用 bind 模拟双向数据绑定。这将在后文详细说明。
.svelte 文件的style
标记中的样式将自动确定组件的范围。
Svelte 将生成的相同的 CSS 类名称(svelte-hash)添加到可能受这些 CSS 规则影响的组件的每个渲染元素中,从而实现了作用域。
全局样式应在 public/global.css 中定义。
与标准 CSS 一样,样式标记中的注释必须使用 / * */ 注释定界符。
"svelte3” ESLint 插件会对未使用的 CSS 选择器发出警告。
可以有条件地将 CSS 类添加到元素。在以下示例中,仅当 status 大于零时才添加 CSS 类 error。
<div class:error={status > 0}>{result}</div>
组件可以在其<script>
标记内导入其他组件。例如:
import Other from './Other.svelte';
可以在组件的 HTML 部分中使用导入的组件。
要渲染一个值为 HTML 字符串的 JavaScript 表达式,请使用语法{@html expression}
。
假设 markup 是一个包含 HTML 字符串的变量。下面的代码将渲染它:
<p>{@html markup}</p>
为了避免跨站点脚本,请 escape 不受信任来源中的 HTML。
插值中引用的顶级变量的更改会自动导致这些插值被重新计算。例如:
<script>
let count = 0;
const increment = () => count++;
</script>
<div>count = {count}</div>
<button on:click={increment}>+</button>
必须分配一个新值以触发此操作。将新元素推送到数组上不会触发它。可以使用以下方法:
myArr = myArr.concat(newValue);
// 另一种技巧
myArr.push(newValue);
myArr = myArr;
在 JavaScript 语句开头写一个名称,后面跟一个冒号,就会创建一个 标签语句。标签语句可以用作 break 和 continue 语句的目标。
有趣的是,在同一范围的多个语句中使用相同的标签名称在 JavaScript 中不是错误。当这个语法用在顶层语句(未嵌套在函数或块中)上且名称为美元符号时,Svelte 会将顶层语句视为 响应式声明。
这是 Svelte 编译器以特殊方式处理有效的 JavaScript 语法的另一个例子。当这类语句引用的任何变量的值更改时,它们自己就会重复。这有点像 Vue 中的 计算属性。例如:
// average 的值一开始就被计算
// 如果 total 或 count 的值更改就重新计算。
$: average = total/count;
// count 的值输出在 devtools 控制面板中
// 当这个语句执行和每次更改时。
// 这方便了调试工作!
$: console.log('count =', count);
将 $: 应用于未声明变量的赋值时(比如上面的 average 赋值),不允许使用 let 关键字。$: 可以应用于一个块。例如:
$: {
// 这里是要重复的语句
}
这也可以应用于多行语句,比如 if 语句。例如:
$: if (someCondition) {
// 主体语句
}
如果条件或主体中引用的任何变量发生更改,上面的示例就会执行,但只有在条件为 true 时主体才执行。例如,如果条件包括对函数的调用,则如果主体中的任何引用发生更改就会调用它们。
-
React 使用 JSX,其中逻辑由大括号中的 JavaScript 代码实现。
-
Angular 和 Vue 支持特定于框架的逻辑属性。例如,Angular 支持 ngIf 和 ngFor,而 Vue 支持 v-if 和 v-for。
Svelte 支持包装元素的类似 Mustache 的自定义语法。例如{#if}和{#each}。
Svelte 的条件逻辑以{#if condition}开始。开头的 # 表示块的起始标记。用{/ if}标记结尾。开头的 / 表示块的结束标记。有条件渲染的 markup 位于这两者之间。它们之间可以包含的其他块标记有{:else if condition}和{:else}。开头的: 表示块的继续标记。例如:
{#if color === 'yellow'}
<div>Nice color!</div>
{:else if color === 'orange'}
<div>That's okay too.</div>
{:else}
<div>Questionable choice.</div>
{/if}
虽说这里的语法乍看起来似乎很奇怪,但它确实能有条件地渲染多个元素。Angular/Vue 中向元素添加特殊属性的方法需要指定一个公共父元素。
Svelte 的迭代从{#each iterable as element}开始。用{/each}标记结尾。每个元素要渲染的 markup 放在两者之间。一般来说可迭代的是数组,但任何可迭代的值都能用。{:else}之后的内容在可迭代内容为空时渲染。例如,假设变量 colors 设置为 ['red', 'green', 'blue']:
<!-- 分开行来使用 color 输出每种颜色。-->
{#each colors as color}
<div >{color}</div>
{/each}
<!-- 在单独的行上输出每种颜色,并在其前面
从 1 开始的位置后跟括号。-->
{#each colors as color, index}
<div>{index + 1}) {color}</div>
{/each}
<!-- 这里使用解构来在"people"的对象中获取特定的属性。-->
{#each people as {name, age}}
<div>{name} is {age} years old.</div>
{:else}
<div>There are no people.</div>
{/each}
如果要添加、删除或修改列表中的项目,则应为每个元素提供唯一的标识符。这类似 React 和 Vue 中所需的 key prop。
在 Svelte 中,唯一标识符是 #each 语法的一部分,而不属于元素 prop。在以下示例中,每个 person 的唯一标识符是对应的 id 属性。
{#each people as person (person.id)}
<div>{person.name} is {person.age} years old.</div>
{/each}
Svelte 提供了 markup 语法来等待 promise 解析或拒绝。它可以根据 promise 的未完成、已解析或已拒绝的状态提供不同的输出。
以下示例假定组件具有返回 Promise 的 getData 函数。在:then 和:catch 之后可以使用任何变量名来接收解析或拒绝的值。
{#await getData()}
<div>Waiting for data ...</div>
{:then result}
<div>result = {result}</div>
{:catch error}
<div class="error">Error: {error.message}</div>
{/await}
下一个示例在等待 Promise 解析时省略了要渲染的 markup。:catch 部分也可以省略。
{#await getData() then result}
<div>result = {result}</div>
{:catch error}
<div class="error">Error: {error.message}</div>
{/await}
插槽允许子内容传递到组件。接收组件可以决定是否渲染它,在何处渲染。请注意,空格算作子内容。接收组件可以标记使用渲染所有子内容的位置。这称为 默认插槽。
它还可以为没有向插槽提供内容的父元素提供默认内容来渲染。例如,<slot>Thanks for nothing!</slot>
。
命名插槽允许父元素提供多组内容,接收组件可以针对这些内容决定是否渲染以及在何处渲染。父元素使用 slot 属性标识它们。子元素则定义它们在何处用带有匹配的 name 属性的 slot 元素来渲染。
下面是来自父元素的 HTML 示例,该 HTML 元素的目标是子元素 ShippingLabel 中的多个命名槽:
<ShippingLabel>
<div slot="address">
123 Some Street,<br />
Somewhere, Some State 12345
</div>
<div slot="name">Mark Volkmann</div>
</ShippingLabel>
这是 ShippingLabel.svelte:
<style>
label {
display: block;
font-weight: bold;
}
</style>
<div>
<label>Ship To:</label>
<slot name="name">unknown</slot>
<slot name="address" />
</div>
像input
、textarea
和select
这样的表单元素可以绑定到变量上。这将模拟双向数据绑定。
除了提供当前值之外,双向数据绑定还提供事件处理,以便在用户更改表单元素值时更新变量。
对于类型为 number 或 range 的input
元素,双向数据绑定会自动将值从字符串强制转换为数字。例如,考虑以下 HTML 表单:
这是使用单个 Svelte 组件的实现。注意在多处使用了 bind:。
<script>
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
const flavors = ['vanilla', 'chocolate', 'strawberry'];
const seasons = ['Spring', 'Summer', 'Fall', 'Winter'];
let favoriteColor = '';
let favoriteFlavors = [];
let favoriteSeason = '';
let happy = true;
let name = '';
let story = '';
</script>
<style>
div {
margin-bottom: 10px;
}
input,
select {
border: solid gray 1px;
border-radius: 4px;
padding: 4px;
}
input[type='checkbox'],
input[type='radio'] {
margin-left: 5px;
}
label {
display: inline-block;
font-weight: bold;
margin-right: 5px;
vertical-align: top;
}
</style>
<div class="form">
<div>
<label>Name</label>
<input type="text" bind:value={name} />
</div>
<div>
<label>Happy?</label>
<!-- 对于 checkbox, 要绑定到"checked"属性而非"value"上。-->
<input type="checkbox" bind:checked={happy} />
</div>
<div>
<label>Favorite Flavors</label>
{#each flavors as flavor}
<label>
<!-- 使用"bind:group"和一组相关的 checkbox
使值成为一组字符串。-->
<input type="checkbox" value={flavor} bind:group={favoriteFlavors} />
{flavor}
</label>
{/each}
</div>
<div>
<label>Favorite Season</label>
{#each seasons as season}
<label>
<!-- 使用"bind:group"和一组相关的 radio 按钮
使值成为单个字符串。-->
<input type="radio" value={season} bind:group={favoriteSeason} />
{season}
</label>
{/each}
</div>
<div>
<label>Favorite Color</label>
<!-- 要将选项更改为可滚动列表以启用多重选项,添加
"multiple"属性。-->
<select bind:value={favoriteColor}>
<option />
{#each colors as color}
<!-- <option> 元素可以有一个"value"属性
其值可以是字符串、数字或对象。-->
<option>{color}</option>
{/each}
</select>
</div>
<div>
<label>Life Story</label>
<textarea bind:value={story} />
</div>
<!-- 这里报告了绑定设置的变量值,但前提是 name 有值。-->
{#if name}
<div>
{name} likes {favoriteColor}, {favoriteSeason},
and is {happy ? 'happy' : 'unhappy'}.
</div>
<div>{name}'s favorite flavors are {favoriteFlavors}.</div>
<div>Story: {story}</div>
{/if}
</div>
除了绑定到基本变量之外,表单元素还可以绑定到对象属性。然后用户输入会使这些对象发生突变。
Svelte 可以将子组件 prop 绑定到父组件中的变量。这将允许子组件更改父组件变量的值。例如,这是父组件:
<script>
import Child from './Child.svelte';
let pValue = 1;
</script>
<div>pValue = {pValue}</div>
<Child bind:cValue={pValue} />
这是子组件:
<script>
export let cValue = '';
const double = () => (cValue *= 2);
</script>
<div>cValue = {cValue}</div>
<button on:click={double}>Double</button>
按下 Child 组件中的按钮时,cValue 会加倍,并且加倍的值由于绑定到 cValue 而成为 pValue 的新值。
事件处理由 on:event-name 属性指定,该属性的值是调度事件时要调用的函数。事件名称可以是标准 DOM 事件或自定义事件的名称。事件对象将传递给给定的函数。例如:
<!-- 函数 "handleClick" 必须在上面的<script>部分中定义。-->
<button on:click={handleClick}>Press Me</button>
<!-- 这里用匿名函数展示了内联事件处理。它为按钮设置了变量"clicked"到 DOM 元素。-->
<button on:click={event => clicked = event.target}>Press Me</button>
可以为同一个事件指定多个事件处理函数,并且在分派事件时将分别调用每个函数。例如:
<button on:click={doOneThing} on:click={doAnother}>Press Me</button>
事件处理程序可以使用修饰符名称前面的竖线指定任意数量的事件修饰符。例如:
<button on:click|once|preventDefault={handleClick}>Press Me</button>
支持的修饰符有:capture它导致处理程序函数仅在捕获阶段,而不是默认的冒泡阶段被调用。once它将在第一次发生事件后删除处理程序。passive它可以提高滚动性能。preventDefault它可以防止事件的默认操作发生。例如,它可以停止表单提交。stopPropagtion它可以防止捕获 / 冒泡流程中的后续处理程序被调用。
从 on: 属性中省略事件处理函数可以快速将事件转发到父组件。例如,假设组件结构的一部分是 A> B> C,并且 C 发出事件“foo”。B 可以使用<C on:foo/>
将其转发到 A。请注意,on: 属性没有值。此方法也可以用于转发 DOM 事件。
组件可以调度事件。例如:
<script>
import {createEventDispatcher} from 'svelte';
// 组件实例化时必须调用,
// 可选或稍后调用都不行。
const dispatch = createEventDispatcher();
function sendEvent() {
// 事件关联的数据可以是一个原语或对象。
// 事件名称不应包含横线。
dispatch('someEventName', optionalData);
}
</script>
这些事件仅转到父组件。它们不会自动在组件结构中冒泡。
父组件使用 on: 侦听子组件的事件。例如,如果父组件定义了函数 handleEvent,则它可以在 Child 组件分派具有给定名称的事件时注册要调用的函数。
<Child on:someEventName = {handleEvent} />
-
已挂载时。
-
更新前。
-
更新后。
被销毁时。
术语“已挂载”表示已将组件实例添加到 DOM。术语“被销毁”表示该组件实例已从 DOM 中删除。要为这些事件注册函数,请从 svelte 包中导入所提供的生命周期函数。
import {afterUpdate, beforeUpdate, onDestroy, onMount} from 'svelte';
然后调用这些函数,并在事件发生时向它们传递要调用的函数。
最常用的生命周期函数是 onMount。一种用法是将焦点移至给定的表单元素。另一种用途是从 REST 服务检索组件所需的数据。
下面是一个移动焦点的示例。
<script>
import {onMount} from 'svelte';
let name = '';
let nameInput;
onMount(() => nameInput.focus());
</script>
<input bind:this={nameInput} bind:value={name} />
属性 bind:this 会将指定为其值的变量设置为对 input 的 DOM 元素的引用。这在传递给 onMount 的函数中使用,以将焦点移至 input。
从 DOM 中删除组件实例时,使用 onDestroy 注册要调用的函数的另一种方法是从通过 onMount 注册的函数返回该函数。这种方法有点像 React 中的 useEffect hook,不同之处在于 Svelte 中传递给 useEffect 的函数在挂载和更新时都运行。
生命周期函数可以从辅助函数中调用。这些可以在单独的.js 文件中定义,从而导入它们并用于多个组件。这很像定义自定义的 React hooks。
建议以“on”开头命名这些辅助函数,就像 React hook 名称以“use”开头那样。
将特定元素添加到 DOM 时,动作(Action) 会注册要调用的函数。
动作是在具有属性 use:fnName = {args}的元素上指定的。已注册的函数将传递 DOM 元素和参数(如果存在)。如果不需要除元素以外的其他参数,则省略 = {args}。
这在某种程度上与 onMount 生命周期函数有关,该函数注册了将组件的每个实例添加到 DOM 时要调用的函数。将组件中的特定元素添加到 DOM 时,就将调用动作。例如:
<script>
let name = '';
const focus = element => element.focus();
</script>
<!-- 输入元素添加到 DOM 时调用焦点函数。-->
<input bind:value={name} use:focus />
动作函数可以选择返回具有 update 和 destroy 属性(也就是函数)的对象。这个功能不常用。
每当参数值更改时都会调用 update 函数。如果没有参数当然是不行的。
从 DOM 中删除元素时将调用 destroy 函数。
上下文(Context) 提供了一种替代方法,可以使用 props 和存储(接下来介绍)来使组件中的数据在其他组件中可用。上下文数据只能在后代组件中访问。
要在组件中定义上下文,请导入 setContext 函数并调用它,并提供上下文键和值。例如:
import {setContext} from 'svelte';
// Must be called during component instantiation.
setContext('favorites', {color: 'yellow', number: 19});
要在后代组件中使用上下文,请导入 getContext 函数并调用它,并提供上下文键。这将从已使用该键定义上下文的最近的祖先组件获取上下文值。例如:
import {getContext} from 'svelte';
// 组件实例化时必须调用。
const favorites = getContext('favorites');
上下文键可以是任何类型的值,不只是字符串。
上下文值可以是任何类型的值,包括具有后代组件可以调用的方法的对象和函数。
如果创建了上下文的组件再使用相同的键和不同的值调用 setContext,则后代组件将不会接收到更新。它们只能看到组件初始化期间可用的内容。
与 props 和存储不同,上下文不是响应式的。
下面是一个使用上下文使数据在后代组件中可用的示例。
<!-- In A.svelte -->
<script>
import {setContext} from 'svelte';
import B from './B.svelte';
setContext('favorites', {color: 'yellow', number: 19});
</script>
<div>
This is in A.
<b ></b>
</div>
<!-- In B.svelte -->
<script>
import C from './C.svelte';
</script>
<div>
This is in B.
<C />
</div>
<!-- In C.svelte -->
<script>
import {getContext} from 'svelte';
const {color, number} = getContext('favorites');
</script>
<div>
This is in C.
<div>favorite color is {color}</div>
<div>favorite number is {number}</div>
</div>
这将渲染以下内容:
This is in A.
This is in B.
This is in C.
favorite color is yellow
favorite number is 19
存储(Stores) 在所有组件外部保持应用程序状态。它们是使用 props 或上下文来使数据在组件中可用的替代方法。
对于应该对所有组件可用的存储,请在 src/stores.js 之类的文件中定义并导出它们,并在需要时从该文件导入它们。
对于应该仅对给定组件的后代可用的存储,请在这个组件中定义它们,然后使用 props 或上下文将它们传递给后代。
-
可写存储——这是唯一可以由组件修改的存储。
-
可读存储——这些存储处理它们自己的数据。
派生存储——这些存储从其他存储的当前值派生数据。
这些存储都有一个 subscribe 方法,该方法返回一个可调用的函数来 unsubscribe。也可以创建自定义存储。它们唯一的限制是成为具有正确实现的 subscribe 方法的对象。
示例:https://svelte.dev/tutorial/custom-stores
要创建可写存储,请调用 svelte/store 包中定义的 writable 函数。然后传递初始值,还可以传递一个带有 set 函数的函数。如果传入了后者,它可以异步确定存储的值。例如,它可以调用 REST 服务,并将返回的值传递给 set。在第一个组件订阅存储之前不会调用此函数。
set(newValue)
update(fn)
这将基于当前值更新存储值。fn 是一个传递当前值并返回新值的函数。
下面是仅使用初始值定义可写存储的示例。
// 在 stores.js 内
import {writable} from 'svelte/store';
// 初始值是空数组。
export const dogStore = writable([]);
这是一个使用函数确定值来定义可写存储的示例。
// 在 stores.js 内
import {writable} from 'svelte/store';
export const dogStore = writable(initialValue, async set => {
// 订阅计数由 0 到 1 时调用。
// 计算初始值并传递给 set 函数。
const res = await fetch('/dogs');
const dogs = await res.json();
set(dogs);
return () => {
// 订阅计数归零时调用。
};
});
可以将表单元素的值绑定到可写存储。当用户更改表单元素值时将更新存储。
<input bind:value={$someStore} />
存储名称上的 $ 前缀接下来会解释。
要创建可读存储,请调用 svelte/store 包中定义的 readable 函数。
与可写存储一样,这里要为它传递一个初始值,还可以传递一个带有 set 函数的函数。例如:
import {readable} from 'svelte/store';
export const dogStore = readable(
[], // 初始值。
set => {
const res = await fetch('/dogs');
const dogs = await res.json();
set(dogs);
// 这里可以返回一个清理函数。
}
);
set 函数可以使用 setInterval 来连续更改值。
-
作为 prop 接受它。
-
从上下文中获取它。
从.js 文件导入(适用于全局范围)。
-
在其上调用 subscribe 方法(有些冗长)。
使用自动订阅的捷径(通常是首选)。
下面是使用 subscribe 方法的示例。
<script>
import {onDestroy} from 'svelte';
import {dogStore} from './stores.js';
let dogs;
const unsubscribe = dogStore.subscribe(value => (dogs = value));
onDestroy(unsubscribe);
</script>
<!-- 在 HTML 中使用 dogs。-->
下面是使用自动订阅的示例。
名称以 $ 开头的所有变量都必须放入存储。通过这种方法,组件在首次使用时会自动订阅存储,而被销毁时会自动取消订阅。
<script>
import {dogStore} from './stores.js';
</script>
<!-- 在 HTML 中使用 $dogStore。-->
下面是更改可写存储的示例。
订阅存储的组件将看到更改。
<script>
import {dogStore} from './stores.js';
import Child from './Child.svelte';
const dog = $dogStore;
function changeDog() {
// 方法 #1 - 创建新对象
//dogStore.set({age: 2, breed: 'GSP', name: 'Oscar'});
// 方法 #2 - 调整并复用对象
dog.age = 2;
dog.breed = 'GSP';
dog.name = 'Oscar';
dogStore.set(dog);
}
</script>
<h1>Store Demo</h1>
<Child />
<button on:click={changeDog}>Change Dog</button>
下面是一个使用 HTML 中的 $ 引用从存储中获取更改的示例。
<script>
import {dogStore} from './stores.js';
</script>
<div>
{$dogStore.name} is a {$dogStore.breed} that is {$dogStore.age} years old.
</div>
下面的代码效果同上,但是使用 JavaScript 代码从存储中获取数据。
<script>
import {dogStore} from './stores.js';
// 这里需要 Parens 才能知道开放的大括号不是块的开头。
$: ({age, breed, name} = $dogStore);
</script>
<div>{name} is a {breed} that is {age} years old.</div>
想要只在组件源文件中运行一次 JavaScript 代码,而不是为创建的每个组件实例都运行一次代码,请将代码包含在指定模块上下文的 script 标记中。
<script context="module">
...
</script>
如果 script 标记未指定其上下文,则它为 实例上下文。
两种 script 标记(实例和 模块上下文)都可以出现在组件源文件中。
两种上下文中都可以导出值。无法指定默认导出,因为组件本身会自动成为默认导出。
模块上下文可以声明变量并定义函数。这些可以在组件所有实例的实例上下文中访问,但它们不是响应式的。组件更改时不会重新渲染。这样就能在所有实例之间共享数据。
实例上下文变量和函数在模块上下文中不可访问。
请注意,不需要将不访问组件状态的函数移至模块上下文,因为(根据 Svelte API 文档)“ Svelte 将从组件定义中提升所有不依赖本地状态的函数。” 但将函数放在模块上下文中的一个目的是从外部导出和调用它们。
可以更改顶级组件变量的值来使组件状态无效。
根据 Svelte 文档,“当你使 Svelte 中的组件状态无效时,它不会立即更新 DOM。相反,它会等到下一个微任务才查看是否需要应用其他任何更改(包括在其他组件中)。这样做避免了不必要的工作,并使浏览器可以更有效地对事物进行批处理。”
tick 函数“返回一个 promise,该 promise 将在任何未决状态更改应用于 DOM 时立即解析(如果没有未决状态更改,则立即解析)。”
应用 DOM 更新后,可以使用此方法进行其他状态更改。
<script>
import {tick} from 'svelte';
...
// 做一些状态更改。
// 下面的内容预防 tick 调用后的批量更新。
await tick();
// DOM 更新后做更多状态更改。
...
</script>
调用 await tick() 在测试中也很有用,可以在测试效果之前等待更改被处理。
-
svelte/animate 包提供了 flip 函数。
-
svelte/motion 包提供 spring 和 tweened 函数。
-
svelte/transition 包提供了 crossfade 函数及过渡值 draw(用于 SVG 元素)、fade、fly、scale 和 slide。
另请参见 svelte/easing 包,这个包提供了控制动画随时间变化的速率的缓动函数。
下面是一个基本的动画示例,其中有一个列表项在装载时淡入并在销毁时淡出。
<script>
import {fade} from 'svelte/transition';
</script>
<li transition:fade>
<!-- 一些内容 -->
</li>
可以创建自定义动画。
示例:https://svelte.dev/tutorial/custom-css-transitions
-
introstart 和 introend。
outrostart 和 outroend。
特殊元素
Svelte 支持几种特殊元素,其形式为<svelte:name props>。总结如下。
<svelte:component this = {expression} optionalProps>
它将渲染 expression 指定的组件。如果 expression 是虚值则不渲染任何内容。可选的 props 会被传递到要渲染的组件。
<svelte:self props>
它允许组件渲染其自身的实例。它支持递归组件,这是必需的,因为组件无法导入自身。
<svelte:window on:eventName={handler}>
它将注册一个由 DOM window 对象调度给定事件时要调用的函数。resize 事件就是一个例子。
<svelte:window bind:propertyName={variable}>
它会将变量绑定到 window 属性。一个例子是 innerWidth。
<svelte:body on:eventName={handler}>
当 DOM body 元素调度给定事件时,此方法注册一个要调用的函数。例子包括 mouseEnter 和 mouseLeave。
<svelte:head>elements</svelte:head>
它会将元素插入 DOM 文档的 head 元素中。例子包括插入 link 和 script 标记。
<svelte:options option={value} />
它位于.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
标记内部。例如:
{@debug var1, var2, var3}
被监视的变量可以具有任何类型的值,包括对象。要在所有状态更改时中断,请省略变量名称。
{@debug}
ESLint 称自己为“针对 JavaScript 和 JSX 的可插入式 linting 实用程序”。它可以报告许多语法错误和潜在的运行时错误。它还可以报告与指定编码准则的差异。
-
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 --quiet src --ext .js,.svelte",
要运行 ESLint,请输入 npm run lint。
有关针对 Svelte 的 ESLint 选项的更多信息,请参见 https://github.com/sveltejs/eslint-plugin-svelte3。
Prettier 称自己为“经过优化的 JavaScript 格式化程序”。它支持多种语言和语言功能,包括 ES2017、TypeScript、JSON、HTML、CSS、LESS、SCSS、JSX、Vue 和 Markdown。
-
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 应用程序的实现,正好过一遍最重要的那些 Svelte 概念。
代码链接:https://github.com/mvolkmann/svelte-todo
要添加新的待办事项时,需要在输入框中输入待办事项的文本,然后按“添加”按钮或 Enter 键。
要将待办事项在已完成和未完成的状态之间切换,需要单击其左侧的复选框。请注意,顶部附近的“remaining”文本显示当前未检查的待办事项数和待办事项总数。
要删除待办事项,需要单击其右侧的“Delete”按钮。
要存档所有已检查的待办事项,需要单击“Archive Completed”按钮。但这个版本的应用并不会真的存储它们,其实它们都被删掉了。
下面是文件 src/main.js,它在文档主体中渲染 TodoList 组件来启动应用程序。
import TodoList from './TodoList.svelte';
const app = new TodoList({target: document.body});
export default app;
下面是文件 src/Todo.svelte 中 Todo 组件的代码。
-
待办事项文本。
-
一个复选框。
一个“Delete”按钮。
它需要一个名为“todo”的 prop 来保存待办事项的文本。切换复选框后,它将调度一个“toggleDone”事件。按下“Delete”按钮时,它将调度一个“删除”事件。
<script>
import {createEventDispatcher} from 'svelte';
const dispatch = createEventDispatcher();
export let todo; // 唯一的 prop
</script>
<style>
/* 在 todo 的文本上画一条线标记其完成状态。*/
.done-true {
color: gray;
text-decoration: line-through;
}
li {
margin-top: 5px;
}
</style>
<li>
<input
type="checkbox"
checked={todo.done}
on:change={() => dispatch('toggleDone')}
/>
<span class={'done-' + todo.done}>{todo.text}</span>
<button on:click={() => dispatch('delete')}>Delete</button>
</li>
下面是文件 src/TodoList.svelte 中 TodoList 组件的代码。
看到这里,关于 Svelte 你已经很熟悉了,所以这些代码应该很容易看懂。
<script>
import Todo from './Todo.svelte';
let lastId = 0;
// 创建一个 todo 对象。
const createTodo = (text, done = false) => ({id: ++lastId, text, done});
let todoText = '';
// 应用程序初始有两个 todo 项目。
let todos = [
createTodo('learn Svelte', true),
createTodo('build a Svelte app')
];
let uncompletedCount = 0;
// 这是"响应式声明"。
// 它保证未完成的代码在 todo 数组修改时被更新。
$: uncompletedCount = todos.filter(t => !t.done).length;
// 这是另一个"响应式声明"。
// 保证当 uncompletedCount 或 todo 数组更改时状态随时更新
$: status = `${uncompletedCount} of ${todos.length} remaining`;
// 创建并添加一个新的 todo.
function addTodo() {
// 回想这里为何必须使用 concat 代替 push。
todos = todos.concat(createTodo(todoText));
todoText = ''; // 清空 input
}
// 删除全部标记为完成的 todo。
const archiveCompleted = () => (todos = todos.filter(t => !t.done));
// 删除特定 todo。
const deleteTodo = todoId => (todos = todos.filter(t => t.id !== todoId));
// 改变给定 todo 的状态。
function toggleDone(todo) {
const {id} = todo;
todos = todos.map(t => (t.id === id ? {...t, done: !t.done} : t));
}
</script>
<style>
button {
margin-left: 10px;
}
/* 从加点(·)列表中移除点。*/
ul.unstyled {
list-style: none;
margin-left: 0;
padding-left: 0;
}
</style>
<div>
<h2>To Do List</h2>
<div>
{status}
<button on:click={archiveCompleted}>Archive Completed</button>
</div>
<br />
<!-- 我们不想真的提交表单。
使用表单使得按下 enter 键时触发"Add"按钮。-->
<form on:submit|preventDefault>
<input
type="text"
size="30"
autofocus
placeholder="enter new todo here"
bind:value={todoText}
/>
<button disabled={!todoText} on:click={addTodo}>
Add
</button>
</form>
<ul class="unstyled">
{#each todos as todo}
<Todo
todo={todo}
on:delete={() => deleteTodo(todo.id)}
on:toggleDone={() => toggleDone(todo)}
/>
{/each}
</ul>
</div>
Svelte 组件的单元测试可以使用 Jest 实现。另外建议使用“Svelte 测试库”。它与 Jest 协作,可以简化 Svelte 组件的单元测试编写。
本文不会深入探究这些测试工具的细节,但下面提供了测试代码示例。要了解这些工具的更多信息,请访问:
https://jestjs.io/
https://testing-library.com/
-
@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';
import Todo from './Todo.svelte';
describe('Todo', () => {
const text = 'buy milk';
const todo = {text};
// 卸载之前测试中挂载的所有组件。
afterEach(cleanup);
test('should render', () => {
const {getByText} = render(Todo, {props: {todo}});
const checkbox = 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';
import TodoList from './TodoList.svelte';
describe('TodoList', () => {
const PREDEFINED_TODOS = 2;
afterEach(cleanup);
// 它被下面的很多测试函数使用。
function expectTodoCount(count) {
return wait(() => {
// 每个 todo 有一个<li>根元素。
const lis = document.querySelectorAll('li');
expect(lis.length).toBe(count);
});
}
test('should render', async () => {
const {getByText} = render(TodoList);
expect(getByText('To Do List'));
expect(getByText('1 of 2 remaining'));
expect(getByText('Archive Completed')); // 按钮
await expectTodoCount(PREDEFINED_TODOS);
});
test('should add a todo', async () => {
const {getByTestId, getByText} = render(TodoList);
const input = getByTestId('todo-input');
const value = 'buy milk';
fireEvent.input(input, {target: {value}});
fireEvent.click(getByText('Add'));
await expectTodoCount(PREDEFINED_TODOS + 1);
expect(getByText(value));
});
test('should archive completed', async () => {
const {getByText} = render(TodoList);
fireEvent.click(getByText('Archive Completed'));
await expectTodoCount(PREDEFINED_TODOS - 1);
expect(getByText('1 of 1 remaining'));
});
test('should delete a todo', async () => {
const {getAllByText, getByText} = render(TodoList);
const text = 'learn Svelte'; // 第一个 todo
expect(getByText(text));
const deleteBtns = getAllByText('Delete');
fireEvent.click(deleteBtns[0]); // 删除第一个 todo
await expectTodoCount(PREDEFINED_TODOS - 1);
});
test('should toggle a todo', async () => {
const {container, getByText} = render(TodoList);
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
fireEvent.click(checkboxes[1]); // 第二个 todo
await tick();
expect(getByText('0 of 2 remaining'));
fireEvent.click(checkboxes[0]); // 第一个 todo
await tick();
expect(getByText('1 of 2 remaining'));
});
});
可以使用 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 的功能。在运行每个规范文件之前,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 应用程序的端到端测试代码。
const baseUrl = '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 文本。
const todoText = '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');
const todoText = '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);
const todoText = '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 VS Code 扩展
Sapper
Svelte Native
Svelte GL
带 Svelte 的 Storybook
Storybook 是用于演示和试验 Web UI 组件的工具。
到这里就结束了!Svelte 是当前流行的 React、Vue 和 Angular 框架的很好的替代品。它有许多好处,包括较小的包体积、简单的组件定义、方便的状态管理以及无需虚拟 DOM 的反应性。
非常感谢 Charles Sharp 和 Kristin Kroeger 审阅本文!
英文原文:
https://objectcomputing.com/resources/publications/sett/july-2019-web-dev-simplified-with-svelte
开发者技术前线 ,汇集技术前线快讯和关注行业趋势,大厂干货,是开发者经历和成长的优秀指南。

