Svelte 最新中文文档教程(22)—— Svelte 5 迁移指南
<h2 id="前言">前言</h2><p>Svelte,一个语法简洁、入门容易,面向未来的前端框架。</p>
<p>从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,<strong>从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1</strong>:</p>
<p><img src="https://yayujs-blog.oss-cn-beijing.aliyuncs.com/405488775-48df16b1-939c-489b-8d52-6071869893f0.png"></p>
<p>Svelte 以其独特的编译时优化机制著称,具有<strong>轻量级</strong>、<strong>高性能</strong>、<strong>易上手</strong>等特性,<strong>非常适合构建轻量级 Web 项目</strong>。</p>
<p>为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。</p>
<p>如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p>
<h2 id="svelte-5-迁移指南">Svelte 5 迁移指南</h2>
<p>Svelte 5 采用了全面改进的语法和响应性系统。虽然开始时可能看起来有所不同,但您很快会注意到许多相似之处。本指南详细介绍了这些变化,并向您展示如何升级。同时,我们还提供了关于我们为什么做出这些改变的信息。</p>
<p>您不必立即迁移至新语法 —— Svelte 5 仍然支持旧的 Svelte 4 语法,您可以将使用新语法的组件与使用旧语法的组件混合使用。我们预计很多人可以通过仅修改几行代码就完成升级。还有一个 迁移脚本 可以帮助您自动完成许多步骤。</p>
<h2 id="响应性语法变化">响应性语法变化</h2>
<p>Svelte 5 的核心是新的符文 API。符文基本上是编译器指令,告诉 Svelte 有关响应性的信息。在语法上,符文是以美元符号开头的函数。</p>
<h3 id="let---state">let -> $state</h3>
<p>在 Svelte 4 中,组件顶层的 <code>let</code> 声明是隐式响应式的。在 Svelte 5 中,事情变得更明确:当使用 <code>$state</code> 符文创建变量时,该变量是响应式的。让我们通过将计数器包装在 <code>$state</code> 中来迁移到符文模式:</p>
<pre><code class="language-svelte"><script>
let count = +++$state(+++0+++)+++;
</script>
</code></pre>
<p>其他方面没有变化。<code>count</code> 仍然是数字本身,您可以直接读写它,没有 <code>.value</code> 或 <code>getCount()</code> 这样的包装器。</p>
<blockquote>
<p>[!DETAILS] 我们为什么这样做<br>
<code>let</code> 在顶层隐式声明响应式工作良好,但这意味着响应性受到限制——在其他地方的 <code>let</code> 声明都不是响应式的。这迫使您在重构代码以便复用时不得不使用 store。这意味着您必须学习一个完全不同的响应模型,结果通常并不那么好用。由于 Svelte 5 中的响应性更明确,您可以在组件顶层之外继续使用相同的 API。请前往 教程 了解更多信息。</p>
</blockquote>
<h3 id="---derivedeffect">$: -> $derived/$effect</h3>
<p>在 Svelte 4 中,组件顶层的 <code>$:</code> 语句可用于声明派生,即完全通过其他状态的计算来定义的状态。在 Svelte 5 中,可以使用 <code>$derived</code> 符文实现这一点:</p>
<pre><code class="language-svelte"><script>
let count = +++$state(+++0+++)+++;
---$:--- +++const+++ double = +++$derived(+++count * 2+++)+++;
</script>
</code></pre>
<p>与 <code>$state</code> 一样,其他方面没有变化。<code>double</code> 仍然是数字本身,您可以直接读取它,而不需要像 <code>.value</code> 或 <code>getDouble()</code> 这样的包装器。</p>
<p><code>$:</code> 语句还可以用于创建副作用。在 Svelte 5 中,可以使用 <code>$effect</code> 符文实现这一点:</p>
<pre><code class="language-svelte"><script>
let count = +++$state(+++0+++)+++;
---$:---+++$effect(() =>+++ {
if (count > 5) {
alert('Count is too high!');
}
}+++);+++
</script>
</code></pre>
<blockquote>
<p>[!DETAILS] 我们为什么这样做<br>
<code>$:</code> 是一个很好的简写,容易上手:您可以在大多数代码前加上 <code>$:</code> 它就能以某种方式工作。这种直观性也是它的缺点,因为您的代码变得更复杂时,它并不那么好理解。代码的意图是创建一个派生,还是创建一个副作用?使用 <code>$derived</code> 和 <code>$effect</code>,您需要进行更多的前期决策(剧透:90% 的时候您想要 <code>$derived</code>),但将来您和团队中的其他开发人员会更容易理解。</p>
<p>还有一些难以发现的陷阱:</p>
<ul>
<li><code>$:</code> 仅在渲染之前直接更新,这意味着在重新渲染之间你可能会读取到过时的值</li>
<li><code>$:</code> 仅在每个 tick 中运行一次,这意味着语句的运行频率可能低于你的预期</li>
<li><code>$:</code> 依赖关系是通过对依赖项的静态分析确定的。这在大多数情况下有效,但在重构过程中可能会以微妙的方式出错,例如依赖项被移动到一个函数中,从而不再可见</li>
<li><code>$:</code> 语句的顺序也是通过对依赖项的静态分析来确定的。在某些情况下可能会出现平局,导致排序错误,需要手动干预。在重构代码时,顺序也可能会出错,某些依赖项因此不再可见。</li>
</ul>
<p>最后,它对 TypeScript 不友好(我们的编辑器工具需要跳过一些环节才能使其对 TypeScript 有效),这是使 Svelte 的响应模型真正通用的障碍。</p>
<p><code>$derived</code> 和 <code>$effect</code> 解决了所有这些问题:</p>
<ul>
<li>始终返回最新值</li>
<li>根据需要运行以保持稳定</li>
<li>在运行时确定依赖关系,因此对重构免疫</li>
<li>根据需要执行依赖关系,因此免受排序问题的影响</li>
<li>对于 TypeScript 友好</li>
</ul>
</blockquote>
<h3 id="export-let---props">export let -> $props</h3>
<p>在 Svelte 4 中,组件的属性是通过 <code>export let</code> 声明的。每个属性都是一个声明。在 Svelte 5 中,所有属性都是通过 <code>$props</code> 符文声明的,通过解构:</p>
<pre><code class="language-svelte"><script>
---export let optional = 'unset';
export let required;---
+++let { optional = 'unset', required } = $props();+++
</script>
</code></pre>
<p>在某些情况下,声明属性变得不如有几个 <code>export let</code> 声明那样简单:</p>
<ul>
<li>您想重命名属性,例如因为名称是保留标识符(例如 <code>class</code>)</li>
<li>您不知道预期还有哪些其他属性</li>
<li>您想将每个属性转发到另一个组件</li>
</ul>
<p>在 Svelte 4 中,所有这些情况都需要特殊语法:</p>
<ul>
<li>重命名:<code>export { klass as class}</code></li>
<li>其他属性:<code>$$restProps</code></li>
<li>所有属性:<code>$$props</code></li>
</ul>
<p>在 Svelte 5 中,<code>$props</code> 符文使这变得简单,无需任何额外的 Svelte 特定语法:</p>
<ul>
<li>重命名:使用属性重命名 <code>let { class: klass } = $props();</code></li>
<li>其他属性:使用展开语法 <code>let { foo, bar, ...rest } = $props();</code></li>
<li>所有属性:不要解构 <code>let props = $props();</code></li>
</ul>
<pre><code class="language-svelte"><script>
---let klass = '';
export { klass as class};---
+++let { class: klass, ...rest } = $props();+++
</script>
<button class={klass} {...---$$restProps---+++rest+++}>点击我</button>
</code></pre>
<blockquote>
<p>[!DETAILS] 我们为什么这样做<br>
<code>export let</code> 是一个颇具争议的 API 决策,围绕您是否应该考虑属性被 <code>export</code> 或 <code>import</code> 存在了很多争论。<code>$props</code> 没有这种特性。这也与其他符文保持一致,总体思路简化为“在 Svelte 中,所有与响应性有关的都是符文”。</p>
<p><code>export let</code> 还存在许多局限性,需要额外的 API 去解决,如上所示。<code>$props</code> 将这些统一为一个语法概念,严重依赖常规的 JavaScript 解构语法</p>
</blockquote>
<h2 id="事件变化">事件变化</h2>
<p>在 Svelte 5 中,事件处理程序进行了改头换面。在 Svelte 4 中,我们使用 <code>on:</code> 指令将事件监听器附加到元素上,而在 Svelte 5 中,它们像其他属性一样(换句话说 —— 去掉冒号):</p>
<pre><code class="language-svelte"><script>
let count = $state(0);
</script>
<button on---:---click={() => count++}>
点击次数:{count}
</button>
</code></pre>
<p>由于它们只是属性,您可以使用正常的简写语法...</p>
<pre><code class="language-svelte"><script>
let count = $state(0);
function onclick() {
count++;
}
</script>
<button {onclick}>
点击次数:{count}
</button>
</code></pre>
<p>...尽管在使用命名事件处理函数时,通常最好使用更具描述性的名称。</p>
<h3 id="组件事件">组件事件</h3>
<p>在 Svelte 4 中,组件可以使用 <code>createEventDispatcher</code> 创建一个调度器来发出事件。</p>
<p>该函数在 Svelte 5 中已弃用。相反,组件应接受 <em>回调属性</em> —— 这意味着您可以将函数作为属性传递给这些组件:</p>
<pre><code class="language-svelte"><!--- file: App.svelte --->
<script>
import Pump from './Pump.svelte';
let size = $state(15);
let burst = $state(false);
function reset() {
size = 15;
burst = false;
}
</script>
<Pump
---on:---inflate={(power) => {
size += power---.detail---;
if (size > 75) burst = true;
}}
---on:---deflate={(power) => {
if (size > 0) size -= power---.detail---;
}}
/>
{#if burst}
<button onclick={reset}>新气球</button>
<span class="boom">💥</span>
{:else}
<span class="balloon" style="scale: {0.01 * size}">
🎈
</span>
{/if}
</code></pre>
<pre><code class="language-svelte"><!--- file: Pump.svelte --->
<script>
---import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
---
+++let { inflate, deflate } = $props();+++
let power = $state(5);
</script>
<button onclick={() => ---dispatch('inflate', power)---+++inflate(power)+++}>
充气
</button>
<button onclick={() => ---dispatch('deflate', power)---+++deflate(power)+++}>
放气
</button>
<button onclick={() => power--}>-</button>
泵的气压:{power}
<button onclick={() => power++}>+</button>
</code></pre>
<h3 id="事件冒泡">事件冒泡</h3>
<p>组件应该接受一个 <code>onclick</code> 回调属性,而不是通过 <code><button on:click></code> 将事件从元素“转发”到组件:</p>
<pre><code class="language-svelte"><script>
+++let { onclick } = $props();+++
</script>
<button ---on:click--- +++{onclick}+++>
点击我
</button>
</code></pre>
<p>请注意,这也意味着您可以将事件处理程序与其他属性一起“展开”到元素上,而不必繁琐地单独转发每个事件:</p>
<pre><code class="language-svelte"><script>
let props = $props();
</script>
<button ---{...$$props} on:click on:keydown on:all_the_other_stuff--- +++{...props}+++>
点击我
</button>
</code></pre>
<h3 id="事件修饰符">事件修饰符</h3>
<p>在 Svelte 4 中,您可以向事件处理程序添加事件修饰符:</p>
<pre><code class="language-svelte"><button on:click|once|preventDefault={handler}>...</button>
</code></pre>
<p>修饰符特定于 <code>on:</code>,因此不适用于现代事件处理程序。在处理程序内部添加 <code>event.preventDefault()</code> 等内容是更可取的,因为所有逻辑都集中在一个地方,而不是拆分在处理程序和修饰符之间。</p>
<p>由于事件处理程序只是函数,您可以根据需要创建自己的封装:</p>
<pre><code class="language-svelte"><script>
function once(fn) {
return function (event) {
if (fn) fn.call(this, event);
fn = null;
};
}
function preventDefault(fn) {
return function (event) {
event.preventDefault();
fn.call(this, event);
};
}
</script>
<button onclick={once(preventDefault(handler))}>...</button>
</code></pre>
<p>有三个修饰符——<code>capture</code>、<code>passive</code> 和 <code>nonpassive</code> —— 不能被表示为包装函数,因为它们需要在事件处理程序绑定时应用,而不是在运行时应用。</p>
<p>对于 <code>capture</code>,我们将修饰符添加到事件名称中:</p>
<pre><code class="language-svelte"><button onclickcapture={...}>...</button>
</code></pre>
<p>更改事件处理程序的 <code>passive</code> 选项并不是轻而易举的事情。如果您有此用例——您可能没有!——那么您需要使用一个 action 来自己应用事件处理程序。</p>
<h3 id="多个事件处理程序">多个事件处理程序</h3>
<p>在 Svelte 4 中,这样做是可以的:</p>
<pre><code class="language-svelte"><button on:click={one} on:click={two}>...</button>
</code></pre>
<p>元素上的重复特性/属性 —— 现在包括事件处理程序 —— 是不允许的。相反,请改为这样做:</p>
<pre><code class="language-svelte"><button
onclick={(e) => {
one(e);
two(e);
}}
>
...
</button>
</code></pre>
<p>在展开属性时,本地事件处理程序必须在展开之后,否则可能会被覆盖:</p>
<pre><code class="language-svelte"><button
{...props}
onclick={(e) => {
doStuff(e);
props.onclick?.(e);
}}
>
...
</button>
</code></pre>
<blockquote>
<p>[!DETAILS] 我们为什么这样做<br>
<code>createEventDispatcher</code> 一直有点模板化:</p>
<ul>
<li>导入函数</li>
<li>调用该函数以获取调度函数</li>
<li>使用字符串和可能的有效负载调用该调度函数</li>
<li>通过 <code>.detail</code> 属性在另一端检索该有效负载,因为事件本身始终是 <code>CustomEvent</code></li>
</ul>
<p>一直可以使用组件回调属性,但由于您必须使用 <code>on:</code> 监听 DOM 事件,因此出于语法一致性,使用 <code>createEventDispatcher</code> 处理组件事件是有意义的。现在我们有了事件属性(<code>onclick</code>),情况正好相反:回调属性现在是更合理的选择。</p>
<p>放弃事件修饰符无疑是对那些喜欢事件修饰符简写语法的人的一种倒退。考虑到它们并不常用,我们用更小的表面积换取了更明确性。修饰符也不一致,因为它们中的大多数只能用于 DOM 元素。</p>
<p>同一事件的多个监听器也不再可能,但这本身就是一种反模式,因为它妨碍了可读性:如果有很多属性,则更难发现有两个处理程序,除非它们紧挨在一起。它还暗示这两个处理程序是独立的,而实际上,如果 <code>one</code> 内部包含 <code>event.stopImmediatePropagation()</code>,会阻止 <code>two</code> 被调用。</p>
<p>通过弃用 <code>createEventDispatcher</code> 和 <code>on:</code> 指令,改为使用回调属性和普通元素属性,我们:</p>
<ul>
<li>降低了 Svelte 的学习曲线</li>
<li>消除了样板代码,特别是在 <code>createEventDispatcher</code> 周围</li>
<li>消除了为可能没有监听者的事件创建 <code>CustomEvent</code> 对象的开销</li>
<li>增加了展开事件处理程序的能力</li>
<li>增加了了解哪些事件处理程序被提供给组件的能力</li>
<li>增加了表达给定事件处理程序是必需的还是可选的能力</li>
<li>提高了类型安全性(之前,Svelte 实际上无法保证组件不发出特定事件)</li>
</ul>
</blockquote>
<h2 id="代码片段而非插槽">代码片段而非插槽</h2>
<p>在 Svelte 4 中,可以使用插槽将内容传递给组件。Svelte 5 用更强大和灵活的代码片段替换了它们,因此插槽在 Svelte 5 中被弃用。</p>
<p>不过,它们仍然可以继续使用,您可以在组件中混合使用代码片段和插槽。</p>
<p>在使用自定义元素时,您仍然应该像以前一样使用 <code><slot /></code>。在未来的版本中,当 Svelte 移除其内部版本的插槽时,它将保持这些插槽不变,即输出一个常规的 DOM 标签,而不是进行转换。</p>
<h3 id="默认内容">默认内容</h3>
<p>在 Svelte 4 中,传递 UI 给子组件的最简单方法是使用一个 <code><slot /></code>。在 Svelte 5 中,改为使用 <code>children</code> 属性,然后通过 <code>{@render children()}</code> 显示:</p>
<pre><code class="language-svelte"><script>
+++let { children } = $props();+++
</script>
---<slot />---
+++{@render children?.()}+++
</code></pre>
<h3 id="多个内容占位符">多个内容占位符</h3>
<p>如果您想要多个 UI 占位符,您必须使用命名插槽。在 Svelte 5 中,改为使用 props,随意命名它们,并 <code>{@render ...}</code> 它们:</p>
<pre><code class="language-svelte"><script>
+++let { header, main, footer } = $props();+++
</script>
<header>
---<slot name="header" />---
+++{@render header()}+++
</header>
<main>
---<slot name="main" />---
+++{@render main()}+++
</main>
<footer>
---<slot name="footer" />---
+++{@render footer()}+++
</footer>
</code></pre>
<h3 id="向上传递数据">向上传递数据</h3>
<p>在 Svelte 4 中,您将数据传递给 <code><slot /></code>,然后在父组件中使用 <code>let:</code> 检索它。在 Svelte 5 中,代码片段承担了这一责任:</p>
<pre><code class="language-svelte"><!--- file: App.svelte --->
<script>
import List from './List.svelte';
</script>
<List items={['one', 'two', 'three']} ---let:item--->
+++{#snippet item(text)}+++
<span>{text}</span>
+++{/snippet}+++
---<span slot="empty">尚无条目</span>---
+++{#snippet empty()}
<span>尚无条目</span>
{/snippet}+++
</List>
</code></pre>
<pre><code class="language-svelte"><!--- file: List.svelte --->
<script>
let { items, +++item, empty+++ } = $props();
</script>
{#if items.length}
<ul>
{#each items as entry}
<li>
---<slot item={entry} />---
+++{@render item(entry)}+++
</li>
{/each}
</ul>
{:else}
---<slot name="empty" />---
+++{@render empty?.()}+++
{/if}
</code></pre>
<blockquote>
<p>[!DETAILS] 我们为什么这样做<br>
插槽易于上手,但随着用例的复杂性增加,语法越发复杂和令人困惑:</p>
<ul>
<li><code>let:</code> 语法让许多人感到困惑,因为它<em>创建</em>了一个变量,而所有其他 <code>:</code> 指令则是<em>接收</em>一个变量</li>
<li>用 <code>let:</code> 声明的变量的作用域并不清晰。在上面的例子中,您可能会认为可以在 <code>empty</code> 插槽中使用 <code>item</code> 插槽属性,但事实并非如此</li>
<li>命名插槽必须使用 <code>slot</code> 属性应用于元素。有时您不希望创建一个元素,因此我们不得不添加 <code><svelte:fragment></code> API</li>
<li>命名插槽也可以应用于组件,这改变了 <code>let:</code> 指令可用范围的语义(即使在今天,我们的维护者也常常不知道它的工作原理)</li>
</ul>
<p>代码片段通过更具可读性和清晰性解决了所有这些问题。同时,它们更加强大,因为它们允许您定义可以在 <em>任何地方</em> 渲染的 UI 部分,而不仅仅是将其作为 props 传递给组件。</p>
</blockquote>
<h2 id="迁移脚本">迁移脚本</h2>
<p>到目前为止,您应该对之前/之后的情况以及旧语法与新语法的关系有了相当不错的理解。您可能也意识到了,很多迁移都是相当技术性和重复的,您并不想手动完成这些事情。</p>
<p>我们也是这样认为的,这就是为什么我们提供了迁移脚本,用于自动完成大部分迁移。您可以使用 <code>npx sv migrate svelte-5</code> 升级您的项目。这将执行以下操作:</p>
<ul>
<li>更新您的 <code>package.json</code> 中的核心依赖项</li>
<li>迁移到符文(<code>let</code> -> <code>$state</code> 等)</li>
<li>将 DOM 元素的事件属性迁移为事件属性(<code>on:click</code> -> <code>onclick</code>)</li>
<li>将插槽创建迁移为渲染标签(<code><slot /></code> -> <code>{@render children()}</code>)</li>
<li>将插槽用法迁移至片段(<code><div slot="x">...</div></code> -> <code>{#snippet x()}<div>...</div>{/snippet}</code>)</li>
<li>迁移明显的组件创建(<code>new Component(...)</code> -> <code>mount(Component, ...)</code>)</li>
</ul>
<p>您还可以通过 VS Code 中的 <code>Migrate Component to Svelte 5 Syntax</code> 命令迁移单个组件,或在我们的 Playground 中通过 <code>Migrate</code> 按钮完成。</p>
<p>并非所有内容都可以自动迁移,一些迁移在之后需要手动清理。以下部分将更详细地描述这些内容。</p>
<h3 id="run">run</h3>
<p>您可能会看到迁移脚本将一些 <code>$:</code> 语句转换为从 <code>svelte/legacy</code> 导入的 <code>run</code> 函数。如果迁移脚本无法可靠地将语句迁移到 <code>$derived</code> 并得出结论这是一个副作用,就会发生这种情况。</p>
<p>在某些情况下,这可能是错误的,最好将其改为使用 <code>$derived</code>。在其他情况下,这可能是正确的,但由于 <code>$:</code> 语句在服务端也会运行,而 <code>$effect</code> 不会,因此不能安全地转换它。于是,<code>run</code> 被用作权宜之计。<code>run</code> 模拟了 <code>$:</code> 的大多数特征,因为它在服务端上运行一次,并在客户端作为 <code>$effect.pre</code> 运行(<code>$effect.pre</code> 在更改应用于 DOM 之前运行;您最有可能想要使用 <code>$effect</code> 代替)。</p>
<pre><code class="language-svelte"><script>
---import { run } from 'svelte/legacy';---
---run(() => {---
+++$effect(() => {+++
// 一些副作用代码
})
</script>
</code></pre>
<h3 id="事件修饰符-1">事件修饰符</h3>
<p>事件修饰符不适用于事件属性(例如,您不能做 <code>onclick|preventDefault={...}</code>)。因此,当将事件指令迁移到事件属性时,我们需要一个函数替代这些修饰符。这些从 <code>svelte/legacy</code> 中导入,帮助支持迁移,例如仅使用 <code>event.preventDefault()</code>。</p>
<pre><code class="language-svelte"><script>
---import { preventDefault } from 'svelte/legacy';---
</script>
<button
onclick={---preventDefault---((event) => {
+++event.preventDefault();+++
// ...
})}
>
点击我
</button>
</code></pre>
<h3 id="不会自动迁移的内容">不会自动迁移的内容</h3>
<p>迁移脚本不会转换 <code>createEventDispatcher</code>。您需要手动调整这些部分。之所以这样做,是因为风险太大,可能会导致组件出现故障,而迁移脚本无法发现这一点。</p>
<p>迁移脚本不会转换 <code>beforeUpdate/afterUpdate</code>。之所以这样做,是因为无法确定代码的实际意图。作为经验法则,您通常可以结合使用 <code>$effect.pre</code>(在与 <code>beforeUpdate</code> 同时运行)和 <code>tick</code>(从 <code>svelte</code> 导入,让您等到更改应用于 DOM,然后再进行一些工作)。</p>
<h2 id="组件不再是类">组件不再是类</h2>
<p>在 Svelte 3 和 4 中,组件是类。在 Svelte 5 中,它们是函数,应该以不同方式实例化。如果您需要手动实例化组件,您应该使用 <code>mount</code> 或 <code>hydrate</code>(从 <code>svelte</code> 导入)。如果您在使用 SvelteKit 时看到此错误,请先尝试更新到最新版本的 SvelteKit,该版本添加了对 Svelte 5 的支持。如果您在没有 SvelteKit 的情况下使用 Svelte,您可能会有一个 <code>main.js</code> 文件(或类似的文件),您需要进行调整:</p>
<pre><code class="language-js">+++import { mount } from 'svelte';+++
import App from './App.svelte'
---const app = new App({ target: document.getElementById("app") });---
+++const app = mount(App, { target: document.getElementById("app") });+++
export default app;
</code></pre>
<p><code>mount</code> 和 <code>hydrate</code> 具有完全相同的 API。不同之处在于 <code>hydrate</code> 会在其目标内提取 Svelte 的服务端渲染 HTML 并进行水合。两者都返回一个包含组件导出的对象以及可能的属性访问器(如果编译时使用 <code>accessors: true</code>)。它们不包含您可能熟悉的类组件 API 中的 <code>$on</code>、<code>$set</code> 和 <code>$destroy</code> 方法。这些是它的替代品:</p>
<p>对于 <code>$on</code>,不要监听事件,而是通过 <code>events</code> 属性在选项参数中传递它们。</p>
<pre><code class="language-js">+++import { mount } from 'svelte';+++
import App from './App.svelte'
---const app = new App({ target: document.getElementById("app") });
app.$on('event', callback);---
+++const app = mount(App, { target: document.getElementById("app"), events: { event: callback } });+++
</code></pre>
<blockquote>
<p>[!NOTE] 请注意,使用 <code>events</code> 是不推荐的——请改为 使用回调</p>
</blockquote>
<p>对于 <code>$set</code>,请使用 <code>$state</code> 来创建一个响应式属性对象并进行操作。如果您在 <code>.js</code> 或 <code>.ts</code> 文件中执行此操作,请调整文件结尾包含 <code>.svelte</code>,即 <code>.svelte.js</code> 或 <code>.svelte.ts</code>。</p>
<pre><code class="language-js">+++import { mount } from 'svelte';+++
import App from './App.svelte'
---const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
app.$set({ foo: 'baz' });---
+++const props = $state({ foo: 'bar' });
const app = mount(App, { target: document.getElementById("app"), props });
props.foo = 'baz';+++
</code></pre>
<p>对于 <code>$destroy</code>,请使用 <code>unmount</code> 代替。</p>
<pre><code class="language-js">+++import { mount, unmount } from 'svelte';+++
import App from './App.svelte'
---const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
app.$destroy();---
+++const app = mount(App, { target: document.getElementById("app") });
unmount(app);+++
</code></pre>
<p>作为权宜之计,您还可以使用 <code>createClassComponent</code> 或 <code>asClassComponent</code>(从 <code>svelte/legacy</code> 导入)来保持 保持在实例化后与 Svelte 4 相同的 API。</p>
<pre><code class="language-js">+++import { createClassComponent } from 'svelte/legacy';+++
import App from './App.svelte'
---const app = new App({ target: document.getElementById("app") });---
+++const app = createClassComponent({ component: App, target: document.getElementById("app") });+++
export default app;
</code></pre>
<p>如果这个组件不在您的控制之下,您可以使用 <code>compatibility.componentApi</code> 编译器选项来实现向后兼容性,这意味着使用 <code>new Component(...)</code> 的代码可以在不做调整的情况下继续工作(请注意,这会给每个组件增加一些开销)。这还将为您通过 <code>bind:this</code> 获取的所有组件实例添加 <code>$set</code> 和 <code>$on</code> 方法。</p>
<pre><code class="language-js">/// svelte.config.js
export default {
compilerOptions: {
compatibility: {
componentApi: 4
}
}
};
</code></pre>
<p>注意 <code>mount</code> 和 <code>hydrate</code> 不是同步的,因此类似 <code>onMount</code> 这样的内容在函数返回时不会被调用,待处理的 Promise 块尚未呈现(因为 <code>#await</code> 等待一个微任务以等待一个可能立即 resolve 的 Promise)。如果您需要这个保证,在调用 <code>mount/hydrate</code> 之后调用 <code>flushSync</code>(从 <code>'svelte'</code> 导入)。</p>
<h3 id="服务端-api-变化">服务端 API 变化</h3>
<p>同样,组件在服务端渲染编译时,不再具有 <code>render</code> 方法。相反,将函数传递给 <code>svelte/server</code> 的 <code>render</code>:</p>
<pre><code class="language-js">+++import { render } from 'svelte/server';+++
import App from './App.svelte';
---const { html, head } = App.render({ props: { message: 'hello' }});---
+++const { html, head } = render(App, { props: { message: 'hello' }});+++
</code></pre>
<p>在 Svelte 4 中,将组件渲染为字符串也会返回所有组件的 CSS。在 Svelte 5 中,默认情况下不再这样,因为大多数情况下您使用工具链以其他方式处理它(例如 SvelteKit)。如果您需要从 <code>render</code> 返回 CSS,您可以将 <code>css</code> 编译器选项设置为 <code>'injected'</code>,它将在 <code>head</code> 中添加 <code><style></code> 元素。</p>
<h3 id="组件类型变化">组件类型变化</h3>
<p>从类到函数的变化也反映在类型定义中:<code>SvelteComponent</code>,Svelte 4 的基类已被弃用,取而代之的是新的 <code>Component</code> 类型,它定义了 Svelte 组件的函数形状。要在 <code>d.ts</code> 文件中手动定义组件形状:</p>
<pre><code class="language-ts">import type { Component } from 'svelte';
export declare const MyComponent: Component<{
foo: string;
}>;
</code></pre>
<p>声明某种类型的组件是必需的:</p>
<pre><code class="language-svelte"><script lang="ts">
import type { ---SvelteComponent--- +++Component+++ } from 'svelte';
import {
ComponentA,
ComponentB
} from 'component-library';
---let component: typeof SvelteComponent<{ foo: string }>---
+++let component: Component<{ foo: string }>+++ = $state(
Math.random() ? ComponentA : ComponentB
);
</script>
<svelte:component this={component} foo="bar" />
</code></pre>
<p>两个工具类型 <code>ComponentEvents</code> 和 <code>ComponentType</code> 已被弃用。因为事件现在被定义为回调属性,而 <code>ComponentEvents</code> 已过时,因为新的 <code>Component</code> 类型已经是组件类型(例如 <code>ComponentType<SvelteComponent<{ prop: string }>></code> == <code>Component<{ prop: string }></code>)。</p>
<h3 id="bindthis-变化">bind:this 变化</h3>
<p>由于组件不再是类,使用 <code>bind:this</code> 不再返回带有 <code>$set</code>、<code>$on</code> 和 <code>$destroy</code> 方法的类实例。它仅返回实例导出(<code>export function/const</code>),并且如果您使用 <code>accessors</code> 选项,则返回每个属性的 getter/setter 对。</p>
<h2 id="空格处理变化">空格处理变化</h2>
<p>此前,Svelte 使用了一个非常复杂的算法来确定是否应该保留空格。Svelte 5 简化了这一点,使开发人员更容易理解。规则如下:</p>
<ul>
<li>节点之间的空格被折叠为一个空格</li>
<li>标签开头和结尾的空格被完全移除</li>
<li>某些例外情况,例如在 <code>pre</code> 标签内保留空格</li>
</ul>
<p>和之前一样,您可以通过在编译器设置中将 <code>preserveWhitespace</code> 选项设置为 <code>true</code> 或在 <code><svelte:options></code> 中针对每个组件设置来禁用空格修剪。</p>
<h2 id="需要现代浏览器">需要现代浏览器</h2>
<p>Svelte 5 需要现代浏览器(换句话说,不支持 Internet Explorer),原因如下:</p>
<ul>
<li>它使用 <code>Proxies</code></li>
<li>具有 <code>clientWidth</code>/<code>clientHeight</code>/<code>offsetWidth</code>/<code>offsetHeight</code> 绑定的元素使用 <code>ResizeObserver</code>,而不是复杂的 <code><iframe></code> 技巧</li>
<li><code><input type="range" bind:value={...} /></code> 仅使用 <code>input</code> 事件监听器,而不是同时监听 <code>change</code> 事件作为后备方案</li>
</ul>
<p>legacy 编译器选项(该选项生成体积较大但兼容 IE 的代码)不再存在。</p>
<h2 id="编译器选项的变化">编译器选项的变化</h2>
<ul>
<li>从 css 选项中移除了 <code>false</code> / <code>true</code>(之前已经弃用)和 <code>"none"</code> 这些有效值</li>
<li><code>legacy</code> 选项被重新调整用途</li>
<li><code>hydratable</code> 选项已被移除。Svelte 组件现在始终是可水合的</li>
<li><code>enableSourcemap</code> 选项已被移除。现在始终生成 source map,工具可以选择忽略它</li>
<li><code>tag</code> 选项已被移除。请改用组件内的 <code><svelte:options customElement="tag-name" /></code></li>
<li><code>loopGuardTimeout</code>、<code>format</code>、<code>sveltePath</code>、<code>errorMode</code> 和 <code>varsReport</code> 选项已被移除</li>
</ul>
<h2 id="children-属性被保留"><code>children</code> 属性被保留</h2>
<p>组件标签里的内容变为名为 <code>children</code> 的代码片段属性。你不能使用相同的名称定义其他属性。</p>
<h2 id="点符号表示组件">点符号表示组件</h2>
<p>在 Svelte 4 中,<code><foo.bar></code> 将创建一个标签名为 <code>"foo.bar"</code> 的元素。在 Svelte 5 中,<code>foo.bar</code> 被视为组件。这在 <code>each</code> 块中特别有用:</p>
<pre><code class="language-svelte">{#each items as item}
<item.component {...item.props} />
{/each}
</code></pre>
<h2 id="符文模式中的重大变化">符文模式中的重大变化</h2>
<p>某些重大变更仅在组件处于符文模式时才适用。</p>
<h3 id="不允许绑定到组件导出">不允许绑定到组件导出</h3>
<p>符文模式下,组件的导出不能直接绑定。例如,组件 <code>A</code> 中有 <code>export const foo = ...</code>,然后执行 <code><A bind:foo /></code>,将导致错误。使用 <code>bind:this</code> 代替——<code><A bind:this={a} /></code>——并通过 <code>a.foo</code> 访问导出。此更改使事情更容易理解,因为它强制了属性和导出之间的清晰分离。</p>
<h3 id="绑定需要使用-bindable-显式定义">绑定需要使用 <code>$bindable()</code> 显式定义</h3>
<p>在 Svelte 4 语法中,每个属性(通过 <code>export let</code> 声明)都是可绑定的,这意味着您可以对其使用 <code>bind:</code>。在符文模式中,属性默认不具有可绑定性:您需要使用 <code>$bindable</code> 符文来标记可绑定的 props。</p>
<p>如果一个可绑定属性有默认值(例如 <code>let { foo = $bindable('bar') } = $props();</code>),当你要绑定该属性时,需要传递一个非 <code>undefined</code> 的值。这可以防止出现模棱两可的行为 —— 父组件和子组件必须具有相同的值,并能获得更好的性能(在 Svelte 4 中,默认值被反映回父组件,导致额外的无用渲染周期)。</p>
<h3 id="accessors-选项被忽略"><code>accessors</code> 选项被忽略</h3>
<p>将 <code>accessors</code> 选项设置为 <code>true</code> 可使组件的属性在组件实例上直接访问。在符文模式下,属性永远不会在组件实例上访问。如果您需要暴露它们,可以使用组件导出。</p>
<h3 id="immutable-选项被忽略"><code>immutable</code> 选项被忽略</h3>
<p>在符文模式下,设置 <code>immutable</code> 选项没有效果。这个概念被 <code>$state</code> 及其变体的工作方式所替代。</p>
<h3 id="类不再是自动响应式">类不再是“自动响应式”</h3>
<p>在 Svelte 4 中,执行以下操作会触发响应性:</p>
<pre><code class="language-svelte"><script>
let foo = new Foo();
</script>
<button on:click={() => (foo.value = 1)}>{foo.value}</button>
</code></pre>
<p>这是因为 Svelte 编译器将对 <code>foo.value</code> 的赋值视为更新所有引用 <code>foo</code> 的内容的指令。在 Svelte 5 中,响应性在运行时而不是编译时确定,因此您应该将 <code>value</code> 定义为 <code>Foo</code> 类上的响应式 <code>$state</code> 字段。将 <code>new Foo()</code> 包装在 <code>$state(...)</code> 中将不会产生任何效果——只有简单的对象和数组会被深度响应式化。</p>
<h3 id="sveltecomponent-不再必要"><code><svelte:component></code> 不再必要</h3>
<p>在 Svelte 4 中,组件是 <em>静态的</em> —— 如果您渲染 <code><Thing></code>,并且 <code>Thing</code> 的值发生变化,不会发生任何事情。要使其动态,必须使用 <code><svelte:component></code>。</p>
<p>在 Svelte 5 中,这不再成立:</p>
<pre><code class="language-svelte"><script>
import A from './A.svelte';
import B from './B.svelte';
let Thing = $state();
</script>
<select bind:value={Thing}>
<option value={A}>A</option>
<option value={B}>B</option>
</select>
<!-- 这些是等效的 -->
<Thing />
<svelte:component this={Thing} />
</code></pre>
<h3 id="触控和滚轮事件是-passive">触控和滚轮事件是 passive</h3>
<p>当使用 <code>onwheel</code>、<code>onmousewheel</code>、<code>ontouchstart</code> 和 <code>ontouchmove</code> 事件属性时,处理程序是 passive,以符合浏览器默认行为。这极大地提高了响应能力,因为浏览器可以立即滚动文档,而不是等待查看事件处理程序是否调用 <code>event.preventDefault()</code>。</p>
<p>在极少数需要阻止这些事件默认行为的情况下,你应该使用 <code>on</code>来代替(例如在 action 内部)。</p>
<h3 id="attribute--prop-语法更严格">Attribute / prop 语法更严格</h3>
<p>在 Svelte 4 中,复杂的属性值不需要加引号:</p>
<pre><code class="language-svelte"><Component prop=this{is}valid />
</code></pre>
<p>这是一个潜在问题。在符文模式下,如果您希望连接内容,必须将值放在引号中:</p>
<pre><code class="language-svelte"><Component prop="this{is}valid" />
</code></pre>
<p>注意,如果你在 Svelte 5 中使用引号包裹单个表达式(例如 <code>answer="{42}"</code>),也会收到警告 —— 在 Svelte 6 中,这将导致值被转换为字符串,而不是作为数字传递。</p>
<h3 id="html-结构更严格">HTML 结构更严格</h3>
<p>在Svelte 4中,你可以编写一些在服务器端渲染时会被浏览器修复的HTML代码。例如你可以这样写...</p>
<pre><code class="language-svelte"><table>
<tr>
<td>你好</td>
</tr>
</table>
</code></pre>
<p>...浏览器将自动插入 <code><tbody></code> 元素:</p>
<pre><code class="language-svelte"><table>
<tbody>
<tr>
<td>你好</td>
</tr>
</tbody>
</table>
</code></pre>
<p>Svelte 5 对 HTML 结构的要求更加严格,在浏览器会修复 DOM 的情况下会抛出编译错误。</p>
<h2 id="其他重大变化">其他重大变化</h2>
<h3 id="更严格的-const-赋值验证">更严格的 <code>@const</code> 赋值验证</h3>
<p>不再允许对const声明的解构部分进行赋值。这种操作本就不应该被允许。</p>
<h3 id="is-和-where-是作用域的">:is(...) 和 :where(...) 是作用域的</h3>
<p>以前,Svelte 不分析 <code>:is(...)</code> 和 <code>:where(...)</code> 内部的选择器,实际上会将它们视为全局选择器。Svelte 5 会在当前组件的上下文中分析它们。因此,如果某些选择器依赖于这种处理方式,现在可能会被视为未使用。要修复这个问题,请在 <code>:is(...)/:where(...)</code> 选择器内使用 <code>:global(...)</code>。</p>
<p>在使用 Tailwind 的 <code>@apply</code> 指令时,添加 <code>:global</code> 选择器以保留使用 Tailwind 生成的 <code>:is(...)</code> 选择器的规则:</p>
<pre><code class="language-css">main +++:global+++ {
@apply bg-blue-100 dark:bg-blue-900;
}
</code></pre>
<h3 id="css-哈希位置不再具有确定性">CSS 哈希位置不再具有确定性</h3>
<p>以前,Svelte 总是会在最后插入 CSS 哈希值。在 Svelte 5 中这一点不再有保证。这只有在 有非常奇怪的 css 选择器 时才会导致问题。</p>
<h3 id="作用域-css-使用-where">作用域 CSS 使用 :where(...)</h3>
<p>为了避免由不可预测的特异性变化引起的问题,作用域 CSS 选择器现在使用 <code>.svelte-xyz123</code>(其中 <code>xyz123</code> 如前所述,是 <code><style></code> 内容的哈希)旁边使用 <code>:where(.svelte-xyz123)</code> 选择器修饰符。您可以在 这里 阅读更多细节。</p>
<p>如果您需要支持不实现 <code>:where</code> 的古老浏览器,您可以手动修改生成的 CSS,但代价是会产生不可预测的特异性变化:</p>
<pre><code class="language-js">// @errors: 2552
css = css.replace(/:where\((.+?)\)/, '$1');
</code></pre>
<h3 id="错误警告代码已重命名">错误/警告代码已重命名</h3>
<p>错误和警告代码已重命名。以前它们使用破折号分隔单词,现在使用下划线(例如,foo-bar 变为 foo_bar)。此外,一些代码的措辞也略有改动。</p>
<h3 id="命名空间数量减少">命名空间数量减少</h3>
<p>您可以传递给编译器选项 <code>namespace</code> 的有效命名空间数量减少到 <code>html</code>(默认)、<code>mathml</code> 和 <code>svg</code>。</p>
<p><code>foreign</code> 命名空间仅对 Svelte Native 有用,我们计划在 5.x 次要版本中以不同方式支持它。</p>
<h3 id="beforeupdateafterupdate-变更">beforeUpdate/afterUpdate 变更</h3>
<p>如果 <code>beforeUpdate</code> 修改了模板中引用的变量,则在初始渲染时不再运行两次。</p>
<p>父组件中的 <code>afterUpdate</code> 回调现在将在任何子组件的 <code>afterUpdate</code> 回调之后运行。</p>
<p>当组件包含 <code><slot></code> 且其内容更新时,<code>beforeUpdate/afterUpdate</code> 不再运行。</p>
<p>这两个函数在符文模式下被禁止 —— 请改为使用 <code>$effect.pre(...)</code> 和 <code>$effect(...)</code>。</p>
<h3 id="contenteditable-行为变化"><code>contenteditable</code> 行为变化</h3>
<p>如果您有一个 <code>contenteditable</code> 节点,并且有一个对应的绑定 <em>和</em> 一个响应式值(例如:<code><div contenteditable=true bind:textContent>count is {count}</div></code>),那么contenteditable 内的值不会因 <code>count</code> 的更新而更新,因为绑定会立即完全控制内容,且内容应该只通过绑定来更新。</p>
<h3 id="oneventname-属性不再接受字符串值"><code>oneventname</code> 属性不再接受字符串值</h3>
<p>在Svelte 4中,可以将 HTML 元素的事件属性指定为字符串:</p>
<pre><code class="language-svelte"><button onclick="alert('你好')">...</button>
</code></pre>
<p>不推荐这种做法,在 Svelte 5 中已不再可用,其中 <code>onclick</code> 属性替代 <code>on:click</code> 成为添加事件处理程序的机制。</p>
<h3 id="null-和-undefined-变为空字符串"><code>null</code> 和 <code>undefined</code> 变为空字符串</h3>
<p>在 Svelte 4 中,<code>null</code> 和 <code>undefined</code> 会被打印为对应的字符串。在 100 个案例中,99 次您希望将其变为空字符串,而这也是其他大多数框架所做的。因此,在 Svelte 5 中,<code>null</code> 和 <code>undefined</code> 变为空字符串。</p>
<h3 id="bindfiles-值只能是-nullundefined-或-filelist"><code>bind:files</code> 值只能是 <code>null</code>、<code>undefined</code> 或 <code>FileList</code></h3>
<p><code>bind:files</code> 现在是一个双向绑定。因此,在设置值时,它需要是 假值( <code>null</code> 或 <code>undefined</code>)或 <code>FileList</code> 类型。</p>
<h3 id="绑定现在会响应表单重置">绑定现在会响应表单重置</h3>
<p>之前,绑定不会考虑表单的 <code>reset</code> 事件,因此值可能会与 DOM 不同步。Svelte 5 通过在文档上放置 <code>reset</code> 监听器并在必要时调用绑定来修复这个问题。</p>
<h3 id="walk-不再导出">walk 不再导出</h3>
<p><code>svelte/compiler</code> 为了方便从 <code>estree-walker</code> 重新导出了 <code>walk</code>。在 Svelte 5 中,这种情况不再存在,如果需要请直接从该包中导入。</p>
<h3 id="在-svelteoptions-里的内容被禁止">在 <code>svelte:options</code> 里的内容被禁止</h3>
<p>在 Svelte 4 中,您可以在 <code><svelte:options /></code> 标签内写入内容。它会被忽略,但您可以在里面写一些东西。在 Svelte 5 中,该标签里的内容会导致编译错误。</p>
<h3 id="声明式-shadow-roots-中的-slot-元素会被保留">声明式 shadow roots 中的 <code><slot></code> 元素会被保留</h3>
<p>Svelte 4 在所有地方都用自己版本的插槽替换了 <code><slot /></code> 标签。Svelte 5 在这些标签作为 <code><template shadowrootmode="..."></code> 元素的子元素时会保留它们。</p>
<h3 id="svelteelement-标签必须是表达式"><code><svelte:element></code> 标签必须是表达式</h3>
<p>在 Svelte 4 中,<code><svelte:element this="div"></code> 是有效的代码。这没有什么意义——您应该直接使用 <code><div></code>。在极少数确实需要使用字面值的情况下,你可以这样做:</p>
<pre><code class="language-svelte"><svelte:element this=+++{+++"div"+++}+++>
</code></pre>
<p>请注意,虽然 Svelte 4 会将 <code><svelte:element this="input"></code>(举例)与 <code><input></code> 视为相同,以确定可以应用哪些 <code>bind:</code> 指令。但 Svelte 5 不会这样做。</p>
<h3 id="mount-默认播放过渡效果"><code>mount</code> 默认播放过渡效果</h3>
<p>用于渲染组件树的 <code>mount</code> 函数默认播放过渡效果,除非将 <code>intro</code> 选项设置为 <code>false</code>。这与传统的类组件不同,后者在手动实例化时默认不播放过渡效果。</p>
<h3 id="img-src-和-html--水合不匹配不会被修复"><code><img src={...}></code> 和 <code>{@html ...}</code> 水合不匹配不会被修复</h3>
<p>在 Svelte 4 中,如果 <code>src</code> 属性或 <code>{@html ...}</code> 标签的值在服务端和客户端不同(即水合不匹配),这种不匹配会被修复。这个过程代价很高:设置 <code>src</code> 属性(即使它计算出相同的结果)会导致图像和 iframe 被重新加载,并且重新插入大量 HTML 是缓慢的。</p>
<p>由于这些不匹配极为罕见,Svelte 5 假定这些值保持不变,但在开发环境中如果它们不匹配会向你发出警告。要强制更新,你可以这样做:</p>
<pre><code class="language-svelte"><script>
let { markup, src } = $props();
if (typeof window !== 'undefined') {
// 储存值...
const initial = { markup, src };
// 取消设置它们...
markup = src = undefined;
$effect(() => {
// ...在我们挂载后重置
markup = initial.markup;
src = initial.src;
});
}
</script>
{@html markup}
<img {src} />
</code></pre>
<h3 id="水合行为不同">水合行为不同</h3>
<p>Svelte 5 在服务端渲染期间使用注释,这些注释用于在客户端进行更健壮和高效的水合。因此,如果您打算对其进行水合,您不应该删除HTML输出中的注释,如果您手动编写了要由 Svelte 组件水合的 HTML,则需要在正确的位置添加这些注释。</p>
<h3 id="onevent-属性被委托"><code>onevent</code> 属性被委托</h3>
<p>事件属性替代事件指令:使用 <code>onclick={handler}</code> 而不是 <code>on:click={handler}</code>。为了向后兼容,<code>on:event</code> 语法仍然受到支持,并且行为与 Svelte 4 中相同。然而,某些 <code>onevent</code> 属性是被委托的,这意味着您需要注意不要手动停止这些事件的传播,因为它们可能永远不会传递到根节点的该事件类型的监听器。</p>
<h3 id="--style-props-使用了不同的元素"><code>--style-props</code> 使用了不同的元素</h3>
<p>在使用 CSS 自定义属性时,<code>Svelte 5</code> 使用额外的<code><svelte-css-wrapper></code> 元素而不是 <code><div></code>来包装组件。</p>
<h2 id="svelte-中文文档">Svelte 中文文档</h2>
<p>点击查看中文文档 - Svelte 5迁移指南</p>
<p>系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p><br><br>
来源:https://www.cnblogs.com/yayujs/p/18765099
頁:
[1]