一、微前端之实践子应用的注册、路由拦截和获取首个子应用
- 子应用的注册,如下所示:
- 首先需要一个子应用注册的列表,在主应用
main
的 store
文件夹中,新建 sub.js
,对外暴露 subNavList
这个子应用的列表。subNavList
是一个数组,每一个子元素都是一个子应用,name
是子应用名称,唯一 ID
; entry
是子应用的入口,获取到子应用的文件;container
是子应用的渲染内容,显示在哪个容器中;activeRule
是子应用激活路由,根路由内容,激活的规则,loading
是子应用的加载状态;loading
是每一个子应用的信息,sub.js
,代码如下:
import { loading } from '../store'
import * as appInfo from '../store'
export const subNavList = [
{
name: 'react15',
entry: '//localhost:9002/',
loading,
container: '#micro-container',
activeRule: '/react15',
appInfo,
},
{
name: 'react16',
entry: '//localhost:9003/',
loading,
container: '#micro-container',
activeRule: '/react16',
appInfo,
},
{
name: 'vue2',
entry: '//localhost:9004/',
loading,
container: '#micro-container',
activeRule: '/vue2',
appInfo,
},
{
name: 'vue3',
entry: '//localhost:9005/',
loading,
container: '#micro-container',
activeRule: '/vue3',
appInfo,
},
];
- 在主应用
main
的 store
文件夹中,有 header.js、loading.js、nav.js、sub.js
和 index.js,loading.js
,代码如下:
import { ref } from 'vue';
export let loadingStatus = ref(true);
export const changeLoading = type => loadingStatus.value = type;
import { ref } from 'vue';
export const headerStatus = ref(true);
export const changeHeader = type => headerStatus.value = type;
import { ref } from 'vue';
export const navStatus = ref(true);
export const changeNav = type => navStatus.value = type;
export * as loading from './loading';
export * as header from './header';
export * as nav from './nav';
- 在主应用
main
中,main.js
,通过 registerApp
将 subNavList
注册到 util
下的 index.js
,微前端框架中,main.js
,代码如下:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { subNavList } from './store/sub'
import { registerApp } from './util'
registerApp(subNavList);
createApp(App).use(router()).mount('#micro_web_main_app');
- 在主应用
main
中,util
下的 index.js
,对外暴露 registerApp
,通过将传入的子应用注册列表 list
注册到微前端框架里,registerMicroApps
,同时也传入了主应用的生命周期,beforeLoad
开始加载,mounted
渲染完成,destoryed
卸载完成。通过调用 start
开启微前端框架,util
下的 index.js
,代码如下:
import { registerMicroApps, start, createStore } from '../../micro'
import { loading } from '../store'
const store = createStore()
window.store = store
store.subscribe((newValue, oldValue) => {
console.log(newValue, oldValue, '---')
})
export const registerApp = (list) => {
registerMicroApps(list, {
beforeLoad: [
() => {
loading.changeLoading(true)
console.log('开始加载')
}
],
mounted: [
() => {
loading.changeLoading(false)
console.log('渲染完成')
}
],
destoryed: [
() => {
console.log('卸载完成')
}
]
})
start()
}
- 在
micro
微前端框架中,之前调用的,registerMicroApps
在 micro
下的 start.js
中,它需要使用到 micro
下的 const
的 subApps.js
,其中 list
是统一管理子应用列表,getList
是获取子应用列表,setList
是将子应用列表注册到 list
中,subApps.js
,代码如下:
let list = []
export const getList = () => list
export const setList = appList => list = appList
- 在
micro
下的 start.js
中,对外暴露 registerMicroApps
这个方法,传入子应用注册列表 appList
和主应用的生命周期 lifeCycle
,分别调用 setList
和 setMainLifecycle
进行注册设置,代码如下:
export const registerMicroApps = (appList, lifeCycle) => {
setList(appList);
setMainLifecycle(lifeCycle);
}
- 路由拦截,如下所示:
- 首先需要在
micro
微前端框架中的 start.js
,调用 rewriteRouter
方法实现路由拦截。在 router
下的 rewriteRouter.js
中,对外暴露出 rewriteRouter
,重写 window
的路由跳转。patchRouter
接收两个参数,原生的事件、事件的名称,所有触发 pushState
和 replaceState
的事件都会被监听到,window.history.pushState
和 window.history.replaceState
。通过 window.addEventListener
事件绑定 micro_push
和 micro_replace
,通过 window.onpopstate
事件监听,监听返回事件,当点了返回按钮后 turnApp
就会执行,rewriteRouter.js
,代码如下:
import { patchRouter } from '../utils'
import { turnApp } from './routerHandle'
export const rewriteRouter = () => {
window.history.pushState = patchRouter(window.history.pushState, 'micro_push');
window.history.replaceState = patchRouter(window.history.replaceState, 'micro_replace');
window.addEventListener('micro_push', turnApp);
window.addEventListener('micro_replace', turnApp);
window.onpopstate = async function () {
await turnApp()
}
}
patchRouter
是在 micro
下的 util
中的 index.js
,给当前的路由跳转打补丁。通过 new Event
创建新的事件,通过 globalEvent.apply
传递过来的原生事件 globalEvent
,代替函数的执行,通过 window.dispatchEvent
触发创建的事件,代码如下:
export const patchRouter = (globalEvent, eventName) => {
return function () {
const e = new Event(eventName)
globalEvent.apply(this, arguments)
window.dispatchEvent(e)
}
}
turnApp
是在 micro
下的 router
中的 routerHandle.js
,如果子应用发生改变在里面可以执行微前端的生命周期执行,代码如下:
import { isTurnChild } from '../utils'
import { lifecycle } from '../lifeCycle'
export const turnApp = async () => {
if (isTurnChild()) {
await lifecycle()
}
}
- 获取首个子应用,如下所示:
- 在
registerApp
中调用 micro
下的 start.js
,对外暴露的 start
方法启动微前端框架。通过 getList
首先验证当前子应用列表是否为空,如果子应用列表为空,抛出异常。如果子应用列表非空,有子应用的内容, 查找到符合当前路由的子应用。如果当前没有在使用的子应用, 抛出一个错误,请访问正确的连接,访问一个默认的路由,通常为首页或登录页面,window.history.pushState
跳转到对应的 url
上。若查找到子应用的内容并且 hash
值存在,进行标记,将当前 app
的内容设置为 app.activeRule
,start.js
,代码如下:
import { setList, getList } from './const/subApps'
import { currentApp } from './utils'
import { rewriteRouter } from './router/rewriteRouter'
import { setMainLifecycle } from './const/mainLifeCycle'
import { prefetch } from './loader/prefetch'
import { Custom } from './customevent'
const custom = new Custom()
custom.on('test', (data) => {
console.log(data)
})
window.custom = custom;
rewriteRouter();
export const registerMicroApps = (appList, lifeCycle) => {
setList(appList);
setMainLifecycle(lifeCycle);
}
export const start = () => {
const apps = getList();
if (!apps.length) {
throw Error('子应用列表为空, 请正确注册');
}
const app = currentApp();
const { pathname, hash } = window.location;
if (!hash) {
window.history.pushState(null, null, '/vue3#/index');
}
if (app && hash) {
const url = pathname + hash;
window.__CURRENT_SUB_APP__ = app.activeRule;
window.history.pushState('', '', url);
}
prefetch();
}
- 调用
currentApp
时,是在 micro
的 utils
下的 index.js
,通过 window.location.pathname
获取到当前子应用的路由 activeRule
,调用 filterApp
子应用在命中后就会返回,isTurnChild
是判断子应用是否做了切换,index.js
,代码如下:
import { getList } from "../const/subApps";
export const patchRouter = (globalEvent, eventName) => {
return function () {
const e = new Event(eventName);
globalEvent.apply(this, arguments);
window.dispatchEvent(e);
}
}
export const currentApp = () => {
const currentUrl = window.location.pathname;
return filterApp('activeRule', currentUrl);
}
export const findAppByRoute = (router) => {
return filterApp('activeRule', router);
}
export const filterApp = (key, value) => {
const currentApp = getList().filter(item => item[key] === value);
return currentApp && currentApp.length ? currentApp[0] : {};
}
export const isTurnChild = () => {
const { pathname, hash } = window.location;
const url = pathname + hash;
const currentPrefix = url.match(/(\/\w+)/g);
if (
currentPrefix &&
(currentPrefix[0] === window.__CURRENT_SUB_APP__) &&
hash === window.__CURRENT_HASH__
) {
return false;
}
window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__;
const currentSubApp = window.location.pathname.match(/(\/\w+)/);
if (!currentSubApp) {
return false
}
window.__CURRENT_SUB_APP__ = currentSubApp[0];
window.__CURRENT_HASH__ = hash;
return true;
}