微前端(qiankun)解决方案

手写 qiankun 微前端框架

手写 qiankun 微前端框架_哔哩哔哩_bilibili

why微前端?

为什么要使用微前端架构?

多人协同开发,解决巨石应用。

把庞大应用分块处理,划分为多个子应用,子应用技术栈无关,独立部署。多个子应用聚合在一起,看起来就是一个完整的应用,用户无感知。

在开发层面来说,不限制技术栈,可以多团队采用不同的技术栈开发,也适合进行渐进式重构升级。

微前端原理

核心逻辑

规则匹配 ⇒ 获取资源 ⇒ 渲染到容器

1
2
3
4
5
6
7
8
9
10
11
// 注册子应用,微前端运行原理和 SPA单页应用非常相似,SPA注册路由与注册子应用概念类似。
registerMicroApps([
// 当匹配到 activeRule 的时候,请求获取 entry 资源,渲染到 container 中
{
name: 'app-react',
entry: '//localhost:9001' // 子应用的 HTML 入口
container: '#subapp-container', // 渲染到哪里
activeRule: '/subapp/app-react' // 路由匹配规则
}
]
)

主子应用出口

主子应用有不同的接入口

主子应用有不同的接入口

子应用如何接入主应用?

  1. 导出三个必要的生命周期钩子函数

    1. bootstrap 渲染之前,做一些初始化
    2. mount 渲染函数, 一般挂载到 子应用入口 => #app 节点上
    3. unmount 卸载函数,清理工作;

      注意:生命周期函数必须返回 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
console.log('[vue] props from main framework', props);
storeTest(props);
render(props); // new vue 实例。
}

export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null; //清空实例,垃圾回收
router = null;
}
  1. 子应用也可以独立运行

    通过qiankun提供的全局变量判断,此时挂载到的节点是 body ⇒ #app

    Untitled

  2. 子应用的配置

    1. 子应用应允许跨域,允许 CORS 跨域

      1
      2
      3
      4
      5
      6
      // vue.config.js
      devServer: {
      headers: {
      'Access-Control-Allow-Origin': '*',
      },
      },
