首页
/ Svelte 自定义元素深入解析:将 Svelte 组件编译为 Web Components 的完整实战指南

Svelte 自定义元素深入解析:将 Svelte 组件编译为 Web Components 的完整实战指南

2026-09-04 17:34:36作者:贡沫苏Truman

本文基于 Svelte 仓库中 自定义元素官方文档,系统讲解如何将 Svelte 组件编译为自定义元素(Custom Elements / Web Components):从 customElement 编译选项与 <svelte:options> 的两种声明方式,到 props 与 DOM 属性的双向映射、自定义元素的生命周期时序、tag / shadow / props / extend 各配置项的细节,再到生产环境必须了解的封装、插槽与跨元素 Context 限制。读完本文,你既能正确地在非 Svelte 应用中分发组件,也能从 Svelte 源码层面理解每一处行为的背后机制。

一、基本形态:编译选项与 <svelte:options>

Svelte 组件可以编译为自定义元素,核心是 customElement: true 这个编译选项。该选项在编译器选项中被定义为一个"参数化"校验器,默认值为 () => false(即默认关闭),在 validate-options.js 中可以看到它的声明。此外,自 Svelte 5 起,旧的顶层 tag 编译选项已被移除,取而代之的是组件内部的声明方式,validate-options.js 中对这一废弃路径给出了明确报错提示:

// packages/svelte/src/compiler/validate-options.js
customElement: parametric(
    (() => false),
    (input, keypath) => {
        if (typeof input !== 'boolean') {
            throw_error(`${keypath} should be true or false`);
        }
        return input;
    }
),
// ...
tag: removed(
    'The tag option has been removed in Svelte 5. Use `<svelte:options customElement="tag-name" />` inside the component instead. ...'
)

组件内声明标签名使用 <svelte:options> 元素的 customElement 属性。传入字符串时该字符串被用作 tag 选项;传入对象时则携带完整配置。一个最典型的示例如下:

<svelte:options customElement="my-element" />

<script>
	let { name = 'world' } = $props();
</script>

<h1>Hello {name}!</h1>
<slot />

官方文档 的表述中,自定义元素内部可以通过 $host rune 访问宿主元素。值得注意的是,从当前仓库源码结构看,编译产物实际是把宿主元素以 $$host prop 的形式传给内部组件:在 transform-client.js 中,$$host 会被加入 rest props 的剔除名单;而在运行时封装 custom-element.js 中,创建内部组件时的 props 里明确包含 $$host: this

// packages/svelte/src/internal/client/dom/elements/custom-element.js(节选)
this.$$c = createClassComponent({
    component: this.$$ctor,
    target: this.$$shadowRoot || this,
    props: {
        ...this.$$d,
        $$slots,
        $$host: this
    }
});

也就是说,内部 Svelte 组件本身"并不知道自己被封装成了自定义元素",宿主引用是通过 props 通道注入的。

二、延迟注册:静态 element 属性与 customElements.define

并非每个组件都需要暴露为自定义元素。你可以为内部组件省略标签名,把它们当普通 Svelte 组件使用。但消费者在需要时仍可通过静态 element 属性完成注册——该属性持有自定义元素构造函数,且仅在 customElement 编译选项为 true 时可用:

import MyElement from './MyElement.svelte';

customElements.define('my-element', MyElement.element);

这个 element 属性在源码中的落点是 custom-element.js 的 create_custom_element 函数。它在生成类之后执行 Component.element = Class; return Class;,把构造函数挂回组件对象上。

而"是否自动 define"由编译阶段决定。在 transform-client.js 中:

  • <svelte:options> 中提供了字符串形式的 tag,编译产物会直接追加 customElements.define('my-element', ...) 语句,导入该组件时即完成注册;
  • 若未提供标签名,则只生成 create_custom_element(...) 调用(并挂载 element 属性),把 define 的时机留给使用者;
  • 在开启 HMR 时,define 会被包进 customElements.get(tag) === null 的判断中,避免热更新时重复注册报错。

三、Props 即 DOM 属性:读写、类型转换与属性映射

自定义元素一旦定义完成,就可以像普通 DOM 元素一样使用:

document.body.innerHTML = `
	<my-element>
		<p>This is some slotted content</p>
	</my-element>
`;

按照 组件 props 的一般约定,所有 props 都会暴露为 DOM 元素的属性(property),并且在可能的情况下也可以以 HTML 属性(attribute)的形式读写:

const el = document.querySelector('my-element');

// 读取 'name' prop 的当前值
console.log(el.name);

// 设置新值,Shadow DOM 会随之更新
el.name = 'everybody';

