发布于 2026-01-05 2 阅读
0

别让 Web 开发过于复杂——试试 Svelte

别让 Web 开发过于复杂——试试 Svelte

Svelte 在 2021 年被开发者评为最受欢迎的 Web 框架(链接)。那么,Svelte 是什么?它为什么如此受欢迎?

Svelte 是一个相当独特的 JavaScript 框架,旨在实现“真正的响应式”,并帮助开发人员“编写更少的代码”。

它拥有一个令人惊叹的特性:通过在编译阶段将框架本身“隐藏”起来,生成极小的代码包,从而交付优化的原生 JavaScript 代码,而不是在运行时加载庞大而复杂的库。
在我看来,这正是 Svelte 区别于其他 JavaScript 框架的关键所在。极小的代码包意味着更快的加载速度,而这似乎正是 Web 的发展方向,越来越多的数据表明,快速的网站至关重要。此外,这种编译阶段也省去了 React 和 Vue 等框架使用的虚拟 DOM 等技术,进一步提升了网站速度。

另一个特点是无需编写样板代码。Svelte 的开发体验非常接近标准的 Web 开发,组件可以完全像原生 HTML 一样。我相信这正是开发者喜爱这个框架的重要原因。

为了介绍 Svelte,我们将使用 Poke API创建一个简单的单页应用,用户可以在其中选择宝可梦,并通过实时搜索栏筛选所有宝可梦。这将以一种实用的方式演示 Svelte 的所有核心功能。完整代码可以在这里找到。


目录

  1. 安装
  2. 组件特性
  3. 变量与反应性
  4. onMount 和异步获取
  5. 响应式声明
  6. 循环
  7. 条件渲染
  8. 组件和道具
  9. 自定义事件
  10. 绑定转发
  11. 商店
  12. 最后说明

安装

我们首先来安装基本的 Svelte。为此,请运行以下命令。

npx degit sveltejs/template new-svelte-project

这会将 Svelte 入门模板复制到您指定的文件夹中。
要启用 TypeScript,请进入新的 Svelte 文件夹并运行以下命令。

node scripts/setupTypeScript.js

现在您只需运行以下命令来安装必要的文件即可。

npm install

组件特性

Svelte 组件是一个以 .svelte 结尾的文件。
正如您在 App.svelte 文件中看到的,一个 Svelte 组件可以非常简单,通常包含三个部分:HTML 代码、用于放置 JavaScript 代码的 script 标签以及用于放置 CSS 代码的 style 标签。
这与 Vue 类似,只是省去了样板代码。

清除 App.svelte 中的 script 和 html 内容,然后让我们在 style 标签中使用给定的 css。

变量与反应性

变量是在 script 标签中创建的。
我们可以使用花括号 {} 轻松创建字符串变量并将其显示在 DOM 中。

<!-- For example -->
<script lang="ts">
  let name: string = 'pokemon searcher'
</script>

<h1>{name}</h1>
Enter fullscreen mode Exit fullscreen mode

要搜索宝可梦名称,我们需要一个输入框和一个用于存储该输入框内容的变量。
为了使变量“pokemonName”等于输入框的内容,我们可以使用 Svelte 的一个特殊关键字“bind”,它支持变量“pokemonName”的双向绑定。

<!-- App.svelte -->
<script lang="ts">
  let pokemonName: string = ''
</script>

<main>
  <span>Search: </span>
  <input type="text" bind:value="{pokemonName}" />

  <h1>Pokemon: {pokemonName}</h1>
</main>
Enter fullscreen mode Exit fullscreen mode

现在,在输入框中输入内容会改变宝可梦名称的输出。
这个 bind 关键字实现了双向绑定,而无需像 React 那样使用 'onInput' 函数来改变 'pokemonName' 变量的值。

onMount 和异步获取

在这个示例应用程序中,我们将来自 pokeapi 的宝可梦名称存储在一个变量中,该变量是一个字符串数组。

我们希望在组件渲染完成后立即获取数据并将其映射到组件中。
为此,我们可以使用 `onMount`,这是一个 Svelte 生命周期函数,会在组件首次渲染到 DOM 后运行。让我们使用它从 PokeAPI 获取数据并将其映射到一个宝可梦名称数组中。

<!-- App.svelte - script tag -->
<script lang="ts">
  import { onMount } from 'svelte'

  let pokemonName: string = ''

  // Fetch from api then store the mapped names.
  let pokemonData: string[] = []
  onMount(() => {
    const setPokemonData = async (): Promise<void> => {
      const rawPokemonData = await (
        await fetch('https://pokeapi.co/api/v2/pokemon?limit=99')
      ).json()

      pokemonData = rawPokemonData.results.map(
        (p: { name: string; url: string }) => p.name
      )
    }
    setPokemonData()
  })