2. 子应用必须打包出一个库文件,文件格式必须是umd

    主应用需要加载子应用资源,子应用js要在主应用运行,必须要得兼容模块格式为umd

    <aside>
    💡 [*UMD](https://link.zhihu.com/?target=https%3A//github.com/umdjs/umd)  — 另外一个模块化系统,建议作为通用的模块化系统,它与 AMD 和 CommonJS 都是兼容的。*

    </aside>

    
1
2
3
4
5
6
7
8
configureWebpack: {
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`, // jsonp的方式加载子应用资源
},
},
![**umd.js 兼容CommonJs、ADM模块规范,如果都不匹配,挂载到window全局对象上**](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/9b75797a-173f-46c1-b247-dc678c488932/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220624%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220624T064536Z&X-Amz-Expires=86400&X-Amz-Signature=8695db39a224f753eab5f1e657bc93851c9761d1c3532c8e0df909f0e826af76&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Untitled.png%22&x-id=GetObject) **umd.js 兼容CommonJs、ADM模块规范,如果都不匹配,挂载到window全局对象上** ![**通过window对象可以获取到子应用的生命周期方法;**](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/314da44f-fbe7-465f-8f99-2abff8ecb44a/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220624%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220624T064517Z&X-Amz-Expires=86400&X-Amz-Signature=a577f36c99bde81f446626783cb713979af5710ae0357753f2a33401dd77e411&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Untitled.png%22&x-id=GetObject) **通过window对象可以获取到子应用的生命周期方法;**

手写qiankun源码

https://github.com/lyctea/qiankun-demo

css沙箱

  • showdom
  • 样式范围前缀

javascript 沙箱

微前端部署

微前端通信原理

了解微前端基本基础运作机制及实现原理

1、注册子应用

2、监听路由变化

3、匹配子应用

4、加载子应用

5、渲染子应用

实现基本的子应用渲染 待完成 css沙箱。js沙箱,数据监听

一、注册子路由

1、注册子应用的入口路由,容器、及匹配路由来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface app {
name:string,
entry:string,
container: string,
activeRule:string
}
export const getApp = ()=>_app
// 注册子应用 保存在缓存里方便调用
let _app: app[]=[]
export const registerMicroApps = (app: app[]) => {
_app = app
}
export const start = () => {
//重写监听路由
rewriteRouter();
// 通过路由变化 匹配子路加载及渲染
handleRouter();
};

二、监听路由变化

1、hash路由:window.onhashchage

2、history路由: history.go history.back history.forward 使用window.onpopstate,

pushState, replaceState 使用函数重写来进行,监听劫持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { handleRouter } from './handle-router';
//上一个路由地址
let prevRouter = '';
//下一个路由地址
let nextRouter = window.location.pathname;
export const getPrevRouter = () => prevRouter;
export const getNextRouter = () => nextRouter;
export const rewriteRouter = () => {
//通过addEventListener追加监听,不能直接重写,防止其他地方调用
window.addEventListener?.('popstate', () => {
prevRouter = nextRouter;
nextRouter = window.location.pathname;
handleRouter();
//跳转完成后执行 所以无法通过window.location.pathname拿到上一个路由
});
// 通过重写监听pushState, replaceState
const rwaPushState = window.history.replaceState;
window.history.pushState = (args) => {
//before
prevRouter = window.location.pathname;
rwaPushState.apply(window.history, args);
nextRouter = window.location.pathname;
//after
handleRouter();
};
const rwaReplaceState = window.history.replaceState;
window.history.replaceState = (...args) => {
//before
prevRouter = window.location.pathname;
rwaReplaceState.apply(window.history, args);
//after
nextRouter = window.location.pathname;
handleRouter();
};
};

三、匹配子应用

1、获取当前路由路径 window.location.pathname,获取注册应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { getApp } from '.';
import { getPrevRouter, getNextRouter } from './rewrite-router';
interface app {
name: string;
entry: string;
container: string;
activeRule: string;
bootstrap?: Function;
mount?: Function;
unmount?: Function;
}
export const handleRouter = async () => {
const apps = getApp();
//卸载上一个应用
const prevApp = apps.find((item) => getPrevRouter().startsWith(item.activeRule));
if (prevApp) {
await unmount(prevApp);
}
const app = apps.find((item) => getNextRouter().startsWith(item.activeRule));
if (!app) {
return;
}
const container = document.querySelector(app.container);
//浏览器处于安全考虑 innerHtml 中的script标签不会加载
//if (container) container.innerHTML = html;
const { template, execScript } = await htmlEntry(app.entry);
container?.append(template);
//配置全局变量 让应用正确渲染到指定的容器中
(window as any).__POWERED_BY_QIANKUN__ = true;
(window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + '/';
//执行子应用中的js脚本 通过模拟的commonJs环境 获取对象
const appExports: any = await execScript();
app.bootstrap = appExports.bootstrap;
app.mount = appExports.mount;
app.unmount = appExports.unmount;
//执行子应用生命周期
await bootstrap(app);
await mount(app);
};
async function bootstrap(app: app) {
app.bootstrap && (await app.bootstrap());
}
async function mount(app: app) {
app.mount &&
(await app.mount({
container: document.querySelector(app.container),
}));
}
async function unmount(app: app) {
app.unmount && (await app.unmount());
}

四 、加载子应用资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const htmlEntry = async (url) => {
//手动执行获取的script标签
//通过请求获取应用的 html css js
const html: string = await fetch(url).then((res) => res.text());
const template = document.createElement('div');
template.innerHTML = html;
//获取所有的script标签
const scripts = template.querySelectorAll('script');
function getExternalScript() {
return Promise.all(
Array.from(scripts).map((script) => {
let src = script.getAttribute('src');
if (!src) {
//是否是内联
return Promise.resolve(script.innerHTML);
} else {
//是否是本地资源 如果是本店则加上应用域名
src = src.startsWith('http') ? src : `${url}${src}`;
return fetch(src).then((res) => res.text());
}
}),
);
}
async function execScript() {
const scripts = await getExternalScript();
//手动构造commonjs 环境
const module = { exports: {} };
const exports = module.exports;
//eval执行代码可以访问上下文
scripts.forEach((code) => {
eval(code);
});
//不够灵活
//return window['vue']
return module.exports;
}
return {
template,
getExternalScript,
execScript,
};
};

五 、渲染子应用

1、子应用打包模式为umd,在执行完后将结果挂载到window上

Untitled

2、更灵活的获取子应用对象 手动构造一个common js环境让子应用挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
async function execScript() {
const scripts = await getExternalScript();
//手动构造commonjs 环境
const module = { exports: {} };
const exports = module.exports;
//eval执行代码可以访问上下文
scripts.forEach((code) => {
eval(code);
});
//不够灵活
//return window['vue']
return module.exports;
}