感谢b站鹏周同学。
qiankun
简单介绍一下qiankun,qiankun框架是一套微前端的架构,理念来源于微服务,目标是将多个技术栈,多个应用,整合到一个项目中,方便在多个应用之间来回切换。
实现mini-qiankun
首先 ,我们做些准备工作,需要搭建几个应用。这里我用vue2构建了三个应用,由于本文章不是介绍如何将项目改造成qiankun,所以有关配置的方面,我就简单一笔带过了
- main 主应用 也叫作基座
- app1 子应用1
- app2 子应用2
main项目的 main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import { registerMicroApps, start } from "qiankun";
Vue.config.productionTip = false;
registerMicroApps([
{
name: "app1", // app name registered
entry: "//localhost:8081",
container: "#sub-app",
activeRule: "/subapp/app1",
},
{
name: "app2", // app name registered
entry: "//localhost:8082",
container: "#sub-app",
activeRule: "/subapp/app2",
},
]);
start();
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
子应用主要是修改 main.js、router.js 以及 vue.config.js
子应用的main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "./public-path";
Vue.config.productionTip = false;
let vm;
function render(props = {}) {
const { container } = props;
vm = new Vue({
router,
props,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {}
export async function mount(props) {
render(props);
}
export async function unmount() {
// 子系统卸载,取消所有的请求
vm.$destroy();
vm.$el.innerHTML = "";
vm = null;
}
子应用的router.js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"),
},
];
const router = new VueRouter({
mode: "history",
base: window.__POWERED_BY_QIANKUN__ ? `/subapp/app2` : process.env.BASE_URL,
routes,
});
export default router;
vue.config.js
const { name } = require("./package.json");
module.exports = {
devServer: {
port: 8082,
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
加入了一个 public-path.js
qiankun 主要暴露了这两个方法 registerMicroApps 和 start,分别是注册子应用,以及启动。我们新建一个libs文件夹,里面加入一个index.js作为qiankun的引入。
main 项目的 main.js
import { registerMicroApps, start } from "@/libs/index";
libs/index.js
import { reWriteRouter } from "./re-router.js";
let _apps = [];
export const getApps = () => _apps;
export const registerMicroApps = (apps) => {
_apps = apps;
};
export const start = () => {
// 1.监视路由变化
reWriteRouter();
};
在libs 我们主要暴露两个方法 和,同时创建了一个所有应用的变量,并且将registerMicroApps传入的参数赋值给_apps,start函数也很简单,调用了一个的方法。
接下来,我们开始实现 ,在这里,我们需要实现四个步骤:
- 监视路由变化
- 匹配子应用
- 加载子应用
- 渲染子应用
首先 我们实现第一个:监视路由变化,在这里,就不得不提路由的两种方式,一种是路由,另外一种是路由,前者比较容易,监听变化很简单,我们可以用这个hashchange方法,不管是前进还是后退,都能监听到。复杂性比较高的为后者,我们来详细说明一下:
history的变化分成两种 ,一种是 ,这些路由变化的时候会触发popstate方法,另外一种是pushState和replaceState这些是没有专门的方法去监听到的,所以这两种,我们需要重写这两个方法,具体代码为下:
export const reWriteRouter = function () {
window.addEventListener("popstate", () => {
console.log("popstate");
});
/**
* 重写pushstate
*/
const rowPushState = window.history.pushState;
window.history.pushState = function (...args) {
rowPushState.apply(window.history, args);
console.log("路由变化了", preRouter + "->" + nextRouter);
};
/**
* 重写replacestate
*/
const rowReplaceState = window.history.replaceState;
window.history.replaceState = function (...args) {
rowReplaceState.apply(window.history, args);
console.log("路由变化了", preRouter + "->" + nextRouter);
};
};
第一步我们已经完成,接下来我们进行第二部分:匹配子路由。由于已经监听到路由变化,我们稍微改造一下上面的方法,在上面加入一个handlerRouter方法,同时新建一个文件handle-router.js,在这里面来处理逻辑匹配子路由的逻辑。
import { handlerRouter } from "./handle-router";
export const reWriteRouter = function () {
window.addEventListener("popstate", () => {
console.log("popstate");
+ handlerRouter();
});
/**
* 重写pushstate
*/
const rowPushState = window.history.pushState;
window.history.pushState = function (...args) {
rowPushState.apply(window.history, args);
console.log("路由变化了", preRouter + "->" + nextRouter);
+ handlerRouter();
};
/**
* 重写replacestate
*/
const rowReplaceState = window.history.replaceState;
window.history.replaceState = function (...args) {
rowReplaceState.apply(window.history, args);
console.log("路由变化了", preRouter + "->" + nextRouter);
+ handlerRouter();
};
};
hand-router.js
/**
* 2.处理路由变化
*
* 获取当前的路径
*/
import { getApps } from "./index";
export const handlerRouter = async () => {
const apps = getApps();
/**
* 加载上一个路由
*/
console.log(window.location.pathname, apps);
const app = apps.find((item) => getNextRouter().startsWith(item.activeRule));
console.log(app);
if (!app) {
return;
}
};
我们获取了所有apps并且根据路由找到了当前的子应用app,接下来,我们处理第三个:加载子应用,我们在上面的后面加上一段逻辑:
/**
* 2.处理路由变化
*
* 获取当前的路径
*/
import { getApps } from "./index";
export const handlerRouter = async () => {
const apps = getApps();
/**
* 加载上一个路由
*/
console.log(window.location.pathname, apps);
const app = apps.find((item) => getNextRouter().startsWith(item.activeRule));
console.log(app);
if (!app) {
return;
}
// 3 获取子应用的html,js css
const html = await fetch(app.entry).then((res) => res.text());
console.log(html);
const container = document.querySelector(app.container);
container.innerHTML = html;
}
如此,我们就将代码渲染到了页面上,虽然现在页面上没有任何信息,但是我们可以打开终端看下,
子应用的代码确实加载进来了,但是页面是空白的,
为什么呢?
原因是浏览器出于安全考虑,并不会自动执行我们引入的js,所以 我们需要手动执行js。
这里就不得不提到一个库了——import-html-entry,qiankun使用的就是这个库,里面暴露很多方法,我们这里只实现获取外部脚本,以及执行脚本,这就是我们的第四步,渲染子应用。
新建一个 import-html.js
/**
*
* 实现import-html-entry
* https://github.com/kuitos/import-html-entry
* 这里只实现 getExternalScripts execScripts
* 从模版中获取script 以及执行js
*/
import { fetchResource } from "./fetch-resource";
export const importHtml = async (url) => {
console.log(url);
const html = await fetchResource(url);
const template = document.createElement("div");
template.innerHTML = html;
const scripts = template.querySelectorAll("script");
//获取所有的script
async function getExternalScripts() {
console.log(scripts);
return Promise.all(
Array.from(scripts).map((script) => {
const src = script.getAttribute("src");
if (!src) {
return Promise.resolve(script.innerHTML);
} else {
console.log(src, url);
return fetchResource(src.startsWith("http") ? src : `${url}${src}`);
}
})
);
}
//执行所有的script
async function execScripts() {
const scripts = await getExternalScripts();
const module = { exports: {} };
// no-unused-vars
const exports = module.exports;
scripts.forEach((res) => {
eval(res);
});
return module.exports;
}
return {
template,
getExternalScripts,
execScripts,
};
};
fetch-resource.js
export const fetchResource = async (url) =>
await fetch(url).then((res) => res.text());
详细说明一下:我们导出一个方法。这个方法接受一个url,我们获取这url上的内容,并且以文本输出他。这部分代码跟我们的第三步一致,后面又有两个方法以及,前者需要注意的点是需要判断有没有src属性,因为会有如下这种方式
<script>
console.log(123)
</script>
另外一种就是外链一个url,这个就需要再次请求资源了。后者为执行js,我们将上一步拿到的js放到这来执行。这里用了commonjs的规范来获取。
为什么?
因为子应用用的umd打包方式,我们可以举个例子来看下子应用umd打包出来的代码
(function webpackUniversalModuleDefinition(root, factory) {
//commonjs
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
//amd
else if(typeof define === 'function' && define.amd)
define([], factory);
//commonjs
else if(typeof exports === 'object')
exports["app2-app"] = factory();
//最后方式为挂载在window上
else
root["app2-app"] = factory();
})(window, function() {
return {a:1}
});
上面是我加的备注,你可以选择一种自己喜欢的方式引入执行js。
接下来我们需要改动 handle-router.js,因为刚才我们加载页面并没有显示,所以
/**
* 2.处理路由变化
*
* 获取当前的路径
*/
import { getApps } from "./index";
import { importHtml } from "./import-html.js";
export const handlerRouter = async () => {
const apps = getApps();
/**
* 加载上一个路由
*/
console.log(window.location.pathname, apps);
const app = apps.find((item) => getNextRouter().startsWith(item.activeRule));
console.log(app);
if (!app) {
return;
}
// 3 获取子应用的html,js css
// const html = await fetch(app.entry).then((res) => res.text());
// console.log(html);
// const container = document.querySelector(app.container);
// container.innerHTML = html;
const { template, execScripts } = await importHtml(app.entry);
const container = document.querySelector(app.container);
// console.log(template, getExternalScripts, execScripts);
container.appendChild(template);
window.__POWERED_BY_QIANKUN__ = true;
const appsScript = await execScripts();
app.bootstrap = appsScript.bootstrap;
app.mount = appsScript.mount;
app.unmount = appsScript.unmount;
await bootstrap(app);
await mount(app);
};
async function bootstrap(app) {
app.bootstrap && (await app.bootstrap());
}
async function mount(app) {
app.mount &&
(await app.mount({ container: document.querySelector(app.container) }));
}
async function unmount(app) {
app.unmount &&
(await app.unmount({ container: document.querySelector(app.container) }));
}
我们获取了子应用的代码,并且执行了,这时候,我们应该能拿到子应用在main.js注册的、、三个方法,我们在主应用调用这个方法。同时 我们设置一个一个变量 __POWERED_BY_QIANKUN__,由于微前端的理念就是子应用能单独访问,也能作为子应用供主应用调用,所以我们在window上定义这个变量,标志着当前运行环境。这样就能渲染出来了。
到此为止,你应该页面能切换了,现在会有两个问题, 一个是图片没有加载出来,另一个问题是之前的应用没有卸载掉。
我们再次修改上面的代码
import { getApps } from "./index";
import { importHtml } from "./import-html.js";
+ import { getNextRouter, getPreRouter } from "./re-router";
...
const apps = getApps();
/**
* 卸载上一个路由
*/
+ const preApp = apps.find((item) =>
+ getPreRouter().startsWith(item.activeRule)
+ );
console.log(preApp, ">>>>>>");
+ if (preApp) {
+ await unmount(preApp);
+ }
/**
* 加载上一个路由
*/
console.log(window.location.pathname, apps);
const app = apps.find((item) => getNextRouter().startsWith(item.activeRule));
console.log(app);
...
window.__POWERED_BY_QIANKUN__ = true;
+ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + "/";
const appsScript = await execScripts();
...
我们加了一段逻辑,获取了上一个 和下一个路由,在切换的时候卸载掉上一个,加载下一个,同时改造了re-router.js, 并且加入了一个__INJECTED_PUBLIC_PATH_BY_QIANKUN__变量,这个是webpack运行时的全局publicPath,对应着public-path.js,这个主要是在静态资源的前面加上地址的部分。这样就能解决图片的问题。
import { handlerRouter } from "./handle-router";
+ let preRouter = "";
+ let nextRouter = window.location.pathname;
+ export const getPreRouter = () => preRouter;
+ export const getNextRouter = () => nextRouter;
export const reWriteRouter = function () {
window.addEventListener("popstate", () => {
+ preRouter = nextRouter;
console.log("popstate");
+ nextRouter = window.location.pathname;
handlerRouter();
});
/**
* 重写pushstate
*/
const rowPushState = window.history.pushState;
window.history.pushState = function (...args) {
+ preRouter = window.location.pathname;
rowPushState.apply(window.history, args);
+ nextRouter = window.location.pathname;
console.log("路由变化了", preRouter + "->" + nextRouter);
handlerRouter();
};
/**
* 重写replacestate
*/
const rowReplaceState = window.history.replaceState;
window.history.replaceState = function (...args) {
+ preRouter = window.location.pathname;
rowReplaceState.apply(window.history, args);
nextRouter = window.location.pathname;
console.log("路由变化了", preRouter + "->" + nextRouter);
handlerRouter();
};
};
如此这般,便大功告成了!!!