在很多地方早就听到过svelte
的大名了,不少工具都有针对svelte
的配置插件,比如vite
\ unocss
\ svelte
. 虽然还没使用过,但是发现它的star数很高哦,简单学习一下它的与众不同。
这名字有点别扭,好几次都写错。
svelte
是一个web开发框架,它通过编译器编译出可以在浏览器中运行的代码,使用的语言还是前端三剑客html / css /js
。优点就是书写简洁、编译快、效率高。
后记写在前
整体的使用感受感觉还行👍有很多基于使用场景的设计解决方案。集众之长,上手不算困难,书写确实简洁、使用也足够灵活,没有那么多的限制。但是由于版本更新频繁,API并不稳定,版本之间变更比较大,感觉为了简洁就会对API进行非常大的调整,这种框架开发模式有人喜欢、也有人忧愁学习成本变高。但也不可否认,它的设计确实让人然眼前一亮,因为没有过多使用暂时还谈不到它的高效,我想在未来的应用开发中,我也许会选择它来进行开发。
因为它是一个编译时框架,组件必须先编译才能在浏览器中运行,这也就能给它带来一些好处,不受固定平台的限制,可以使用中间件编译为其它系统平台的应用。
初识svelte
当前svelte
的版本是^5.**
,版本发布很是频繁。初始化项目svelte-app
项目,安装
sh
npm add svelte
svelte
是一个编译时框架,它需要先编译后才能在浏览器中运行。svelte
声明组件文件后缀名是*.svelte
,可以从一个简单的组件App.svelte
感受一下组件内容组成结构:
svelte
<script>
let name = "world";
</script>
<h1>Hello {name}</h1>
<style>
h1 {
color: chocolate;
}
</style>
-
标签
script
用于书写js逻辑,可以通过lang='ts'
标记为使用类型typescript书写。 对于js部分,也可以提取到外部文件,文件后缀名为*.svelte.js
jslet name = "world";
-
标签
style
用于书写样式, -
对于html结构代码可以直接书写。
不同于vue / react
可以直接引入到html中使用,svelte
需要编译打包后才能引入到html中。所以我们不能直接使用App.svelte
组件,我们写一个简单的编译逻辑,执行编译完成后看看生成的代码。
创建compile.js
编译 App.svelte
,svelte/compiler
包提供了用于编译.svelte
文件的方法。
compile
方法用于编译.svelte
编译文件导出一个js模块。compileModule
方法用于带有响应式变量的.svelte.js
文件,编译后导出一个js模块。
js
import { compile as SvelteCompile } from "svelte/compiler";
import fs from "node:fs";
// 读取.svelte文件内容
const source = fs.readFileSync("./src/App.svelte", "utf-8");
// 编译输出组件内容
const result = SvelteCompile(source, {
name: "App",
});
// 写入文件 js
fs.writeFileSync("./dist/bundle.js", result.js.code);
// css
fs.writeFileSync("./dist/bundle.css", result.css.code);
通过使用compile
方法编译,看看编译后的产物bundle.js
:
js
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root = $.template(`<h1 class="svelte-13s5wg8"></h1>`);
export default function App($$anchor) {
let name = "world";
var h1 = root();
h1.textContent = 'Hello world';
$.append($$anchor, h1);
}
js内容是esm格式,并且导入了svelte/internal
模块,要想直接引入到html中使用是不可能的。需要通过svelte
提供的挂载方法,再通过打包工具来对编译过的js文件再进行一次编译打包,这里使用rspack
,当然也可以使用其他打包工具。
svelte
版本 4 中可以配置输出的内容格式,可以配置format:"iife"
输出的内容可以在html中直接使用。最新版本5
仅仅输出esm格式的内容了。
sh
npm add @rspack/core @rspack/cli -D
使用rspack
对我们编译过的svelte内容在进行一次依赖关系构建,使得可以在html引入使用。我们将组件挂载到元素#app
上,增加入口文件entry.js
js
import App from "./dist/bundle.js";
import { mount } from "svelte";
mount(App, {
target: document.getElementById("app"),
});
我们不能直接使用App.svelte
编译的产物,需要使用svelte
提供的挂载方法mount
指定挂载元素。通过npx rspck
指定入口文件进行构建
sh
npx rspack --entry entry
执行完成后,可以在dist
看到生成的产物mina.js
,我们在index.html
中直接引入即可。通过静态服务器访问html,可以看到页面正常渲染。
对于使用rspck
更复杂的构建,可以通过配置文件rspack.config.js
配置构建配置项。
我们仅处理了产物js
的构建,并未处理css
,感兴趣的可以自行配置实现。
实现一个简易的svelte-loader
解析.svelte
文件
为了方便后续的API测试,我们实现一个简单的svelte-loader
,使得rspack
可以解析.svelte
文件。
增加rspack.config.js
配置文件,关于rspack
可以查看往期文章
- 💥vue-cli项目升级rsbuild,效率提升50%+
rspack 使用构建vue3脚手架这篇文章发布时间较早,可能已经没有参考价值了。
js
import { defineConfig } from "@rspack/cli";
export default defineConfig({
entry: {
index: "./src/main.js",
},
});
main.js
不同于entry.js
,无需提前编译.svelte
文件,直接引入即可。
js
import { mount } from "svelte";
import App from "./App.svelte";
mount(App, {
target: document.getElementById("app"),
});
我们现在启动项目npx rspack dev
会报错,无法解析App.svelte
文件,增加解析规则,支持解析.svelte
文件。
我们在目录loaders/svelte-loader.cjs
创建一个loader,这里我们仅处理编译之后的js
内容。
js
const { compile: SvelteCompile } = require("svelte/compiler");
module.exports = function (source) {
let result = SvelteCompile(source, {});
return result.js.code;
};
修改rspack
配置文件rspack.config.js
增加loader配置,支持解析.svelte
文件。
js
import { defineConfig } from "@rspack/cli";
export default defineConfig({
entry: {
index: "./src/main.js",
},
module: {
rules: [
{
test: /\.svelte$/,
loader: "./loaders/svelte-loader.cjs",
},
],
},
});
为了我们可以在浏览器中查看视图,还需要设置html模板,使用rspack
内部的插件HtmlRspackPlugin
,并指定我们自己的模板文件plubic/index.html
js
export default defineConfig({
// ... other
plugins: [
new rspack.HtmlRspackPlugin({
title: "Svelte App",
template: "./public/index.html",
}),
],
});
启动服务,访问 http://localhost:8080
可以看到页面正常渲染了。我们实现了一个简单的.svelte
文件编译loader提供给rspack
使得svelte
文件可以被正常编译。
Runes
符文
svelte
的核心概念,具有神秘魔法的标记。前缀通常为$
,可以在.svelte
或.svelte.js
中使用。
$state
响应式声明
$state
用于声明响应式状态。包括基本类型、数组、对象、set、map等。数组操作方法可以触发更新;对象深度代理,修改属性值会触发更新。
svelte
<script>
let name = $state("world");
</script>
<h1>Hello {name},Svelte!</h1>
<button onclick={() => (name = "hboot")}>change</button>
我们再来编译一下文件,查看是如何处理编译响应式变量的。相对于无状态,则对响应式变量做了更多的处理,将$state
转换为了$.state()
;在操作变量时,通过$.set()
来更新变量,通过$.get()
来获取变量;
对于引用变量的DOM结构则提取出来通过副作用函数$.template_effect()
建立了状态依赖关系,可以在状态更新时重新生成DOM并渲染。
js
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
var on_click = (_, name) => $.set(name, "hboot");
var root = $.template(`<h1 class="svelte-13s5wg8"> </h1> <button>change</button>`, 1);
export default function App($$anchor) {
let name = $.state("world");
var fragment = root();
var h1 = $.first_child(fragment);
var text = $.child(h1);
$.reset(h1);
var button = $.sibling(h1, 2);
button.__click = [on_click, name];
$.template_effect(() => $.set_text(text, `Hello ${$.get(name) ?? ''},Svelte!`));
$.append($$anchor, fragment);
}
$.delegate(['click']);
对于数组、对象的深度监听,往往会带来性能问题,如果我们不需要深度监听,则可以使用$state.raw()
来创建响应式变量,这样创建的变量在改变内部值时不会触发更新。
js
let user = $state.raw({
name: "hboot",
email: "",
});
// 不会触发更新
user.name = "svelte";
// 整个变量重新赋值会触发
user = {
name: "svelte",
email: "",
};
$derived
派生状态
$derived
可以创建派生值,可以从一个响应式变量计算得到另一个值。相当于vue的computed
计算值。
js
let name = $state("world");
let welcome = $derived(`Good ${name}`);
$derived
可以接受一个简单的表达式,也可以通过$derived.by()
接受一个函数来处理复杂的计算。为什么不直接接受一个函数呢?🧐
js
let welcome = $derived.by(() => `Good ${name}`);
当依赖的状态更新后,svelte
不会立马触发derived
的计算,而是标记它,等下次有地方读取时再重新计算。
$derived
派生的值可以直接修改,这在版本>5.25
才支持。可以预先设置值,这对于一些耗时的计算可以提前给出反馈。这一点很有意思🤩
$effect
函数
$effect
可以执行副作用函数。包括调用第三方库、操作DOM、接口请求等。这和reactuseEffect
hook 概念一样,但是它可以自动收集在其内部使用的响应式状态,无需手动声明。在svelte
中$effect
可以在任何地方使用,不局限于组件顶层;不局限于script
内,在模板语法中也可以使用。
在
$effect
内部更新state要谨慎,避免死循环。
$effect
接收一个回调函数用于执行副作用逻辑,该回调函数可以返回一个清理函数。这个清理函数会在$effect
重新执行或者组件销毁时自动调用执行。
js
$effect(() => {
let timer = setInterval(() => {
console.log(name);
}, 1000);
return () => {
clearInterval(timer);
};
});
现在已知的响应式状态来源包括$state
和$derived
创建,后续还有一个$props
来自于父组件的响应式变量。都会被$derived
探测收集。异步任务比如setTimeout
或者await
之后的响应式变量是不会被收集的,也就是它们更新不会触发$effect
的执行。
$effect
更新的时机是在DOM更新之后,它会合并同一时间变更的响应式变量只调用一次。$effect
的执行是异步的。
对于对象类型的$state
响应式变量,更改对象属性不会触发$effect
的执行。但是$derived
派生的对象会被执行,因为派生每次都会重新计算,得到的是不同的对象。
对于在$effect
内部使用了条件判断的语句中存在的响应式变量,这会根据条件分执行决定是否收集该变量
js
let bool = $state(false);
$effect(() => {
if (bool) {
console.log(name);
} else {
console.log("nothing");
}
});
如果bool
为true,则name
会被收集,name
的更新也会触发$effect
的执行;如果bool
为false,则name
不会被收集,仅在bool
更新时触发$effect
的执行。
$effect.pre()
提供了更早的更新时机,可以在DOM更新之前执行。
$props
父传子参数
父子组件传参很常见,通过$props()
接收来自父组件的参数。
js
let props = $props();
父组件通过{ }
绑定需要传递的参数。
svelte
<script>
import User from "./views/user.svelte";
</script>
<User {name} />
强烈不建议直接更改$props
,$svelte
提供了一种方式用于更新props,在下一节可以通过bindable
$props.id()
可以生成当前组件实例的唯一ID,可以用于服务端、客户端之间的组件关联。
$bindable
双向绑定
为了合理的更新来自父组件的props变量,svelte
提供了一种机制,通过$bindable()
标记的变量,可以在父组件通过bind
语法接受来自组件对于变量的更新。
svelte
<script>
let { name = $bindable() } = $props();
</script>
在父组件如果不需要监听来自子组件的更新,也可以不需要使用bind
.
svelte
<User bind:name />
$bindable()
可以接收一个值,用于默认值。当父组件没有传递值时,$bindable()
会返回默认值。
$inspect
跟踪变化
仅在开发模式下有用,用于跟踪输出响应式变量的值,可以替代console.log
。
svelte
<script>
let { name = $bindable() } = $props();
$inspect(name);
</script>
$inspect().with((type,value)=>fn)
可以覆盖默认的输出行为,with
接收的第一个参数type
,值为init \ update
,回调函数将会在跟踪的值更新时被调用。
svelte
<script>
$inspect().with((type,value)=>{
// 自定义输出逻辑
})
</script>
$inspect.trace()
可以展示当前执行的跟踪栈。可以用于$effect()
和$derive()
,重新执行时可以看到是由哪些变量引起的。
svelte
<script>
$effect(() => {
// 执行时,输出执行的
$inspect.trace();
});
</script>
模板语法
一个比较重要的框架能力就是模板语法了,对于前端开发这是否亲和,是否更容易上手使用。
通常建议html标签都是用小写的div / p
等。对于用户自定义组件则使用驼峰书写MyComponent
。
svelte
<User name="hboot" {...userInfo} />
绑定属性时,可以通过{...}
将多个属性传递给组件。按照传递的顺序,如果属性有冲突,则后面的属性会覆盖前面的属性。
#if
条件渲染
{#if condition} .. {:else if condition} .. {:else} .. {/if}
里处理条件渲染。
svelte
{#if name == "hboot"}
<User bind:name />
{/if}
#each
循环渲染
{#each array as item,index} .. {/each}
里处理循环渲染。
svelte
{#each [1, 2, 3, 4] as item}
<p>{item}</p>
{/each}
获取数组下标index
.
svelte
{#each [1, 2, 3, 4] as item,index}
<p>{item}-{index}</p>
{/each}
为每个元素绑定key
,需要在语法后加标识(key)
可以。这个挺让人意外的,不是绑定在元素上,而是声明在循环表达式中。
svelte
{#each [1, 2, 3, 4] as item,index (item)}
<p>{item}-{index}</p>
{/each}
如果需要渲染某个视图多次,提供了简洁的写法。 👍
svelte
{#each { length: 5 }, name}
<p>{name}</p>
{/each}
对于渲染的数组如果是空情况下提供了语法渲染占位 :else
👍
svelte
{#each [1, 2, 3, 4] as item}
<p>{item}</p>
{:else}
<p>no data</p>
{/each}
#key
标记渲染
{#key expression} ... {/key}
标记一块区域在表达式的值发生变更时,内部的节点会重新渲染。
感觉这个API也不错,手动控制渲染更加灵活 👍👍👍
#await
异步渲染
{#await promise} ... {:then} ... {:catch} ... {/await}
异步渲染,在promise
执行的各个阶段渲染不同状态下的UI。
这对于异步组件或者耗时任务封装组件也很方便。避免了手动控制状态渲染,意图更清晰。
如果不关注pending状态,还可以简写为:{#await promise then value} ... {:catch} ... {/await}
svelte
{#await import("./User.svelte")}
<span>loading...</span>
{:then { default: User }}
<User />
{:catch error}
<span>error</span>
{/await}
#snippet
渲染片段
{#snippet name(params)} ... {/snippet}
可以创建一个代码片段,然后通过{@render name(image)}
来渲染,可以实现UI复用。
svelte
{#snippet list(item)}
<p>{item.name}</p>
{/snippet}
{#each UserList as item}
{@render list(item)}
{/each}
{#each NoticeList as item}
{@render list(item)}
{/each}
渲染片段不仅可以使用传递进去的参数,也可以使用最外层声明的变量,比如script
标签里的变量。它们也有自己的作用域,在当前标签中声明时,仅对当前标签以及子节点有效。
svelte
<div>
{#snippet avatar(url)}
<img src={url} alt="avatar" />
{/snippet}
</div>
<!-- 这个是错的,它访问不到div元素中创建的片段 -->
{@render avatar(userInfo.avatUrl)}
可以作为组件的props传递,这相当于web中的slot概念,但是更加的灵活。
svelte
<div>
{#snippet avatar(url)}
<img src={url} alt="avatar" />
{/snippet}
<User {avatar} />
</div>
还有一个隐形传递内容的方式,而无需使用$snippet
创建,就是在标签中的内容,他可以直接在子组件通过children()
访问。有点像默认的slot.
svelte
<script>
let { children } = $props();
</script>
<div>
{@render children()}
</div>
所以最好避免使用children
作为props名传递参数。如果不确定是否传递了默认内容,可以通过{@render children?.()}
渲染或者通过{#if children}
条件判断。
还可以在<script module>
文件导出声明的片段渲染。这对于一些公共的渲染逻辑提取很有用。
svelte
<script module>
export { NavSider };
</script>
{#snippet NavSider()}
<p>渲染左侧菜单</p>
{/snippet}
感觉这比slot
要灵活的多,覆盖了很多使用常见,组件内复用,跨组件复用,父传子复用等。👍👍👍
@render
渲染代码片段
渲染由$snippet
创建的代码片段。
@html
渲染HTML片段
一个常用的场景可以渲染html片段。
svelte
<div>{@html content}</div>
因为在 .svelte
文件中,style
定义的样式具有作用区域限制。而渲染的html不会被svelte检测,所以样式不会生效,需要增加:global
svelte
<style>
.html-container :global {
p {
color: red;
}
}
</style>
@attach
DOM附件
{@attach fn()->callback}
可以绑定到DOM节点上,在DOM节点渲染完后调用执行。返回一个回调函数callback
在附件函数执行之前执行;也会在DOM节点卸载后执行。
svelte
<script>
let divAttch = (element) => {
// DOM节点渲染后执行
return () => {
// 卸载
};
};
</script>
<div {@attach divAttch}>content</div>
如果函数内部存在响应式变量的使用,那么svelte
会收集并在它们更新时重新执行附件函数fn
svelte
<script>
let divAttch = (element) => {
element.textContent = name;
return () => {
// 卸载
};
};
</script>
有时候函数内部逻辑太多,避免重复执行,我们可以将有使用了响应状态的逻辑使用$effect
包裹,这样只有$effect
内部的逻辑会由响应式状态更新而触发执行。外部的逻辑则仅会执行一次,不会重复执行。
svelte
<script>
let divAttch = (element) => {
console.log("divAttch", element);
$effect(() => {
element.textContent = name;
});
return () => {
// 卸载
};
};
</script>
附件函数的定义可以行内定义,也就是在模板中定义逻辑。而无需在script
中声明一个方法。
#const
模板中的变量定义
在使用模板语法时如果需要定义变量,可以使用{#const variableName = value}
定义,可以关注一下使用地方限制,比如可以在块中#if
#each
等语法中使用。
#debug
标记断点
通过{#debug variable1,variable2,...}
标记指定变量在更新时启动断点,以便查看更新情况。
svelte
{@debug name}
bind:
双向绑定
在前面的svelte
符文$bindable
中简单介绍了它的作用,可用于子组件更新父组件的数据。它的实现是同时给元素绑定了一个事件监听器来更新绑定值,如果不小心绑定了同名的监听事件,手动绑定的事件会先于更新值之前触发。
svelte
<User bind:name />
还可以通过绑定回调函数显示定义get/set
函数,以便我们实现对绑定值的处理bind:variable={ setFn, getFn}
。
svelte
<User bind:name={() => name, (val) => (name = val)} />
可以通过设置setFn
为null
,从而实现绑定值只读,这对于一些元素属性不可设置时有用,比如DOM元素的clientWidth
双向绑定对于form表单来说比较重要,业务中永远存在form表单,那对于form组件的绑定便捷性则尤为重要。
svelte
<!-- input -->
<input bind:value={name} defaultValue="hboot" />
<!-- input number-->
<input bind:value={total} type="number" defaultValue={100} min={0} max={200} />
<!-- input checkbox -->
<input bind:checked type="checkbox" defaultChecked={true} />
<!-- input select -->
<select bind:value={name}>
<option value="hboot">hboot</option>
<option value="admin" checked>admin</option>
<option value="test">test</option>
</select>
<!-- input radio -->
<input bind:group={names} type="radio" value="hboot" />
<input bind:group={names} type="radio" value="admin" />
<!-- input files-->
<input bind:files multiple type="file" />
对于原生的html标签的属性,都可以通过bind:
去实现双向绑定控制,如果有多个属性可控制,则可以同时绑定。而对于只读的属性通过bind:
绑定时,默认就是只读的,而无需再次设置只读处理。
想要获取DOM的实例或者自定义组件的实例,可以通过bind:this
绑定获取,对于获取到自定义组件的实例后,可以访问到子组件export
导出的方法。
svelte
<script>
let divRef = null;
$effect(() => {
console.log(divRef.getBoundingClientRect());
});
</script>
<div bind:this={divRef}></div>
获取元素实例需要等待挂载后才能获取到,所以我们可以在$effect
执行对于DOM元素的操作。
transition:
过程动画
当DOM元素进入/离开时追加的动画效果,svelte/transition
子包提供了一些常见的转换效果。
svelte
<script>
import { fade } from "svelte/transition";
</script>
{#if name == "hboot"}
<div transition:fade>
<User />
</div>
{/if}
仅能使用到DOM节点,不能应用到自定义组件,因为自定义组件没有根部元素。默认元素的转换效果仅在当前块作用域中有效,也就是当有多层{#if ...}
嵌套时,只做用于就近的条件渲染。可以通过gloabl
设置对父级节点条件显隐时也触发效果。
svelte
{#if total > 50}
{#if name == "hboot"}
<div transition:fade|global>
<User />
</div>
{/if}
{/if}
可以通过定义参数对转换效果自定义设置,比如针对fade
可设置参数
svelte
<div transition:fade|global={{duration:500, delay: 200, easing: easingFn}}>
<!-- inner element -->
</div>
不仅可以覆盖默认的参数设置外,还可以自定义转换效果的实现,自定义函数的实现返回值类型必须是TransitionConfig
ts
interface TransitionConfig {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}
自定义函数的参数固定格式(node: HTMLElement, params: any, options: { direction: 'in' | 'out' | 'both' })
node
是当前绑定的DOM节点;params
是绑定的自定义参数;options
可以指定转换效果生效的模式,默认进入/离开都执行;可以指定仅进入或仅离开执行效果。也可以分别指定进入/离开不同的转换效果。
svelte
{#if name == "hboot"}
<div in:myFade>
<User />
</div>
{/if}
animate:
变化动画
和transition:
不同的是它针对的是列表渲染,因顺序变化而重排实现的动画效果。内置动画可从svelte/animate
查看。
svelte
<script>
import { flip } from "svelte/animate";
</script>
{#each [1, 2, 3, 4] as item (item)}
<p animate:flip>{item}</p>
{:else}
列表渲染时必须设置唯一键值key
,也可以自定义动画实现,自定义动画函数固定的参数及响应值
ts
const animatieFn = (node: HTMLElement, { from: DOMRect, to: DOMRect } , params: any) => {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}
style:
样式绑定
可以直接在DOM元素上设置style=""
属性,也可以通过style:
指令绑定样式,绑定变量值。
svelte
<h2 style="font-size:26px;" style:color="red">{welcome}</h2>
要标记某个样式的权重,可以增加|important
svelte
<h2 style:color|important="red">{welcome}</h2>
指定style:
绑定的样式优先级高于属性style
设置的样式。
class:
类名绑定
除了常规的字符串、数组、对象绑定到属性class
,值得关注的是指令class:
可以直接绑定变量类名
svelte
<button class:buttonPrimary>change</button>
style
样式定义
在.svelte
文件中可以通过<style>
标签定义样式,默认样式具有作用域的,也就是只针对当前文件里的类名生效,可以看一下编译后的样式。
svelte
<style>
h1 {
color: chocolate;
}
</style>
执行编译后生成的css文件。可以看到追加了.svelte-13s5wg8
这是编译后追加的类名,在所有<h1>
标签上会增加类名svelte-13s5wg8
css
h1.svelte-13s5wg8 {
color: chocolate;
}
要想全局样式生效,可以使用:global()
指定某个类名适用全局;使用:global {...}
定义一组类样式适用全局。
svelte
<style>
.html-container :global {
p {
color: red;
}
h1 {
font-weight: normal;
}
}
</style>
对于自定义动画帧@keyframes
名称也会被修改作用限制,可以追加前缀-global-
保证全局生效,使用时则忽略-global-
.svelte
编译时会移除掉。这一块会让人感觉有点疑惑🧐
<style>
不仅可以在顶部声明使用,还可以出现在元素块或逻辑中。嵌套的<style>
没有作用域的限制,会全局生效,所以不建议这样写。
svelte
<h1>
<style>
h1 {
color: blue;
}
p {
color: green;
}
</style>
Hello {name}
</h1>
内置元素
svelte
内置了一些元素用于处理一些特殊场景下的问题。
<svelte:boundary>
错误边界
<svelte:boundary>
可以捕获内部子组件渲染中发生的错误,提供了两种方式帮助我们处理渲染错误
{#snippet failed(error, reset)}
可以提供failed
命名的错误处理片段函数,当渲染错误时会调用并渲染该片段。函数有两个参数,第一个参数是错误对象,第二个参数是重新渲染的函数。
svelte
<svelte:boundary>
<User bind:name />
{#snippet failed(error, reset)}
<button onclick={() => reset()}>Oops, Try again</button>
{/snippet}
</svelte:boundary>
第二种方式是监听错误事件onerror
,接收参数和failed
一样,可以通过监听方法上报错误信息。
svelte
<script>
const onerror = (error, reset) => {
console.error("Error occurred:", error);
// 可以在这里处理错误
// 比如重置状态或显示错误信息
};
</script>
<svelte:boundary {onerror}>
<User bind:name />
</svelte:boundary>
<svelte:window>
访问window对象
可以通过在组件使用<svelte:window>
来监听window对象上的事件,或者通过bind:
获取window对象的属性。
svelte
<svelte:window onresize={handleResize} />
与之类似的还有<svelte:document>
和 <svelte:body>
,它们必须在组件最外层使用,不能嵌套或块中使用。
<svelte:head>
定义头部信息
通过<svelte:head>
可以设置页面的头部信息,比如<title>
,<meta>
,<link>
等等。
svelte
<svelte:head>
<title>Svelte App</title>
</svelte:head>
<svelte:element>
动态替换标签
<svelte:element>
可以动态替换标签,比如<svelte:element this="div">
可以替换成<div>
。
也可以使用非标准的标签,为了使得svelte
识别它,可以指定xmlns
属性显示设置。
<svelte:options
编译配置项
通过<svelte:options>
标签,可以配置组件在编译时的编译选项。
runes={true}
设置是否启用符文模式,启用后不再兼容svelte
3和4.namespace="..."
标签命名空间,默认是html
,可选svg
和mathml
。customElement = {...}
自定义标签配置项,可以是字符串类型表示标签名;也可以通过对象配置其他属性。css="injected"
将组件样式注入到js中。
生命周期
在svelte
中最小更新单元不是组件,所以组件的生命周期只关注于创建和销毁。因为更新内部状态可能并不会引起组件视图的更新,所以不存在更新前后的钩子。这点和react
不同,又和vue
有点像,状态依赖收集,仅在需要更新视图时更新。
onMount
挂载
svelte
组件DOM挂载完成后调用。可以接受一个返回值函数,在组件卸载的时候调用。
svelte
<script>
import { onMount } from 'svelte';
onMount(() => {
console.log("mounted");
return () => {
console.log("unmounted");
};
});
</script>
onDestroy
卸载
svelte
组件卸载时调用。
tick
更新完成
有时候我们需要等待视图更新完成再执行某些操作。可以使用tick()
,它返回一个Promise
在UI更新完成之后会被调用。
在$effect
中,提供了.pre()
可以监听到DOM更新之前调用,搭配tick()
svelte
<script>
import { tick } from "svelte";
$effect.pre(() => {
console.log("dom update");
tick().then(() => {
console.log("dom update done");
});
});
</script>
数据共享
除了父子组件通过$props
传递共享数据之外,常见的还有跨组件的数据共享。跨组件的数据共享又可以分为两种,有嵌套关系的组件数据共享和无关系的组件数据共享。
svelte
提供了上下文Context
允许后代组件共享来自父级提供的上下文数据。
setContext(key,value)
设置上下文数据。getContext(key)
获取上下文数据。hasContext(key)
判断是否存在指定的数据。getAllContexts()
获取所有上下文数据。
在上级组件App.svelte
中设置数据
svelte
<script>
import { setContext } from "svelte";
setContext("name", "hboot");
</script>
在后代组件About.svelte
组件中获取数据,它们之间的关系About->User->App
成嵌套关系。
svelte
<script>
import { getContext } from "svelte";
let name = getContext("name");
</script>
<h4>About! {name}</h4>
运行项目可以看到展示数据为顶层组件设置的数据。
使用响应式数据以便得到数据的同步更新,目前仅支持对象类型的响应式数据,不能是基本类型和数组。使用其他类型时会有warn
提示,而且数据也不会响应式更新。
svelte
<script>
let userInfo = $state({
name: "svelte",
});
setContext("userInfo", userInfo);
</script>
为了保持共享数据的响应式链接,对于更新状态userInfo
,不能直接赋值一个对象,需要指定更新某一属性,比如:userInfo.name = "hboot"
为了更好的组织跨组件数据共享,可以将这部分提取到.svelte.js
文件,然后在需要的组件导入使用即可
js
import { setContext, getContext } from "svelte";
const userContextId = Symbol("userInfo");
export default {
get() {
return getContext(userContextId);
},
set(data) {
setContext(userContextId, data);
},
};
这样我们提供了统一的地方用于获取、设置共享的用户数据,还可以针对数据做权限验证控制等操作。对于使用typescript
时的数据类型验证可以更方便的管理。
非关系组件数据共享
对于非嵌套关系的组件,可以使用响应式$state
在外部.svelte.js
中声明响应式状态,然后在使用的组件导入。比如创建一个userStore.svelte.js
js
/**
* 共享用户信息
*/
export const userStore = $state({
name: "hboot",
});
在需要的组件中导入使用,我们可以在user.svelte
组件中设置数据;然后在home.svelte
中使用它。它们的关系是同级,都在App.svelte
中渲染
svelte
<script>
import { userStore } from "./store.js";
$effect(() => {
userStore.name = "Admin";
});
</script>
直接设置共享值,在需要的地方导入访问即可实现共享,并同步更新响应值。可以看到我们创建的响应式数据类型是对象,如果是基础数据类型,svelte
无法实现数据的响应式更新。对于更新,也不能直接赋值整个对象。
为了处理不能是基础类型的麻烦,svelte
提供一个实现最小存储状态的方案svelte/store
,通过包装变量,并提供操作方法,实现数据响应式更新。
svelte/store
是一个完整的数据状态管理方案,当直接使用符文$state
无法满足数据存储时,可以选择使用。
svelte/store
最小存储实现
svelte/store
提供了按照最简单约定实现数据存储共享。在组件内访问数据时,需要使用$
前缀,它定义了访问数据时采取订阅模式,这也导致在svelte
声明变量时最好不要使用$
前缀来声明变量。
相较于使用符文$state
来共享数据,Stores
提供了控制更新值的方法,可以监听到数据变化的事件。如果你需要这些能力,选择Stores
则更适合。
我们重新定义之前的userStore.svelte.js
,通过svelte/store
提供工的API可以直接定义基本类型变量。
js
import { writable } from "svelte/store";
export const userName = writable("Store hboot");
// 订阅变化
userName.subscribe((val) => {
console.log("The store userName changed:", val);
});
在需要使用的组件中导入使用,导入到Home.svelte
组件进行展示。使用之前必须加前缀$
,因为导入的userName
是一个对象,挂载了一些其他的操作方法。使用$userName
访问,svelte
会解析并自动订阅Store
数据,监听变化以便实时更新数据。
svelte
<script>
import { userName } from "../stores/userStore.svelte.js";
</script>
<h4>Home! {$userName}</h4>
对于更新值,在实例化创建store
后,导入的对象提供了两个方法set
和update
. set
直接接受一个值用于更新(比较是否相等);update
接受一个回调函数(oldVal)=>newVal
它有一个参数是当前变量值,函数返回一个值作为变量的新值被设定。
svelte
<script>
import { userName } from "../stores/userStore.svelte.js";
$effect(() => {
setTimeout(() => {
userName.set("Store Admin");
}, 4 * 1000);
});
</script>
.subscribe()
订阅方法会在初始化时调用一次。
writable(val,callback)
还接受一个可选参数callback
函数,函数有两个参数set
和update
,同上可用于更新状态值。该回调函数用于在有人订阅该变量时触发调用一次(从0-1的变化)。该回调函数返回一个停止订阅的函数(从1-0的变化)触发调用。
readable
外部只读共享状态
svelte/store
提供了外部只读共享数据状态,不允许外部更改。通过readable
定义,用法和writable
一样,区别就是实例对象上没有set
和update
方法。仅可通过readable
的第二个回调函数去修改状态值。
js
import { readable } from "svelte/store";
export const userAge = readable(18);
derived
派生状态
derived
派生状态,类似于计算值与之前符文的$derived
概念一样,不同之处在于它依赖的是共享数据状态变量,并且需要在第一个参数指定依赖的变量;第二个参数回调函数callback
参数为依赖的变量值,返回值作为派生状态的值。
js
import { derived } from "svelte/store";
export const welcomeUser = derived(userName, (name) => {
return `Welcome ${name}`;
});
回调函数可选的第二个参数set
和第三个参数update
用于手动更新值。
js
export const welcomeUser = derived(userName, (name, set) => {
set(`Welcome ${name}`);
});
因为set
和update
执行是异步的,派生值初始值可能会是undefined
,所以derived
接受第三个参数设置默认值initialValue
。
js
export const welcomeUser = derived(
userName,
(name, set) => {
set(`Welcome ${name}`);
},
"Welcome User"
);
derived
可以通过第一个参数为数组类型指定多个依赖项,则接受依赖值的回调函数的第一个参数也是数组访问依赖值。
js
export const welcomeUser = derived(
[userName, userAge],
([name, age], set) => {
set(`Welcome ${name}. You are ${age} years old.`);
}
);
readonly
只读共享状态
readonly
使得共享变量变为只读状态,它不能被修改,但是仍可以共享数据变化。不同于readable
,它没有任何可以修改的方法,只能由原始store
变量自己去修改。
js
export const userNameReadonly = readonly(userName);