JavaScript Library – Svelte
前言
上一回我介绍了 Alpine.js。作为我开发企业网站 draft 版本的 render engine。
用了一阵子后,我觉得它真的非常不好用。所以打算换一个。
前端有好几个 framework / library / compiler 都可以用来做 MVVM render engine。比如 Angular、React、Vue、Lit、Solid、Qwik、Svelte。
Angular 太重
React、Solid、Qwik 我不爱 JSX
Vue 半吊子
Lit 我不能接受 html`` 语法
所以最后选了 Svelte。
安装
它有 2 个用法,第一个是搭配 SvelteKit,类似于 Angular CLI,官方出的脚手架。
另一个方法是搭配 Vite,我是要轻巧的,自然是不想引入多一个 SvelteKit 概念,用 Vite 足也。
Vite 我之前介绍过了,没用过的人可以先看这篇。
更新:17-11-2023,新版本只需要一行 command 就可以创建给予 vite 的 svelte 了。
下面是以前的安装方式,留作纪念。
yarn create vite
然后填写 project name 选择 Svelte 模板和 TypeScript
进入 folder 安装 node_modules 就可以了
cd play-svelte
yarn install
yarn dev --open
效果
vite config with svelte
我们看看是怎样 config svelte to vite 的
首先 vite.config.ts 多了一个 svelte plugin
多了一个 svelte.config.js 里面是 vite plugin
这样一来一往,两家就连上了。
还有一个重要的是 tsconfig.json
大致上是这样。
Svelte 的基本使用概念
我的目的是用它来替代 Razor。所以我只关心一些小功能而已。
index.html 作为 page 的入口
导入 main.ts 这个和一般的 Vite 使用是一样的。
然后 main.ts 长这样
关键就在那个 App.svelte。
.svelte 类似于 Blazor 的 .razor file。它是 Script (JS / TS) + HTML + Style (CSS / Scss) 的合体。
一个 .svelte file 等于一个组件。它也有 isolated css 的概念哦,但不关我的事,我用不上。
Svelte 的基本语法
binding text
<script lang="ts"> const name = 'Derrick'; </script> <main> <h1>{name}</h1> </main>
非常简单直接
binding html raw text
<script lang="ts"> const rawHtml = '<h1>Hello World</h1>'; </script> <main> { @html rawHtml } </main>
click event and change binded text
<script lang="ts"> let name = 'Derrick'; </script> <main> <h1>{name}</h1> <button on:click={() => name = 'new name'}>change name</button> </main>
效果
简单到爆,不需要像 Solid、Angular 那样去搞什么 Signal / Reactive 做追踪,逼迫开发者写出恶心代码。直接了当就是 Svelte 的风格。
当然这种黑箱有时候也很恐怖。类似重新定义 HTML、CSS、JS 了,但是这种事后端经常干,见怪不怪。
binding property / attribute
<h1 class="{ 'abc ' + (true ? 'xyz' : 'kknd') }">Hello World</h1> <input type="text" required={true}> <h1 class:active={true} >Hello World</h1>
直接写 JS 拼接 class 也是可以的。简单直接
if else
{#if Math.random() > 0.1} <h1>Hello World</h1> {:else if Math.random() > 0.4} <p>too cold!</p> {:else} <h1>false value</h1> {/if}
Angular 也正在考虑用上面这个写法。比起 Razor 确实有点丑。
for loop
{#each values as value, index} <h1 class="item">{value}</h1> {/each}
component
counter.svelte
一个 .svelte 代表一个组件
<script lang="ts"> export let startNumber; let count = startNumber; </script> <main> <h1>{count}</h1> <button on:click={() => count++}>increase</button> </main> <style> h1 { color: red; } </style>
script 里面的 export let startNumber 组件的 property,外部通过修改这个影响组件内部。类似 Angular 的 @Input
app.svelte
<script lang="ts"> import Counter from './Counter.svelte'; let name = 'Derrick'; </script> <main> <h1>{name}</h1> <button on:click={() => name = 'new name'}>change name</button> <Counter startNumber="5" /> </main>
使用组件的方式是 import 组件,然后写入 HTML。
component slot
外面传进去
<Counter startNumber="5"> <div class="header" slot="header">header</div> <div slot="footer">footer</div> </Counter>
里面接收
<main> <slot name="header"> No header was provided </slot> <h1>{count}</h1> <button on:click={() => count++}>increase</button> <slot name="footer"></slot> </main>
简单直接。题外话: <slot> 在 HTML 规范里不是 self-closing tag 哦,但 Svelte 依然可以正确解释的。
component slot 和 fragment slot
<HeaderComponent slot="header" />
组件和 element 一样可以直接写 slot property。
fragment slot 长这样,有点类似 Angular 的 ng-container
<svelte:fragment slot="footer"> <p>All rights reserved.</p> <p>Copyright (c) 2019 Svelte Industries</p> </svelte:fragment>
slot use as ng-template
angular 有一个概念叫 ng-template,在 svelte 可以用 slot 做到相同的效果。
<HelloWorld items={ [{ name: 'Derrick' }, { name: 'Richard' }] } let:item let:index> <p>{ index }. { item.name }</p> </HelloWorld>
首先是传了一个 array 进去组件,组件内会 for loop,然后把每一次 loop 的 item 和 index 传出来
let:item 和 let:index 就是接受每一次 loop 传出来的值。
然后再传入一个 template,template 内可以使用 item 和 index 做 binding。
HelloWorld 组件
<script lang="ts"> type T = $$Generic; export let items: T[] = []; </script> {#each items as item, index} <slot item={item} index={index}></slot> {/each}
用 slot 接受 template,并且 loop,然后传入每一次 loop 的 item 和 index。
对比起 Angular 的 ng-template,Svelte 的写法简单到不可思议。
注:$$Generic 是为了 typesafe。
#snippet and @render
用 slot 来实现 ng-template 不是太合适的,在 Angular 这两个概念是分开的。
svelte 5 推出了 #snippet and @render,它基本上可以取代 slot,而且更灵活。
但 slot 在绝大部分情况下还是能用。
定义 template
{#snippet templateName()} <h1>Hello World</h1> {/snippet}
使用 template
{@render templateName()}
{@render templateName()}
{@render templateName()}
这样就会显然出 3 个 <h1>Hello World</h1>。
也支持传参数
{#snippet templateName(value: string)} <h1>Hello World { value }</h1> {/snippet} {@render templateName('a')} {@render templateName('b')} {@render templateName('c')}
搭配组件的用法也是一样的
<HelloWorld> {#snippet template1(value)} <p>{ value } Lorem ipsum dolor sit amet, consectetur adipisicing elit. Unde, quasi?</p> {/snippet} </HelloWorld>
像 slot 那样传进去。
里面接收比较繁琐,但理解还是简单的。
<script lang="ts"> import type { Snippet } from "svelte"; // 从 $props 里抽出 template1,$props 就是所有的 properties let { template1 } : { template1?: Snippet<[string]> } = $props(); </script> <main> <!-- 如果是 optional 那就要 if else 做一个 fallback content --> {#if template1} {@render template1('value')} {:else} <p>fallback content</p> {/if} </main>
component pass multiple properties
<MyComponent {...props} />
get rest properties
<script lang="ts"> let { className, ...restProps}: { className?: string; [x: string]: unknown; } = $props(); </script> <button class="class-a { className }" {...restProps}></button>
可以用来传 data attirbutes
<Counter className="class-b" data-value="value" />
效果
dynamic tag
<script lang="ts"> export let tag : 'section' | 'aside' = 'section'; </script> <svelte:element this={tag}> <h1>Hello World</h1> </svelte:element>
这样就可以创建 dynamic tag 了,不像 Angular,完全不支持这个功能🙄
import img
<img src="/images/nana.jpg" alt="nana">
这个路径会拿到 public folder 内的图片
下面这个路径
<img src="./images/nana.jpg" alt="nana">
拿不到图片
我们有两个解法,第一个是直接 import 那个路径,然后 binding
<script lang="ts"> import nanaImg from './images/nana.jpg'; </script> <main> <img src={ nanaImg } alt="nana"> </main>
另一个是更动态一点,import 一整个 folder
<script lang="ts"> const imageFolder = import.meta.glob('./images/*.{jpg,jpeg}', { eager: true, import: 'default' }); const getImageUrl = (filename: string) => { // 用绝对路径也可以,比如 '/src/images/${filename}' return imageFolder[`./images/${filename}`] as string; }; </script> <main> <img src={ getImageUrl('nana.jpg') } alt="nana"> </main>
Svelte 其它知识点
获取 DOM 的时机
new Component 后,DOM 就渲染好了。
总结
目前我就用到这些而已。感觉无论如何都比 Alpine.js 合适。我喜欢他的简单直接,希望他越来越火,有朝一日我可以用它于 production 场景。