跳转到内容

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 将自动处理适当的属性 / 属性之间的反射。

    • 属性总是反射到相应的属性。

    • 具有原始值(stringbooleannumber)的属性会反射为属性。

  • Vue 还会自动将声明为 BooleanNumber 类型的 props 转换为所需的类型,当它们作为属性(属性始终是字符串)设置时。例如,给定以下 props 声明

    js
    props: {
      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.0vue-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平台的最新标准保持同步,并且如果它使我们的工作变得更容易,我们将乐于利用平台提供的任何东西。然而,我们的目标是提供既好用又实用的解决方案。这意味着我们必须以批判性的心态吸收新的平台功能——这涉及到填补标准不足的地方。

Vue和Web Components已加载