什么是qiankun
qiankun是一种微前端框架,可以将多个前端应用集成为一个整体。每个子应用可以使用不同的框架和技术栈,它们之间可以相互独立开发和部署,也可以共享一些公共资源和状态。
qiankun提供了一套完整的生命周期函数和通信机制,可以让不同的子应用之间进行跨框架和跨域的通信和交互。它还提供了一些工具和插件,可以帮助开发者更好地管理和调试整个微前端系统。
使用qiankun可以使前端开发更加模块化、高效和可维护,同时提供更好的用户体验和性能。
乾坤的使用
1.安装依赖
$ yarn add qiankun # 或者 npm i qiankun -S
这里只需要在主工程安装即可
2.注册和使用
在主工程的main.js中将你的子工程注册进来
js
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'app-one', // 子工程名称
entry: '//localhost:30090', // 子工程的地址
container: '#yourContainer', // 子工程需要渲染在哪一个DOM节点下
activeRule: '/yourActiveRule', // 如果路由匹配会激活子应用
},
{
name: 'app-two',
entry: '//localhost:30070',
container: '#yourContainer',
activeRule: '/yourActiveRule2',
},
]);
start();
3.子应用的设置
在微应用中的main.js中配置如下
js
if (window.__POWERED_BY_QIANKUN__) {
window.__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 子应用挂载前的钩子
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
// 子应用挂载时的钩子
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
// 子应用销毁时的钩子
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
在vue.config.js也要进行配置
这里配置的主要原因方便于在主工程解析子工程的时候处理
js
const { name } = require('./package');
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
},
},
};
4.项目结构和实现效果
这里主要应用和子应用都是用都使用的是vue3
,乾坤不限制你项目使用的框架,如果你上面的配置都成功了,那你就可以把所有项目都启动起来,效果如下:
4.1主工程
4.2子工程1
4.3子工程2
你会发现只要你主工程的路由发生改变就会渲染出你想要的子工程。 我这里只是介绍了一下最简单的实现了一下,如果想实现React,Angular等可以去官网文档中查看
5.小技巧
上面我们都是一个项目一个项目的启动起来,在开发过程中非常的反人类,所以我们可以想个办法让程序来帮我们启动
js
const childProcess = require('child_process')
const path = require('path')
// 配置每个应用的路径
const filePath = {
'app-one': path.join(__dirname, '../apps/app-one'),
'app-two': path.join(__dirname, '../apps/app-two'),
main: path.join(__dirname, '../qiankun-base')
}
// cd 子应用的目录 npm start 启动项目
function runChild () {
Object.values(filePath).forEach(item => {
childProcess.spawn(`cd ${item} && npm run serve`, { stdio: "inherit", shell: true })
})
}
runChild()
然后我们修改一下项目的启动命令
json
{
"name": "qiankun",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"serve": "node ./script/run.js"
},
"author": "",
"license": "ISC"
}
以后我们只要npm run serve就会一下把我们的项目全部启动啦
手写一个简单版的qiankun
接下来就是我们的重头戏啦!我们先简单的归纳一下要实现的话分为哪几个步骤
- 手写我们需要知道路由发送了变化且能拿到变化结果,类似数据劫持
- 我们要根据变化的路由匹配上我们的应用
- 加载我们对应的子应用
- 最后就事把他渲染在正确的位置上
看上去是不是很简单那我们就来实现一下
1.监听路由的变化
首先将原来引入乾坤qiankun的依赖,改成我们自己的,我们也实现它这两个方法
js
import {onMounted} from 'vue';
import AppSelect from '@/components/app-select/app-select.vue';
import { registerMicroApps, start } from './my-qiankun/index.ts';
// import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'app-one', // app name registered
entry: '//localhost:30090',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'app-two',
entry: '//localhost:30070',
container: '#yourContainer',
activeRule: '/yourActiveRule2',
},
]);
onMounted(() => {
window.localStorage.setItem('zhd', '123456')
start()
})
index.ts
ts
import { handleRouter } from "./handleRouter";
import { rewriteRouter } from "./rewrite-router";
// 需要定义一个子应用参数的类型
type code = {
name: string, // app name registered
entry: string,
container: string,
activeRule: string,
}
// 用来保存注册进来的子应用
let _apps: code[] = []
// 给外界使用
function getApps(): code[] {
return _apps
}
// 注册子应用的方法--就是在main.js的
function registerMicroApps(codes: code[]): void {
_apps = codes
}
function start(): void {
/* eslint-disable */
// @ts-ignore
window['__POWERED_BY_QIANKUN__'] = true
// 获取路由的变化
rewriteRouter() //路由的监听函数
handleRouter() // 获取子应用后面会说到
}
export {
registerMicroApps,
start,
getApps
}
到这一步就已经给外界了两个方法,一个注册一个启动,然后我们就要来实现一下rewriteRouter
这个方法。
rewriteRouter.ts
ts
import { handleRouter } from "./handleRouter"
let prevRouter = '' // 前一步的路由
let nextRouter = window.location.pathname // 当前的路由
export function getPrevRouter(): string {
return prevRouter
}
export function getNextRouter(): string {
return nextRouter
}
// 获取路由的变化
export function rewriteRouter(): void {
// 1如果是hash模式下直接调用window.onHashChange
// 2如果是history模式下
// 2.1 处理history.forward history.go history.back
window.addEventListener('popstate', () => {
prevRouter = nextRouter
nextRouter = window.location.pathname
handleRouter()
})
// 2.2 处理pushState和replaceState
const rawPushState = window.history.pushState
window.history.pushState = (...arg) => {
prevRouter = window.location.pathname
rawPushState.apply(window.history, arg)
nextRouter = window.location.pathname
handleRouter()
}
const rawReplaceState = window.history.replaceState
window.history.replaceState = (...arg) => {
prevRouter = window.location.pathname
rawReplaceState.apply(window.history, arg)
nextRouter = window.location.pathname
handleRouter()
}
}
这里可以根据路由类型分为hash和history两种模式,hash模式只要通过onHashChange来就可以知道路由的变化,这里就不在实现了,主要来实现一下history模式下的路由变化
第一种就会前进后退history.forward history.go history.back可以通过onpopstate来监听路由变化 但是这样会有一个问题可以会覆盖别人已经添加的事件,我们可以通过addEventListener来追加一个,
第二种就是pushState和replaceState一个事添加一个,一个事将但前的替换成想要的,这两个没有对应的事件,那我们只能重写这两个方法,先将原来的保存下来,然后给他匿名函数这样就然后在函数内部点用他原来的方法就达到我们的要求啦
2.匹配对应的路由
handleRouter.ts
js
import { getApps } from ".";
import { getNextRouter, getPrevRouter } from "./rewrite-router";
export function handleRouter():void {
// 匹配对应的子应用
const apps = getApps() // 获取注册的子应用
const app = apps.find(item => getNextRouter() == item.activeRule) // 匹配和当前对应的
if(!app) return
console.log(app);
}
这里的代码比较简单,但是会发现如果是第一进入子应用,就不会调用这个方法,所以第一次我们自己需要在state这个函数里调用一下handleRouter()
3.加载子应用和渲染
handleRouter.ts
js
import { getApps } from ".";
import { getNextRouter, getPrevRouter } from "./rewrite-router";
export function handleRouter():void {
// 匹配对应的子应用
const apps = getApps() // 获取注册的子应用
const app = apps.find(item => getNextRouter() == item.activeRule) // 匹配和当前对应的
if(!app) return
console.log(app);
fetch(app.entry).then(res => {
const container = document.querySelector(app.container)
container.innerHTML(res.text())
})
}
到这里会发现我们确实将html渲染上去了为什么不显示呢,这里我们要知道innerHTML里是不会帮我们执行script标签里的内容的,这是处于浏览器的安全去考虑,我们可以使用import-HTML-entry这个库,但是在这里我们就来模拟一下这个库的实现
handleRouter.ts
ts
import { getApps } from ".";
import { importApp, mount, unmount } from "./importApp";
import { getNextRouter, getPrevRouter } from "./rewrite-router";
export function handleRouter():void {
// 匹配对应的子应用
const apps = getApps()
const app = apps.find(item => getNextRouter() == item.activeRule)
if(!app) return
console.log(app);
// 加载子应用
importApp(app.entry).then(async res => {
const container = document.querySelector(app.container)
if (!container) return
if(app && getPrevRouter() && getPrevRouter() !== getNextRouter()) {
unmount({container: container})
}
container?.appendChild(res.template)
await res.execScript()
await mount({container: container})
})
// 渲染子应用
}
接下来我们要实现importApp这方法和拿到子应用抛出来的钩子函数
这里我们需要知道一点前置的知识就是我们通过运行umd个数的js文件,他会把一下抛出的方法,变量抛出到全局在浏览器环境下的可以在window下拿到
但是这里有一个弊端就是就是我们不知道后面的应用名称会叫什么,所以不好使,所以我们可以模仿他这里的第一个条件,我们只要有一个exports和module他们都会对象类型这样就可以吧这个指赋个module.exports里
ts
const module = { exports: {} }
let exports:qiankun = module.exports
scriptStr.forEach((element: string) => {
eval(element) // umd脚步
});
exports = module.exports
console.log(exports);
是不是也可以实现,接下来就是保存调用即可代码如下
importApp.ts
ts
import { Request } from "./request";
type entry = {
template: HTMLElement,
getExternalScript: () => Promise<any>,
execScript: () => void
}
// 保存钩子的类型
type qiankun = {
bootstrap?: () => void,
mount?: (params: {container: Element}) => void,
unmount?: (params: {container: Element}) => void
}
// 保存子应用的钩子
const quankunHook: qiankun = {}
export function getQuankunHook (): qiankun{
return quankunHook
}
// 运行钩子的函数
export async function bootstrap() {
quankunHook.bootstrap && (await quankunHook.bootstrap())
}
// 运行钩子的函数
export async function mount(params: {container: Element}) {
quankunHook.mount && (await quankunHook.mount(params))
}
// 运行钩子的函数
export async function unmount(params: {container: Element}) {
quankunHook.unmount && (await quankunHook.unmount(params) )
}
// 模仿import-html-entry包文本文件中的html
export async function importApp(url: string): Promise<entry> {
const appText = await Request(url) // 获取子应用
const template = document.createElement('div') // 创建一个DOM
template.innerHTML = appText // 将子应用添加到创建的DOM中方便后续查找script元素
const script = template.querySelectorAll('script') // 查找script
// 获取脚步内容
function getExternalScript(): Promise<any> {
return Promise.all(Array.from(script).map(item => {
const src = item.getAttribute('src') // 处理两种类型的script
if (src) {
return Request(src.startsWith('http://')? src: `${url}${src}`)
} else {
return Promise.resolve(item.innerText)
}
}))
}
// 执行脚步
async function execScript() {
const scriptStr = await getExternalScript()
// 这一步是未来拿到钩子
const module = { exports: {} }
let exports:qiankun = module.exports
// 通过遍历拿到脚步文件,并运行
scriptStr.forEach((element: string) => {
eval(element)
});
exports = module.exports
// 保存
quankunHook.bootstrap = exports.bootstrap
quankunHook.mount = exports.mount
quankunHook.unmount = exports.unmount
await bootstrap()
}
return {
template,
getExternalScript,
execScript
}
}
request.ts
ts
export function Request(url: string) {
return fetch(url).then(res => res.text())
}
4.项目结构和实现效果
最后你点击切换发现也可以实现类型qiankun的效果了