vue 子应用实现:
- import { createApp } from 'vue';
- import App from './App.vue';
- export default (container: htmlElement) => {
- // 创建
- const App = createApp(App);
- return {
- mount() {
- // 装载
- App.mount(container);
- },
- unmount() {
- // 卸载
- App.unmount();
- },
- };
- };
主应用实现
React 实现
其核心代码仅十余行, 主要处理与子应用交互 (为了易读性, 隐藏了错误处理代码):
- export function MicroApp({ entry, ...props }: MicroAppProps) {
- // 传递给子应用的节点
- const containerRef = useRef(null);
- // 子应用配置
- const configRef = useRef();
- useLayoutEffect(() => {
- import(/* @vite-ignore */ entry).then((res) => {
- // 将 div 传给子应用渲染
- const config = res.default(containerRef.current);
- // 调用子应用的装载方法
- config.mount?.(props);
- configRef.current = config;
- });
- return () => {
- // 调用子应用的卸载方法
- configRef.current?.unmount?.();
- configRef.current = undefined;
- };
- }, [entry]);
- return {configRef.current?.render?.(props)};
- }
完成, 现在已经实现了主应用与子应用的装载, 更新, 卸载的操作. 现在, 它是一个组件, 可以同时渲染出多个不同的子应用, 这点就比 single-spa 优雅很多.
entry 子应用地址, 当然真实情况会根据 dev 和 prod 模式给出不同的地址:
Vue 实现
- import { onMounted, onUnmounted, ref } from 'vue';
- const { entry, ...props } = defineProps<{ entry: string }>();
- const container = ref(null);
- const config = ref();
- onMounted(() => {
- const element = container.value;
- import(/* @vite-ignore */ entry).then((res) => {
- // 将 div 传给子应用渲染
- const config = res.default(element);
- // 调用子应用的装载方法
- config.mount?.(props);
- config.value = config;
- });
- });
- onUnmounted(() => {
- // 调用子应用的卸载方法
- config.value?.unmount?.();
- });
如何让子应用也能独立运行
single-spa 等众多方案, 都是将一个变量挂载到 Windows 上, 通过判断该变量是否处于微前端环境, 这样很不优雅. 在 ESM 中, 我们可以通过 import.meta.url 传入参数来判断:
- if (!import.meta.url.includes('microAppEnv')) {
- ReactDOM.render(
- ,
- document.getElementById('root'),
- );
- }
入口导入修改:
- // 添加环境参数和当前时间避免被缓存
- import(/* @vite-ignore */ `${entry}?microAppEnv&t=${Date.now()}`);
浏览器兼容性
IE 浏览器已经逐步退出我们的视野, 基于 Vite, 我们只需要支持 import 的特性浏览器就够了. 当然, 如果考虑 IE 浏览器的话也不是不可以, 很简单: 将上面代码的 import 替换为 System.import 即 systemjs, 也是 single-spa 的所推崇的用法.
浏览器 | Chrome | Edge | Firefox | Internet Explorer | Safari |
---|---|---|---|---|---|
import | 61 | 16 | 60 | No | 10.1 |
Dynamic import | 63 | 79 | 67 | No | 11.1 |
import.meta | 64 | 79 | 62 | No | 11.1 |
模块公用
我们的子组件必须要使用 mount ,unount 模式吗? 答案是不一定, 如果我们的技术栈都是 React 的话. 我们的子应用只导出一个 render 就够了. 这样用的就是同一个 React 来渲染, 好处是子应用可以消费父应用的 Provider. 但有个前提是两个应用之间的 React 必须为同一个实例, 否则就会报错.
我们可以将 react,react-dom ,styled-componets 等常用模块提前打包成 ESM 模块, 然后放到文件服务中使用.
更改 Vite 配置添加 alias:
- defineConfig({
- resolve: {
- alias: {
- react: '//localhost:8000/react@17.js',
- 'react-dom': '//localhost:8000/react-dom@17.js',
- },
- },
- });
这样就能愉快地使用同一份 React 代码了. 还能抽离出主应用和子应用之间的公用模块, 让应用总体积更小. 当然如果没上 http2 的话, 就需要考虑颗粒度的问题了.
在线 CDN 方案: https://esm.sh
还有个 importmap 方案, 兼容性不太好, 但未来是趋势:
- {
- "imports": {
- "react": "//localhost:8000/react@17.js"
- }
- }
父子通信
组件式微应用, 可以传递参数而通信, 完全就是 React 组件通信的模型.
资源路径
- import logo from './images/logo.svg';
- ;
在 Vite 的 dev 模式中, 子应用里面静态资源一般会这样引入:
- import logo from './images/logo.svg';
- ;
图片的路径:/basename/src/logo.svg, 在主应用显示就会 404. 因为该路径只是存在于子应用. 我们需要配合 URL 模块使用, 这样路径前面会带上 origin 前缀:
- const logoURL = new URL(logo, import.meta.url);
- ;
当然这样使用比较繁琐, 我们可以将其封装为一个 Vite 插件自动处理该场景.
路由同步
项目使用 react-router, 那么它可能会存在路由不同步的问题, 因为不是同一个 react-router 实例. 即路由之间出现不联动的现象.
在 react-router 支持自定义 history 库, 我们可以创建:
- import { createBrowserHistory } from 'history';
- export const history = createBrowserHistory();
- // 主应用: 路由入口
- {children};
- // 主应用: 传递给子应用
- path="/child-app/*"
- element={}
- />;
- // 子应用: 路由入口
- {children}
- ;
最终子应用使用同一份 history 模块. 当然这不是唯一的实现, 也不是优雅的方式, 我们可以将路由实例 navigate 传递给子应用, 这样也能实现路由的交互.
注意: 子应用的 basename 必须与主应用的 path 名称保持一致. 这里还需要修改 Vite 的配置 base 字段:
- export default defineConfig({
- base: '/child-app/',
- server: {
- port: 3002,
- },
- plugins: [react()],
- });
JS 沙箱
因为沙箱在 ESM 下不支持, 因为无法动态改变执行环境中模块 Windows 对象, 也无法注入新的全局对象.
一般 React,Vue 项目也很少修改全局变量, 做好代码规范检查才是最主要的.
CSS 样式隔离
自动 CSS 样式隔离是有代价的, 一般我们建议子应用使用不同的 CSS 前缀, 再配合 CSS Modules 基本上能实现需求.
打包部署
部署可以根据子应用的 base 放置在不同的目录, 并将名称对应. 配置好 nginx 转发规则就可以了. 我们可以将子应用统一路由前缀, 便于 nginx 将主应用区分开并配置通用规则.
比如将主应用放置在 system 目录, 子应用放置在 App- 开头的目录:
- location ~ ^\/App-.*(\..+)$ {
- root /usr/share/nginx/HTML;
- }
- location / {
- try_files $uri $uri/ /index.HTML;
- root /usr/share/nginx/HTML/system;
- index index.HTML index.htm;
- }
优点
1. 简单 核心不足 100 行代码, 无需多余的文档
2. 灵活 通过约定的方式接入, 也可以渐进增强
3. 透明 无任何劫持方案, 更多逻辑透明性
4. 组件化 组件化的渲染及参数通信
5. 基于 ESM 支持 Vite, 面向未来
6. 向下兼容 可选 SystemJS 方案, 兼容低版本浏览器
有示例吗
示例代码在 GitHub, 感兴趣的朋友可以 clone 下来学习. 由于我们的技术栈是 React, 所以这里示例的主应用的实现用的是 React .
微前端组件 (React):https://github.com/MinJieLiu/micro-app
微前端示例: https://github.com/MinJieLiu/micro-app-demo
结语
微前端的方案适合团队场景的最好, 打造一个团队能掌控的方案尤为重要.
参考资料:
- https://developer.mozilla.org/zh-CN/docs/web/JavaScript/Reference/Statements/import.meta
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import
来源: http://developer.51cto.com/art/202201/699398.htm