一定要使用微前端吗?
不一定,任何技术都是取决于你的需求,过度设计反而导致程序更加糟糕,因为微前端并不是万能的,各个微前端框架都存在一些问题,甚至无法解决你的问题,如果你不知道自己是否需要微前端,那么大概率是不需要。
微前端的核心目标我认为有两个
- 将"巨石应用"拆解成若干可以自治的松耦合微应用
- 多个团队独立开发、部署、管理,共同构建现代化 web 应用
微前端架构要解决什么问题?
- 独立-每个微应用可独立开发、运行、部署,具备完全自主权
- 隔离-每个微应用之间状态隔离,样式隔离,js隔离,运行不冲突
- 共享-应用间上下文可以共享,系统间可以通讯,数据同步
我认为最核心的就是这三点、至于其他比如性能、简单易用等问题,这是每种架构设计都应该考虑和解决的范围,这里就不赘述。
iframe(内联框架)
在HTML的中,大家都应该认识这个标签,<iframe>
,用于在网页中嵌入另外一个独立的HTML文档,同时它还具有浏览器原生支持的沙盒环境,天然具备安全隔离的功能,可以让每个iframe标签内的子应用实现独立和隔离,这简直就是为微前端量身设计的,但显然iframe存在一些问题不能很好的解决上面所提到的问题,不然也不会有那么多的微前端框架出现(多个微前端架构的出现是不是也意味着每个架构间都有自己无法解决的问题呢?哈哈)
那为什么不选iframe呢?上面提到了三点(独立-隔离-共享),有点像不可能三角,没办法做到同时满足这三点要求,iframe
在共享这一块也因为它的强隔离,变得复杂困难,跨域通讯困难,状态同步问题,URL管理问题,另外还有性能开销、加载保活、样式交互、用户体验等问题
这里也有标准答案 为什么不是iframe
接下来的所有记录,都只会与Vue技术栈相关,因为我主要使用Vue相关技术栈开发。
qiankun(阿里)
qiankun
是一个基于single-spa
的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统, single-spa
是通过监听 url change 事件,在路由变化时匹配到渲染的子应用并进行渲染,这个思路也是目前实现微前端的主流方式
css
┌──────────────────────┐
│ qiankun │ ← 阿里开源,企业级解决方案
│(HTML Entry、沙箱、预加载、通信)│
└─────────┬────────────┘
│ 依赖/封装
┌─────────▼────────────┐
│ single-spa │ ← 社区开源,微前端核心框架
│(生命周期、路由匹配、应用注册)│
└──────────────────────┘
特性
- 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
- 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
- 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 🛡 样式隔离,确保微应用之间样式互相不干扰。
- 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- 🔌 umi 插件 ,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
上手
主应用
主应用不限技术栈,只要提供一个容器DOM,然后注册微应用并start
即可。
bash
$ pnpm add qiankun # 或者 npm i qiankun -S
基于路由配置自动加载微应用
js
import { registerMicroApps, start, setDefaultMountApp, runAfterFirstMounted } from 'qiankun';
//registerMicroApps(apps, lifeCycles?)
// apps: 必选,微应用的一些注册信息
// lifeCycles: 可选,全局的微应用生命周期钩子
registerMicroApps([
{
name: 'vueApp1',
// string,必选,微应用的名称,微应用之间必须确保唯一
entry: '//localhost:9501',
// string | { scripts?: string[]; styles?: string[]; html?: string },必选,微应用的入口
//为字符串是表示微应用访问地址;
//为对象是,html的值是微应用的html内容字符串;微应用的publicPath将会被设置成'/'
container: '#container', // string | HTMLElement,必选,微应用的容器节点的选择器或者 Element 实例
activeRule: '/app-vue2', // string | (location: Location) => boolean | Array<string | (location: Location) => boolean> 必选,微应用的激活规则
// 支持直接配置字符串或字符串数组;支持配置一个 active function 函数或一组 active function
props: {
appName:'vueApp1'
}
// `object` - 可选,主应用需要传递给微应用的数据
},
{
name: 'vueApp2',
entry: '//localhost:9502',
container: '#container',
activeRule: '/app-vue3',
props: {
appName:'vueApp2'
}
},
],
{
beforeLoad: (app) => console.log('before load', app.name),
beforeMount: [(app) => console.log('before mount', app.name)],
afterMount: [(app) => console.log('after mount', app.name)],
beforeUnmount: [(app) => console.log('before ummount', app.name)],
afterUnmount: [(app) => console.log('after ummount', app.name)],
// Lifecycle | Array<Lifecycle> - 可选
}
);
// 启动 qiankun
// start(opts?)
start({
prefetch:true,
// boolean | 'all' | string[] | (( apps: RegistrableApp[] ) => { criticalAppNames: string[]; minorAppsName: string[] }) 可选,是否开启预加载,默认为 `true`
// 配置为 `true` 则会在第一个微应用 mount 完成后开始预加载其他微应用的静态资源
// 配置为 `'all'` 则主应用 `start` 后即开始预加载所有微应用静态资源
// 配置为 `string[]` 则会在第一个微应用 mounted 后开始加载数组内的微应用资源
// 配置为 `function` 则可完全自定义应用的资源加载时机 (首屏应用及次屏应用)
sandbox:true,
// boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean } - 可选,是否开启沙箱,默认为 `true`
// 默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离
// 当配置为 { strictStyleIsolation: true } 时表示开启严格的样式隔离模式,这种模式下qiankun会为每个微应用容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局影响
// 当 { experimentalStyleIsolation: true } 时,qiankun 会改写子应用所添加的样式为所有样式规则增加一个特殊的选择器规则来限定其影响范围;
// 注意: @keyframes, @font-face, @import, @page 将不会被改写
singular:true,
// boolean | ((app: RegistrableApp<any>) => Promise<boolean>),可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true
// fetch - `Function` - 可选,自定义的 fetch 方法。
// getPublicPath - `(entry: Entry) => string` - 可选,参数是微应用的 entry 值。
// getTemplate - `(tpl: string) => string` - 可选。
// excludeAssetFilter - `(assetUrl: string) => boolean` - 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理。
})
// 设置主应用启动后默认进入
// setDefaultMountApp(appLink),- appLink - `string` - 必选
setDefaultMountApp('/vueApp2')
// 第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本
// runAfterFirstMounted(effect), - effect - `() => void` - 必选
runAfterFirstMounted(() => startMonitor());
手动加载/预加载微应用
js
import { loadMicroApp, prefetchApps } from 'qiankun'
// loadMicroApp(app, configuration?)
// app - 必选,微应用的基础信息
// configuration - 可选,微应用的配置信息
// 返回微应用实例,实例方法有
// mount(): Promise<null>;
// unmount(): Promise<null>;
// update(customProps: object): Promise<any>;
// getStatus(): | "NOT_LOADED" | "LOADING_SOURCE_CODE" | "NOT_BOOTSTRAPPED" | "BOOTSTRAPPING" | "NOT_MOUNTED" | "MOUNTING" | "MOUNTED" | "UPDATING" | "UNMOUNTING" | "UNLOADING" | "SKIP_BECAUSE_BROKEN" | "LOAD_ERROR";
// loadPromise: Promise<null>;
// bootstrapPromise: Promise<null>;
// mountPromise: Promise<null>;
// unmountPromise: Promise<null>;
// 如果需要能支持主应用手动 update 微应用,需要微应用 entry 再多导出一个 update 钩子:
// 增加 update 钩子以便主应用手动更新微应用
// export async function update(props) {
//...
//}
loadMicroApp({
name: 'vueApp2',
entry: '//localhost:9502',
container: '#container',
activeRule: '/app-vue3',
props: {
appName:'vueApp2'
}
},
{
// sandbox
// singular
// fetch
// getPublicPath
// getTemplate
// excludeAssetFilter
})
// prefetchApps(apps, importEntryOpts?)
// apps - 必选 - 预加载的应用列表
// importEntryOpts - 可选 - 加载配置
prefetchApps([
{ name: 'vueApp1', entry: '//localhost:9501' },
{ name: 'vueApp2', entry: '//localhost:9502' },
]);
添加/移除全局的异常处理器
js
import { addErrorHandler, removeErrorHandler } from 'qiankun';
const handler = (error: AppError) => void
// addErrorHandler(handler) - handler - `(error: AppError) => void` - 必选
addGlobalUncaughtErrorHandler(handler);
// removeErrorHandler(handler) - handler - `(error: AppError) => void` - 必选
removeGlobalUncaughtErrorHandler(handler);
添加/移除全局的未捕获异常处理器
js
import { addGlobalUncaughtErrorHandler, removeGlobalUncaughtErrorHandler } from 'qiankun';
const handler = (event) => console.log(event)
// addGlobalUncaughtErrorHandler(handler) - handler - `(...args: any[]) => void` - 必选
addGlobalUncaughtErrorHandler(handler);
// removeGlobalUncaughtErrorHandler(handler) - handler - `(...args: any[]) => void` - 必选
removeGlobalUncaughtErrorHandler(handler);
定义全局状态
js
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
// initGlobalState(state) 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
// (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void, 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
actions.setGlobalState(state);
// setGlobalState: (state: Record<string, any>) => boolean, 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
actions.offGlobalStateChange();
// offGlobalStateChange: `() => boolean`,移除当前应用的状态监听,微应用 umount 时会默认调用
微应用(Vue)
Webpack 构建
- 新增
public-path.js
文件,用于修改运行时的publicPath
。什么是运行时的 publicPath ?。 在src
目录新增public-path.js
js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代
- 微应用建议使用
history
模式的路由,需要设置路由base
,值和它的activeRule
是一样的 3. 在入口文件最顶部引入public-path.js
,修改并导出三个生命周期函数
js
import './public-path';
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';
Vue.config.productionTip = false;
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app-vue2/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
- 修改
webpack
打包,允许开发环境跨域和umd
打包
js
const { name } = require('./package');
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
},
},
};
Vite构建
由于Vite是基于原生ES Module的按需加载和输出格式问题,所以需要通过插件适配不同框架和需求,同时保持开发速度的优势
- 需要安装帮助应用快速接入乾坤的vite插件,
vite-plugin-qiankun
bash
$ pnpm add vite-plugin-qiankun # 或者 npm i vite-plugin-qiankun -S
- 微应用建议使用
history
模式的路由,需要设置路由base
,值和它的activeRule
是一样的 - 在
vite.config.ts
中配置插件
ts
// vite.config.ts
import qiankun from 'vite-plugin-qiankun';
export default {
// 这里的 'vueApp2' 是子应用名,主应用注册时AppName需保持一致
plugins: [
qiankun('vueApp2',{
useDevMode: true
})
],
// 生产环境需要指定运行域名作为base
base: 'http://xxx.com/'
}
- 使用插件导出的方法加载微应用,配置生命周期函数
ts
// main.ts
import { createApp } from 'vue'
import type { App } from 'vue'
import AppComponent from './App.vue'
import store from './store'
import routes from './router'
import { createRouter, createWebHistory, Router } from 'vue-router'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
let app: App | null = null
let router: Router | null = null
const renderApp = (props?: any) => {
const { container } = props
app = createApp(AppComponent)
router = createRouter({
history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? '/app-vue3/' : '/'),
routes
})
app.use(router)
app.use(store)
app.mount(container ? container.querySelector('#app') : '#app')
}
const initQianKun = () => {
renderWithQiankun({
bootstrap() {},
mount(props) {
renderApp(props)
},
update(props) {},
unmount(props) {}
})
}
if (qiankunWindow.__POWERED_BY_QIANKUN__) {
initQianKun()
} else {
renderApp()
}
wujie(腾讯)
wujie
是基于WebComponent容器 + iframe沙箱的微前端框架
特性
- 原生隔离;
- css 样式通过 Web Components 可以做到严格的原生隔离
- js 运行在 iframe 中做到严格的原生隔离
- 多种模式
- 单例模式
- 保活模式
- 重建模式
- 去中心化通信
- 支持插件系统
上手
主应用(Vue)
bash
# vue2 框架
# pnpm add wujie-vue2 # npm i wujie-vue2 -S
# vue3 框架
pnpm add wujie-vue3 # npm i wujie-vue3 -S
js
// vue2
// import WujieVue from "wujie-vue2";
// vue3
import WujieVue from "wujie-vue3";
const { bus, setupApp, preloadApp, destroyApp } = WujieVue;
Vue.use(WujieVue);
bus(事件管理)
- $on - 监听事件并提供回调
- $onAll - 监听所有事件并提供回调,回调函数的第一个参数是事件名
- $once - 一次性的监听事件
- $off - 取消事件监听
- $offAll - 取消监听所有事件
- $emit - 触发事件
- $clear - 清空
EventBus
实例下所有监听事件- 子应用在被销毁或重新渲染(非保活模式)时,框架会自动调用清空上次渲染所有的订阅事件
- 子应用内部组件的渲染可能导致反复订阅(比如在mounted生命周期调用了
$wujie.bus.$on
),需要用户在unmount生命周期中手动调用$wujie.bus.off
来取消订阅
setupApp(注册应用)
setupApp
设置子应用默认属性,非必须。startApp
、preloadApp
会从这里获取子应用默认属性,如果有相同的属性则会直接覆盖
ts
type lifecycle = (appWindow: Window) => any;
type loadErrorHandler = (url: string, e: Error) => any;
type baseOptions = {
/** 唯一性用户必须保证 */
name: string;
/** 需要渲染的url */
url: string;
/** 需要渲染的html, 如果用户已有则无需从url请求 */
html?: string;
/** 代码替换钩子 */
replace?: (code: string) => string;
/** 自定义fetch */
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
/** 注入给子应用的属性 */
props?: { [key: string]: any };
/** 自定义运行iframe的属性 */
attrs?: { [key: string]: any };
/** 自定义降级渲染iframe的属性 */
degradeAttrs?: { [key: string]: any };
/** 子应用采用fiber模式执行 */
fiber?: boolean;
/** 子应用保活,state不会丢失 */
alive?: boolean;
/** 子应用采用降级iframe方案 */
degrade?: boolean;
/** 子应用插件 */
plugins?: Array<plugin>;
/** 子应用window监听事件 */
iframeAddEventListeners?: Array<string>;
/** 子应用iframe on事件 */
iframeOnEvents?: Array<string>;
/** 子应用生命周期 */
beforeLoad?: lifecycle;
beforeMount?: lifecycle;
afterMount?: lifecycle;
beforeUnmount?: lifecycle;
afterUnmount?: lifecycle;
activated?: lifecycle;
deactivated?: lifecycle;
loadError?: loadErrorHandler;
};
type preOptions = baseOptions & {
/** 预执行 */
exec?: boolean;
};
type startOptions = baseOptions & {
/** 渲染的容器 */
el: HTMLElement | string;
/**
* 路由同步开关
* 如果false,子应用跳转主应用路由无变化,但是主应用的history还是会增加
* https://html.spec.whatwg.org/multipage/history.html#the-history-interface
*/
sync?: boolean;
/** 子应用短路径替换,路由同步时生效 */
prefix?: { [key: string]: string };
/** 子应用加载时loading元素 */
loading?: HTMLElement;
};
type optionProperty = "url" | "el";
/**
* 合并 preOptions 和 startOptions,并且将 url 和 el 变成可选
*/
type cacheOptions = Omit<preOptions & startOptions, optionProperty> & Partial<Pick<startOptions, optionProperty>>;
startApp(启动应用)
startApp
启动子应用,异步返回 destroy函数,可以销毁子应用,一般不建议用户调用,除非清楚的理解其作用
- 一般情况下不需要主动调用
destroy
函数去销毁子应用,除非主应用再也不会打开这个子应用了,子应用被主动销毁会导致下次打开该子应用有白屏时间 name
、replace
、fetch
、alive
、degrade
这五个参数在preloadApp
和startApp
中须保持严格一致,否则子应用的渲染可能出现异常
ts
type lifecycle = (appWindow: Window) => any;
type loadErrorHandler = (url: string, e: Error) => any;
type startOption {
/** 唯一性用户必须保证,如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候建议将 name 设置为同一个,这样可以共享一个实例 */
name: string;
/** 需要渲染的url
如果子应用为单例模式,改变url则可以让子应用跳转到对应子路由
如果子应用为保活模式,改变url则无效,需要采取通信的方式通知子应用路由进行跳转
如果子应用为重建模式,改变url子应用的路由会跳转对应路由,但在路由同步场景并且子应用的路由同步参数已经同步到主应用url上时则无法生效,因为改变url后会导致子应用销毁重新渲染,此时如果有同步参数则同步参数优先级最高
*/
url: string;
/** 需要渲染的html, 如果用户已有则无需从url请求 */
html?: string;
/** 渲染的容器,最好设置好宽高防止渲染问题,在`webcomponent`元素上无界还设置了`wujie_iframe`的`class`方便用户自定义样式 */
el: HTMLElement | string;
/** 子应用加载时loading元素,如果不想出现默认加载,可以赋值一个空元素:`document.createElement('span')` */
loading?: HTMLElement;
/** 路由同步开关,false刷新无效,但是前进后退依然有效;true, wujie会把子应用name作为一个url查询参数,实时同步子应用的路径作为这个查询参数的值,这样分享URL或者刷新浏览器子应用路由都不会丢失,这个同步是单向的,只有打开 URL 或者刷新浏览器的时候,子应用才会从 URL 中读回路由 */
sync?: boolean;
/** 子应用短路径替换,路由同步时生效,如果子应用链接过长,可以采用短路径替换的方式缩短同步的链接 */
prefix?: { [key: string]: string };
/** 子应用保活模式,state不会丢失,切换子应用只是对`webcomponent`的热插拔
如果子应用不想做生命周期的改造,子应用切换又不想有白屏时间,可以采用保活模式
如果主应用有多个菜单栏跳转到子应用不同页面,此时不建议采用保活模式。因为子应用在保活模式下 startApp 无法更改子应用路由,不同菜单无法跳转到指定子应用路由,推荐单例模式
预执行模式结合保活模式可以实现类似`ssr`的效果,包括页面数据的请求和渲染全部提前完成,用户可以瞬间打开子应用*/
alive?: boolean;
/** 注入给子应用的数据 */
props?: { [key: string]: any };
/** js采用fiber模式执行,间断执行js,防止阻塞主应用渲染进程;如果打开主应用就要加载子应用可以设置为false */
fiber?: boolean;
/** 子应用采用降级iframe方案,一旦降级,弹窗由于在iframe内部无法覆盖整个应用 */
degrade?: boolean;
/** 子应用运行在iframe内,可以自定义运行iframe的属性 */
attrs?: { [key: string]: any };
/** 自定义降级渲染iframe的属性 */
degradeAttrs?: { [key: string]: any };
/** 代码替换钩子,`replace`函数可以在运行时处理子应用的代码,如果子应用不方便修改代码,可以在这里进行代码替换,子应用的`html`、`js`、`css`代码均会做替换 */
replace?: (codeText: string) => string;
/** 自定义fetch,资源和接口 */
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
/** 子应用window监听事件 */
iframeAddEventListeners?: Array<string>;
/** 子应用iframe on事件 */
iframeOnEvents?: Array<string>;
/** 子应插件 */
plugins: Array<plugin>;
/** 子应用生命周期 */
beforeLoad?: lifecycle;
/** 没有做生命周期改造的子应用不会调用 */
beforeMount?: lifecycle;
afterMount?: lifecycle;
beforeUnmount?: lifecycle;
afterUnmount?: lifecycle;
/** 非保活应用不会调用 */
activated?: lifecycle;
deactivated?: lifecycle;
/** 子应用资源加载失败后调用 */
loadError?: loadErrorHandler
};
preloadApp(预加载应用)
预加载可以极大的提升子应用首次打开速度
- 资源的预加载会占用主应用的网络线程池
- 资源的预执行会阻塞主应用的渲染线程
name
、replace
、fetch
、alive
、degrade
这五个参数在preloadApp
和startApp
中须保持严格一致,否则子应用的渲染可能出现异常
ts
type lifecycle = (appWindow: Window) => any;
type loadErrorHandler = (url: string, e: Error) => any;
type preOptions {
/** 唯一性用户必须保证 */
name: string;
/** 需要渲染的url */
url: string;
/** 需要渲染的html, 如果用户已有则无需从url请求 */
html?: string;
/** 注入给子应用的数据 */
props?: { [key: string]: any };
/** 自定义运行iframe的属性 */
attrs?: { [key: string]: any };
/** 自定义降级渲染iframe的属性 */
degradeAttrs?: { [key: string]: any };
/** 代码替换钩子 */
replace?: (code: string) => string;
/** 自定义fetch,资源和接口 */
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
/** 子应用保活模式,state不会丢失 */
alive?: boolean;
/** 预执行模式, 预执行模式,为`false`时只会预加载子应用的资源,为`true`时会预执行子应用代码,极大的加快子应用打开速度 */
exec?: boolean;
/** js采用fiber模式执行 */
fiber?: boolean;
/** 子应用采用降级iframe方案 */
degrade?: boolean;
/** 子应用window监听事件 */
iframeAddEventListeners?: Array<string>;
/** 子应用iframe on事件 */
iframeOnEvents?: Array<string>;
/** 子应插件 */
plugins: Array<plugin>;
/** 子应用生命周期 */
beforeLoad?: lifecycle;
/** 没有做生命周期改造的子应用不会调用 */
beforeMount?: lifecycle;
afterMount?: lifecycle;
beforeUnmount?: lifecycle;
afterUnmount?: lifecycle;
/** 非保活应用不会调用 */
activated?: lifecycle;
deactivated?: lifecycle;
/** 子应用资源加载失败后调用 */
loadError?: loadErrorHandler
};
destroyApp(销毁应用)
主动销毁子应用,承载子应用的iframe
和shadowRoot
都会被销毁,无界实例也会被销毁,相当于所有的缓存都被清空,除非后续不会再使用子应用,否则都不应该主动销毁。
ts
destroyApp(name)
微应用
$wujie
$wujie.bus
$wujie.shadowRoot -
$wujie.props
$wujie.location
- 由于子应用的
location.host
拿到的是主应用的host
,无界提供了一个正确的location
挂载到挂载到$wujie
上 - 当采用
vite
编译框架时,由于script
的标签type
为module
,所以无法采用闭包的方式将location
劫持代理,子应用所有采用window.location.host
的代码需要统一修改成$wujie.location.host
- 当子应用发生降级时,由于
proxy
无法正常工作导致location
无法代理,子应用所有采用window.location.host
的代码需要统一修改成$wujie.location.host
- 当采用非
vite
编译框架时,proxy
代理了window.location
,子应用代码无需做任何更改
micro-app(京东)
micro-app
是借鉴 WebComponent,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类WebComponent的组件,从而实现微前端的组件化渲染。
特性
- 组件化渲染
- 类WebComponent的组件
- 依赖于CustomElements和Proxy两个较新的API
- 虚拟路由系统
- MicroApp通过拦截浏览器路由事件以及自定义的location、history,实现了一套虚拟路由系统,子应用运行在这套虚拟路由系统中,和主应用的路由进行隔离,避免相互影响。
- 多种沙箱
- with 沙箱 (默认)
- iframe 沙箱
- 支持插件系统
上手
主应用(Vue)
bash
pnpm add @micro-zoe/micro-app --save
# npm i @micro-zoe/micro-app --save
ts
import microApp from '@micro-zoe/micro-app'
microApp.start()
vue
<template>
<div>
<h1>子应用👇</h1>
<!-- name:应用名称, url:应用地址 -->
<micro-app name='my-app' url='http://localhost:3000/'></micro-app>
</div>
</template>
配置项
js
import microApp from '@micro-zoe/micro-app'
// 全局配置
microApp.start({
iframe: true, // 全局开启iframe沙箱,默认为false
inline: true, // 全局开启内联script模式运行js,默认为false
destroy: true, // 全局开启destroy模式,卸载时强制删除缓存资源,默认为false
ssr: true, // 全局开启ssr模式,默认为false
'disable-scopecss': true, // 全局禁用样式隔离,默认为false
'disable-sandbox': true, // 全局禁用沙箱,默认为false
'keep-alive': true, // 全局开启保活模式,默认为false
'disable-memory-router': true, // 全局关闭虚拟路由系统,默认值false
'keep-router-state': true, // 子应用在卸载时保留路由状态,默认值false
'disable-patch-request': true, // 关闭子应用请求的自动补全功能,默认值false
iframeSrc: location.origin, // 设置iframe沙箱中iframe的src地址,默认为子应用所在页面地址
})
vue
<!-- 单独配置 -->
<micro-app
name='xx'
url='xx'
iframe='false'
inline='false'
destroy='false'
ssr='false'
disable-scopecss='false'
disable-sandbox='false'
keep-alive='false'
disable-memory-router='false'
keep-router-state='false'
disable-patch-request='false'
></micro-app>
虚拟路由
MicroApp通过拦截浏览器路由事件以及自定义的location、history,实现了一套虚拟路由系统,子应用运行在这套虚拟路由系统中,和主应用的路由进行隔离,避免相互影响。
keep-alive
开启keep-alive后,应用卸载时不会销毁,而是推入后台运行。 micro-app的keep-alive是应用级别的,它只会保留当前正在活动的页面状态,如果想要缓存具体的页面或组件,需要使用子应用框架的能力,如:vue的keep-alive。
数据通信
micro-app
提供了一套灵活的数据通信机制,方便主应用和子应用之间的数据传输。
主应用和子应用之间的通信是绑定的,主应用只能向指定的子应用发送数据,子应用只能向主应用发送数据,这种方式可以有效的避免数据污染,防止多个子应用之间相互影响。
同时也提供了全局通信,方便跨应用之间的数据通信。
Js沙箱
使用Proxy
拦截了用户全局操作的行为,防止对window的访问和修改,避免全局变量污染。micro-app
中的每个子应用都运行在沙箱环境,以获取相对纯净的运行空间
子应用(Vue+Vite)
设置跨域
必须,vite默认开启跨域支持,不需要额外配置。
注册卸载函数
ts
const app = createApp(App)
app.mount('#app')
// 卸载应用
window.unmount = () => {
app.unmount()
}
总结
维度 | qiankun | wujie(无界) | micro-app |
---|---|---|---|
实现原理 | 路由代理+沙箱(基于single-spa封装) | Web Components + iframe | Shadow DOM + JS 沙箱(Proxy) |
沙箱隔离强度 | 高(Proxy沙箱) | 极高(iframe原生隔离) | 高(JS代理沙箱+CSS隔离) |
Vite支持 | 需插件(vite-plugin-qiankun) | 原生支持(无需额外配置) | 原生支持(1.0版本) |
接入成本 | 中等(需适配生命周期钩子) | 极低(直接URL嵌入) | 低(无需修改子应用代码) |
调试难度 | 中(需沙箱调试) | 高(iframe上下文) | 低(可视化工具支持) |
定制灵活性 | 高(插件机制完善) | 低(框架固化) | 中(API丰富) |
性能损耗 | 动态代理(高) | iframe通信(低) | 资源拦截(中等) |
通信机制 | props + globalState | 数据通信 + 事件总线 | props + window通信 |
路由处理 | 路由级调度(支持路由冲突) | 主子应用路由同步 | 虚拟路由(解决刷新问题) |
子应用保活 | 需额外实现 | 内置保活机制 | 应用级别保活(需配合Vue/React) |
多应用激活 | 支持(需配置) | 支持(多实例激活) | 支持(虚拟路由) |
社区活跃度 | 高 | 中 | 中 |
技术栈兼容性 | 任意框架(React/Vue/Angular等) | 任意框架(支持Vite) | 任意框架(兼容Vue/React等) |
浏览器兼容性 | IE11+(需polyfill) | IE11+(iframe fallback) | IE11+(Web Components polyfill) |
安全特性 | 沙箱隔离(非绝对) | iframe物理隔离(最高) | Shadow DOM隔离(高) |
构建工具集成 | Webpack 5+(需配置) | Webpack/Vite(原生支持) | Webpack/Vite(原生支持) |
代码复杂度 | 中等 | 低 | 低 |
插件生态 | 丰富(qiankun-plugin-*) | 有限(原生功能完备) | 有限(社区插件较少) |
热更新体验 | 需手动配置 | 原生支持 | 原生支持 |
资源加载方式 | 动态加载(JS/CSS) | iframe加载 | 资源劫持加载 |
首屏加载速度 | 中等(依赖主应用) | 快(预加载优化) | 中等(虚拟路由优化) |
错误隔离 | 部分隔离(沙箱限制) | 完全隔离(iframe) | 部分隔离(沙箱) |
后面我需要实践一个多页签的微前端项目,目标是 把原来"巨石前端"拆成多个 Vue3 + Vite5 + TypeScript 子应用,全部应用使用一样的基础依赖,会使用pnpm-workspace monorepo的形式管理依赖,基座应用负责管理菜单,向子应用提供菜单、状态等其他共享信息;每个子应用内部都有多个路由菜单,可以使用基座应用的菜单/tab页签切换,需要实现切换时页面保活。