如何将 Vue 组件库作为 `Web Components` 发布,使其可以在 React、Angular 等其他框架中使用?

大家好,我是你们今天的导游,不对,是讲师!今天咱们要聊聊一个挺有意思的话题:如何把 Vue 组件库包装成 Web Components,让它在 React、Angular 甚至更古老的 jQuery 项目里都能自由驰骋。

这就像把自家精心烹饪的美食,做成方便面,方便大家随时享用。听起来有点儿“降维打击”,但实际上能解决不少跨框架协作的难题。

咱们一步步来,先了解一下什么是 Web Components,然后看看怎么用 Vue 来“生产” Web Components,最后再聊聊发布和使用方面的一些注意事项。

第一部分:Web Components 是个啥?

Web Components 并不是一个具体的框架,而是一套 Web 标准,它允许你创建可重用的自定义 HTML 元素。你可以把它想象成乐高积木,每个积木(组件)都封装了自己的功能和样式,可以随意组合。

Web Components 主要包含三个核心技术:

  1. Custom Elements (自定义元素):允许你定义自己的 HTML 标签,比如 <my-button>
  2. Shadow DOM (影子 DOM):为你的组件提供了一个独立的 DOM 树,组件内部的样式和脚本不会影响到外部,反之亦然。这就像给每个组件都穿了一件“隐身衣”,避免了样式冲突。
  3. HTML Templates (HTML 模板)<template> 标签允许你定义可复用的 HTML 片段,这些片段在页面加载时不会被渲染,只有在需要时才会被克隆和使用。

Web Components 的优势显而易见:

  • 框架无关性:可以在任何支持 Web 标准的框架中使用。
  • 可重用性:组件可以被多次使用,减少代码冗余。
  • 封装性:Shadow DOM 保证了组件的内部实现不会泄露。
  • 标准化:遵循 Web 标准,具有良好的兼容性和未来发展潜力。

第二部分:Vue + Web Components = 无限可能

Vue 本身就是一个优秀的组件化框架,它提供了很多便捷的 API 来创建和管理组件。 我们可以借助 Vue 的力量,更轻松地创建 Web Components。

首先,我们需要安装一个 Vue 提供的官方库:@vue/compiler-sfc。这个库可以将 Vue 的单文件组件(.vue 文件)编译成 Web Components 所需的 JavaScript 代码。

npm install --save-dev @vue/compiler-sfc

接下来,咱们创建一个简单的 Vue 组件,比如一个计数器:

<!-- MyCounter.vue -->
<template>
  <div>
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
  </div>
</template>

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  props: {
    initialCount: {
      type: Number,
      default: 0,
    },
  },
  data() {
    return {
      count: this.initialCount,
    };
  },
  methods: {
    increment() {
      this.count++;
      this.$emit('count-changed', this.count); // 重要:触发事件
    },
    decrement() {
      this.count--;
      this.$emit('count-changed', this.count); // 重要:触发事件
    },
  },
  watch: {
    initialCount(newVal) {
      this.count = newVal;
    },
  },
});
</script>

<style scoped>
button {
  padding: 5px 10px;
  margin: 0 5px;
}
</style>

这个组件接收一个 initialCount prop,并提供 increment 和 decrement 两个方法来修改计数器的值。 注意,我们还触发了一个 count-changed 事件,这样外部框架就可以监听计数器的变化。 watch监听了props的变化,这样可以保证props的更新会同步到组件内部。

现在,我们需要编写一个脚本,将这个 Vue 组件编译成 Web Component。 创建一个 build-wc.js 文件:

// build-wc.js
const fs = require('fs');
const path = require('path');
const { compile } = require('@vue/compiler-sfc');

const componentPath = path.resolve(__dirname, './MyCounter.vue');
const componentCode = fs.readFileSync(componentPath, 'utf-8');

const { descriptor, errors } = compile(componentCode, {
  filename: 'MyCounter.vue',
  sourceMap: false,
  scoped: true, // Important: Enable scoped styles
});

if (errors && errors.length) {
  console.error(errors);
  process.exit(1);
}

const script = descriptor.script ? descriptor.script.content : '';
const template = descriptor.template ? descriptor.template.content : '';
const styles = descriptor.styles.map(style => style.content).join('n');

const wcCode = `
(function() {
  const template = document.createElement('template');
  template.innerHTML = `${template}`;

  class MyCounter extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
      this._count = 0;  // Initialize _count

      // Create a Vue instance
      this.vm = new Vue({
        data: {
          count: this._count,
        },
        methods: {
          increment() {
            this.count++;
            this.dispatchEvent(new CustomEvent('count-changed', { detail: this.count }));
          },
          decrement() {
            this.count--;
            this.dispatchEvent(new CustomEvent('count-changed', { detail: this.count }));
          },
        },
        watch: {
          count(newVal) {
            this._count = newVal;
          }
        },
        template: `<div>
                      <button @click="decrement">-</button>
                      <span>{{ count }}</span>
                      <button @click="increment">+</button>
                    </div>`,
      }).$mount(this.shadowRoot.querySelector('div'));
    }

    static get observedAttributes() {
      return ['initial-count'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
      if (name === 'initial-count') {
        this._count = parseInt(newValue, 10) || 0;
        this.vm.count = this._count;
      }
    }

    connectedCallback() {
      // Apply styles
      const style = document.createElement('style');
      style.textContent = `${styles}`;
      this.shadowRoot.appendChild(style);
    }

    get initialCount() {
      return this._count;
    }

    set initialCount(value) {
      this.setAttribute('initial-count', value);
    }

  }

  customElements.define('my-counter', MyCounter);
})();
`;

