qiankun微前端运行原理:
- 监听路由变化(两种路由方式:hash/history)
- 匹配子应用
- 加载子应用
- 渲染子应用
手写核心源代码
index.js
import { handelRouter } from './handle-router';
import { rewriteRouter } from './rewrite-router';
let _apps = [];
// 用于获取注册的子应用信息
export const getApps = () => _apps;
// 注册子应用
export const registerMicroApps = (app) => {
_apps = app;
};
// 启动微前端
export const start = () => {
// 微前端运行原理:
// 1.监听路由变化(两种路由方式:hash/history,此项目用history方式)
// 2.匹配子应用
// 3.加载子应用
// 4.渲染子应用
rewriteRouter();
handelRouter();
};
- 监听路由变化(以history模式为例)
rewrite-router.js
import { handelRouter } from './handle-router';
// 自己维护一个路由的历史记录。浏览器出于安全考虑没有提供历史记录
let prevRoute = ''; //上一个路由
let nextRoute = window.location.pathname; //下一个路由
export const getPreveRoute = () => prevRoute;
export const getNextRoute = () => nextRoute;
// 采用history路由示例
// hisotry.go ,history.back, history.forward 使用popstate事件监听
// pushState/replaceState 需要重写方法,添加路由变化处理逻辑
export const rewriteRouter = () => {
// 不能通过window.popstate = function(){}去覆写,会覆盖以前的监听,而是通过如下方式,添加监听
window.addEventListener('popstate', () => {
// popstate触发的时候,路由已经完成导航了
// 利用nextRoute和prevRoute记录路由变化
prevRoute = nextRoute; //之前的
nextRoute = window.location.pathname; // 最新的
handelRouter();
});
// 先做备份
const rowPushState = window.history.pushState;
window.history.pushState = (...args) => {
prevRoute = window.location.pathname;
rowPushState.apply(window.history, args);
nextRoute = window.location.pathname;
handelRouter();
};
const rawReplaceState = window.history.replaceState;
window.history.replaceState = (...args) => {
prevRoute = window.location.pathname;
rawReplaceState.apply(window.history, args);
nextRoute = window.location.pathname;
handelRouter();
};
};
- 匹配子应用
handle-router
import { getApps } from '.';
import { importHTML } from './import-html';
import { getNextRoute, getPreveRoute } from './rewrite-router';
/**
* 处理路由变化
*/
export const handelRouter = async function () {
// 1.匹配子应用
const apps = getApps();
// 1.1获取当前的路由路由
const prevRoute = apps.find((ele) =>
getPreveRoute().startsWith(ele.activeRule)
);
if (prevRoute) {
await unmount(prevRoute);
}
// 1.2 获取当前路由对应的子应用
const app = apps.find((ele) => getNextRoute().startsWith(ele.activeRule));
if (!app) {
return;
}
// 2.加载子应用
const { template, execScripts } = await importHTML(app.entry);
const container = document.querySelector(app.container);
container.appendChild(template);
// 配置全局环境变量
window.__POWERED_BY_QIANKUN__ = true;
window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + '/';
// 执行子应用的代码,获取子应用导出的生命周期函数(bootstrap/mount/unmount)
// 子应用打包出来的格式是umd格式。
const appExport = await execScripts();
app.bootstrap = appExport.bootstrap;
app.mount = appExport.mount;
app.unmount = appExport.unmount;
await bootstrap(app);
await mount(app);
// const html = await fetch(app.entry).then((res) => {
// return res.text();
// });
// console.log(html);
// const container = document.querySelector(app.container);
// // 浏览器处于安全考虑,不会加载innerHTML中的js
// // 故手动加载js
// container.innerHTML = html;
// 3.渲染子应用
};
async function bootstrap(app) {
if (app.bootstrap) {
await app.bootstrap();
}
}
async function mount(app) {
if (!app.mount) return;
await app.mount({
container: document.querySelector(app.container),
});
}
async function unmount(app) {
if (!app.unmount) return;
await app.unmount();
}
- 加载并渲染子应用
import { fetchResource } from './fetch-resource';
// 注:qiankun采用的是 import-entry-html 库
export const importHTML = async (url) => {
const html = await fetchResource(url);
const template = document.createElement('div');
template.innerHTML = html;
const scripts = template.querySelectorAll('script');
// 获取所有script标签的代码:
// 返回的是文本组成的数组,形式:【代码,代码】
function getExternalScripts() {
return Promise.all(
Array.from(scripts).map((script) => {
const src = script.getAttribute('src');
// 内联脚本,不需要src
if (!src) {
return Promise.resolve(script.innerHTML);
} else {
// 外部脚本,需要src
// 分情况是因为src可能没有域名,只有pathname ,如<script src="/js/app.js"></script>
return fetchResource(src.startsWith('http') ? src : `${url}/${src}`);
}
})
);
}
// 获取并执行所有的script脚本代码
// 用eval执行script中的代码
async function execScripts() {
const scripts = await getExternalScripts();
// 手动构造一个commonjs的环境,以获取子应用导出的接口
const module = { exports: {} };
const exports = module.exports;
scripts.forEach((script) => {
eval(script);
});
// module.exports 就是子应用暴露出来的接口
// 因为子应用打包出来的umd格式的内容,导出了生命周期函数:bootstrap/mount/unmount
// 注:子应用构建后的umd代码大致形式如下:
/*
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['sub-app2'] = {}));
})(window, (function () {
// 内部的代码
function bootstrap() { ... }
function mount() { ... }
function unmount() { ... }
exports.bootstrap = bootstrap;
exports.mount = mount;
exports.unmount = unmount;
}));
*/
return module.exports;
}
return {
template,
getExternalScripts,
execScripts,
};
};
fetch-resource.js
export const fetchResource = (url) => fetch(url).then((res) => res.text());