关于micro-app
在micro-app 之前,业内已经有一些开源的微前端框架,比较流行的有2个:single-spa 和qiankun 。
single-spa 是通过监听url change 事件,在路由变化时匹配到渲染的子应用并进行渲染,这个思路也是目前实现微前端的主流方式。同时single-spa 要求子应用修改渲染逻辑并暴露出三个方法:bootstrap、mount、unmount ,分别对应初始化、渲染和卸载,这也导致子应用需要对入口文件进行修改。因为qiankun 是基于single-spa进行封装 ,所以这些特点也被qiankun 继承下来,并且需要对webpack 配置进行一些修改。
micro-app 并没有沿袭single-spa 的思路,而是借鉴了WebComponent 的思想,通过CustomElement 结合自定义的ShadowDom ,将微前端封装成一个类WebComponent 组件,从而实现微前端的组件化渲染。并且由于自定义ShadowDom 的隔离特性,micro-app 不需要像single-spa 和qiankun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack 配置,是目前市面上接入微前端成本最低的方案。
概念图
micro-app的优势
1、使用简单
我们将所有功能都封装到一个类WebComponent组件中,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。
同时micro-app 还提供了js沙箱 、样式隔离 、元素隔离 、预加载 、数据通信 、静态资源补全 等一系列完善的功能。
2、零依赖
micro-app 没有任何依赖,这赋予它小巧的体积和更高的扩展性。
3、兼容所有框架
为了保证各个业务之间独立开发、独立部署的能力,micro-app 做了诸多兼容,在任何技术框架中都可以正常运行。
以React 搭建
本篇以React 16、17作为案例介绍react的接入方式,其它版本react的接入方式以此类推。我们默认开发者掌握了各版本react的开发技巧,如示例中useEffect,在不支持hooks的版本中转换为componentDidMount。
基座应用
我们强烈建议基座应用采用history模式,hash路由的基座应用只能加载hash路由的子应用,history模式的基座应用对这两种子应用都支持。
在以下案例中,我们默认基座的路由为history模式 。
1、安装依赖
npm i @micro-zoe/micro-app --save
2、在入口处引入
import microApp from '@micro-zoe/micro-app'
microApp.start()
3、分配一个路由给子应用
// router.js
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import MyPage from './my-page'
export default function AppRoute () {
return (
<BrowserRouter>
<Switch>
// 👇 非严格匹配,/my-page/* 都指向 MyPage 页面
<Route path='/my-page'>
<MyPage />
</Route>
</Switch>
</BrowserRouter>
)
}
4、在页面中嵌入子应用
export function MyPage () {
return (
<div>
<h1>子应用</h1>
<micro-app
name='app1' // name(必传):应用名称
url='http://localhost:3000/' // url(必传):应用地址,会被自动补全为http://localhost:3000/index.html
baseroute='/my-page' // baseroute(可选):基座应用分配给子应用的基础路由,就是上面的 `/my-page`
></micro-app>
</div>
)
}
子应用
1、设置跨域支持
使用create-react-app 脚手架创建的项目,在 config/webpackDevServer.config.js 文件中添加headers 。
其它项目在webpack-dev-server 中添加headers 。
headers: {
'Access-Control-Allow-Origin': '*',
}
2、设置基础路由(如果基座是history路由,子应用是hash路由,这一步可以省略)
import { BrowserRouter, Switch, Route } from 'react-router-dom'
export default function AppRoute () {
return (
<BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}>
...
</BrowserRouter>
)
}
3、设置 publicPath
这一步借助了webpack的功能,避免子应用的静态资源使用相对地址时加载失败的情况,详情参考webpack文档 publicPath
如果子应用不是webpack 构建的,这一步可以省略。
步骤1: 在子应用src目录下创建名称为public-path.js 的文件,并添加如下内容
if (window.__MICRO_APP_ENVIRONMENT__) {
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
步骤2: 在子应用入口文件的最顶部引入public-path.js
import './public-path'
4、监听卸载
子应用被卸载时会接受到一个名为unmount 的事件,在此可以进行卸载相关操作。
window.addEventListener('unmount', function () {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
})
实战案例
以上介绍了react如何接入微前端,但在实际使用中会涉及更多功能,如数据通信、路由跳转、打包部署,为此我们提供了一套案例,用于展示react作为基座嵌入(或作为子应用被嵌入) react、vue、angular、vite、nextjs、nuxtjs等框架,在案例中我们使用尽可能少的代码实现尽可能多的功能。
案例地址:https://github.com/micro-zoe/micro-app-demo
常见问题
1、create-react-app创建的子应用,被嵌入微前端后sockjs-node报错
报错信息: WebSocket connection to 'ws://localhost:3000/sockjs-node' failed
原因: 子应用的sockjs-node会根据当前页面的端口号进行通信,嵌入微前端后,端口号为基座的,而非子应用的,导致报错。 虽然这个问题不影响应用的正常运行,但还是要进行处理。
解决方式: 使用插件系统补全子应用sockjs-node的端口号。
microApp.start({
plugins: {
modules: {
子应用名称: [{
loader(code) {
if (process.env.NODE_ENV === 'development' && code.indexOf('sockjs-node') > -1) {
code = code.replace('window.location.port', 子应用端口)
}
return code
}
}],
}
}
})
以Vue 搭建
本篇以Vue 2、3作为案例介绍vue的接入方式,其它版本vue的接入方式以此类推,我们默认开发者掌握了各版本vue的开发技巧,比如示例中vue2的代码如何转换为vue1。
基座应用
我们强烈建议基座应用采用history模式,hash路由的基座应用只能加载hash路由的子应用,history模式的基座应用对这两种子应用都支持。
在以下案例中,我们默认基座的路由为history模式 。
1、安装依赖
npm i @micro-zoe/micro-app --save
2、在入口处引入
import microApp from '@micro-zoe/micro-app'
microApp.start()
3、分配一个路由给子应用
Vue2版本
import Vue from 'vue'
import VueRouter from 'vue-router'
import MyPage from './my-page.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/my-page/*',
name: 'my-page',
component: MyPage,
},
]
export default routes
Vue3版本
import { createRouter, createWebHistory } from 'vue-router'
import MyPage from './my-page.vue'
const routes = [
{
path: '/my-page/:page*',
name: 'my-page',
component: MyPage,
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
4、在页面中嵌入子应用
<template>
<div>
<h1>子应用</h1>
<micro-app name='app1' url='http://localhost:3000/' baseroute='/my-page'></micro-app>
</div>
</template>
子应用
1、设置跨域支持
在vue.config.js 中添加配置
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
}
}
2、设置基础路由(如果基座是history路由,子应用是hash路由,这一步可以省略)
Vue2版本
import VueRouter from 'vue-router'
import routes from './router'
const router = new VueRouter({
mode: 'history',
base: window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL,
routes,
})
Vue3版本
import { createRouter, createWebHistory } from 'vue-router'
import routes from './router'
const router = createRouter({
history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL),
routes,
})
3、设置 publicPath
这一步借助了webpack的功能,避免子应用的静态资源使用相对地址时加载失败的情况,详情参考webpack文档 publicPath
如果子应用不是webpack构建的,这一步可以省略。
步骤1: 在子应用src目录下创建名称为public-path.js 的文件,并添加如下内容
if (window.__MICRO_APP_ENVIRONMENT__) {
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
步骤2: 在子应用入口文件的最顶部引入public-path.js
import './public-path'
4、监听卸载
子应用被卸载时会接受到一个名为unmount 的事件,在此可以进行卸载相关操作。
Vue2版本
const app = new Vue(...)
window.addEventListener('unmount', function () {
app.$destroy()
})
Vue3版本
const app = createApp(App)
app.mount('#app')
window.addEventListener('unmount', function () {
app.unmount()
})
实战案例
以上介绍了vue如何接入微前端,但在实际使用中会涉及更多功能,如数据通信、路由跳转、打包部署,为此我们提供了一套案例,用于展示vue作为基座嵌入(或作为子应用被嵌入) react、vue、angular、vite、nextjs、nuxtjs等框架,在案例中我们使用尽可能少的代码实现尽可能多的功能。
案例地址:https://github.com/micro-zoe/micro-app-demo
常见问题
1、基座应用中抛出警告,micro-app未定义
报错信息:
vue2: [Vue warn]: Unknown custom element: <micro-app> vue3: [Vue warn]: Failed to resolve component: micro-app
参考issue: vue-next@1414
解决方式: 在基座应用中添加如下配置
Vue2版本
在入口文件main.js中设置ignoredElements,详情查看:https://cn.vuejs.org/v2/api/#ignoredElements
import Vue from 'vue'
Vue.config.ignoredElements = [
'micro-app',
]
Vue3版本
在vue.config.js中添加chainWebpack配置,如下:
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compilerOptions = {
...(options.compilerOptions || {}),
isCustomElement: (tag) => /^micro-app/.test(tag),
};
return options
})
}
}
Vite + Vue3版本
在vite.config.js中通过vue插件设置isCustomElement,如下:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: tag => /^micro-app/.test(tag)
}
}
})
],
})
2、当基座和子应用都是vue-router4,点击浏览器返回按钮页面丢失
原因: vue-router4 没有对路由堆栈state做唯一性标记,导致基座和子应用相互影响,vue-router3及其它框架路由没有类似问题。
测试版本: vue-router@4.0.12
相关issue: 155
解决方式: 在子应用中添加如下设置
if (window.__MICRO_APP_ENVIRONMENT__) {
const realBaseRoute = window.__MICRO_APP_BASE_ROUTE__
router.beforeEach(() => {
if (typeof window.history.state?.current === 'string') {
window.history.state.current = window.history.state.current.replace(new RegExp(realBaseRoute, 'g'), '')
}
})
router.afterEach(() => {
if (typeof window.history.state === 'object') {
window.history.state.current = realBaseRoute + (window.history.state.current || '')
}
})
}
3、vue-router在hash模式无法通过base设置基础路由
**解决方式:**创建一个空的路由页面,将其它路由作为它的children,具体设置如下:
import RootApp from './root-app.vue'
const routes = [
{
path: window.__MICRO_APP_BASE_ROUTE__ || '/',
component: RootApp,
children: [
],
},
]
root-app.vue内容如下:
<template>
<router-view />
</template>
|