一、微前端之实践主应用的生命周期、微前端的生命周期、 加载和解析 html 及 js
- 主应用的生命周期,如下所示:
- 在主应用
main
下的 util
中的 index.js
中,在 registerApp
中的 registerMicroApps
方法注册到微前端框架里,传入第二个参数生命周期,beforeLoad
是开始加载,mounted
是渲染完成,destoryed
是卸载完成,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
下的 start.js
中,registerMicroApps
接收两个参数,appList
子应用参数列表和 lifeCycle
生命周期,然后通过 setMainLifecycle
设置主应用的生命周期,代码如下:
export const registerMicroApps = (appList, lifeCycle) => {
setList(appList);
setMainLifecycle(lifeCycle);
}
- 在
micro
下的 const
中 mainLifeCycle.js
,lifecycle
是主应用的生命周期,getMainLifecycle
是获取主应用的生命周期,setMainLifecycle
是缓存主应用的生命周期,代码如下:
let lifecycle = {};
export const getMainLifecycle = () => lifecycle;
export const setMainLifecycle = data => lifecycle = data;
- 微前端的生命周期,如下所示:
- 在
micro
下的 util
中 index.js
,findAppByRoute
查找上一个和下一个 app
里面的内容,isTurnChild
判断子应用是否做了切换,通过 window.__CURRENT_SUB_APP__
判断当前路由以改变,修改当前路由,通过 window.__CURRENT_HASH__
判断当前 hash
值是否改变,代码如下所示:
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;
}
- 在
micro
下的 lifeCycle
中 index.js
,在 lifecycle
中,prevApp
是获取到上一个子应用,卸载掉上一个子应用,nextApp
是获取到要跳转到的子应用,执行对应的子应用生命周期。若没有获取到下一个子应用,直接退出,后面的就不用执行。获取到上一个子应用,并且有它自己的生命周期,由其它子应用切换过来的,卸载掉上一个子应用。beforeLoad、mounted 和 destoryed
是主应用的生命周期,runMainLifeCycle
是运行主应用生命周期,等待所有的方法执行完成,subApp
是获取的是子应用的内容,index.js
,代码如下:
import { findAppByRoute } from '../utils'
import { getMainLifecycle } from '../const/mainLifeCycle'
import { loadHtml } from '../loader'
export const lifecycle = async () => {
const prevApp = findAppByRoute(window.__ORIGIN_APP__);
const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__);
if (!nextApp) {
return
}
if (prevApp && prevApp.unmount) {
if (prevApp.proxy) {
prevApp.proxy.inactive();
}
await destoryed(prevApp)
}
const app = await beforeLoad(nextApp);
await mounted(app);
}
export const beforeLoad = async (app) => {
await runMainLifeCycle('beforeLoad');
app && app.beforeLoad && app.beforeLoad();
const subApp = await loadHtml(app);
subApp && subApp.beforeLoad && subApp.beforeLoad();
return subApp;
}
export const mounted = async (app) => {
app && app.mount && app.mount({
appInfo: app.appInfo,
entry: app.entry
})
await runMainLifeCycle('mounted');
}
export const destoryed = async (app) => {
app && app.unmount && app.unmount();
await runMainLifeCycle('destoryed');
}
export const runMainLifeCycle = async (type) => {
const mainlife = getMainLifecycle();
await Promise.all(mainlife[type].map(async item => await item()));
}
- 加载和解析
html
及 js
,如下所示:
- 在
micro
下的 loader
中 index.js
,loadHtml
是加载 html
的方法,container
是第一个子应用需要显示在哪里,#id
内容,entry
是子应用的入口。parseHtml
是解析 html
,在 fetchResource
后解析、加载、执行流程。通过 script.concat
处理 fetchedScripts
,将得到且携带标签里面的内容、外部链接引入的内容结合到一起,构成子应用所有 script
的集合,在其它方法中将这些集合运行起来,得到完整的子应用渲染,最后将 dom、allScript
直接缓存,后面可以直接使用 cache
,获取到对应的子应用。在 getResources
中,scriptUrl
是链接、src
和 href
,script
是写在 script
中的 js
脚本内容,dom
是需要展示到子应用上的 DOM
结构。deepParse
是深度解析,获取到所有 script
和 link
的链接、内容,先处理位于 script
中的内容,link
也会有 js
的内容,递归遍历进行处理,index.js
,代码如下:
import { fetchResource } from '../utils/fetchResource'
import { sandBox } from "../sandbox";
export const loadHtml = async (app) => {
let container = app.container;
let entry = app.entry;
const [ dom, scripts ] = await parseHtml(entry, app.name);
const ct = document.querySelector(container);
if (!ct) {
throw new Error('容器不存在,请查看');
}
ct.innerHTML = dom;
scripts.forEach(item => {
sandBox(app, item);
})
return app;
}
const cache = {};
export const parseHtml = async (entry, name) => {
if (cache[name]) {
return cache[name];
}
const html = await fetchResource(entry);
let allScript = [];
const div = document.createElement('div');
div.innerHTML = html;
const [dom, scriptUrl, script] = await getResources(div, entry);
const fetchedScripts = await Promise.all(scriptUrl.map(async item => fetchResource(item)));
allScript = script.concat(fetchedScripts);
cache[name] = [dom, allScript];
return [dom, allScript];
}
export const getResources = async (root, entry) => {
const scriptUrl = [];
const script = [];
const dom = root.outerHTML;
function deepParse(element) {
const children = element.children;
const parent = element.parent;
if (element.nodeName.toLowerCase() === 'script') {
const src = element.getAttribute('src');
if (!src) {
script.push(element.outerHTML);
} else {
if (src.startsWith('http')) {
scriptUrl.push(src);
} else {
scriptUrl.push(`http:${entry}/${src}`);
}
}
if (parent) {
parent.replaceChild(document.createComment('此 js 文件已经被微前端替换'), element);
}
}
if (element.nodeName.toLowerCase() === 'link') {
const href = element.getAttribute('href');
if (href.endsWith('.js')) {
if (href.startsWith('http')) {
scriptUrl.push(href);
} else {
scriptUrl.push(`http:${entry}/${href}`);
}
}
}
for (let i = 0; i < children.length; i++) {
deepParse(children[i]);
}
}
deepParse(root);
return [dom, scriptUrl, script];
}
- 在
micro
下的 utils
中 fetchResource.js
,处理加载资源请求,代码如下:
export const fetchResource = url => fetch(url).then(async res => await res.text());