有 React 经验的开发者如何学习 Svelte

Avatar of Adam Rackis
Adam Rackis

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

本文档是针对具有扎实 React 经验的开发者,对 Svelte 进行快速介绍。我将提供一个简要的介绍,然后重点关注状态管理和 DOM 互操作性等方面。我计划快速推进,以便涵盖更多主题。归根结底,我主要希望激发大家对 Svelte 的兴趣。

要了解 Svelte 的基础知识,没有任何博文能比官方的 教程文档 更好。

Svelte 风格的“Hello, World!”

让我们从快速了解 Svelte 组件的外观开始。

<script>
  let number = 0;
</script>

<style>
  h1 {
    color: blue;
  }
</style>

<h1>Value: {number}</h1>

<button on:click={() => number++}>Increment</button>
<button on:click={() => number--}>Decrement</button> 

该内容位于 .svelte 文件中,并由 Rollupwebpack 插件处理以生成 Svelte 组件。这里包含几个部分。让我们逐一了解。

首先,我们添加一个 <script> 标签,其中包含我们需要的任何状态。

我们还可以添加一个 <style> 标签,其中包含我们想要的任何 CSS。这些样式以作用域限定到组件的方式,使得此处组件中的 <h1> 元素将显示为蓝色。是的,作用域样式内置于 Svelte 中,无需任何外部库。在 React 中,通常需要使用第三方解决方案来实现作用域样式,例如 css-modulesstyled-components 或类似的库(有数十种,甚至数百种选择)。

