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 Attribute
和 Property
。这两个的区别就无须过多介绍了。Property 就是普通的类实例属性。而 HTML Attribute 相对特殊:
- HTML Attribute 可以在 HTML 中携带,或者通过
Element.setAttribute
设置 - 并且它的值只能是字符串形式,因此它只适合传递一些简单的原始类型。我们可能需要进行转换
- Attribute key 不区分大小写。通常习惯使用
kebab-case
形式。
最佳实践
关于怎么设计 Attribute 和 Property,社区已经积累了很多经验,常见的最佳实践有:
- 尽量同时提供 HTML Attribute 和 Property 两种形式。并在命名和行为上保持统一
- 不要通过 Attribute 传递复杂数据(非原始类型数据)
- 单数据源(Source of truth)。即不管是 Attribute 还是 Property 都是来源于单一的数据源。