Vue与Web组件
Web Components 是一组原生Web API的总称,允许开发者创建可重用的自定义元素。
我们认为Vue和Web Components主要是互补的技术。Vue对使用和创建自定义元素都有很好的支持。无论是将自定义元素集成到现有的Vue应用中,还是使用Vue构建和分发自定义元素,你都不是孤军奋战。
在Vue中使用自定义元素
Vue在 Custom Elements Everywhere测试中得分完美100%。在Vue应用中消费自定义元素与使用原生HTML元素基本相同,但有一些需要注意的事项
跳过组件解析
默认情况下,Vue 会尝试将非原生 HTML 标签解析为已注册的 Vue 组件,然后再将其作为自定义元素渲染。这将在开发期间导致 Vue 发出“无法解析组件”的警告。为了让 Vue 知道某些元素应该被当作自定义元素处理并跳过组件解析,我们可以指定 compilerOptions.isCustomElement
选项。
如果你在使用带有构建设置的 Vue,该选项应通过构建配置传递,因为它是一个编译时选项。
示例浏览器配置
js
// Only works if using in-browser compilation.
// If using build tools, see config examples below.
app.config.compilerOptions.isCustomElement = (tag) => tag.includes('-')
示例 Vite 配置
js
// vite.config.js
import vue from '@vitejs/plugin-vue'
export default {
plugins: [
vue({
template: {
compilerOptions: {
// treat all tags with a dash as custom elements
isCustomElement: (tag) => tag.includes('-')
}
}
})
]
}
示例 Vue CLI 配置
js
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => ({
...options,
compilerOptions: {
// treat any tag that starts with ion- as custom elements
isCustomElement: (tag) => tag.startsWith('ion-')
}
}))
}
}
传递 DOM 属性
由于 DOM 属性只能是字符串,我们需要将复杂数据作为 DOM 属性传递给自定义元素。当在自定义元素上设置属性时,Vue 3 会自动使用 in
运算符检查 DOM 属性的存在,并且如果键存在,将优先将其值设置为 DOM 属性。这意味着,在大多数情况下,如果你遵循 推荐的最佳实践,你通常不需要考虑这个问题。
然而,可能存在一些罕见情况,数据必须作为 DOM 属性传递,但自定义元素没有正确地定义/反映该属性(导致 in
检查失败)。在这种情况下,你可以使用 .prop
修饰符强制将 v-bind
绑定设置为 DOM 属性。
template
<my-element :user.prop="{ name: 'jack' }"></my-element>
<!-- shorthand equivalent -->
<my-element .user="{ name: 'jack' }"></my-element>
使用 Vue 构建 Web 组件
自定义元素的主要好处是它们可以与任何框架一起使用,甚至可以在没有框架的情况下使用。这使得它们非常适合分发组件,特别是当最终用户可能不在使用相同的 frontend 栈时,或者当你想隔离最终应用程序与它使用的组件的实现细节时。
defineCustomElement
Vue 支持通过 defineCustomElement
方法使用与 defineComponent
相同的 Vue 组件 API 创建自定义元素。此方法接受与 defineComponent
相同的参数,但返回一个扩展 HTMLElement
的自定义元素构造函数。
template
<my-vue-element></my-vue-element>
js
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
// normal Vue component options here
props: {},
emits: {},
template: `...`,
// defineCustomElement only: CSS to be injected into shadow root
styles: [`/* inlined css */`]
})
// Register the custom element.
// After registration, all `<my-vue-element>` tags
// on the page will be upgraded.
customElements.define('my-vue-element', MyVueElement)
// You can also programmatically instantiate the element:
// (can only be done after registration)
document.body.appendChild(
new MyVueElement({
// initial props (optional)
})
)
生命周期
当元素的
connectedCallback
首次被调用时,Vue 自定义元素将在其阴影根内部挂载一个内部 Vue 组件实例。当元素的
disconnectedCallback
被调用时,Vue 将检查元素是否在微任务周期后已从文档中分离。如果元素仍然在文档中,这是一个移动操作,组件实例将被保留;
如果元素已从文档中分离,这是一个删除操作,组件实例将被卸载。
属性
使用
props
选项声明的所有属性都将定义为自定义元素上的属性。Vue 将自动处理适当的属性 / 属性之间的反射。属性总是反射到相应的属性。
具有原始值(
string
、boolean
或number
)的属性会反射为属性。
Vue 还会自动将声明为
Boolean
或Number
类型的 props 转换为所需的类型,当它们作为属性(属性始终是字符串)设置时。例如,给定以下 props 声明jsprops: { selected: Boolean, index: Number }
以及自定义元素的使用
template<my-element selected index="1"></my-element>
在组件中,
selected
将被转换为true
(布尔值)和index
将被转换为1
(数字)。
事件
通过 this.$emit
或设置 emit
发射的事件将以原生的 CustomEvents 在自定义元素上分发。额外的事件参数(负载)将作为数组在 CustomEvent 对象的 detail
属性上公开。
插槽
在组件内部,可以使用 <slot/>
元素像往常一样渲染插槽。然而,当消费生成的元素时,它只接受 原生插槽语法
作用域插槽 不受支持。
当传递命名插槽时,使用
slot
属性而不是v-slot
指令template<my-element> <div slot="named">hello</div> </my-element>
提供 / 注入
Provide / Inject API 和其 Composition API 等价物 也在 Vue 定义的自定义元素之间工作。然而,请注意,这仅在自定义元素之间工作。也就是说,Vue 定义的元素无法注入非自定义元素 Vue 组件提供的属性。
应用级别配置
您可以使用 configureApp
选项配置 Vue 自定义元素的应用实例。
js
defineCustomElement(MyComponent, {
configureApp(app) {
app.config.errorHandler = (err) => {
/* ... */
}
}
})
SFC 作为自定义元素
defineCustomElement
也与 Vue 单文件组件(SFC)一起工作。然而,在默认的工具设置中,SFC 中的 <style>
仍然会在生产构建期间提取并合并到单个 CSS 文件中。当使用 SFC 作为自定义元素时,通常希望将 <style>
标签注入到自定义元素的 shadow root 中。
官方的 SFC 工具支持以“自定义元素模式”导入 SFC(需要 @vitejs/plugin-vue@^1.4.0
或 vue-loader@^16.5.0
)。在自定义元素模式下加载的 SFC 将其 <style>
标签内联为 CSS 字符串,并在组件的 styles
选项下公开。这将由 defineCustomElement
捕获并在实例化时注入到元素的 shadow root 中。
要选择此模式,只需在组件文件名后缀为 .ce.vue
即可。
js
import { defineCustomElement } from 'vue'
import Example from './Example.ce.vue'
console.log(Example.styles) // ["/* inlined css */"]
// convert into custom element constructor
const ExampleElement = defineCustomElement(Example)
// register
customElements.define('my-example', ExampleElement)
如果您想自定义在自定义元素模式下应导入哪些文件(例如,将 所有 SFC 视为自定义元素),可以将 customElement
选项传递给相应的构建插件
Vue 自定义元素库的技巧
使用 Vue 构建自定义元素时,元素将依赖于 Vue 的运行时。这取决于使用的功能,大约有 ~16kb 的基线大小成本。这意味着如果你只发送单个自定义元素,使用 Vue 不是很理想 - 你可能想使用纯 JavaScript、petite-vue 或专门针对小运行时大小进行优化的框架。然而,如果您正在发送带有复杂逻辑的自定义元素集合,基线大小是合理的,因为 Vue 将允许每个组件以更少的代码编写。你一起发送的元素越多,这种权衡就越好。
如果自定义元素将用于同时使用 Vue 的应用程序中,您可以选择将 Vue 从构建包外部化,以便元素将使用宿主应用程序相同的 Vue 版本。
建议导出单个元素构造函数,以便您的用户可以按需导入并使用他们想要的标签名注册。您还可以导出一个方便的函数来自动注册所有元素。以下是一个 Vue 自定义元素库的示例入口点:
js
import { defineCustomElement } from 'vue'
import Foo from './MyFoo.ce.vue'
import Bar from './MyBar.ce.vue'
const MyFoo = defineCustomElement(Foo)
const MyBar = defineCustomElement(Bar)
// export individual elements
export { MyFoo, MyBar }
export function register() {
customElements.define('my-foo', MyFoo)
customElements.define('my-bar', MyBar)
}
如果您有大量组件,您还可以利用构建工具功能,例如 Vite 的 glob 导入 或 webpack 的 require.context
从目录中加载所有组件。
Web 组件和 TypeScript
如果您正在开发应用程序或库,您可能想对 Vue 组件进行类型检查,包括定义为自定义元素的组件。
自定义元素使用原生 API 在全局范围内注册,因此默认情况下,在 Vue 模板中使用时不会有类型推断。为了为注册为自定义元素的 Vue 组件提供类型支持,我们可以在 Vue 模板和/或 JSX 中使用 GlobalComponents
接口 来注册全局组件类型。
typescript
import { defineCustomElement } from 'vue'
// vue SFC
import CounterSFC from './src/components/counter.ce.vue'
// turn component into web components
export const Counter = defineCustomElement(CounterSFC)
// register global typings
declare module 'vue' {
export interface GlobalComponents {
Counter: typeof Counter
}
}
Web 组件与 Vue 组件
一些开发者认为应避免框架专有的组件模型,并且仅使用自定义元素可以使应用程序“面向未来”。在这里,我们将尝试解释为什么我们认为这是一种过于简单化的看法。
自定义元素和 Vue 组件之间确实存在一定程度的特性重叠:它们都允许我们定义具有数据传递、事件发射和生命周期管理的可重用组件。然而,Web 组件 API 相对低级且基础。要构建实际的应用程序,我们需要许多额外的功能,而这些功能平台并未涵盖。
一个声明式和高效的模板系统;
一个便于跨组件逻辑提取和重用的响应式状态管理系统;
一种在服务器上渲染组件并在客户端(SSR)水合它们的性能方式,这对于 SEO 和 Web Vitals 指标,如 LCP 非常重要。原生自定义元素的 SSR 通常涉及在 Node.js 中模拟 DOM,然后序列化已更改的 DOM,而 Vue SSR 会在可能的情况下编译为字符串连接,这要高效得多。
Vue 的组件模型正是为了满足这些需求而设计的。
有了有能力的工程团队,您可能在原生自定义元素之上构建等效的功能 - 但这也意味着您正在承担长期维护自建框架的负担,同时失去了像 Vue 这样的成熟框架的生态系统和社区优势。
还有一些框架是以自定义元素作为组件模型的基础构建的,但它们不可避免地必须引入他们自己的解决方案来解决上述问题。使用这些框架意味着接受他们对如何解决这些问题的技术决策——尽管可能被宣传,但这并不自动使你免受未来潜在变动的风险。
也有一些领域我们发现自定义元素存在局限性
渴望的槽位评估阻碍了组件组合。Vue的作用域槽是组件组合的一种强大机制,由于原生槽位的渴望特性,自定义元素无法支持。渴望的槽位还意味着接收组件无法控制何时或是否渲染槽位内容。
如今,要将带有作用域CSS的自定义元素与阴影DOM一起发布,需要将CSS嵌入JavaScript中,以便在运行时将其注入到阴影根中。它们还会在SSR场景中导致标记中出现重复的样式。在这个领域有一些平台功能正在开发中——但截至目前,它们尚未得到普遍支持,并且仍存在生产性能/SSR问题需要解决。在此期间,Vue SFCs提供了CSS作用域机制,支持将样式提取到纯CSS文件中。
Vue将始终与Web平台的最新标准保持同步,并且如果它使我们的工作变得更容易,我们将乐于利用平台提供的任何东西。然而,我们的目标是提供既好用又实用的解决方案。这意味着我们必须以批判性的心态吸收新的平台功能——这涉及到填补标准不足的地方。