然后是 HTML 标记。正如您所料,您需要学习一些 HTML 绑定,例如 {#if}{#each} 等。这些特定于领域的语言特性可能看起来像是从 React 退步了一步,在 React 中,所有内容都是“仅仅是 JavaScript”。但有一些值得注意的地方:Svelte 允许您在这些绑定内部放置任意 JavaScript 代码。因此,类似这样的代码是完全有效的

{#if childSubjects?.length}

如果您是从 Knockout 或 Ember 迁移到 React 并从未回头,那么这可能会让您感到惊喜(惊喜)。

此外,Svelte 处理组件的方式与 React 非常不同。React 在组件内部或任何祖先(除非您“记忆化”)的任何状态发生变化时,都会重新运行所有组件。这可能会导致效率低下,这就是 React 提供 useCallbackuseMemo 等功能来防止不必要的重新计算数据的原因。

另一方面,Svelte 会分析您的模板,并在任何相关状态发生变化时创建有针对性的 DOM 更新代码。在上面的组件中,Svelte 将看到 number 发生变化的位置,并在突变完成后添加更新 <h1> 文本的代码。这意味着您无需担心记忆化函数或对象。实际上,您甚至不必担心副作用依赖项列表,尽管我们稍后会讲到这一点。

但首先,让我们谈谈……

状态管理

在 React 中,当我们需要管理状态时,我们会使用 useState 钩子。我们向它提供一个初始值,它会返回一个元组,其中包含当前值和一个用于设置新值的函数。它看起来像这样

import React, { useState } from "react";

export default function (props) {
  const [number, setNumber] = useState(0);
  return (
    <>
      <h1>Value: {number}</h1>
      <button onClick={() => setNumber(n => n + 1)}>Increment</button>
      <button onClick={() => setNumber(n => n - 1)}>Decrement</button>
    </>
  );
}

我们的 setNumber 函数可以传递到任何我们想要的地方,例如子组件等。

在 Svelte 中,事情变得更简单。我们可以创建一个变量,并根据需要更新它。Svelte 的提前编译(与 React 的即时编译相反)将完成跟踪其更新位置并强制更新 DOM 的工作。上面相同的简单示例可能如下所示

<script>
  let number = 0;
</script>

<h1>Value: {number}</h1>
<button on:click={() => number++}>Increment</button>
<button on:click={() => number--}>Decrement</button>

这里还需要注意的是,Svelte 不需要像 JSX 那样进行单一包装元素。Svelte 没有 React 片段 <></> 语法的等效项,因为它不需要。

但是,如果我们想将更新器函数传递给子组件以便它可以更新这部分状态,就像我们可以在 React 中做的那样,该怎么办?我们可以像这样编写更新器函数

<script>
  import Component3a from "./Component3a.svelte";
        
  let number = 0;
  const setNumber = cb => number = cb(number);
</script>

<h1>Value: {number}</h1>

<button on:click={() => setNumber(val => val + 1)}>Increment</button>
<button on:click={() => setNumber(val => val - 1)}>Decrement</button>

现在,我们将它传递到需要的地方——或者继续关注更自动化的解决方案。

Reducers 和 Stores

React 还有 useReducer 钩子,它允许我们建模更复杂的状态。我们提供一个 reducer 函数,它会提供给我们当前值和一个 dispatch 函数,该函数允许我们使用给定参数调用 reducer,从而触发状态更新,更新为 reducer 返回的任何内容。我们上面提到的计数器示例可能如下所示

import React, { useReducer } from "react";

function reducer(currentValue, action) {
  switch (action) {
    case "INC":
      return currentValue + 1;
    case "DEC":
      return currentValue - 1;
  }
}

export default function (props) {
  const [number, dispatch] = useReducer(reducer, 0);
  return (
    <div>
      <h1>Value: {number}</h1>
      <button onClick={() => dispatch("INC")}>Increment</button>
      <button onClick={() => dispatch("DEC")}>Decrement</button>
    </div>
  );
}

Svelte 没有直接提供类似的功能,但它提供了一个名为Store的功能。最简单的 Store 类型是可写 Store。它是一个保存值的 Object。要设置新值,您可以调用 Store 上的 set 并传递新值,或者您可以调用 update 并传入一个回调函数,该函数接收当前值并返回新值(与 React 的 useState 完全一样)。

要在某个时间点读取 Store 的当前值,有一个 get 函数 可以调用,它会返回其当前值。Store 还有一个 subscribe 函数,我们可以向它传递一个回调函数,并且只要值发生变化,该回调函数就会运行。

Svelte 毕竟是 Svelte,所有这一切都有一些不错的语法快捷方式。例如,如果您在组件内部,您可以简单地在 Store 前缀上添加美元符号以读取其值,或者直接为其赋值以更新其值。以下是上面使用 Store 的计数器示例,以及一些额外的副作用日志,以演示 subscribe 的工作原理

<script>
  import { writable, derived } from "svelte/store";
        
  let writableStore = writable(0);
  let doubleValue = derived(writableStore, $val => $val * 2);
        
  writableStore.subscribe(val => console.log("current value", val));
  doubleValue.subscribe(val => console.log("double value", val))
</script>

<h1>Value: {$writableStore}</h1>

<!-- manually use update -->
<button on:click={() => writableStore.update(val => val + 1)}>Increment</button>
<!-- use the $ shortcut -->
<button on:click={() => $writableStore--}>Decrement</button>

<br />

Double the value is {$doubleValue}

请注意,我在上面还添加了一个派生 Store。文档 对此进行了深入介绍,但简而言之,derived Store 允许您使用与可写 Store 相同的语义,将一个 Store(或多个 Store)投影到一个新的值。

Svelte 中的 Store 非常灵活。我们可以将它们传递给子组件、更改、组合它们,甚至可以通过派生 Store 传递它们来使它们只读;如果我们要将一些 React 代码转换为 Svelte,我们甚至可以重新创建我们可能喜欢或需要的某些 React 抽象。

使用 Svelte 模拟 React API

解决了这些问题后,让我们回到之前 React 的 useReducer 钩子。

假设我们非常喜欢定义 reducer 函数来维护和更新状态。让我们看看利用 Svelte Store 模拟 React 的 useReducer API 会有多么困难。我们基本上希望调用我们自己的 useReducer,传入一个带有初始值的 reducer 函数,并获得一个包含当前值的 Store 以及一个调用 reducer 并更新 Store 的 dispatch 函数。实现这一点实际上并不太困难。

export function useReducer(reducer, initialState) {
  const state = writable(initialState);
  const dispatch = (action) =>
    state.update(currentState => reducer(currentState, action));
  const readableState = derived(state, ($state) => $state);

  return [readableState, dispatch];
}

在 Svelte 中的使用方式几乎与 React 完全相同。唯一的区别是我们的当前值是一个 Store,而不是一个原始值,因此我们需要在它前面加上 $ 来读取值(或手动调用 getsubscribe)。

<script>
  import { useReducer } from "./useReducer";
        
  function reducer(currentValue, action) {
    switch (action) {
      case "INC":
        return currentValue + 1;
      case "DEC":
        return currentValue - 1;
    }
  }
  const [number, dispatch] = useReducer(reducer, 0);      
</script>

<h1>Value: {$number}</h1>

<button on:click={() => dispatch("INC")}>Increment</button>
<button on:click={() => dispatch("DEC")}>Decrement</button>

useState 怎么样?

如果您真的喜欢 React 中的 useState 钩子,那么实现它也同样简单。在实践中,我发现这不是一个有用的抽象,但它是一个有趣的练习,它确实展示了 Svelte 的灵活性。

export function useState(initialState) {
  const state = writable(initialState);
  const update = (val) =>
    state.update(currentState =>
      typeof val === "function" ? val(currentState) : val
    );
  const readableState = derived(state, $state => $state);

  return [readableState, update];
}

双向绑定真的很糟糕吗?

在结束本状态管理部分之前,我想谈谈 Svelte 独有的最后一个技巧。我们已经看到,Svelte 允许我们以任何 React 可以使用的方式将更新器函数向下传递到组件树。这通常是为了允许子组件通知其父组件状态更改。我们已经做了无数次了。子组件以某种方式更改状态,然后调用从父组件传递给它的函数,以便父组件可以知道该状态更改。

除了支持此回调传递之外,Svelte 还允许父组件双向绑定到子组件的状态。例如,假设我们有这个组件

<!-- Child.svelte -->
<script>
  export let val = 0;
</script>

<button on:click={() => val++}>
  Increment
</button>

Child: {val}

这将创建一个组件,并带有一个 val prop。export 关键字是 Svelte 中组件声明 prop 的方式。通常,使用 prop 时,我们会将它们传递给组件,但这里我们会做一些不同的事情。如我们所见,此 prop 由子组件修改。在 React 中,此代码将是错误的且存在 bug,但在 Svelte 中,渲染此组件的组件可以执行以下操作

<!-- Parent.svelte -->
<script>
  import Child from "./Child.svelte";
        
  let parentVal;
</script>

<Child bind:val={parentVal} />
Parent Val: {parentVal}

在这里,我们将父组件中的一个变量绑定到子组件的 val prop。现在,当子组件的 val prop 发生变化时,我们的 parentVal 将由 Svelte 自动更新。

双向绑定对某些人来说是有争议的。如果您不喜欢它,那么请随意,不要使用它。但如果谨慎使用,我发现它是一个非常方便的工具,可以减少样板代码。

Svelte 中的副作用,无需泪水(或过时的闭包)

在 React 中,我们使用 useEffect 钩子来管理副作用。它看起来像这样

useEffect(() => {
  console.log("Current value of number", number);
}, [number]);

我们在函数末尾编写依赖项列表。在每次渲染时,React 会检查列表中的每个项目,如果任何项目与上次渲染时的引用不同,则回调函数会重新运行。如果我们希望在上次运行后进行清理,则可以从效果中返回一个清理函数。

对于简单的事情,例如数字变化,这很容易。但是,任何有经验的 React 开发人员都知道,对于非平凡的用例,useEffect 可能非常难以处理。意外地遗漏依赖项数组中的某些内容并最终导致过时的闭包,这出奇地容易。

在 Svelte 中,处理副作用的最基本形式是反应式语句,它看起来像这样

$: {
  console.log("number changed", number);
}

我们在代码块前加上 $: 并将我们希望执行的代码放在其中。Svelte 会分析读取了哪些依赖项,并在它们发生变化时重新运行我们的代码块。没有直接的方法让清理从反应式代码块上次运行时开始运行,但如果我们确实需要它,可以很容易地解决。

let cleanup;
$: {
  cleanup?.();
  console.log("number changed", number);
  cleanup = () => console.log("cleanup from number change");
}

不,这不会导致无限循环:反应式代码块内部的重新赋值不会重新触发代码块。

虽然这可以工作,但通常这些清理效果需要在组件卸载时运行,而 Svelte 为此内置了一个功能:它有一个 onMount 函数,它允许我们返回一个在组件销毁时运行的清理函数,更直接地,它还有一个 onDestroy 函数,它可以执行您期望的操作。

使用 action 增添趣味

以上所有方法都足够有效,但 Svelte 凭借 action 更加出色。副作用通常与我们的 DOM 节点相关联。我们可能希望在一个 DOM 节点上集成一个旧的(但仍然很棒的)jQuery 插件,并在该节点离开 DOM 时将其拆除。或者我们可能希望为一个节点设置一个 ResizeObserver,并在该节点离开 DOM 时将其拆除,等等。这是一个非常常见的要求,因此 Svelte 使用 action 内置了它。让我们看看如何操作。

{#if show}
  <div use:myAction>
    Hello                
  </div>
{/if}

请注意 use:actionName 语法。在这里,我们将此 <div> 与名为 myAction 的 action 关联,该 action 只是一个函数。

function myAction(node) {
  console.log("Node added", node);
}

此 action 在 <div> 进入 DOM 时运行,并将 DOM 节点传递给它。这是我们添加 jQuery 插件、设置 ResizeObserver 等的机会。不仅如此,我们还可以从中返回一个清理函数,如下所示

function myAction(node) {
  console.log("Node added", node);

  return {
    destroy() {
      console.log("Destroyed");
    }
  };
}

现在,destroy() 回调函数将在节点离开 DOM 时运行。在这里,我们将拆除我们的 jQuery 插件等。

但是,还有更多!

我们甚至可以将参数传递给 action,如下所示

<div use:myAction={number}>
  Hello                
</div>

该参数将作为第二个参数传递给我们的 action 函数

function myAction(node, param) {
  console.log("Node added", node, param);

  return {
    destroy() {
      console.log("Destroyed");
    }
  };
}

如果您希望在该参数发生变化时执行其他工作,则可以返回一个更新函数

function myAction(node, param) {
  console.log("Node added", node, param);

  return {
    update(param) {
      console.log("Update", param);
    },
    destroy() {
      console.log("Destroyed");
    }
  };
}

当 action 的参数发生变化时,更新函数将运行。要将多个参数传递给 action,我们传递一个对象

<div use:myAction={{number, otherValue}}>
  Hello                
</div>

……并且 Svelte 在对象的任何属性发生变化时重新运行我们的更新函数。

Action 是我最喜欢的 Svelte 功能之一;它们功能非常强大。

其他功能

Svelte 还提供了一些在 React 中没有对应功能的强大特性。它有许多表单绑定(教程中有介绍),以及 CSS 辅助工具

来自 React 的开发者可能会惊讶地发现,Svelte 也自带动画支持。无需在 npm 上搜索并寄希望于最好的,它……是内置的。它甚至包括对 弹簧物理和进入/退出动画的支持,Svelte 将其称为转换

Svelte 对 React.Chidren 的答案是插槽,可以命名也可以不命名,并且在 Svelte 文档中进行了很好的介绍。我发现它们比 React 的 Children API 更容易理解。

最后,我最喜欢的一个几乎隐藏的 Svelte 功能是,它可以将其组件编译成真正的 Web Components。该 svelte:options 辅助工具有一个 tagName 属性可以启用此功能。但请确保在 webpack 或 Rollup 配置中设置相应的属性。使用 webpack,它看起来像这样

{
  loader: "svelte-loader",
  options: {
    customElement: true
  }
}

有兴趣尝试 Svelte 吗?

以上任何一项都可以成为一篇很棒的博文。虽然我们可能只是触及了状态管理和 action 等内容的表面,但我们看到了 Svelte 的功能不仅与 React 相匹配,甚至可以模仿 React 的许多 API。这还是在我们简要提及 Svelte 的便利功能之前,例如内置动画(或转换)以及将 Svelte 组件转换为真正的 Web Components 的能力。

我希望我成功地激发了一些兴趣,如果我做到了,那么在深入探讨这些主题(以及更多主题)方面,文档、教程、在线课程等资源并不少。如果您在此过程中有任何疑问,请在评论中告诉我!