看完本文你将会学习到以下知识:
- 微组件定义
- 微组件适用场景
- Web Components 发展情况和优劣势
- 字节开源作品 magic-microservices
- 京东开源作品 micro-app
- 微组件实现思路
一、什么是微组件?
目前业界并没有对微组件有一个明确的定义,我个人的理解是,微组件是较微前端更细粒度的拆分和组合方案。
一个完整的微前端方案应该包含以下功能:
- JS 沙箱
- 样式隔离
- 数据通信
- 技术栈无关
- 路由功能
- HTML entry 接入方式
一个完整微组件解决方案,相对上述微前端应该包含以下能力:
- JS 沙箱(可选)
- 样式隔离(可选)
- 数据通信
- 技术栈无关(可选)
路由功能HTML entry 接入方式(UMD JS 接入或直接引用)
二、什么“奇葩”场景下会用到微组件?
微组件并不是页面组合和拆分的银弹,而是在极其特殊场景下的特定解决方案。
至于网上有人吹代码共享能力,我个人着实不建议这样搞的,没有必要实现在 React 中使用 Vue 组件,或者在 Vue 中使用 React 组件(虽然能做到)。
2.1 旧项目细颗粒化改造
所谓旧项目细粒度化改造是指,老 angular 项目需要重构成 Vue 项目,或者 Vue 项目要重构成 React 项目,并且每次改造还不是以页面为单位,而是以组件级别的改造为主,这种场景其实就比较适合。
但说实话这种场景是极其罕见的,有搞微组件的功夫,还不如以页面为单位进行重构,然后以微前端的方式进行接入,或者通过反向代理的方式,匹配到改造后的路由 URL,则代理到的新应用。
2.2 扩展点或外部代码嵌入
所谓扩展点就是用户在拿不到 SaaS 产品源码的情况下,提供插槽的能力,让用户实现一些个性化的需求。目前国内做的最好的应该是有赞的扩展点实现:
这种让别人代码运行在自家 SaaS 系统上的做法,就比较适合使用微组件。微组件既能提供沙箱、通信等能力,其颗粒度也不是页面级别的。
2.3 老项目无力改造,但需要加新功能
有一些老项目,比如 angular1 写的项目,需要增加新功能,大多数新人对 angular1 既不愿意学也觉得难学,学了也没用。当这种老项目需要增加新功能时,可以考虑以微组件的方式嵌入 React 或者 Vue 代码。
三、微组件实现探索
3.1 Web Components
Web Components 优势
谈到跨技术栈和沙箱,很多人第一时间就想到了 Web Components
,其天然有以下优势:
- 浏览器原生能力,完美跨技术栈
- 样式隔离
而 Google 开源的 lit 工具,让我们能以类似 React 的方式快速开发 Web Components 组件。
import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
static styles = css`p { color: blue }`;
@property()
name = 'Somebody';
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}
<!DOCTYPE html>
<head>
<script type="module" src="./simple-greeting.js"></script>
</head>
<body>
<simple-greeting name="World"></simple-greeting>
</body>
并且社区已经有了基于 Web Components 的组件库,例如张鑫旭大佬的 LuLu UI 和 @shoelace-style/shoelace。
所谓天下大势分久必合,前端框架因为 JS 的期初羸弱而百花齐放,未来势必因为 JS 的强大而重新归一,相信未来会越来越多人接受并使用它。
Web Components 劣势
虽然 Web Components 前途一片光明,但现在还存在很多不足,主要有以下几点:
- 兼容性问题
- 无 JS 沙箱能力
- 样式隔离一般不符合需求
- 数据通信受限
- 不能与常用框架融合
兼容性问题
从 caniuse 上看,IE 全军覆没,Safari 部分支持,虽然社区上有兼容性 polyfill,但使用后也还是会有各种问题。如果有这两个浏览器要求的产品,基本就告别 Web Components 了。
无 JS 沙箱能力
很多人误以为 Web Components 的
Shadow DOM
有 JS 沙箱的能力,但其实并没有,也就是 Web Components 没有任何 JS 沙箱的能力。
样式隔离不符合要求
Web Components 不开启 Shadow DOM
时,是没有样式隔离能力的,也就是组件里面的样式会影响到外面,外面的样式也会影响到里面。
如果开启了 Shadow DOM
,则又是完全隔离,也就是组件里面无法影响到外面,但也无法继承外面的公共样式。比如我们外部引入了 ant-design
的样式,里面用了 ant-design
组件,还需要重复引入,这一般是不符合我们的需求的。
数据通信受限 Web Components 组件你可以理解为和原生组件具有相同的性质。我们知道原生组件是不能够传递引用类型的属性给组件的,也就是你想传递一个函数、数组或者对象到组件内部是做不到的。对于习惯了 React 或者 Vue 中可以传递任意类型属性的我们,感觉十分难受。
对于对象和数组还是可以 JSON.stringify 传递进去,然后在里面再 JSON.parse。
不能与常用框架融合 虽然有了 lit.dev ,但目前仍然是三大框架的天下,使用纯 Web Components 开发目前并不为国内所接受。
总结
如果你可以无视以上缺点,那么 Web Components 是你的不二之选。
3.2 magic-microservices
magic-microservices 是字节跳动开源的极其轻量级的微前端工厂函数。
优势
- 轻量 (不到 4 kb)
- 抹平框架(通过简单的生命周期适配器,可以将任何技术栈包裹在
Web Components
里面,也就是Web Components
是皮,React
、Vue
是肉) - 通信方案(提供了一个
useProps
解决了Web Components
通信中无法传递引用类型的问题)
其通过以 Web Components
为桥梁,可以做到链接不同技术栈,并提供了属性通信的解决方案。
缺点
- 兼容性(毕竟还是 Web Components)
- 无 JS 沙箱能力
- 无样式隔离方案
总结
如果你的项目不需要样式隔离、JS 沙箱(一般旧项目重构场景),且不需要考虑兼容性,magic-microservices 是不二之选,具体的 DEMO 示例可以参考我之前写的一篇 《跨技术栈渲染研究》。
3.3 micro-app
micro-app
是京东开源的一套微前端框架,与 single-spa 和 qiankun 不同,其借鉴了 Web Components 的思想,将微前端封装成一个 Web Components 组件,从而实现微前端的组件化渲染。
优势
缺点
- 微前端方案,无法加载
UMD JS 组件
- 兼容性问题
我们今天是探讨微组件,并非微前端,为什么要提一个微前端解决方案呢?通过上述能力可以看出,它除了不能够加载 UMD JS
和兼容性问题外,其他的功能完全满足。
结论
考虑改造成本比较低,只不过把获取 html 并渲染变成获取 JS,所以在我们公司的场景中最终选择了它。
3.4 总结
Web Components | magic-microservices | micro-app | |
---|---|---|---|
跨技术栈 | ✅ | ✅ | ✅ |
CSS 隔离 | ✅ | ✅ | ✅ |
CSS 隔离后可继承公共样式 | ❌ | ❌ | ✅ |
JS 沙箱 | ❌ | ❌ | ✅ |
可融合三大框架 | ❌ | ✅ | ✅ |
不同技术栈源码方式引入 | ❌ | ✅ | ❌ |
不同技术栈 UMD 方式引入 | ❌ | ❌ | ✅(改造后) |
引用类型属性通信 | ❌ | ✅ | ✅ |
四、micro-app 改造方案
我司的场景是扩展点能力,也就是可以让别人的代码以扩展点的方式跑在我们的应用上。其开发方式是,外部人员开发好组件,将其打包成 umd
格式的 JS(需要将 CSS 也打进去),然后上传到我们的开发平台,当命中匹配条件时,则加载该 js 资源,并进行渲染。
所以我们最终敲定是通过给 micro-app
增加加载 umd js
的能力实现微组件。
效果演示
默认情况下,没有匹配到扩展点,所以是只有两个表单项;当输入
orgcode=aaa
时,匹配到扩展点,扩展点的内容是渲染第三个表单项。
思路详解
一个正常的 micro-app 子应用可以这样注册:
<micro-app name='app1' url='http://localhost:3000/' baseroute='/my-page'></micro-app>
name
:要全局唯一,用于通信url
:子应用 HTML 入口地址baseroute
:子应用路由
为了实现加载 umd js 的能力,我们需要增加或更改如下属性:
baseroute
:不需要了componentMode
:表示使用组件渲染模式(新增)url
:不再是 html 地址,而是 UMD 模块格式的 JS 地址(更改含义)exportName
:可选参数,如果是具名导出的,比如export { Foo, Bar }
,我们需要渲染Foo
组件,此时就需要传递exportName='foo'
(新增)
<micro-app name='xx' componentMode="true" url='http://foo.com/index.umd.js' exportName="Foo"></micro-app>
其最核心的逻辑改动就是这一行,如果判断是
componentMode
则返回只有一个 js
的 html 模板,否则的话就走原逻辑。
组件适配器
我们知道微前端一般都需要导出声明周期,类似下面的东西:
// 👇 将渲染操作放入 mount 函数 -- 必填
export function mount () {
ReactDOM.render(<App />, document.getElementById("root"))
}
// 👇 将卸载操作放入 unmount 函数 -- 必填
export function unmount () {
ReactDOM.unmountComponentAtNode(document.getElementById("root"))
}
// 微前端环境下,注册mount和unmount方法
if (window.__MICRO_APP_ENVIRONMENT__) {
window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
// 非微前端环境直接渲染
mount()
}
我们这边的想法是,对于扩展点开发者而言,想让他感觉到就是在开发一个普通组件或者 npm 包,不想让他知道什么乱七八糟的生命周期,类似的感觉是如下:
import Foo from './Foo'
export default Foo
但是声明周期这一步还是要做的,所以我们决定将其放到主应用去做,最终给其增加了声明周期适配器的功能,其使用效果如下:
import microApp from '@私仓/micro-app'
import adapter from './adapter.js'
microApp.start({
adapter
})
// adapter.js
/**
* 适配器
* App: 根组件
* eventCenter: 事件中心
* onerror:错误处理回调
**/
export function adapter(App, eventCenter, onerror) {
let _container;
/**
* 挂载时
* container 挂载节点
**/
function mount(container) {
_container = container;
const props = eventCenter.getData() || {};
ReactDOM.render(React.createElement(App, props), _container);
}
// 更新时,重新渲染
eventCenter.addDataListener((data) => {
const props = data || {};
ReactDOM.render(React.createElement(App, props), _container);
});
function unmount() {
// 卸载应用
ReactDOM.unmountComponentAtNode(_container);
}
return {
mount,
unmount,
};
}
其代码实现也很简单,就是在原来获取 UMD 逻辑上,将获取到的结果传递给适配器函数执行即可。
但是注意,这样的话,扩展点的框架就限定了,不能跨技术栈,除非有多个框架适配器,并且在扩展点接口中有相关字段标识。
总结
从我个人认知方面,微组件只是极个别场景下的解决方案(目前识别到的只有扩展点和细粒度重构),其他场景并不建议使用微组件。
微组件实现方面基本上是基于 Web Components 能力做跨技术栈,在我司的扩展点需求中最终选择了 micro-app
+ umd JS 链接
的方式。
除了上述提的几种解决方案,你还有更好的方案吗,有的话,欢迎评论、吐槽。