</script>
Enter fullscreen mode Exit fullscreen mode

现在我们有了一个名为“pokemonData”的宝可梦名称列表,我们可以在我们的简单项目中使用它。

响应式声明

对于实时搜索功能,我们需要一个数组来保存根据用户输入的宝可梦名称筛选出的项目。

Svelte 提供了一个非常棒的工具来处理从其他属性派生的状态,即响应式声明。
它们看起来像这样。

$: reactiveVar = otherVar * 2;
Enter fullscreen mode Exit fullscreen mode

现在,`reactiveVar` 是一个变量,但它的值会在每次 `otherVar` 变量发生变化时重新计算(Svelte 会在计算中使用的变量发生变化时运行计算)。
我们可以将保存筛选后的宝可梦名称的变量声明为响应式变量,并将其命名为 `suggestions`。

<!-- App.svelte - bottom of script tag -->
<script>
  // ...

  let suggestions: string[]
  $: suggestions = 
       pokemonName.length > 0
         ? pokemonData.filter(
             (name) => name.includes(pokemonName)
           )
         : pokemonData
</script>
Enter fullscreen mode Exit fullscreen mode

所以,`suggestions` 是一个包含宝可梦名称的数组,其中包含了输入框中输入的字符串。
响应式赋值在 TypeScript 中不起作用,因此我们可以像平常一样声明一个 `suggestions` 变量来保留类型检查。

循环

我们需要将“suggestions”数组的内容显示在页面上,这可以通过使用 Svelte 循环来实现。Svelte 有一个特殊的关键字“each”,它允许我们为给定的可迭代对象中的每个元素显示相应的 DOM 元素。
要显示每个宝可梦的名字,只需使用“each”关键字遍历“pokemonData”变量即可。

