来一瓶 Web Component 魔法胶水

2023-10-23 14:22:13 浏览数 (2)

Web Component 已经被浏览器广泛支持,不再是新鲜玩意了,它有很多使用场景,比如编写跨框架的组件库、微前端,完全用它开发复杂的应用也没问题。

而今天我要介绍的是 Web component 如何扮演框架/微应用之间的胶水层这个角色。

Web Component 是前端通用协议

在软件系统中 ,前端通常作为各种后端服务的聚合层,一个页面中可能承载来自多个业务域的内容:

因此前端的业务边界并那么清晰,很难做到和后端微服务一一映射:

就像微服务一样,微应用之间也会互相依赖。比如在微前端中,我们可能会依赖其他子应用的业务组件,并且这些子应用有可能是异构的,比如 React 引用 Vue 的组件、或者 Vue 3 引用老旧 Vue 2 组件。

尽管在大部分情况下,我们并不建议子应用之间产生耦合,但是考虑迁移的成本等现实因素,我们只能妥协。下面是一些常见的解决办法有:

  • 将这些组件剥离出来,放置到通用的业务组件库。
  • 子应用暴露服务方法,传入 DOM 挂载点,让目标子应用将元素渲染到这个 DOM 元素上
  • 子应用通过 Web Component 暴露服务。

第一种方式。很多情况,这些组件很难剥离出来,就算剥离出来为了应付异构消费,我们可能还是得通过 Web component 重构。

而第二种方式,还不如直接使用 Web Component , 这是一种标准组件 API,主流的视图框架都支持。

就如异构的微服务使用通用的 HTTP、RPC 协议来通信一样,Web Component 就是异构前端应用之间的「通用协议」。

所以本文的话题就是围绕着:怎么把现有的组件包装成 Web Component 展开。

Web component 概览

Web Component ,或者说自定义元素(Custom Element) 更加贴切,它就是支持我们创建自定义 HTML 元素的相关’技术集合’

上面的思维导图,基本覆盖了你需要掌握的内容了。如果读者熟悉主流视图框架(比如 Vue),只要花一两个小时就可以掌握啦。这块资料也比较多, 我就展开细节了,推荐 MDN 的相关教程。

先简单写几个 Hello world 吧:

创建一个自定义元素:

代码语言:javascript复制
<!DOCTYPE html>
<html>
<head>
    <style>
        hello-world { color: red; }
    </style>
    <script>
        class HelloWorld extends HTMLElement {
            connectedCallback() {
                this.innerHTML = `<p>Hello, World!</p>`;
            }
        }
        customElements.define('hello-world', HelloWorld);
    </script>
</head>
<body>
    <hello-world></hello-world>
</body>
</html>

Shadow DOM 版本:

代码语言:javascript复制
<!DOCTYPE html>
<html>
<head>
    <script>
        class HelloWorld extends HTMLElement {
            constructor() {
                super();
                this.attachShadow({ mode: 'open' });
            }

            connectedCallback() {
                this.shadowRoot.innerHTML = `
                    <style>:host { color: red; }</style>
                    <p>Hello, World!</p>
                `;
            }
        }

        customElements.define('hello-world', HelloWorld);
    </script>
</head>
<body>
    <hello-world></hello-world>
</body>
</html>

包装 Vue 3 组件, 官方支持:

代码语言:javascript复制
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)

包装 React 组件:

代码语言:javascript复制
class XSearch extends HTMLElement {
  connectedCallback() {
    const mountPoint = document.createElement('span');
    this.attachShadow({ mode: 'open' }).appendChild(mountPoint);

    const name = this.getAttribute('name');
    const url = 'https://www.google.com/search?q='   encodeURIComponent(name);
    const root = ReactDOM.createRoot(mountPoint);
    root.render(<a href={url}>{name}</a>);
  }
}
customElements.define('x-search', XSearch);

将原有的组件包装成 Web Component, 我们需要做以下工作:

  • 怎么把自定义元素的 Attribute 或者 Property 映射到组件的 Props?
  • 怎么将组件的事件定义映射成 自定义元素 的事件?
  • 组件的插槽又怎么处理?
  • Shadow DOM 要不要用?

下面开始详细介绍这些细节。

映射 Props

HTML 自定义元素有两种输入参数形式:HTML AttributeProperty。这两个的区别就无须过多介绍了。Property 就是普通的类实例属性。而 HTML Attribute 相对特殊:

  • HTML Attribute 可以在 HTML 中携带,或者通过 Element.setAttribute 设置
  • 并且它的值只能是字符串形式,因此它只适合传递一些简单的原始类型。我们可能需要进行转换
  • Attribute key 不区分大小写。通常习惯使用 kebab-case 形式。

最佳实践

关于怎么设计 Attribute 和 Property,社区已经积累了很多经验,常见的最佳实践有:

  • 尽量同时提供 HTML Attribute 和 Property 两种形式。并在命名和行为上保持统一
  • 不要通过 Attribute 传递复杂数据(非原始类型数据)
  • 单数据源(Source of truth)。即不管是 Attribute 还是 Property 都是来源于单一的数据源。

0 人点赞