将Vue组件封装为Web Component

10,021 阅读4分钟

什么是Web Component

WebCompoent用官方的说法是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。

也就是说,它是一套可以支持原生实现组件化的技术。这意味这Web Component是浏览器原生支持的组件方式,可以在任何的框架原生环境下中使用!

从MDN的描述中可以看到,Web Components旨在解决代码复用、组件自定义、复用管理等问题,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。

如何写Web Component

在Vue3.2以前,写原生的Web Component组件需要额外的学习Web Component的语法,生命周期,了解Custom elementsShadow DOMHTML templates等一些概念,需要一定的学习成本。

而Vue3.2的defineCustomElement API就可以很好的解决这个问题,将你的vue组件通过简单的方式封装成Web Component。

defineCustomElement API

现在 Vue 3.2 已经发布,借助 defineCustomElement,从 Vue 组件创建原生自定义元素比以往任何时候都容易。

在这篇文章中,我们将学习如何使用这个新的API将Vue组件转换为Web组件,如何将其打包为库,以及如何在纯HTML中使用它。

create-custom-element脚手架

借助create-custom-element这个脚手架可以很方便的创建一个支持defineCustomElementAPI的vue项目

npm:
npm init custom-element

yarn:
yarn create custom-element

pnpm: 
pnpm create custom-element

这里使用脚手架创建了一个支持ts的custom-element项目 image.png

打开项目,使用pnpm i安装好依赖后,我们通过学习这个脚手架的模板来学习如何使用defineCustomElement

目录结构

image.png

这里的目录结构与vite-Vue项目的目录结构类似

创建自定义元素

首先看src/CustomElement/index.ce.vue

第一步是创建一个我们想要用作自定义元素的 Vue 组件。在此示例中,我们将构建一个可以在网站上切换深色主题的按钮。

<script setup lang='ts'>
import { ref } from 'vue'
const isDark = ref(false)
</script>

<template>
  <button @click="isDark = !isDark">
    <span v-if="isDark">🌙</span>
    <span v-else>🌞</span>
  </button>
</template>

这里没什么特别的,就是使用vue语法创建了一个vue组件

从Vue 组件到Web Component

再看到src/CustomElement/index.ts

import { defineCustomElement } from 'vue'
import VueDarkModeSwitch from './index.ce.vue'

// Vue generates a new HTML element class from the component definition.
export const DarkModeSwitch = defineCustomElement(VueDarkModeSwitch)

// Register the custom element so that it can be used as <dark-mode-switch>.
export function register (tagName: string = 'dark-mode-switch') {
  customElements.define(tagName, DarkModeSwitch)
}

这个文件是使Vue组件封装为WebComponet的关键:

  1. 首先从vue中引入defineCustomElement

    import { defineCustomElement } from 'vue'

  2. 再引入之前写的vue组件并命名为VueDartModeSwitch(这里的名字可以根据需求自定义)

    import VueDarkModeSwitch from './index.ce.vue'

  3. 使用defineCustomElement包装VueDartModeSwitch,并导出

    export const DarkModeSwitch = defineCustomElement(VueDarkModeSwitch)

  4. 导出register方法,方便自定义组件标签名

    export function register (tagName: string = 'dark-mode-switch') { 
        customElements.define(tagName, DarkModeSwitch) 
    }
    

给组件添加样式

重新回到src/CustomElement/index.ce.vue文件

这里可能有人会奇怪,这里的vue文件,为什么会以 .ce.vue 结尾?

这里的ce是CustomElemtn的简写,vue在默认情况下,以.ce.vue 结尾的文件将以Web Component导入。

这样做的目的是为了让vue组件支持Shadow DOM,在以.ce.vue结尾的文件中。写style标签不需要加上scoped属性也能使css属性隔离,互不影响。

了解原因后,现在为我们的组件加上样式:

<style>
:host {
  --color: #fbbf24;
  --bg-normal: #fAfAf9;
  --bg-active: #f5f5f4;
  --font-size: 24px;
}

:host([dark]) {
  --color: #fef3c7;
  --bg-normal: #262626;
  --bg-active: #2d2d2d;
}

button {
  background-color: var(--bg-normal);
  border: none;
  border-radius: .5rem;
  color: var(--color);
  cursor: pointer;
  display: flex;
  font-size: var(--font-size);
  overflow: hidden;
  padding: 0.4em;
  transition: background-color 0.3s ease, color 0.3s cubic-bezier(0.64, 0, 0.78, 0);
}

button:hover,
button:focus {
  background-color: var(--bg-active);
  outline: none;
}

.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s ease-out;
}

.slide-enter-from {
  transform: translateY(-150%);
}

.slide-enter-to,
.slide-leave-from {
  transform: translateY(0);
}

.slide-leave-to {
  transform: translateY(150%);
}
</style>

使用Web Component

现在再来到src/APP.vue文件

<script setup lang="ts">
import { register } from './CustomElement';
register('dart-element')
</script>

<template>
  <dart-element></dart-element>
</template>

<style>
#app {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>
  1. 首先从封装完成的src/CustomElement/index.ts中引入之前导出的register方法
  2. 执行register()方法,register方法接收一个字符串参数,为自定义的标签名称
  3. 在模板语法里使用<dart-element></dart-element>标签

在vue中使用

如果我们想在Vue中使用这些Web Component组件,我们需要到vite.config.ts里进行配置plugins,让vue能够识别Web Component组件,并跳过vue的编译部分,直接执行。

plugins: [vue({
    template: {
      compilerOptions: {
        isCustomElement: (tag: string[]) => tag.includes('-')
      }
    }
  })]

当然这些配置,都通过create-custom-element脚手架自动生成,你无需操心。

运行Web Component

是时候,将这个组件跑起来看看了

pnpm dev

image.png 出现这个小太阳就证明你已经成功跑起来了!!

从devtools的element节点可以看出这里确实是一个WebComponent,我们通过defineCustomElement API将一个vue组件封装得到WebComponent

可以在这个页面进行运行,调试你的组件

打包Web Component

使用vue写一个Web Component最主要的目的是打包成一个公共的包,可以在不同的框架下使用

Vite.js提供了一个打包模式,我们这里直接使用Vite.js进行打包。

build: {
    target: 'esnext',
    minify: 'terser',
    lib: {
      entry: 'src/CustomElement/index.ts',
      formats: ['es', 'cjs', 'iife'],
      name: 'CustomElement'
    }
  }

这里将src/CustomElement/index.ts作为入口文件,打包分别生成三种模块化规范的包。

使用pnpm build进行打包

image.png

即可以在dist文件夹下找到打包的三个模块

在原生环境下使用Web Component

创建一个index.html文件引入打包的模块

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script type="module">
    import { register } from './dist/custom-element.es.js'
    register('custom-element')
  </script>
</head>
<body>
  <custom-element></custom-element>
</body>
</html>

完美运行 可以看到我们之前在vue里写的组件,也在原生的HTML环境下成功运行

image.png

End👋🏼

Vue defineCustomElement使开发Web Component变得前所未有的简单。

都看到这里了,点个赞吧👍