微组件实践

4,508 阅读4分钟

看完本文你将会学习到以下知识:

  • 微组件定义
  • 微组件适用场景
  • Web Components 发展情况和优劣势
  • 字节开源作品 magic-microservices
  • 京东开源作品 micro-app
  • 微组件实现思路

一、什么是微组件?

目前业界并没有对微组件有一个明确的定义,我个人的理解是,微组件是较微前端更细粒度的拆分和组合方案。

一个完整的微前端方案应该包含以下功能:

  • JS 沙箱
  • 样式隔离
  • 数据通信
  • 技术栈无关
  • 路由功能
  • HTML entry 接入方式

一个完整微组件解决方案,相对上述微前端应该包含以下能力:

  • JS 沙箱(可选)
  • 样式隔离(可选)
  • 数据通信
  • 技术栈无关(可选)
  • 路由功能
  • HTML entry 接入方式(UMD JS 接入或直接引用)

二、什么“奇葩”场景下会用到微组件?

微组件并不是页面组合和拆分的银弹,而是在极其特殊场景下的特定解决方案。 至于网上有人吹代码共享能力,我个人着实不建议这样搞的,没有必要实现在 React 中使用 Vue 组件,或者在 Vue 中使用 React 组件(虽然能做到)。 image.png

2.1 旧项目细颗粒化改造

所谓旧项目细粒度化改造是指,老 angular 项目需要重构成 Vue 项目,或者 Vue 项目要重构成 React 项目,并且每次改造还不是以页面为单位,而是以组件级别的改造为主,这种场景其实就比较适合。

但说实话这种场景是极其罕见的,有搞微组件的功夫,还不如以页面为单位进行重构,然后以微前端的方式进行接入,或者通过反向代理的方式,匹配到改造后的路由 URL,则代理到的新应用。

image.png

2.2 扩展点或外部代码嵌入

所谓扩展点就是用户在拿不到 SaaS 产品源码的情况下,提供插槽的能力,让用户实现一些个性化的需求。目前国内做的最好的应该是有赞的扩展点实现:

image.png 这种让别人代码运行在自家 SaaS 系统上的做法,就比较适合使用微组件。微组件既能提供沙箱、通信等能力,其颗粒度也不是页面级别的。

2.3 老项目无力改造,但需要加新功能

有一些老项目,比如 angular1 写的项目,需要增加新功能,大多数新人对 angular1 既不愿意学也觉得难学,学了也没用。当这种老项目需要增加新功能时,可以考虑以微组件的方式嵌入 React 或者 Vue 代码。

三、微组件实现探索

3.1 Web Components

image.png

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>

image.png

并且社区已经有了基于 Web Components 的组件库,例如张鑫旭大佬的 LuLu UI@shoelace-style/shoelace

image.png

所谓天下大势分久必合,前端框架因为 JS 的期初羸弱而百花齐放,未来势必因为 JS 的强大而重新归一,相信未来会越来越多人接受并使用它。

Web Components 劣势

虽然 Web Components 前途一片光明,但现在还存在很多不足,主要有以下几点:

  • 兼容性问题
  • 无 JS 沙箱能力
  • 样式隔离一般不符合需求
  • 数据通信受限
  • 不能与常用框架融合

兼容性问题caniuse 上看,IE 全军覆没,Safari 部分支持,虽然社区上有兼容性 polyfill,但使用后也还是会有各种问题。如果有这两个浏览器要求的产品,基本就告别 Web Components 了。 image.png 无 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

image.png

magic-microservices 是字节跳动开源的极其轻量级的微前端工厂函数。

优势

  • 轻量 (不到 4 kb)
  • 抹平框架(通过简单的生命周期适配器,可以将任何技术栈包裹在 Web Components里面,也就是 Web Components是皮,ReactVue 是肉)
  • 通信方案(提供了一个 useProps解决了 Web Components通信中无法传递引用类型的问题)

其通过以 Web Components 为桥梁,可以做到链接不同技术栈,并提供了属性通信的解决方案。

缺点

  • 兼容性(毕竟还是 Web Components)
  • 无 JS 沙箱能力
  • 无样式隔离方案

总结

如果你的项目不需要样式隔离、JS 沙箱(一般旧项目重构场景),且不需要考虑兼容性,magic-microservices 是不二之选,具体的 DEMO 示例可以参考我之前写的一篇 《跨技术栈渲染研究》

3.3 micro-app

image.png

micro-app 是京东开源的一套微前端框架,与 single-spa 和 qiankun 不同,其借鉴了 Web Components 的思想,将微前端封装成一个 Web Components 组件,从而实现微前端的组件化渲染。

优势image.png

缺点

  • 微前端方案,无法加载 UMD JS 组件
  • 兼容性问题

我们今天是探讨微组件,并非微前端,为什么要提一个微前端解决方案呢?通过上述能力可以看出,它除了不能够加载 UMD JS和兼容性问题外,其他的功能完全满足。

结论

考虑改造成本比较低,只不过把获取 html 并渲染变成获取 JS,所以在我们公司的场景中最终选择了它。

3.4 总结

Web Componentsmagic-microservicesmicro-app
跨技术栈
CSS 隔离
CSS 隔离后可继承公共样式
JS 沙箱
可融合三大框架
不同技术栈源码方式引入
不同技术栈 UMD 方式引入✅(改造后)
引用类型属性通信

四、micro-app 改造方案

我司的场景是扩展点能力,也就是可以让别人的代码以扩展点的方式跑在我们的应用上。其开发方式是,外部人员开发好组件,将其打包成 umd格式的 JS(需要将 CSS 也打进去),然后上传到我们的开发平台,当命中匹配条件时,则加载该 js 资源,并进行渲染。

所以我们最终敲定是通过给 micro-app 增加加载 umd js的能力实现微组件。

效果演示

aa.gif 默认情况下,没有匹配到扩展点,所以是只有两个表单项;当输入 orgcode=aaa 时,匹配到扩展点,扩展点的内容是渲染第三个表单项。 image.png

思路详解

一个正常的 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>

image.png 其最核心的逻辑改动就是这一行,如果判断是 componentMode 则返回只有一个 js的 html 模板,否则的话就走原逻辑。

image.png

组件适配器

我们知道微前端一般都需要导出声明周期,类似下面的东西:

// 👇 将渲染操作放入 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 逻辑上,将获取到的结果传递给适配器函数执行即可。 image.png 但是注意,这样的话,扩展点的框架就限定了,不能跨技术栈,除非有多个框架适配器,并且在扩展点接口中有相关字段标识。

总结

从我个人认知方面,微组件只是极个别场景下的解决方案(目前识别到的只有扩展点和细粒度重构),其他场景并不建议使用微组件。 微组件实现方面基本上是基于 Web Components 能力做跨技术栈,在我司的扩展点需求中最终选择了 micro-app + umd JS 链接 的方式。

除了上述提的几种解决方案,你还有更好的方案吗,有的话,欢迎评论、吐槽。