跨框架的前端UI组件库|使用Vue开发Web Components

4,088 阅读5分钟

前言

通过这篇文章你将了解到:

  1. 使用Vue3开发Web Components
  2. 使用本地git仓库进行离线组件库管理
  3. 在Vue项目中使用Web Components

什么是Web Components?

Web Components 是一组 Web 原生 API 的总称,允许开发人员创建可重用的自定义组件。
谷歌公司一直在推动浏览器的原生组件,即 Web Components API。相比第三方框架,原生组件简单直接,符合直觉,不用加载任何外部模块,代码量小。目前,它还在不断发展,但已经可用于生产环境。

项目背景

项目A是使用Vue2开发的已有项目,项目B和其他几个项目是使用Vue3开发的新项目,各项目中有一部分业务重叠,需要将重叠部分作为业务组件单独抽离出来。C为通用组件库项目。本文围绕A、B、C三个项目进行讲解。
关键构成如下:

  • 项目A: Vue2 + Vue-cli
  • 项目B: Vue3 + Vite + Element Plus
  • 项目C: Vue3 + Vite + Element Plus

项目C,开发组件库

先从组件库项目C入手,项目结构如下:
image.png
几个关键的文件:

  • src/components/Example/Example.ce.vue 示例组件
  • src/components/Example/Example.scss 示例组件样式
  • src/components/index.ts 组件库入口 先上代码

Example.ce.vue

<script setup lang="ts">
import { reactive, ref } from 'vue';
import { ElButton, ElTable, ElTableColumn } from 'element-plus';

withDefaults(defineProps<{ title: string }>(), { title: 'title' });

const emits = defineEmits<{
  (eventName: 'testEvent', val: number): void;
}>();

const num = ref(0);

const tableData = reactive([
  {
    date: '2016-05-03',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles'
  },
  {
    date: '2016-05-02',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles'
  },
  {
    date: '2016-05-04',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles'
  },
  {
    date: '2016-05-01',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles'
  }
]);

const onClick = () => {
  num.value++;
  tableData.push({
    date: '2016-05-0' + num.value,
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles'
  });
  emits('testEvent', num.value);
};
</script>

<template>
  <div class="vx-example">
    <div>{{ title }}</div>
    <ElButton @click="onClick">{{ num }}</ElButton>
    <ElTable :data="tableData" border style="width: 100%">
      <ElTableColumn prop="date" label="Date" width="180" />
      <ElTableColumn prop="name" label="Name" width="180" />
      <ElTableColumn prop="address" label="Address" />
    </ElTable>
  </div>
</template>

<style lang="scss">
@use 'element-plus/theme-chalk/src/button.scss';
@use 'element-plus/theme-chalk/src/table.scss';
@use './Example.scss';
</style>

Example.scss

.vx-example {
  width: 800px;
  background: lightblue;
  padding: 10px;
}

index.ts

import { defineCustomElement } from 'vue';
import Example from './Example/Example.ce.vue';

// 转换为自定义元素构造器
const ExampleElement = defineCustomElement(Example);

// 注册
function registerAll() {
  customElements.define('vx-example', ExampleElement);
}

export { Example, registerAll };

作为Vue3组件引入

首先让我们在项目C中新建一个测试页面,先引入组件,并且添加测试响应事件。

import Example from '@/components/Example/Example.ce.vue';

const onVueEvent = (val: number) => {
  console.log('vue', val);
};

在template中添加标签

<Example title="Vue Component" @test-event="onVueEvent" />

在style中添加如下引入

@use '@/components/Example/Example.scss';

不出意外测试页面会显示如下组件,点击按钮数字会增加,并且控制台会打印对应的数字。 image.png
这里相信大家会有几个疑问:

  1. 为什么组件要以.ce.vue结尾?
  2. 为什么样式不写在组件的style部分,而是单独引入?
  3. 为什么组件要引入element-plus的组件再使用?
  4. 为什么组件要引入element-plus的组件样式? 这里引用官网教程里的一段话

官方 SFC 工具支持以“自定义元素模式”(需要 @vitejs/plugin-vue@^1.4.0 或 vue-loader@^16.5.0 )导入 SFC。以自定义元素模式加载的 SFC 将其 <style> 标签作为 CSS 字符串内联,并在组件的 styles 选项中暴露出来,然后会被 defineCustomElement 获取并在实例化时注入隐式根。要选用此模式,只需使用 .ce.vue 作为文件拓展名即可

因为使用.ce.vue结尾,做为vue组件引入时不会加载组件内的样式,所以使用了一个单独的样式文件引入来解决这个问题。
问题3是因为只有明确引入的内容才会打包进Web Components组件,如果不引入的话,element的组件标签会被当作原生标签加载,那么页面就会错误。
问题4是因为Web Components组件的样式使用的是shadow dom内的style,所以需要将用到的样式单独引入一份。
更多的介绍可以在官网教程的Vue 与 Web Components部分进行了解。
我们继续进行,看看如何以Web Components加载组件。

作为Web Components引入

首先我们在项目入口添加如下代码:

import { registerAll } from '@/components';

registerAll();