fs.writeFileSync(path.resolve(__dirname, './my-counter.js'), wcCode);

console.log('Web Component built successfully!');

这个脚本做了以下几件事:

  1. 读取 MyCounter.vue 文件的内容。
  2. 使用 @vue/compiler-sfc 将 Vue 组件编译成 JavaScript 代码。
  3. 创建一个 Web Component 类 MyCounter,继承自 HTMLElement
  4. constructor 中,创建 Shadow DOM,并将编译后的模板和样式添加到 Shadow DOM 中。
  5. 定义 observedAttributesattributeChangedCallback 方法,用于监听属性的变化,并更新 Vue 实例的数据。
  6. 使用 customElements.define 方法注册自定义元素 my-counter

注意事项:

  • Vue 实例的创建: 我们需要在 Web Component 的 constructor 中创建一个 Vue 实例,并将它挂载到 Shadow DOM 中的一个元素上。 这里我们手动创建了Vue实例,并将其中的数据和方法绑定到Web Component上。
  • 属性的传递: Web Component 的属性需要通过 observedAttributesattributeChangedCallback 方法来监听和处理。 外部设置的属性会通过 attributeChangedCallback 方法传递到Vue实例。
  • 事件的触发: Vue 组件中触发的事件需要使用 dispatchEvent 方法来传递到外部。 我们使用了 CustomEvent 来创建自定义事件。
  • 样式的处理: 样式需要手动添加到 Shadow DOM 中。 @vue/compiler-sfc 已经编译了 scoped 样式,所以我们可以直接将样式字符串添加到 Shadow DOM 中。
  • Prop 的同步:使用 watch 监听 props 的变化,确保 Vue 实例的数据与 Web Component 的属性保持同步。
  • Initial Count 优化:在构造函数中初始化 _count,确保组件在没有 initial-count 属性时也能正常工作。

现在,运行这个脚本:

node build-wc.js

这会在当前目录下生成一个 my-counter.js 文件,这就是我们编译后的 Web Component。

第三部分:发布和使用 Web Components

发布 Web Components 的方式有很多种,最简单的方式是直接将 my-counter.js 文件上传到你的网站服务器,然后在 HTML 页面中引入它:

<!DOCTYPE html>
<html>
<head>
  <title>Web Component Demo</title>
  <script src="./my-counter.js"></script>
</head>
<body>
  <h1>Web Component Demo</h1>
  <my-counter initial-count="10"></my-counter>

  <script>
    const myCounter = document.querySelector('my-counter');
    myCounter.addEventListener('count-changed', (event) => {
      console.log('Count changed:', event.detail);
    });
  </script>
</body>
</html>

在 React 中使用:

import React, { useRef, useEffect } from 'react';

function MyComponent() {
  const myCounterRef = useRef(null);

  useEffect(() => {
    if (myCounterRef.current) {
      myCounterRef.current.addEventListener('count-changed', (event) => {
        console.log('Count changed in React:', event.detail);
      });
    }
  }, []);

  return (
    <div>
      <h1>React Component using Web Component</h1>
      <my-counter initial-count="20" ref={myCounterRef}></my-counter>
    </div>
  );
}

export default MyComponent;

在 Angular 中使用:

首先,需要在 app.module.ts 中配置 schemas,允许 Angular 使用自定义元素。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule { }

然后,在组件中使用 Web Component:

import { Component, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h1>Angular Component using Web Component</h1>
    <my-counter initial-count="30"></my-counter>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
  constructor(private el: ElementRef) {}

  ngAfterViewInit() {
    const myCounter = this.el.nativeElement.querySelector('my-counter');
    myCounter.addEventListener('count-changed', (event) => {
      console.log('Count changed in Angular:', event.detail);
    });
  }
}

可以看到,在不同的框架中,使用 Web Components 的方式非常相似,只需要引入 JavaScript 文件,然后就可以像使用普通的 HTML 元素一样使用它们。

更高级的发布方式

  • NPM 发布:可以将 Web Components 打包成 NPM 包发布到 NPM 仓库,这样其他开发者就可以通过 npm install 命令来安装和使用你的组件。 需要配置 package.json 文件,并使用 npm publish 命令发布。
  • CDN 发布:可以将 Web Components 上传到 CDN 服务商,比如 unpkg 或 cdnjs,然后通过 CDN 链接在 HTML 页面中引入。 这种方式可以提高组件的加载速度。

第四部分:一些最佳实践和注意事项

  • 属性命名:Web Components 的属性应该使用 kebab-case 命名,比如 initial-count,而不是 camelCase 命名,比如 initialCount。 这是因为 HTML 属性是不区分大小写的。
  • 事件命名:Web Components 的事件应该使用 lowercase 命名,比如 count-changed
  • Shadow DOM 的使用:尽量使用 Shadow DOM 来封装组件的内部实现,避免样式冲突。
  • 版本控制:对于发布的 Web Components,应该遵循语义化版本控制规范,方便其他开发者了解组件的变化。
  • 文档编写:为你的 Web Components 编写清晰的文档,说明组件的属性、事件和使用方法。可以使用 Storybook 或其他文档工具来生成文档。
  • TypeScript 支持: 使用 TypeScript 可以提高代码的可维护性和可读性。

总结

将 Vue 组件库作为 Web Components 发布,可以实现跨框架的组件共享,提高代码的复用性和可维护性。 虽然需要一些额外的工作,比如编译和属性/事件的处理,但带来的好处是显而易见的。

希望今天的讲座对大家有所帮助! 祝大家在 Web Components 的世界里玩得开心!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注