Svelte从入门到精通——特定标签
发表于 阅读时长12分钟
目录
一些特殊的标签,在平时的开发中比较少遇到。但在特定的场合,却又能发挥大用处。本章中,我们将了解以下标签:
<svelte:self><svelte:component><svelte:element><svelte:window><svelte:head><svelte:fragment><svelte:options>
调用自身
<svelte:self>表示组件可以调用自己。这让人非常容易联想到,凡是涉及到树形结构的组件,都能使用这个标签。
<script>
// App.svelte
import Folder from "./Folder.svelte";
let data = [
{
name: "1.txt",
},
{
name: "folder1",
files: [
{
name: "2.png",
},
{
name: "folder2",
files: [
{
name: "3.doc",
},
],
},
],
},
{
name: "3.pdf",
},
];
</script>
<Folder {data} name={'Home'}/>
<script>
// Folder.svelte
export let data = [];
export let name = '';
</script>
<p>{name}</p>
<ul>
{#each data as item}
<li>
{#if item.files && item.files.length}
<svelte:self data={item.files} name={item.name} />
{:else}
{item.name}
{/if}
</li>
{/each}
</ul>
<style>
ul {
padding: 0.2em 0 0 0.5em;
margin: 0 0 0 0.5em;
list-style: none;
border-left: 1px solid #eee;
}
li {
padding: 0.2em 0;
}
</style>
使用这个标签时,需要注意终止条件的判断,否则很可能造成无限递归调用。
动态渲染
component
<svelte:component>用于动态渲染组件,可以看成是策略模式的简单使用。比如我们要设计一个组件,这个组件有不同的状态,然后我们根据不同的状态展示不同的icon。
正常情况下,我们可能会写出以下代码:
{#if type === 'success'}
{:else if type === 'warning'}
{:else if type === 'error'}
{:else}
{/if}
这种面条式的状态判断好处是直观,但一旦我们的状态多起来,代码会变得非常冗长。
看下<svelte:component>的使用:
<script>
import SuccessIcon from "./icon/SuccessIcon.svelte";
import InfoIcon from "./icon/InfoIcon.svelte";
import WarningIcon from "./icon/WarningIcon.svelte";
import ErrorIcon from "./icon/ErrorIcon.svelte";
$: icon = {
success: SuccessIcon,
info: InfoIcon,
warning: WarningIcon,
error: ErrorIcon,
}[type];
</script>
<svelte:component this={icon} />
这是我们的一个Icon组件,能够根据传递的type参数来显示不同的icon。所有状态及对应状态的映射内容收拢在一个对象里,根据状态的不同直接取对应状态的内容返回。

element
<svelte:element>的功能和<svelte:component>很像,只是component是针对自定义的组件,而element是针对内建的html元素。
我们同样可以动态指定element:
<script>
let htmlType = 'div';
</script>
<select bind:value={htmlType}>
<option value="div">div</option>
<option value="h2">h2</option>
<option value="button">button</option>
</select>
<svelte:element this={htmlType}>text</svelte:element>
但需要注意的是,动态绑定的元素不能够使用bind:value。
当我们使用静态指定时,能够正常使用bind:value:
<svelte:element this="input" bind:value={text} />
而当我们使用动态指定时,bind:value失效

同时需要注意的是,如果我们动态指定的元素是自闭合标签,并且在<svelte:element>内有其他内容,内容不会展示。
<script>
let type = "input";
</script>
<select bind:value={type}>
<option value="input">input</option>
<option value="div">div</option>
</select>
<svelte:element this={type}>text</svelte:element>

BOM & DOM
window
使用<svelte:window>来对window对象进行监听。
<svelte:window on:event={handler} />
也可以使用bind对window对象进行属性绑定,只是支持绑定的属性有限:innerWidth、innerHeight、outerWidth、outerHeight、 scrollX、 scrollY、online 、devicePixelRatio。
比如我们可以:
<svelte:window bind:scrollY={y}>
我们注意官网的一段提示:
Note that the page will not be scrolled to the initial value to avoid accessibility issues. Only subsequent changes to the bound variable of
scrollXandscrollYwill cause scrolling. However, if the scrolling behaviour is desired, callscrollTo()inonMount().
举个例子试验一下:
<script>
let y = 600;
$: console.log('scrolly', y);
</script>
<svelte:window bind:scrollY={y} />
<div class="box red">red</div>
<div class="box green">green</div>
<button on:click={() => {y = 400}}>change</button>
<style>
.box {
width: 500px;
}
.red {
height: 500px;
background-color: red;
}
.green {
height: 1000px;
background-color: green;
}
</style>
当我们初次设置成400后,因为页面在最顶部,此时scrollY为0。初始的设置并没有生效,等我们滑到底部时,点击按钮成功让页面滚动到400px的位置。
与<svelte:window>类似的标签还有<svelte:document>和<svelte:body>,显而易见,这两个标签分别操作document和document.body。
head
<svelte:head>
往<head>标签中添加<script>、<link>、<title>和<meta>等元素。

操作window对象或document对象的这几个标签<svelte:window>、<svelte:document>、<svelte:body>、<svelte:head>,都只能在代码顶层中进行添加。不像<svelte:self>和<svelte:component>等标签可以在循环判断或条件判断中嵌套添加。
fragment
在一些场景中,我们希望我们的页面下可以放置多个并排层级的元素,比如:
<section>模块一</section>
<section>模块二</section>
然而现实却是我们会遇到框架的限制提示。
在Vue 2.x中:

在React中:
为了符合框架的要求,我们不得不用一个毫无意义的标签来包裹他们,以符合框架中只能返回一个顶级标签的需求:
<div>
<section>模块一</section>
<section>模块二</section>
</div>
在Vue和React中,都提供了一种称为Fragment的组件来解决此类问题。Fragment是一个虚拟组件,它可以将组件功能绑定到一个单一的元素中,而不需要创建一个多余的DOM节点
虽然Svelte的html的标签填写非常灵活,可以直接写成:
<script></script>
<section>模块一</section>
<section>模块二</section>
<style></style>
但在某些场景下,仍旧需要类似Fragment的功能支持。就比如下面这个例子:
<!-- Child.svelte -->
<div>
<header>
<slot name="header">头部</slot>
</header>
<main>
<slot>内容</slot>
</main>
<footer>
<slot name="footer">底部</slot>
</footer>
</div>
<style>
footer {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
<script>
import Child from "./Child.svelte";
</script>
<Child>
<div slot="footer">
<div>底部左边</div>
<div>底部右边</div>
</div>
</Child>

我们原本在组件内容实现了底部内容的flex布局,然而因为我们在外部,需要一个标签来设置slot="footer"而导致需要对从外部传的内容进行一层包裹。导致原本期望的flex布局并不生效。一种解决方式是我们在自己外部自己再手动实现flex布局,另一种方式便是使用<svelte:fragment>标签。
<Child>
<svelte:fragment slot="footer">
<div>底部左边</div>
<div>底部右边</div>
</svelte:fragment>
</Child>
可以看到<svelte:fragment>不会被当成实际标签编译展示到页面上。
编译
<svelte:options>为我们提供了自定义编译功能的能力,使用方式如下:
<svelte:options option={value} />
可能会使用到的option参数有:
- immutable
- accessors
- namespace
- customElement
在大多数情况下,我们并不需要使用到这个标签,除非有深层次的定制。更多参数详见compiler。
immutable
<script>
let obj = {
name: 'hello'
}
</script>
<input bind:value={obj.name} />
name: {obj.name}

当我们添加<svelte:options immutable={true} />时,我们的对象将变成不可变对象:
<svelte:options immutable={true} />
<script>
let obj = {
name: 'hello'
}
</script>
<input bind:value={obj.name} />
name: {obj.name}
可以看到,对象数据没有改变。
accessors
如果我们配置accessors为true,编译器会为组件的props创建getters和setters。如果没配置,默认是false, 编译器只会为只读的导出数据(如const,class,function)创建getters。如果我们配置了 customElement: true ,accessors默认为true。
上面这段话有点让人费解,我们直接看例子:
<script>
// Child.svelte
let age = 18;
export let name = 'Svelte';
export function getAge() {
console.log(age);
}
</script>
<button on:click={getAge}>{name}</button>
<script>
// App.svelte
import Child from './Child.svelte';
let childRef;
function onClick() {
console.log(childRef);
console.log(childRef.name);
console.log(childRef.getAge());
}
</script>
<button on:click={onClick}>Get Child</button>
<Child bind:this={childRef} />
首先我们定义了一个子组件,对外导出name属性和getAge方法,然后我们在父组件中引用,并试图通过ref直接调用子组件的name和getAge。在《dom引用》章节,我们曾试验过直接调用子组件实例的数据和方法,答案很明显,我们无法直接使用childRef.name。

我们直接查看Child.svelte编译后的结果,发现只有getAge方法提供了getter。
/* Child.svelte generated by Svelte v4.2.12 */
let age = 18;
function getAge() {
console.log(age);
}
class Child extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, { name: 0, getAge: 1 });
}
get getAge() {
return getAge;
}
}
export default Child;
而当我们开启了accessors={true}后,Child.svelte的编译结果为:
/* Child.svelte generated by Svelte v4.2.12 */
let age = 18;
function getAge() {
console.log(age);
}
class Child extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, { name: 0, getAge: 1 });
}
get name() {
return this.$$.ctx[0];
}
set name(name) {
this.$$set({ name });
flush();
}
get getAge() {
return getAge;
}
}
export default Child;
Child.svelte的name提供了getter和setter。重新执行页面内容:
可以看到内容都被正确打印出来。
我们甚至可以直接childRef.name = 'xxx';来操作子组件的值。
customElement
customElement支持我们使用自定义标签。 我们先自定义一个标签:
<!-- Custom.svelte -->
<svelte:options customElement="my-element" />
<script>
export let name = 'world'
</script>
<h1>hello {name}</h1>
<style>
h1 {
color: orange;
}
</style>
然后修改我们的vite.config.js:
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
svelte({
compilerOptions: {
customElement: true,
},
}),
],
});
在main.js中引入组件,这样我们就无需在其他组件中反复导入:
// main.js
import './app.css'
import App from './App.svelte'
import './Custom.svelte';
const app = new App({
target: document.getElementById('app'),
})
export default app
最后在App.svlete中测试一下:
<script>
let name = 'svelte';
</script>
<input bind:value={name} />
<my-element {name} />

customElement除了支持传入一个字符串,还支持传入对象。
<svelte:options
customElement={{
tag: 'xxx',
shadow: 'xxx',
props: {},
extend: (customElementConstructor) => {}
/>
小结
本章我们学习了:
- Svelte内置的一些特殊标签:
<svelte:slef>能使组件调用自身;<svelte:component>和<svelte:element>能进行动态渲染;<svelte:fragment>能提供一个专门用于包裹多个子节点的功能标签;<svelte:options>可以修改Svelte的编译能力;还有一些操作DOM和Window的标签等