别让 Web 开发过于复杂——试试 Svelte
Svelte 在 2021 年被开发者评为最受欢迎的 Web 框架(链接)。那么,Svelte 是什么?它为什么如此受欢迎?
Svelte 是一个相当独特的 JavaScript 框架,旨在实现“真正的响应式”,并帮助开发人员“编写更少的代码”。
它拥有一个令人惊叹的特性:通过在编译阶段将框架本身“隐藏”起来,生成极小的代码包,从而交付优化的原生 JavaScript 代码,而不是在运行时加载庞大而复杂的库。
在我看来,这正是 Svelte 区别于其他 JavaScript 框架的关键所在。极小的代码包意味着更快的加载速度,而这似乎正是 Web 的发展方向,越来越多的数据表明,快速的网站至关重要。此外,这种编译阶段也省去了 React 和 Vue 等框架使用的虚拟 DOM 等技术,进一步提升了网站速度。
另一个特点是无需编写样板代码。Svelte 的开发体验非常接近标准的 Web 开发,组件可以完全像原生 HTML 一样。我相信这正是开发者喜爱这个框架的重要原因。
为了介绍 Svelte,我们将使用 Poke API创建一个简单的单页应用,用户可以在其中选择宝可梦,并通过实时搜索栏筛选所有宝可梦。这将以一种实用的方式演示 Svelte 的所有核心功能。完整代码可以在这里找到。
目录
安装
我们首先来安装基本的 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>
要搜索宝可梦名称,我们需要一个输入框和一个用于存储该输入框内容的变量。
为了使变量“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>
现在,在输入框中输入内容会改变宝可梦名称的输出。
这个 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>
现在我们有了一个名为“pokemonData”的宝可梦名称列表,我们可以在我们的简单项目中使用它。
响应式声明
对于实时搜索功能,我们需要一个数组来保存根据用户输入的宝可梦名称筛选出的项目。
Svelte 提供了一个非常棒的工具来处理从其他属性派生的状态,即响应式声明。
它们看起来像这样。
$: reactiveVar = otherVar * 2;
现在,`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>
所以,`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>
当我们在输入框中输入内容时,可以看到建议列表随之变化。对于如此简单的代码来说,这真是太棒了。
条件渲染
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>
组件和道具
组件属性(Props)用于在组件之间传递数据。这在框架中相当普遍,语法也很简单。
要表明某个组件接受一个属性,需要使用 `export` 关键字来声明变量。
<script lang="ts">
export let stringProp: string
</script>
现在,要传递“stringProp”的值,只需在编写组件时使用导出变量的名称即可。
<script lang="ts">
import NewComponent from './NewComponent.svelte'
</script>
<NewComponent stringProp="prop value" />
对于我们的应用程序,让我们为每个建议创建一个组件。
在 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>
现在,我们可以导入这个组件,并在我们的 #each 循环中使用它。
<!-- App.svelte - top of script tag -->
<script lang="ts">
import Suggestion from './Suggestion.svelte'
// ...
// ...
</script>
<!-- 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>
目前这样做意义不大,所以我们以事件的形式为“建议”组件添加一些逻辑。
自定义事件
自定义事件可以从一个组件分发到另一个组件,从而实现父子组件之间的通信。
在我们的应用中,我们希望能够点击建议来选择宝可梦。我们可以通过从“建议”组件向“应用”组件分发一个自定义事件,然后设置一个变量的值来实现这一点,该变量存储了我们选择的宝可梦。
首先,在 App.svelte 中创建新的“chosenPokemon”变量,并将其显示在屏幕上。
<!-- App.svelte - bottom of script tag -->
<script lang="ts">
// ...
// ...
let chosenPokemon: string = ''
</script>
<!-- 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>
现在,在 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>
我们现在有了一个“choosePokemon”函数,它会向父组件分发一个自定义的“choosePokemon”事件。
要在点击建议时调用此函数,我们需要像这样使用 svelte 的 'on:click' 事件。
<!-- Suggestion.svelte - html -->
<div class="suggestion" on:click="{chosePokemon}">
{suggestion}
</div>
回到 App.svelte 文件中,我们可以使用 'on:(事件名称)' 语法来处理这个自定义事件。
<!-- App.svelte - 'Suggestion' component in html -->
<Suggestion
suggestion="{suggestion}"
on:chosePokemon="{(e) => {
chosenPokemon = e.detail.pokemon
}}"
/>
此处理程序将 chosenPokemon 变量的值设置为自定义事件中传入的宝可梦名称(位于“detail”属性中)。
当我们点击建议时,就会显示该宝可梦名称。
我这样设置了“chosenPokemon”变量来引入自定义事件,但是,有一种更简洁、更简单的方法可以做到这一点:绑定转发。
绑定转发
我们看到了如何使用 bind 关键字在创建输入字段时设置双向绑定,但这个关键字也可以在我们的组件中使用。
在 App.svelte 中,我们可以将 chosenPokemon 事件处理程序替换为 chosenPokemon 属性上的 bind 关键字。
<!-- App.svelte - 'Suggestion' component in html -->
<Suggestion suggestion="{suggestion}" bind:chosenPokemon />
在“建议”组件中,我们可以接受这个属性,并使“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>
现在,我们仅用了之前代码的一小部分就实现了相同的功能。
商店
最后我想介绍一下 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'
一个可读性强的函数,其第一个参数是 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([])
})
就这样。我们现在有了一个中央存储库,名为“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>
我们的应用程序运行方式相同,但我们的 API 数据现在集中存储,可以在任何组件中使用。
虽然 Svelte 本身就支持可写和可读的 store,但任何遵循 Svelte store“契约”并实现了 `subscribe` 方法的对象都可以被视为 store。
这意味着 store 非常灵活,可以根据你的需求进行定制。你甚至可以使用其他语言(例如 Rust)来创建 store,就像这里展示的那样。
最后说明
在众多 JavaScript 框架中,Svelte 脱颖而出,因为它既能兼顾用户体验,又不会为了开发者体验而牺牲用户体验。Svelte
提供了一系列强大的工具,让应用开发变得轻松便捷,同时其编译器还能生成体积小巧的软件包供最终用户使用,显著缩短下载时间。
Svelte 一直是备受青睐的框架之一,其使用量持续增长,这或许预示着它未来将与 Vue 或 React 比肩。这与近期 Web 性能提升的趋势不谋而合,即从向客户端提供庞大的 JavaScript 包转向服务器端渲染或混合渲染。
Svelte 团队目前正在开发 SvelteKit,它是 Svelte 版本的 Next.js,您可以在这里了解更多信息。
如果您喜欢这篇文章,请考虑分享。也欢迎访问我的GitHub 页面并
阅读我的其他文章。