文章目录
微前端:micro-frontends (MFE),将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一的应用。各个前端应用可以独立开发,独立部署;也可以进行并行开发——通过 NPM、Git TagGit 或者 Submodule 来管理
微前端是模块共享管理方式,主要解决多项目并存问题,多项目并存的最大问题就是模块共享,不能有冲突
微前端技术:
- 样式隔离
- window 沙箱
- 通信机制(生命周期管理)
各种通信机制:
- 基于URL来进行数据传递,但是传递消息能力弱
- 基于customEvent实现通信
- 基于props主子应用间通信
- 使用全局变量、redux进行通信
公共依赖:
缺点:
- 拆分的粒度越小,意味着架构变得越复杂、维护成本越高
- 技术栈一旦多样化,便意味着技术栈是混乱
微前端的技术机制
路由分发方式(nginx)
通过http服务器的反向代理功能,将请求路由到对应的应用上。
因为两个应用都运行在一个域里,所以可以通过localstorage、cookies、indexedDB等方式共享数据
缺点:一个页面只有唯一一个应用
前端微服务化
在不同的框架之上设计通信和加载机制,在一个页面内加载对应的应用。
每个前端应用都是完全独立的(技术栈、构建、开发、部署)、自主运行的,最后通过模块化方式组合出完整的前端应用
需要做到两点:
- 在页面适合的地方引入或创建DOM
- 用户操作时,加载对应的应用(触发应用的启动),并能卸载应用
还需要保证应用间的第三方依赖不冲突,可以通过向上游开发者提pull request来修复这个问题
微应用(组合式集成)
通过软件工程方式,在部署构建环境中,把多个独立的应用组件成一个单体应用
每个模板都成为一个单独的项目,同时拥有一些通用的共享模板、应用脚手架。当构建的时候,再复制所有的模块到一个项目中,进行集成构建。
缺点:只能使用唯一的一种前端框架
微件化
开发一个新的构建系统,将部分业务功能构建成一个独立的chunk代码,使用时只需要远程加载即可。即微件就是一段可以直接嵌入应用上运行的预先编译好的代码。
每个业务团队编写自己的业务代码,并将编译好的代码部署到指定服务器。在运行时只需要加载相应的业务模块
非单页实现微件,从远程加载js代码并在浏览器上执行
单页应用,实现微件化没有那么容易。应用由提前编译变成运行时才编译:
- 需要一个运行时及编译的环境
- 性能受到影响
- 需要提前规划依赖
前端容器化(iframe)
将iframe作为容器来容纳其他前端应用 。
iframe能有效将一个页面嵌入当前页面中,两个页面间的css和js相互隔离,
相当于创建一个独立的宿主环境,可以用来做沙箱隔离
缺点:
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用
- ui不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中.
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程
- 不支持seo
应用组件化(web Components)
借助web Components技术,来构建跨框架的前端应用
web-component是浏览器自带的一套 runtime,它提供 shadow dom,能够完全隔离 css
缺点:兼容问题,国产浏览器很多都阉割掉了shadow dom(chrome、opera支持)
沙箱隔离
样式隔离
不希望外面能够访问内部代码,Shadow DOM就把内部代码隐藏起来
dom api Element.attachShadow()给指定的元素挂载一个影子dom, 划分区域,外面访问不到
<div>
<p>hello world</p>
<div id='abc'></div>
</div>
<script>
// 外界无法访问 shadow dom
let shadowDOM = doccument.getElementById('abc').attachShadow({mode:'closed'})
let pElm = document.createElement('p')
pElm.innerHTML = 'hello abc'
let styleElm = document.createElement('style')
styleElm.textContent = `p{color:red}`
shadowDOM.appendChild(styleElm)
shadowDOM.appendChild(pElm)
// 缺点:比如弹窗挂载在body上,那就控制不了样式了
// document.body.appendChild(pElm)
</script>
js隔离
加载a应用,给A应用设置全局属性window.a,再切换到B应用,也可以访问到window.a,这就污染了
单页切换中,切换B时把window.a属性去掉,切换回A时再重新设置,这就是一个沙箱,创建一个干净的环境给子应用使用,当切换时,可以选择丢弃属性或恢复
- 快照沙箱(无法支持多实例)
在应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境
一年前拍照,一年后再拍照,做对比把区别保存起来,再回到一年前,如果想还原,再把刚才的区别应用回来
代理沙箱可以实现多应用沙箱。把不同的应用用不同的代理来处理
1)激活时将当前window属性进行快照处理
2)失活时用快照中的内容和当前window属性比对
3)如果属性发生变化保存到 modifyPropsMap 中,并用快照还原window属性
4)在次激活时,再次进行快照,并用上次修改的结果还原window
/*
变化表(保存所有的修改):this.modifyPropsMap = {}
激活沙箱
1)拍照:看window下有没有prop属性,如果有就保存到快照里 this.windowSnapshot[prop] = window[prop]
2)拿到变化表里的修改过的,赋值到window上 window[p] = this.modifyPropsMap[p]
丢弃沙箱:判断是否失活,失活还原(拿快照表的还原)
1)判断是否失活:拿当前window里的属性和之前快照里的做对比,如果不一样了,说明属性有变化了,
有变化就保存到变化表里(当前属性变化了,变化的值为window上的值)this.modifyPropsMap[prop] = window[prop]
2)如果失活,变回一年前,即快照赋值到window上 window[prop] = this.windowSnapshot[prop]
*/
class SnapshotSandbox{
constructor(){
this.proxy = window
this.modifyPropsMap = {} // 记录window属性的修改
this.active()
}
active(){ // 激活
this.windowSnapshot = {} // 快照表
for(const prop in window){
if(window.hasOwnProperty(prop)){ // 看window上有没有prop属性,有就拍快照
this.windowSnapshot[prop] = window[prop]
}
}
Object.keys(this.modifyPropsMap).forEach(p=>{
window[p] = this.modifyPropsMap[p]
})
}
inactive(){ // 失活
for(const prop in window){
if(window.hasOwnProperty(prop)){
// 如果跟快照表的不一样,就保存到修改表里,然后window的属性还原回去(拿快照表的赋值)
if(window[prop] !== this.windowSnapshot[prop]){
this.modifyPropsMap[prop] = window[prop]
window[prop] = this.windowSnapshot[prop]
}
}
}
}
}
let sandbox = new SnapshotSandbox();
((window)=>{
window.a=1
window.b=2
console.log(window.a, window.b)
sandbox.inactive() // 失活
console.log(window.a, window.b)
sandbox.active() // 激活
console.log(window.a, window.b)
})(sandbox.proxy) // sandbox.proxy就是window
- Proxy 代理沙箱(可以实现多实例)
window对象的属性劫持,内部用一个对象缓存子应用window下面属性方法,达到隔离的目的
每个应用都创建一个proxy来代理window,好处是每个应用都是相对独立,不需要直接更改全局window属性
缺点:低版本浏览器不支持
class ProxySandbox {
constructor() {
const rawWindow = window;
const fakeWindow = {}
const proxy = new Proxy(fakeWindow, {
set(target, p, value) {
target[p] = value;
return true
},
get(target, p) {
return target[p] || rawWindow[p];
}
});
this.proxy = proxy
}
}
let sandbox1 = new ProxySandbox();
let sandbox2 = new ProxySandbox();
window.a = 1;
((window) =>{
window.a = 'hello';
console.log(window.a)
})(sandbox1.proxy);
((window) =>{
window.a = 'world';
console.log(window.a)
})(sandbox2.proxy);
- nodeJS沙箱
使用nodeJS提供的VM Module
const vm = require('vm');
const sandbox = { a: 1, b: 1 };
const script= new vm.Script('a + b');
const context = new vm.createContext(sandbox);
script.runInContext(context);
webpack5模块联邦(公共依赖)
微前端一般有两种打包方式:
- 子应用独立打包,模块更解耦,但无法抽取公共依赖等。
- 整体应用一起打包,很好解决上面的问题,但打包速度实在是太慢了,不具备水平扩展能力
模块联邦:直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。实现多个不同技术栈共存,而不需要任何框架
让应用具备模块化输出能力,其实开辟了一种新的应用形态,即 “中心应用”,这个中心应用用于在线动态分发 Runtime 子模块,并不直接提供给用户使用
single-spa
缺点:不够灵活、不能动态加载js文件,样式不隔离,没有js沙箱机制,加载不同应用,都是同一个tab页面
过程:
1)注册应用,加载子应用脚本
2)子应用把打包后的脚本挂载到window上,调用mount的话就会加载options上的方法,放到父应用对应的id上
// 创建子应用,选babel、router、history
vue create child-vue
npm i single-spa-vue
vue create parent-vue
npm i single-spa
// parent-vue/src/main.js
/* 1. singleSpaVue(Vue, appOptions)
- Vue(必须参数),Vue对象,通过import暴露或者通过require('vue')的方式
- appOptions(必须参数),是用于创建你的vue应用的一个实例对象,通过new Vue(appOptions)的方式创建
2. __webpack_public_path__ = 'xxx',相当于webpack里的公共路径,output: {publicPath: 'http://123.com/'},
*/
import Vue from 'vue'
import singleSpaVue from 'single-spa-vue'
const appOptions = {
el: '#vue', // el是挂载到父应用的哪个标签上
router,
render: h => h(App)
}
const vueLifeCycle = singleSpaVue({ // 生成包装后的生命周期
Vue,
appOptions
})
// 父应用引用了我
if (window.singleSpaNavigate) {
/* eslint-disable */
__webpack_public_path__ = 'http://localhost:1000/' // 给打包的时候加目录
}
if (!window.singleSpaNavigate) {
delete appOptions.el
new Vue(appOptions).$mount('#app')
}
// 定义好的协议,会被父应用调用
export const bootstrap = vueLifeCycle.bootstrap
export const mount = vueLifeCycle.mount
export const unmount = vueLifeCycle.unmount
// parent-vue/src/main.js
// ...
import { registerApplication, start } from 'single-spa'
async function loadScript(url) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
script.src = url
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}
registerApplication(
'myVueApp', // 名字随便起
async () => {
console.log('加载模块')
// 先加载公共脚本,再加载app
await loadScript(`http://localhost:1000/js/chunk-vendors.js`)
await loadScript(`http://localhost:1000/js/app.js`)
return window.singleVue
},
location => location.pathname.startsWith('/vue')
)
start()
qiankun
qiankun(乾坤) 就是一款由蚂蚁金服推出的比较成熟的微前端框架,基于 single-spa 进行二次开发,用于将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用
- qiankun底层应用之间的加载使用single-spa,上层实现HTML entry作为子应用入口、样式隔离、js沙箱、资源预加载(浏览器空闲时间加载)、umi插件(一键切换成微前端架构系统)等上层能力(single-spa + js隔离+css隔离)
- 无关技术栈,只要匹配到路由,就加载对应页面
注册微应用的基础配置信息。当浏览器url发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活。
基座会把挂载到的html页再插入到自己身上
- 注册应用:
registerMicroApps 函数的作用是注册子应用,并且在子应用激活时,创建运行沙箱,在不同阶段调用不同的生命周期钩子函数 - 启动start
js隔离:
如果是单个应用(多实例),且支持Proxy,就用Proxy沙箱(用proxy代理window),否则用快照沙箱(拷贝)。如果是多个实例,用Proxy沙箱
原理:通过激活沙箱时还原子应用状态,卸载时还原主应用状态(子应用挂载前的全局状态)实现的
样式隔离
启动作用域css (css module,给每应用都加上前缀)
获取应用内容后进行包装,把内容插到沙箱里
处理动态样式
拦截定时器
qiankun demo
// 创建主应用和子应用
vue create qiankun-base
vue create pro-vue
npx create-react-app pro-react
- base
// src/main.js
import { registerMicroApps, start } from 'qiankun'
const apps = [
{
name: 'vueApp', // 应用名字
entry: '//localhost:8081', // 默认会加载这个html 解析里面的js 动态执行 (子应用必须支持跨域), fetch
container: "#vue", // 容器名称
activeRule: '/vue' // 激活路径
},
{
name: 'reactApp',
entry: '//localhost:3000',
container: "#react",
activeRule: '/react',
// props: { a: 1 } // 传给子应用
}
]
registerMicroApps(apps); // 注册
start(); // 启动
// src/App.vue
<div id='base'>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/vue">vue</router-link> |
<router-link to="/react">react</router-link>
</div>
<router-view></router-view>
<div id="vue"></div>
<div id="react"></div>
</div>
- pro-vue
// src/main.js
let instance = null;
// 挂载到子应用html中之后,基座会拿到这个挂载后的html,将其插入进去
function render() {
instance = new Vue({
router,
render: h => h(App)
}).$mount("#app"); // 这里是挂载到自己的html中 基座会拿到这个挂载后的html 将其插入进去
}
if (window.__POWERED_BY_QIANKUN__) {
// 动态添加publicPath
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// 使用这个全局变量来区分当前是否运行在qiankun的主应用的上下文中
// 没有父应用 独自开启运行子应用
if (!window.__POWERED_BY_QIANKUN__) {
// 默认独立运行
render();
}
// 导出方法
export async function bootstrap() {}
export async function mount(props) {
// 接收主应用传过来的参数
console.log(props);
render(props);
}
export async function unmount() {
instance.$destroy();
}
// src/router
const router = new VueRouter({
mode: "history",
base: "/vue",
routes
});
// vue.config.js
module.exports = {
lintOnSave: false,
devServer: {
port: 8081,
headers: {
"Access-Control-Allow-Origin": "*"
}
},
configureWebpack: {
output: {
library: "vueApp",
libraryTarget: "umd"
}
}
};
- pro-react
// 安装
npm i react-app-rewired -D
// package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
// src/index.js
function render(){
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
}
if(!window.__POWERED_BY_QIANKUN__){
render();
}
export async function bootstrap(){
}
export async function mount() {
render()
}
export async function unmount(){
ReactDOM.unmountComponentAtNode( document.getElementById('root'));
}
// config-overrides.js
module.exports = {
webpack:(config)=>{
config.output.library = 'reactApp';
config.output.libraryTarget = 'umd';
config.output.publicPath = 'http://localhost:3000/';
return config;
},
devServer:(configFunction)=>{
return function (proxy,allowedHost){
const config = configFunction(proxy,allowedHost);
config.headers = {
"Access-Control-Allow-Origin":'*'
}
return config
}
}
}
.env
PORT=3000
WDS_SOCKET_PORT=3000
qiankun 通信
使用initGlobalState生成全局参数,当子应用改变的时候,主应用也可以监听到变化
// base/src/main.js
import { registerMicroApps, start, initGlobalState } from 'qiankun'
// 可以通过 initGlobalState 生成全局参数
const actions = initGlobalState({
password: store.state.password,
username: store.state.username
})
actions.onGlobalStateChange((options, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log('主应用state改变')
console.log(options, prev)
})
actions.setGlobalState({
password: '123',
username: 'hhh'
})
const apps = [
{
name: 'vueApp', // 应用名字
entry: '//localhost:8081', // 默认会加载这个html 解析里面的js 动态执行 (子应用必须支持跨域), fetch
container: '#vue', // 容器名称
activeRule: '/vue', // 激活路径
props: { // 传给子组件
actions,
components
}
}
]
// pro-vue/src/main.js
export async function mount(props) {
// 接收主应用传过来的参数
render(props)
Vue.prototype.$actions = props.actions
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log('子应用state改变')
console.log(state, prev)
// store.commit('update', { // 可以在这里设置store
// name})
})
}
// pro-vue/src/views/home.vue
handleLogin() {
let params = { username: this.username, password: this.password }
this.$actions.setGlobalState(params) // 子应用变化的时候,主应用也可以监听到
}
源码解读
- 底层使用了single-spa来兼容多种应用框架,把上一层获取到的参数再传给single-spa来启动这个微前端架构。
- 在registerApplication时,把参数做了一层处理(加载子应用资源、沙箱等)再赋值给single-spa的registerApplication
初始化子应用:(只执行一次)- 加载子应用资源 importEntry
- 创建子应用沙箱 sandboxInstance
- 获取生命周期 bootstrap, mount, unmount, update
// 源代码apis.ts
import { mountRootParcel, registerApplication, start as startSingleSpa } from 'single-spa';
import { loadApp } from './loader';
// ...省略代码
registerApplication({
name,
app: async () => {
loader(true);
await frameworkStartedDefer.promise;
const { mount, ...otherMicroAppConfigs } = await loadApp(
{ name, props, ...appConfig },
frameworkConfiguration,
lifeCycles,
);
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
// 源代码loader.ts
import { importEntry } from 'import-html-entry';
import { createSandbox, css } from './sandbox'; // 沙箱
/* 使用import-html-entry 库从 entry 进入加载子应用,加载完成后将返回一个对象,包含
template内容模板、assetPublicPath资源路径、execScripts应用的所有js脚本、
getExternalScripts 外部src引入的脚本文件(数组)、
getExternalStyleSheets 外部href引入的样式表文件(数组)
*/
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
// 沙箱
let global = window;
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
if (sandbox) { // 是参数,默认为true,你也可以关闭沙箱
const sandboxInstance = createSandbox( // 创建子应用沙箱
appName,
containerGetter,
Boolean(singular),
enableScopedCSS,
excludeAssetFilter,
);
// 用沙箱的代理对象作为接下来使用的全局对象
global = sandboxInstance.proxy as typeof window;
mountSandbox = sandboxInstance.mount;
unmountSandbox = sandboxInstance.unmount;
}
/* execScripts()执行应用中所有js,把global(就是proxy沙箱代理对象,实际是window)当作参数传进去,用来改变指向,屏蔽全局的
里面有一个getExecutableScript用来改变脚本执行时候的window/self/this的指向
(function(window, self){
${ scriptText } ${ sourceUrl }
}).bind(window.proxy)(window.proxy, window.proxy)
给脚本字符串构件了一个简单的执行环境,该环境屏蔽了全局了this 、window和self
*/
const scriptExports: any = await execScripts(global, !singular); // 单例推断为null
// 从模块导出生命周期钩子
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(scriptExports, appName, global);
// 源码 loader.ts
// isEnableScopedCSS,判断是否要启动作用域css,如果要,就给每应用都加上前缀
if (element && isEnableScopedCSS(configuration)) {
// 获取所有<style></style>里面的熟悉,给他加载前缀
const styleNodes = element.querySelectorAll('style') || [];
styleNodes.forEach(stylesheetElement => {
css.process(element!, stylesheetElement, appName); // 第二个参数是prefix
});
}
// 源码 sandbox/patchers/css.ts
rewrite(rules: CSSRule[], prefix: string = '') {
let css = '';
rules.forEach(rule => {
switch (rule.type) {
case RuleType.STYLE:
css += this.ruleStyle(rule as CSSStyleRule, prefix);
break;
case RuleType.MEDIA:
css += this.ruleMedia(rule as CSSMediaRule, prefix);
break;
case RuleType.SUPPORTS:
css += this.ruleSupport(rule as CSSSupportsRule, prefix);
break;
default:
css += `${rule.cssText}`;
break;
}
});
return css;
}
// 源码 sandbox/index.ts
/* 是否支持proxy。是就用proxy,否则用沙箱快照
是否单例singular。是就用LegacySandbox(快照沙箱模式),否则用ProxySandbox(proxy沙箱)
LegacySandbox:的沙箱隔离是通过激活沙箱时还原子应用状态,卸载时还原主应用状态(子应用挂载前的全局状态)实现的
ProxySandbox:完全隔离了对 window 对象的操作
*/
if (window.Proxy) {
sandbox = singular ? new LegacySandbox(appName) : new ProxySandbox(appName);
} else {
sandbox = new SnapshotSandbox(appName);
}
qiankun源码
qiankun文档
微前端之import-html-entry
实现简单single-spa
single-spa的生命周期包含5种状态:
- load:决定加载哪个应用,并绑定生命周期
- bootstrap:获取静态资源
- mount:安装应用,如创建DOM节点
- unload:删除应用的生命周期
- unmount:卸载应用,如删除DOM节点,取消事件绑定
1、应用加载、挂载、卸载
- 注册应用:就是把应用放到数组里(把参数组成对象放到数组里),调start启动的时候再去加载
- 利用状态机,维护应用的所有状态
没有被加载过not loaded -> 加载资源loading_source_code(loadApp加载资源) ->
没有启动not bootstraped -> 启动中bootstraping(调用start) -> 没有挂载not mounted -> 挂载中mounting (调用mount) -> 挂载完毕mounted (被激活) (<=> 应用更新updating) -> unmounting (只是改变状态,回到not mounted) -> 完全卸载unmounted(回到not loaded)
- 注册
1)定义一个数组apps
,把注册的应用都push进来;
2)区分哪些是要加载的、挂载的、要卸载的app,把它们存储在对应的数组里
3)加载应用(start启动的时候会调用)
3.1)判断是已经加载过,start为true表示加载过,加载过就卸载掉,否则注册应用时,需要预先加载
预加载应用:遍历加载的数组,把bootstrap,mount和unmount方法绑定到每个app上
// 例子:遍历数组,把方法绑定都每个对象中;且只绑定一次
let arr = [{},{},{}]
// 把当前方法存起来,第二次加载时发现有把原来的返回,用的还是以前的
function toLoadPromise(app) {
if (app.loadPromise) {
return app.loadPromise; //缓存机制
}
return (app.loadPromise = Promise.resolve().then(async () => {
app.bootstrap = function(){};
app.mount = function(){};
app.unmount = function(){};
delete app.loadPromise;
return app;
}))
}
let nArr = arr.map(toLoadPromise)
console.log(nArr)
3.2)装载应用
先卸载不需要的应用,再去加载需要的应用,分为:
要加载的:遍历数组拿到应用,分别调用对应的函数:load加载 => bootstrap启动 => mount挂载
要挂载的:遍历数组拿到应用,分别调用对应的函数:启动 => 挂载
async function performAppChanges() { // 加载应用
// 先卸载不需要的应用
let unmountPromises = appsToUnmount.map(toUnmountPromise); // 需要去卸载的app
// 去加载需要的应用
appsToLoad.map(async (app)=>{ // 将需要求加载的应用拿到 => 加载 => 启动 => 挂载
app = await toLoadPromise(app);
app = await toBootstrapPromise(app);
return toMountPromise(app);
})
appsToMount.map(async (app)=>{
app = await toBootstrapPromise(app);
return toMountPromise(app);
});
}
// 改变应用状态和调用应用的bootstrap函数,toMountPromise也是类似
export async function toBootstrapPromise(app) {
if(app.status !== NOT_BOOTSTRAPPED){
return app;
}
app.status = BOOTSTRAPPING;
await app.bootstrap(app.customProps);
app.status = NOT_MOUNTED;
return app;
}
-
mount 挂载
1)挂载的时候判断应用状态是否已经挂载过,已加载返回当前应用app
2)没有挂载,改变应用状态MOUNTING
,调用挂载函数app.mount()
,挂载完成再把应用状态改为MOUNTED
-
unmount 卸载
1)当前应用没有被挂载直接什么都不做了,直接返回应用app
2)否则,把应用状态改为UNMOUNTING
,调用app.unmount()
,卸载完再把应用状态改为NOT_MOUNTED
2、拦截路由变化,重写路由,去注册不同的应用
路由处理:匹配用户有没有绑定hashchange或popstate这两个方法。
- 每当hashchange或popstate变化就去刷新reroute方法,重新注册应用
- 子应用也可能会绑定自己的路由事件,比如vue、react
当应用切换后,还需要拦截处理子应用路由事件方法,保证在应用切换后执行
思路:
1)先把子应用路由事件的拦截存起来存到数组capturedEventListeners里(等应用切换完后调用,可以遍历数组让它依次执行)
2)拦截:监听hashchange或popstate事件时,判断是否有被拦截过存在capturedEventListeners里,如果没有就存到capturedEventListeners里
如果有,就执行刷新reroute(重写的方法),重新注册应用
3)注意:地址栏输入路径不会触发popstate事件
// 拦截子应用的路由事件
export const routingEventsListeningTo = ['hashchange', 'popstate'];
function urlReroute() { // 重写路由
reroute([], arguments); // 会根据路径重新加载不同的应用
}
const capturedEventListeners = { // 后续挂载的事件先暂存起来
hashchange: [],
popstate: [] // 当应用切换完成后可以调用
}
window.addEventListener('hashchange', urlReroute);
window.addEventListener('popstate', urlReroute);
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function(eventName, fn) {
if (routingEventsListeningTo.indexOf(eventName) >= 0 && !capturedEventListeners[eventName].some(listener => listener == fn)) {
capturedEventListeners[eventName].push(fn);
return;
}
return originalAddEventListener.apply(this, arguments)
}
window.removeEventListener = function(eventName, fn) {
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[eventName].filter(l => l !== fn);
return;
}
return originalRemoveEventListener.apply(this, arguments)
}
技术选型
- qiankun:采用运行时集成主、子应用,HTML entry作为子应用入口的中心路由基座式微前端方案
- 应用间通信:使用@ice/stark-data,自定义发布-订阅通信的实现,处理全局消息通信和数据管理,解决如用户登录后,主子应用间共享用户信息的问题
- 主应用:负责权限、路由、布局管理
一个判断业务关联是否紧密的标准:看这个微应用与其他微应用是否有频繁的通信需求。如果有可能说明这两个微应用本身就是服务于同一个业务场景,合并成一个微应用可能会更合适