这段"属性读写"背后是一条完整的运行时链路,全部位于 custom-element.js

  1. getter/setter 注入create_custom_element 遍历 props_definition,对每个 prop 在类原型上 define_property 一个 getter/setter(L308-L330)。getter 优先读取已挂载组件实例上的值,组件尚未创建时则回落到暂存数据 $$d;setter 在组件存在时调用 component.$set({ [prop]: value }) 更新。
  2. 属性观察static get observedAttributes() 会把所有 prop(或其自定义的 attribute 名)小写后列出(L302-L306),于是 setAttribute 会触发 attributeChangedCallback,后者通过 get_custom_element_value 完成"属性字符串 → prop 值"的转换后再 $setL194-L199)。
  3. 类型转换规则get_custom_element_valueL234-L264)按 prop 的 type 做双向转换——Object/Array 序列化/反序列化为 JSON 字符串,Boolean 反射为空字符串或移除,Number 通过一元 + 转换,默认则按 String 处理。

有一个关键约束需要注意:必须显式列出所有属性。如果写成 let props = $props() 而没有在解构中声明 name,编译器无法确定哪些 prop 要暴露为 DOM 元素属性,相应 getter/setter 就不会生成。这一点与源码对应:transform-client.js 只会为解析到的 properties(即解构声明出的具名 props)生成条目,未在 <svelte:options> 中列出的 prop 会被补一个空配置对象 {},但完全匿名的 props 不会进入映射。

另外一个源码细节可以佐证"类型默认推断":当某 prop 未声明 type、但其默认值是布尔字面量时,编译器会自动将其类型推断为 'Boolean'transform-client.js L600-L606),这解释了为什么布尔型 prop 以属性形式存在/缺失时能正确映射为 true/false

四、组件生命周期:Wrapper 模式与"下一个 tick"

Svelte 的自定义元素采用 wrapper(包装器)方式 从 Svelte 组件生成:内部的 Svelte 组件完全不知道自己是自定义元素,生命周期由外层 wrapper(即运行时中的 SvelteElement 类,custom-element.js L17-L226)负责协调。理解以下时序对排查真实问题至关重要:

  • 创建延迟一个 tickconnectedCallback 被触发后,内部组件不会立即创建。源码中 connectedCallback 先执行 await Promise.resolve() 再初始化,目的是"让可能的子插槽元素先被创建/挂载"。
  • 提前赋值不丢失:元素插入 DOM 前通过 JS 直接赋值的属性,会被暂存到 $$d 字段,组件创建时统一端口过去(源码注释原话:Port over props that were set programmatically before ce was initializedL133-L142)。但注意:这不适用于调用导出的函数——它们在元素挂载前不可用。若确需在组件创建前调用函数,可用下文的 extend 选项绕过,把方法定义在扩展类上而非组件内。
  • Shadow DOM 更新是批量的:创建或更新时,shadow DOM 在下一个 tick 才反映最新值,而非立即。这样更新可以合并批处理,且某些会"临时(同步地)把元素移出 DOM"的操作不会导致内部组件被意外卸载。
  • 销毁同样延迟一个 tickdisconnectedCallback 之后,源码通过 Promise.resolve().then(...) 的微任务判断元素是否仍 $$cn === false,是才销毁内部组件(L201-L211),注释明确写道这是为了区分"真正的移除"与"DOM 内部的移动"。

五、Component options:tag、shadow、props、extend 详解

自 Svelte 4 起,可以把 <svelte:options> 中的 customElement 写为对象,精细定制以下方面:

  • tag: string:可选的标签名。设置后,导入该组件时即会向文档的 customElements 注册表定义该标签。

  • shadow:可选,修改 shadow root 的创建方式,接受三种取值:

    • "none":不创建 shadow root。此时样式不再是封装(encapsulated)而是普通作用域样式,且不能使用 slots
    • "open":以 mode: "open" 创建 shadow root(未指定时的默认行为,见 transform-client.js L635-L636:布尔值、'open' 或未指定都回落到 { mode: 'open' });
    • ShadowRootInit 对象:原样传给 attachShadow()
  • props:可选,逐 prop 修改属性映射行为,每个 prop 支持:

    • attribute: string:prop 与 HTML 属性名之间的映射名。默认属性名就是小写后的属性名,可用 attribute: "<desired name>" 修改;
    • reflect: boolean:默认 prop 值的变化不会回写到 DOM 属性;设为 true 后开启反射。实现上由一个受控的 render effect 驱动:组件创建后挂载 this.$$me,在每次更新时遍历开启 reflect 的 prop,把值经 toAttribute 转换后 setAttributeL153-L174),并用 $$r 标志位避免"反射触发 attributeChangedCallback 再改组件"的回环;
    • type: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object':属性与 prop 相互转换时的类型,默认按 String 处理;例如数值型应显式声明 type: "Number"

    无需列出全部属性,未列出的使用默认配置。

  • extend:可选,接受一个函数。Svelte 生成的自定义元素类会作为参数传入,期望你返回扩展后的类。适合有非常具体的生命周期需求,或者想通过 ElementInternals 增强表单集成的场景。

文档给出的完整示例如下:

<svelte:options
	customElement={{
		tag: 'custom-element',
		shadow: {
			mode: import.meta.env.DEV ? 'open' : 'closed',
			clonable: true,
			// ...
		},
		props: {
			name: { reflect: true, type: 'Number', attribute: 'element-index' }
		},
		extend: (customElementConstructor) => {
			// Extend the class so we can let it participate in HTML forms
			return class extends customElementConstructor {
				static formAssociated = true;

				constructor() {
					super();
					this.attachedInternals = this.attachInternals();
				}

				// Add the function here, not below in the component so that
				// it's always available, not just when the inner Svelte component
				// is mounted
				randomIndex() {
					this.elementIndex = Math.random();
				}
			};
		}
	}}
/>

<script>
	let { elementIndex, attachedInternals } = $props();
	// ...
	function check() {
		attachedInternals.checkValidity();
	}
</script>

编译器如何校验这些配置:解析逻辑集中在 read/options.js。它要求 customElement 属性值要么是纯文本(即 tag 字符串),要么是对象字面量;props 必须是对象且每个 prop 的值只能是 type / reflect / attribute 三个字面量属性的组合,type 只接受 StringNumberBooleanArrayObject 五个值(L108-L129);shadow 只能是 'open' / 'none' 字面量或对象(L134-L143)。tag 名本身还有一道校验(L252-L262):必须符合 HTML 规范的有效自定义元素名正则(小写字母开头、必须含连字符),且不能是 annotation-xmlcolor-profilefont-face 等保留名。

关于 extend 中的 TypeScriptextend 函数内支持 TypeScript,但有限制——必须把某个 <script> 标记为 lang="ts",且只能使用可擦除语法(erasable syntax);extend 中的代码不会经过 script 预处理器处理。

六、注意事项与限制

把组件打包为自定义元素,是将其提供给非 Svelte 应用(原生 HTML/JS 或绝大多数框架)消费的实用途径。但官方文档明确列出了与"普通 Svelte 组件"的重要差异,务必逐条了解:

  • 样式是封装(encapsulated)而非仅仅作用域(scoped)(除非设置 shadow: "none")。这意味着全局样式文件(如 global.css)中的规则不会作用于自定义元素,包括带 :global(...) 修饰符的样式。
  • 样式不再抽成独立的 .css 文件,而是以内联 JS 字符串的形式打进组件。
  • 自定义元素通常不适合服务端渲染(SSR):在 JavaScript 加载之前,shadow DOM 是不可见的。
  • 插槽内容的渲染时机不同:在 Svelte 中 slotted 内容是懒渲染(lazily)的,在 DOM 中则是立即渲染(eagerly)。换言之,即使组件的 <slot> 位于 {#if ...} 块内,插槽内容也一定会被创建;同样,把 <slot> 放进 {#each ...} 块也不会让插槽内容渲染多次。
  • 已废弃的 let: 指令无效:自定义元素没有把数据传回"填充插槽的父组件"的机制。
  • 老浏览器需要 polyfill 才能支持自定义元素。
  • Context 不能跨越自定义元素边界:同一自定义元素内部的普通 Svelte 组件之间可以使用 Context;但父自定义元素里 setContext 的内容,无法被子自定义元素里的 getContext 读取。
  • 不要声明以 on 开头的属性或属性名:它们会被解释为事件监听器。例如 <custom-element oneworld={true}> 会被 Svelte 当作 customElement.addEventListener('eworld', true),而不是 customElement.oneworld = true

七、验证与测试入口

以上行为在仓库中均有对应的测试与实现可供查证:

  • 解析与校验:read/options.jscustomElement 属性解析、tag/props/shadow/extend 的合法性检查)
  • 编译选项定义:validate-options.jscustomElement 选项)、analyze/index.js(把编译选项与 <svelte:options> 中的声明合并为 custom_element 分析结果)
  • 客户端代码生成:transform-client.jscreate_custom_element 调用、observedAttributes 映射、customElements.define 注入)
  • 运行时封装:custom-element.jsSvelteElement 基类、connectedCallback/disconnectedCallback、类型转换与反射)
  • 行为测试:runtime-browser/custom-elements-samples 目录下存放了大量自定义元素的运行时测试样例(覆盖 prop 反射、类型转换、生命周期等场景),可用于回归验证本文描述的每个行为

小结

Svelte 的自定义元素能力本质上是一条"编译期生成配置 + 运行时包装器"的流水线:<svelte:options customElement=...> 在解析阶段被严格校验并固化为 props/slots/shadow 配置;客户端转换阶段据此生成 create_custom_element 调用(以及可选的 customElements.define);运行时的 SvelteElement 包装器负责生命周期时序、属性双向映射与类型转换。掌握 tagshadowprops(attribute/reflect/type)、extend 四个配置项,并牢记第六节的限制清单,你就能把 Svelte 组件可靠地交付给任何 DOM 宿主使用。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384