在很多地方早就听到过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.jsjslet 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、接口请求等。这和reactuseEffecthook 概念一样,但是它可以自动收集在其内部使用的响应式状态,无需手动声明。在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}设置是否启用符文模式,启用后不再兼容svelte3和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);