还不会Svelte?快来一起学习吧🤓

在很多地方早就听到过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

    js 复制代码
    let name = "world";
  • 标签style用于书写样式,

  • 对于html结构代码可以直接书写。

不同于vue / react可以直接引入到html中使用,svelte需要编译打包后才能引入到html中。所以我们不能直接使用App.svelte组件,我们写一个简单的编译逻辑,执行编译完成后看看生成的代码。

创建compile.js编译 App.sveltesvelte/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可以查看往期文章

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)} />

可以通过设置setFnnull,从而实现绑定值只读,这对于一些元素属性不可设置时有用,比如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,可选svgmathml
  • 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后,导入的对象提供了两个方法setupdate. 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函数,函数有两个参数setupdate,同上可用于更新状态值。该回调函数用于在有人订阅该变量时触发调用一次(从0-1的变化)。该回调函数返回一个停止订阅的函数(从1-0的变化)触发调用。

readable 外部只读共享状态

svelte/store 提供了外部只读共享数据状态,不允许外部更改。通过readable定义,用法和writable一样,区别就是实例对象上没有setupdate方法。仅可通过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}`);
});

因为setupdate执行是异步的,派生值初始值可能会是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);

参考

svelte docs

svelte example

rspack-cli

相关推荐
好好好明天会更好几秒前
Vue2 中 ref 的使用指南
前端
北京_宏哥几秒前
🔥《刚刚问世》系列初窥篇-Java+Playwright自动化测试-17- 如何优雅地切换浏览器多窗口(详细教程)
java·前端·浏览器
刺客_Andy1 分钟前
React 第三十五节 Router 中useNavigate 的作用及用途详解
前端·react.js
哄哄5752 分钟前
微信小程序键盘弹起时,底部输入框抬起至键盘上方,顶部导航栏固定不动
前端
南囝coding4 分钟前
这款AI自动生成播客工具,必须收藏!
前端·后端
Mapmost4 分钟前
【AI技术闲谈】AI一键生成前端代码?实测4款工具后的操作指南
前端·ai编程
玲小珑44 分钟前
Auto.js 入门指南(六)多线程与异步操作
android·前端
白瓷梅子汤1 小时前
跟着官方示例学习 @tanStack-table --- Header Groups
前端·react.js
喝牛奶的小蜜蜂1 小时前
个人小程序:不懂后台,如何做数据交互
前端·微信小程序·小程序·云开发
front_explorers1 小时前
Umi项目必看,从Webpack到Rspack,KMI引领性能革命🚀
前端