前言
哈喽大家好,我是海怪。
在微前端的领域里,相信大家都听说过阿里的 qiankun。我自己在上几个月也一直用它来做一些实践:
- 微前端x重构实践落地总结
- 当企微侧边栏遇上微前端
在使用过程中我发现 qiankun 还是有一些缺点的:
- 项目的侵入性依然很强。之前为了在组内推广,都做了 3 次分享,可把我口水说干了
- qiankun 在沙箱方面依然有不少坑。看去年 qiankun 一年的 Change Log,发现都是在修复沙箱相关的问题
不仅我用 qiankun 难受,京东的小伙伴也用得很难受,所以,京东在去年 7 月推出了自己微前端解决方案 —— MicroApp。今天就带大家初探一下这个新星微前端框架吧。
特色
MicroApp 一上来就表明了自己的立场:
micro-app
并没有沿袭single-spa
的思路 。
借鉴了 WebComponent
的思想,micro-app
通过 CustomElement
结合自定义的 ShadowDom
,将微前端封装成一个类 WebComponent
组件,从而实现微前端的组件化渲染://upload-images.jianshu.io/upload_images/2979799-8de71b95b8148643.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
它在 基座应用 和 子应用 之间充当桥梁胶水的作用。
有下面的优势:
- 使用简单。 将功能封装到 WebComponent 中
- 零依赖。 无依赖、更高的扩展性
- 兼容所有框架 技术栈无关
下面我们直接开箱看看 micro-app
能给我们带来什么样的惊喜吧。
注:所有代码都放在 Github 项目 中。
架构
虽然官方也给出了完整的 样例 ,不过里面代码太多了,所以我就弄一个简单一点的项目,架构如下:
起步
主应用使用 create-react-app 创建 React 项目:
npx create-react-app bigass-micro-app
然后安装 micro-app
:
npm i @micro-zoe/micro-app --save
创建完了后,先用 React Router 来搭建基座应用的路由系统:
const App = () => {
return (
基座 Home
基座 About
}/>
}/>
);
}
就有最简单的路由了:
添加 React 微应用
接下来我们再添加一个 React 应用作为这个项目的微应用。 /apps
目录下也用 create-react-app
创建一个新的 React 应用,并在 pcakge.json
里更改访问端口:
"scripts": {
"start": "BROWSER=none PORT=3001 react-scripts start",
}
添加 public-path.js
:
// __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量
if (window.__MICRO_APP_ENVIRONMENT__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
在微应用的 入口的第一行 引入它:
import './public-path'
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render( , document.getElementById('root'));
这一步主要是避免子应用的静态资源使用相对地址时加载失败的情况。
并在 devServer 设置跨域访问:
headers: {
'Access-Control-Allow-Origin': '*',
}
同样添加简单的路由:
function App() {
return (
react-app Home
react-app About
}/>
}/>
);
}
现在 npm run start
后就可以在 http://localhost:3001
里访问它了。
接入 React 微应用
首先在基座应用的 入口 处启用 micro-app
:
// index.jsx
import microApp from '@micro-zoe/micro-app'
microApp.start()
ReactDOM.render( , document.getElementById('root'));
接下来在基座应用里添加一个承载微应用的组件 ReactMicroApp
:
// ReactMicroApp.jsx
const ReactMicroApp = () => {
return (
react-app
)
}
最后在 App
组件中添加 react-app
的路由:
const App = () => {
return (
基座 Home
基座 About
React 子应用
}/>
}/>
}/>
);
}
现在我们就已经完美地接入了 React 应用了:
整个接入的过程非常的简单,侵入性的操作总结有:
- 主应用
microApp.start()
- 添加微应用的容器组件
- 添加路由指向这个容器组件
- 微应用
- 修改 public-path
- 添加跨域访问
- 自动切换路由的
basename
对比 qiankun
在完成一个 Hello World 后,我们来对比一下 qiankun
的起手式。
主应用
micro-app | qiankun |
---|---|
指定 container element | 添加自定义标签元素 |
registerMicroApps |
普通路由切换 |
start() |
microApp.start() |
微应用:
micro-app | qiankun |
---|---|
更改 public path | 更改 public path |
指定路由的 basename | 指定路由的 basename |
- | 导出生命周期 |
配置跨域访问 | 配置跨域访问 |
- | 配置 Webpack 的 output |
不得不说,micro-app
确实对新手更友好了,使用上也更简洁明了。
接入 Vue 微应用
有了上面的了解后,接入 Vue 微应用就更快了。
微应用
直接用 vue-cli
创建 Vue 项目,这里我选用了 Vue3。
第一步,添加 public-path.js
:
// public-path.js
if (window.__MICRO_APP_ENVIRONMENT__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
并在 入口 main.js
引入:
// main.js
import './public-path'
import {createApp} from 'vue'
...
第二步,更改 Vue Router 的 basename
:
// main.js
const router = createRouter({
history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL),
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
],
})
const app = createApp(App).use(router)
app.mount('#app')
// 监听卸载操作
window.addEventListener('unmount', function () {
app.unmount()
})
第三步,更改 Webpack DevServer 的跨域访问:
// vue.config.js
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
}
}
}
主应用
添加 Vue3MicroApp
容器:
const Vue3MicroApp = () => {
return (
vue3-app
子应用消息 {microAppState ? microAppState.msg : '无'}
)
}
然后添加路由:
const App = () => {
return (
基座 Home
基座 About
React 子应用
Vue3 子应用
}/>
}/>
}/>
}/>
);
}
很快啊(马保国.jpg)就接入了新的微应用了:
数据通信
数据通信也是 micro-app
的一大亮点,在使用层面上比 qiankun
的 EventBus 要好用那么 一丢丢。啊对,也就是那么一丢丢,因为使用方式还是比较 Hacky 的。
父传子
在基座应用的容器组件里添加一个 microApp.setData
就好了:
// ReactMicroApp
const ReactMicroApp = () => {
const [microAppState, setMicroAppState] = useState(microApp.getData('react-app'));
// 发送数据给 react-app
const sendReactAppData = () => {
microApp.setData('react-app', {
name: `react-app 随机数: ${Math.random()}`
})
}
return (
react-app
子应用消息 {microAppState ? microAppState.msg : '无'}
)
}
需要注意的是 setData
的值必须为对象!
微应用在接收数据的时候也是一样通过事件监听来获取:
// react-app App.js
function App() {
const [mainAppData, setMainAppData] = useState( window.microApp.getData());
// 监听基座数据
useEffect(() => {
if (window.microApp) {
const dataListener = (data) => {
console.log('主应用传的数据', data);
setMainAppData(data);
}
window.microApp.addDataListener(dataListener)
return () => {
window.microApp.clearDataListener()
}
}
})
return (
主应用的数据:{mainAppData ? mainAppData.name : '无'}
react-app Home
react-app About
}/>
}/>
);
}
子传父
同时地,微应用发送数据也是一行 window.microApp.dispatch
就 OK 了,而且入参必须也为对象:
// react-app App.js
function App() {
const [mainAppData, setMainAppData] = useState( window.microApp.getData());
useEffect(() => {
if (window.microApp) {
const dataListener = (data) => {
console.log('主应用传的数据', data);
setMainAppData(data);
}
window.microApp.addDataListener(dataListener)
return () => {
window.microApp.clearDataListener()
}
}
})
// 发送数据给基座
const sendToMain = () => {
window.microApp.dispatch({msg: `我是 react-app 随机数 ${Math.random()}`})
}
return (
主应用的数据:{mainAppData ? mainAppData.name : '无'}
react-app Home
react-app About
}/>
}/>
);
}
基座的监听稍微有点 Hacky:
// ReactMicroApp.js
import microApp from '@micro-zoe/micro-app'
import {useState} from "react";
/** @jsxRuntime classic */
/** @jsx jsxCustomEvent */
import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event'
const ReactMicroApp = () => {
const [microAppState, setMicroAppState] = useState(microApp.getData('react-app'));
const sendReactAppData = () => {
microApp.setData('react-app', {
name: `react-app 随机数: ${Math.random()}`
})
}
const onDataChange = (e) => {
console.log('react-app onDataChange', e);
console.log('react-app 数据', e.detail.data);
setMicroAppState(e.detail.data);
}
return (
react-app
子应用消息 {microAppState ? microAppState.msg : '无'}
)
}
虽然没有用到 jsxCustomEvent
,但是也一定要 import 进来,且不能把注释给干掉!
然后我们就可以看到数据通信的效果了:
高级功能
上面都是 micro-app
入门的一些基础操作,它还提供了一些高级功能。
KeeyAlive
保持微应用的状态 Keep-Alive
:
生命周期
/** @jsxRuntime classic */
/** @jsx jsxCustomEvent */
import jsxCustomEvent from '@micro-zoe/micro-app/polyfill/jsx-custom-event'
const App = () => {
return (
console.log('micro-app元素被创建')}
onBeforemount={() => console.log('即将被渲染,只在初始化时执行一次')}
onMounted={() => console.log('已经渲染完成,只在初始化时执行一次')}
onAfterhidden={() => console.log('已卸载')}
onBeforeshow={() => console.log('即将重新渲染,初始化时不执行')}
onAftershow={() => console.log('已经重新渲染,初始化时不执行')}
onError={() => console.log('渲染出错')}
/>
)
}
应用之间跳转
- window.history
- 通过数据通信控制跳转
- 传递路由实例方法
隔离
JS 方面使用 Proxy
拦截了用户全局操作的行为,防止对 window
的访问和修改,避免全局变量污染。
CSS 方面有两种隔离:
- 默认添加 CSS 选择器前缀
- ShadowDOM
元素隔离方面,micro-app
模拟实现了类似 ShadowDom
的功能,元素不会逃离
元素边界,子应用只能对自身的元素进行增、删、改、查的操作。
静态资源处理
使用 globalAssets
共享资源:
// index.js
import microApp from '@micro-zoe/micro-app'
microApp.start({
globalAssets: {
js: ['js地址1', 'js地址2', ...], // js地址
css: ['css地址1', 'css地址2', ...], // css地址
}
})
或者使用 global
属性:
以及对资源的过滤:
渲染微前端模式
- 默认模式:每次都按顺序执行一次 JS,具有幂等性
- umd 模式:只在初次渲染时执行所有 JS,对于需要频繁切换微应用的项目可以提高其性能
插件系统
插件系统的主要作用就是对js进行修改,每一个js文件都会经过插件系统,我们可以对这些js进行拦截和处理,它通常用于修复js中的错误或向子应用注入一些全局变量。
这个插件系统主要可以在中间层处理一下 JS,避免一些由于固定模板而无法处理的 JS 报错。总的来说,这个系统还是个新生儿,需要后续其它开发者一起共建。
总结
总的来说,micro-app
是一个京东推出的一个微前端新星框架,在使用上比较简洁易懂,配置方面也无需太多操作,类似 Vue 风格的 API 对新手算是十分友好了,也推荐大家一起玩玩这个新框架。
当然了因为这个项目也是 21 年 7 月才推出的,现在版本(2022年1月28号)为 0.8.4
,还没到 1.0
,所以想要在生产环境上使用的话,建议还是要谨慎一点。
qiankun
虽然侵入性较大,但是它有着 3 年的微前端使用场景以及 Issue 经验积累,在解决问题方面更稳一点。