然后在刚才的测试页面增加一些测试代码,最终完整的测试页面代码如下:

<script setup lang="ts">
import Example from '@/components/Example/Example.ce.vue';
import { onMounted } from 'vue';

onMounted(() => {
  const example = document.getElementById('example');
  if (example) {
    example.addEventListener('testEvent', (e) => {
      console.log('web', e);
    });
  }
});

const onVueEvent = (val: number) => {
  console.log('vue', val);
};
</script>

<template>
  <div>
    <Example title="Vue Component" @test-event="onVueEvent" />
    <vx-example id="example" style="margin-top: 20px" title="Web Component" />
  </div>
</template>

<style lang="scss" scoped>
@use '@/components/Example/Example.scss';
</style>

此时页面会显示如下: image.png
Web Components组件的事件会被调度为原生的CustomEvents,附加的事件参数 (payload) 会作为数组暴露在CustomEvent对象的details property上。
点击组件内按钮控制台会打印如下内容,这个detail就是我们自定义事件所传递的参数。 image.png
细心的朋友可能会发现虽然此时页面可以正确加载,但是加载Web Components组件时控制台会产生下面这样的警告:
image.png
只要在vite.config.ts内增加如下配置即可。

plugins: [
    vue({
      template: {
        compilerOptions: {
          // 将所有以 vx- 开头的标签作为自定义元素处理
          isCustomElement: tag => tag.startsWith('vx-')
        }
      }
    })
  ]

完整的vite.config.ts文件如下:

import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import eslintPlugin from 'vite-plugin-eslint';
import { visualizer } from 'rollup-plugin-visualizer';
import copy from 'rollup-plugin-copy';

export default ({ mode }) =>
  defineConfig({
    plugins: [
      vue({
        template: {
          compilerOptions: {
            // 将所有以 vx- 开头的标签作为自定义元素处理
            isCustomElement: (tag) => tag.startsWith('vx-')
          }
        }
      }),
      visualizer({ filename: './lib/stats.html' }),
      copy({
        hook: 'generateBundle',
        //执行拷贝
        targets: [
          {
            src: './node_modules/element-plus/theme-chalk/base.css',
            dest: './lib'
          }
        ]
      })
    ],
    build: {
      lib: {
        entry: resolve(__dirname, 'src/components/index.ts'), // 设置入口文件
        name: 'data-jump-components', // 起个名字,安装、引入用
        fileName: () => `data-jump-components.js`, // 打包后的文件名
        formats: ['es']
      },
      outDir: 'lib'
    }
  });

重点有两部分:

  1. copy插件,用来拷贝element的css
  2. build配置,打包为lib build后的lib文件夹如下,这就是我们的Web Components组件库。
    image.png
    这里将一下为什么要拷贝element的base.css,是因为element-plus使用了css变量,加载在shadow dom内的样式里无法读取这些变量,需要在页面顶层声明这些变量,也就是引入组件库时需要在项目引入base.css。
    到这里我们的组件库就剩最后一步了,提交代码到git仓库。

项目B,Vue3项目引入组件库

首先我们在项目B中运行如下命令:

npm i git+http://192.168.20.51/data-jump/data-jump-components.git

这个地址就是我们项目C的git地址,这里要注意需要有项目C拉取代码的权限
然后在项目中这样按照正常的Vue组件一样使用就可以了:

<script setup lang="ts">
import Example from 'data-jump-components/src/components/Example/Example.ce.vue';
</script>

<template>
  <div>
    <Example title="Vue Component" />
  </div>
</template>

<style lang="scss" scoped>
@use 'data-jump-components/src/components/Example/Example.scss';
</style>

项目A,Vue2项目引入组件库

运行如下命令:

npm i git+http://192.168.20.51/data-jump/data-jump-components.git

vue.config.js增加如下配置:

// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // 将所有以 vx- 开头的标签作为自定义元素处理
          isCustomElement: tag => tag.startsWith('vx-')
        }
      }))
  }
}

在项目入口增加如下代码:

import 'data-jump-components/lib/base.css';
import { registerAll } from 'data-jump-components/lib/data-jump-components';

registerAll();

在需要使用组件的位置加入下面的标签即可:

<vx-example id="example" title="Web Component" />

到这里我们就完成了整个组件库的创建和本地安装。

扩展,在任何地方使用组件库

只需要新建一个html文件,与打包好的lib文件夹放在一起,输入以下代码就可以使用我们的组件了。

<!DOCTYPE html>
<html>
  <head>
    <link type="text/css" href="./lib/base.css" rel="stylesheet" />
    <script type="module">
      import { registerAll } from './lib/data-jump-components.js';
      registerAll();
    </script>
  </head>
  <body>
    <div id="app">
      <vx-example id="example" title="Web Component" />
    </div>
  </body>
</html>

回顾

容易出现问题的几个关键点:

  1. 组件需要以.ce.vue结尾
  2. 组件内的使用的组件和样式需要明确引入
  3. 使用时需要全局引入用到的css变量
  4. 使用时需要在构建工具的配置文件内配置自定义元素规则,来跳过组件的解析