<!-- App.svelte - html -->
<main>
  <span>Search: </span>
  <input type="text" bind:value="{pokemonName}" />

  {#each suggestions as suggestion}
    <h2>{suggestion}</h2>
  {/each}
</main>
Enter fullscreen mode Exit fullscreen mode

当我们在输入框中输入内容时,可以看到建议列表随之变化。对于如此简单的代码来说,这真是太棒了。

条件渲染

Svelte 还有其他关键字。另一个很有用的关键字是 #if。
#if 关键字允许使用条件逻辑。
例如,我们可以在从 Pokeapi 获取数据时渲染加载屏幕。

<!-- App.svelte - html -->
<main>
  {#if pokemonData && pokemonData.length > 0}
    <span>Search: </span>
    <input type="text" bind:value="{pokemonName}" />

    {#each suggestions as suggestion}
      <h2>{suggestion}</h2>
    {/each}
  {:else}
    <h2>Loading...</h2>
  {/if}
</main>
Enter fullscreen mode Exit fullscreen mode

组件和道具

组件属性(Props)用于在组件之间传递数据。这在框架中相当普遍,语法也很简单。
要表明某个组件接受一个属性,需要使用 `export` 关键字来声明变量。

<script lang="ts">
  export let stringProp: string
</script>
Enter fullscreen mode Exit fullscreen mode

现在,要传递“stringProp”的值,只需在编写组件时使用导出变量的名称即可。

<script lang="ts">
  import NewComponent from './NewComponent.svelte'
</script>

<NewComponent  stringProp="prop value"  />
Enter fullscreen mode Exit fullscreen mode

对于我们的应用程序,让我们为每个建议创建一个组件。
在 src/ 目录下创建一个名为“Suggestion.svelte”的新文件,并简单地接收和显示一个“suggestion”属性。

<!-- Suggestion.svelte -->
<script lang="ts">
  export let suggestion: string
</script>

<div class="suggestion">{suggestion}</div>

<style>
    .suggestion {
        font-size: 1.25rem;
    }
</style>

Enter fullscreen mode Exit fullscreen mode

现在,我们可以导入这个组件,并在我们的 #each 循环中使用它。

<!-- App.svelte - top of script tag -->
<script lang="ts">
  import Suggestion from './Suggestion.svelte'

  // ...
  // ...
</script>
Enter fullscreen mode Exit fullscreen mode
<!-- App.svelte - html -->
<main>
  {#if pokemonData && pokemonData.length > 0}
    <span>Search: </span>
    <input type="text" bind:value="{pokemonName}" />

    {#each suggestions as suggestion}
      <Suggestion suggestion="{suggestion}"/>
    {/each}
  {:else}
    <h2>Loading...</h2>
  {/if}
</main>
Enter fullscreen mode Exit fullscreen mode

目前这样做意义不大,所以我们以事件的形式为“建议”组件添加一些逻辑。

自定义事件

自定义事件可以从一个组件分发到另一个组件,从而实现父子组件之间的通信。
在我们的应用中,我们希望能够点击建议来选择宝可梦。我们可以通过从“建议”组件向“应用”组件分发一个自定义事件,然后设置一个变量的值来实现这一点,该变量存储了我们选择的宝可梦。

首先,在 App.svelte 中创建新的“chosenPokemon”变量,并将其显示在屏幕上。

<!-- App.svelte - bottom of script tag -->
<script lang="ts">
  // ...
  // ...

  let chosenPokemon: string = ''
</script>
Enter fullscreen mode Exit fullscreen mode
<!-- App.svelte - html -->
<main>
  {#if pokemonData && pokemonData.length > 0}
    <h1>Chose Your Pokemon</h1>
    <h2>Chosen Pokemon: {chosenPokemon}</h2>

    <div>
      <span>Search: </span>
      <input type="text" bind:value="{pokemonName}" />

      {#each suggestions as suggestion}
        <Suggestion suggestion="{suggestion}"/>
      {/each}
    </div>
  {:else}
    <h2>Loading...</h2>
  {/if}
</main>
Enter fullscreen mode Exit fullscreen mode

现在,在 Suggestion.svelte 中,我们可以在点击建议时触发自定义的 'chosePokemon' 事件。
要创建自定义事件,我们需要从 svelte 导入 'createEventDispatcher'。

<!-- Suggestion.svelte - script tag -->
<script lang="ts">
  import { createEventDispatcher } from 'svelte'

  export let suggestion: string

  // Function to dispatch a custom event.
  const dispatch = createEventDispatcher()
  const chosePokemon = (): void => {
    dispatch('chosePokemon', {
      pokemon: suggestion
    })
  }
</script>
Enter fullscreen mode Exit fullscreen mode

我们现在有了一个“choosePokemon”函数,它会向父组件分发一个自定义的“choosePokemon”事件。

要在点击建议时调用此函数,我们需要像这样使用 svelte 的 'on:click' 事件。

<!-- Suggestion.svelte - html -->
<div class="suggestion" on:click="{chosePokemon}">
  {suggestion}
</div>
Enter fullscreen mode Exit fullscreen mode

回到 App.svelte 文件中,我们可以使用 'on:(事件名称)' 语法来处理这个自定义事件。

<!-- App.svelte - 'Suggestion' component in html -->
<Suggestion
  suggestion="{suggestion}"
  on:chosePokemon="{(e) => {
    chosenPokemon = e.detail.pokemon
  }}"
/>
Enter fullscreen mode Exit fullscreen mode

此处理程序将 chosenPokemon 变量的值设置为自定义事件中传入的宝可梦名称(位于“detail”属性中)。
当我们点击建议时,就会显示该宝可梦名称。

我这样设置了“chosenPokemon”变量来引入自定义事件,但是,有一种更简洁、更简单的方法可以做到这一点:绑定转发。

绑定转发

我们看到了如何使用 bind 关键字在创建输入字段时设置双向绑定,但这个关键字也可以在我们的组件中使用。

在 App.svelte 中,我们可以将 chosenPokemon 事件处理程序替换为 chosenPokemon 属性上的 bind 关键字。

<!-- App.svelte - 'Suggestion' component in html -->
<Suggestion suggestion="{suggestion}" bind:chosenPokemon />
Enter fullscreen mode Exit fullscreen mode

在“建议”组件中,我们可以接受这个属性,并使“on:click”函数简单地设置这个“chosenPokemon”变量。

<!-- Suggestion.svelte -->
<script lang="ts">
  export let suggestion: string
  export let chosenPokemon: string = ''
</script>

<div 
  class="suggestion" 
  on:click="{() => chosenPokemon = suggestion}"
>
  {suggestion}
</div>
Enter fullscreen mode Exit fullscreen mode

现在,我们仅用了之前代码的一小部分就实现了相同的功能。

商店

最后我想介绍一下 store。
使用 Svelte,我们不需要像 Redux 这样的外部库来实现中心化 store,框架本身就自带这个功能。

从本质上讲,store 是一个带有 subscribe 方法的对象,它允许 Svelte 组件在 store 值发生任何更改时收到通知。Svelte 定义了两种不同类型的 store:可写 store 和可读 store。顾名思义,可写 store 允许读写操作,而可读 store 只允许读取操作。

在我们的应用中,我们会将“pokemonData”变量发送到 store 中。由于该变量只是收集和存储宝可梦数据,我们将使用可读的 store。

首先,我们需要在 src 文件夹中创建一个新文件(我将其命名为“stores.ts”)。
我们可以从 Svelte 导入易于理解的 store 函数以及所需的类型定义。

// stores.ts
import { readable, Readable, Subscriber } from 'svelte/store'
Enter fullscreen mode Exit fullscreen mode

一个可读性强的函数,其第一个参数是 store 的初始值,第二个参数是一个“start”函数。
当 store 获得第一个订阅者时,会调用这个“start”函数,因此我们将从中获取 API 数据。
该函数接收一个“set”回调函数,用于设置 store 的值,并返回一个“stop”函数,该函数会在最后一个订阅者取消订阅时调用(我们可以在这里执行一些清理工作)。

在我们的商店中,我们可以简单地复制“setPokemonData”函数的内容,但是我们不给“pokemonData”赋值,而是调用“set”函数。

import { readable, Readable, Subscriber } from 'svelte/store'

const setPokemonData = 
  async (set: Subscriber<string[]>): Promise<void> => {
    const rawPokemonData = await (
      await fetch('https://pokeapi.co/api/v2/pokemon?limit=99')
    ).json()

    set(
      rawPokemonData.results.map(
        (p: { name: string; url: string }) => p.name
      )
    )
  }

// Export the new store 'pokemonData' variable.
export const pokemonData: Readable<string[]> = 
  readable([], (set) => {
    setPokemonData(set)

    return () => set([])
  })

Enter fullscreen mode Exit fullscreen mode

就这样。我们现在有了一个中央存储库,名为“pokemonData”,用于存储我们的宝可梦名称。

要使用我们的 store,我们需要从 store 文件中导入 'pokemonData' 变量。
然后我们可以使用 Svelte 特有的 '$' 符号来引用 store 的值。

<!-- App.svelte -->
<script lang="ts">
  import { pokemonData } from './stores.js'
  import Suggestion from './Suggestion.svelte'

  let pokemonName: string = ''

  let suggestions: string[]
  // $pokemonData instead of pokemonData
  $: suggestions =
    pokemonName.length > 0
      ? $pokemonData.filter((name) => 
          name.includes(pokemonName)
        )
      : $pokemonData

  let chosenPokemon: string = ''
</script>

<main>
  {#if $pokemonData && $pokemonData.length > 0}
    <h1>Chose Your Pokemon</h1>
    <h2>Chosen Pokemon: {chosenPokemon}</h2>

    <div>
      <span>Search: </span>
      <input type="text" bind:value="{pokemonName}" />
      {#each suggestions as suggestion}
        <Suggestion 
          suggestion="{suggestion}" 
          bind:chosenPokemon 
        />
      {/each}
    </div>
  {:else}
    <h2>Loading...</h2>
  {/if}
</main>
Enter fullscreen mode Exit fullscreen mode

我们的应用程序运行方式相同,但我们的 API 数据现在集中存储,可以在任何组件中使用。

虽然 Svelte 本身就支持可写和可读的 store,但任何遵循 Svelte store“契约”并实现了 `subscribe` 方法的对象都可以被视为 store。
这意味着 store 非常灵活,可以根据你的需求进行定制。你甚至可以使用其他语言(例如 Rust)来创建 store,就像这里展示的那样。

最后说明

在众多 JavaScript 框架中,Svelte 脱颖而出,因为它既能兼顾用户体验,又不会为了开发者体验而牺牲用户体验。Svelte
提供了一系列强大的工具,让应用开发变得轻松便捷,同时其编译器还能生成体积小巧的软件包供最终用户使用,显著缩短下载时间。

Svelte 一直是备受青睐的框架之一,其使用量持续增长,这或许预示着它未来将与 Vue 或 React 比肩。这与近期 Web 性能提升的趋势不谋而合,即从向客户端提供庞大的 JavaScript 包转向服务器端渲染或混合渲染。

Svelte 团队目前正在开发 SvelteKit,它是 Svelte 版本的 Next.js,您可以在这里了解更多信息。


如果您喜欢这篇文章,请考虑分享。也欢迎访问我的GitHub 页面
阅读我的其他文章

文章来源:https://dev.to/emmanuilsb/stop-overcomplicating-web-development-try-svelte-47ln