qiankun框架简介
*qiankun 是一个基于
single-spa
框架实现的一个微前端框架,
single-spa
虽然实现了路由劫持和应用加载,但是没有实现样式隔离和js隔离,并不是一个完善的微前端框架;*
qiankun 在实现了路由劫持和应用加载的同时还实现了沙箱和import-html-entry
一、qiankun 特性
- 基于 single-spa 封装了更加开箱即用的api
- 技术栈无关
- html-entry 接入方式,让接入微应用如同使用iframe一样简单(资源加载机制)
- 样式隔离
- js沙箱
- 资源预加载:在浏览器空闲时间预加载未打开的微应用资源,加速为应用打开速度
- umi插件
二、qiankun 隔离方案
<一>、js 隔离
1. 主流浏览器(支持Proxy), 使用 基于 proxy 的多实例沙箱实现
!!#ff0000 实现原理是:通过Proxy劫持沙箱全局window对象, 劫持对全局对象属性的更改,来修改window对象的属性和方法,在卸载和加载应用时关闭/激活沙箱!!
class ProxySandbox {
constructor(name, context){
this.name = name;
this.context = context; // 共享执行上下文
this.proxy = null; // 代理对象
this.fakeWindow = Object.create({}); // sandbox 全局对象
// 记录沙箱的激活状态
this.sandboxRunning = true;
const proxy = new Proxy(fakeWindow, {
get(target, prop) {
// 如果共享对象中有该属性,则优先使用共享对象中的属性
if(Object.keys(this.context).inclueds(props)) {
return this.context[prop]
}
return target[prop]
},
set(target, prop, value) {
if(this.sandboxRunning) {
if(Object.keys(this.context).inclueds(prop)) {
// 更新共享对像中的属性
this.context[props] = value
}
// 更新sandbox全局对象中的属性
target[prop] = value
}
}
})
this.proxy = proxy
}
// 激活沙箱
activeSandbox() {
this.sandboxRunning = true;
}
// 关闭沙箱
inactiveSandbox() {
this.sandboxRunning = false;
}
}
2. 针对不支持Proxy 的浏览器,使用基于diff 的沙箱实现
!!#ff0000 实现的原理是: 在运行子应用时保存一个全局共享对象window的快照对象,将当前全局共享对象的所有属性方法全部复制到这个快照对象中;子应用卸载时将全局共享对象window对象和快照进行diff比对,将有差异的属性保存下载,等再次挂载子应用的时候再添加上这些属性!!
class DiffSandbox {
constructor(name) {
this.name = name;
this.modifyProps = {}; // 记录变更过的属性
this.windowSnapshot = {}; // 快照对象
}
// 激活沙箱
activeSandbox() {
// 先清空快照对象
this.windowSnapshot = {}
for(let key in window) {
// 拷贝全局对象
this.windowSnapshot[key] = window[key]
}
// 将变更属性添加到全局对象中
Object.keys(this.modifyProps).forEach(prop => {
window[prop] = this.modifyProps[prop]
})
}
// 关闭沙箱
inactiveSandbox() {
this.modifyProps = {}
for(const key in window) {
if(this.windowSnapshot[key] !=== window[key]) {
// 记录变更属性
this.modifyProps[key] = window[key]
// 还原全局共享对象
window[key] = this.windowSnapshot[key]
}
}
}
}
<二>、样式隔离
1. 支持Shadow Dom 的浏览器使用 Shadow Dom
2. 不支持Shadow Dom 的浏览器使用 Scope Css
!!#ff0000 原理是为当前激活子应用的样式添加上唯一的命名空间!!
三、Qiankun 通信方案
1. 使用
!!#38761d 主应用!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UnFRtrOF-1677465300443)(/download/attachments/2405664573/%E6%88%AA%E5%B1%8F2023-02-21%2020.24.40.png?version=1&modificationDate=1676982284622&api=v2)]
!!#38761d 微应用!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A84xtzKF-1677465300444)(/download/attachments/2405664573/%E6%88%AA%E5%B1%8F2023-02-21%2020.25.12.png?version=1&modificationDate=1676982318323&api=v2)]
2. 实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xmTAEsaP-1677465300445)(/download/attachments/2405664573/%E6%88%AA%E5%B1%8F2023-02-21%2019.54.16.png?version=1&modificationDate=1676980462197&api=v2)]
let globalState = {}; // 全局状态
let deps = {}; // 微应用状态变更依赖
function emitGlobal(state, preState) {
Object.keys(deps).forEach(id => {
if(deps[i] instance of Funtion){
deps[i](cloneDeep(state), cloneDeep(prevState))
}
})
}
function initGlobalState(state) {
if(state === globalState) {
consle.log('state is not changed!')
} else {
// 更新之前的全局依赖
const prevGlobalState = cloneDeep(globalState);
// 更新全局依赖
globalState = cloneDeep(state);
// 执行依赖更新
emitGlobal(globalState, prevGlobalState)
}
return getMicroAppStateActions(`global-${new Date()}`, true)
}
function getMicroAppStateActions(id, isMaster) {
return {
onGlobalStateChange(callback, fireImmediately) {
if(!deps[id]) {
// 依赖收集
deps[id] = callback
if(fireImmediately) {
// 立即执行更新依赖的操作
callback(cloneDeep(globalState), cloneDeep(globalState))
}
}
},
setGlobalState(state) {
const changedKeys = []; // 记录变更的依赖项
const prevGlobalState = cloneDeep(globalState); // 记录变更之前的依赖
// 更新全局状态
globalState = cloneDeep(
Object.keys(state).reduce((_globalState, changeKey) => {
if(isMater || _globalState.hasOwnProperty(changeKey)) {
changeKeys.push(changeKey)
}
return _globalState
}, globalState)
)
emitGlobal(globalState, prevGlobal)
return true;
},
offGlobalStateChange() {
delete deps[i]
return true;
}
}
}
四、 资源预加载
JS Entry
- single-spa在加载微应用的时候就用到的了js entry, 在加载微应用时我们加载的不是微应用本身,而是微应用导出的
js文件
。在微应用的入口文件中会导出一个包含微应用生命周期的对象 - 采用JS Entry 存在的一个问题就是,对微应用的改造入侵性太强,而且和主应用的耦合性太强
HTML Entry
- 通过http请求加载制定地址的首屏内容即html页面
- 然后解析这个html页面得到
template
、scripts
、entry
、styles
{
template, // 经过处理的脚本,link、script标签都被注释掉了
scripts, // 脚本的http地址或者"{async: true, src: xxx}"代码块
styles // 样式的http地址,
entry: // 入口脚本地址
}
- 通过http从远程加载stypes文件,将template中注释掉的
link
、script
标签替换为对应的style文件 - 最后向外暴露一个Promise对象
{
// template 是link替换为style之后的template
template: embedHTML,
// 静态资源文件
assetPublicPath,
// 获取外部脚本
getExternalScripts: () => getExternalScripts(scripts, fetch),
// 获取外部样式文件
getExternalStyleSheets: () => getExternalStylesSheets(styles, fetch),
// 脚本执行器, 让js代码在指定上下文中运行
execScripts: (proxy, strictGlobal) => {
if(!scripts.length){
return Promise.resolve()
}
return execScripts(entry, scripts, proxy, {fetch, strictGlobal})
}
}
1. 资源加载基本实现
// 调用 requestIdCallback方法在浏览器空闲时载入资源
function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
if (!navigator.onLine || isSlowNetwork) {
// Don't prefetch if in a slow network or offline
return;
}
requestIdleCallback(async () => {
// html-entry
const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});
}
2. 资源加载策略
- 监听 single-spa 提供的
single-spa:first-mount
方法,第一个应用加载之后,再加载其他应用的资源 - 传入一个微应用列表, 在第一个微应用资源加载之后再加载指定微应用的资源
- 主应用执行了start方法之后,直接开始加载所有微应用的资源
- 接收 一个自定义函数,自定义函数返回两个微应用组成的数组,一个是关键微应用列表是需要立即加载的微应用资源,第二个是一个普通的微应用列表在第一个微应用资源加载之后加载列表中的资源
/**
* 微应用预加载策略
* @param apps 微应用列表
* @param prefetchStrategy 预加载策略(一共有四种策略)
* 1. true - 第一个微应用挂载以后加载其他应用的静态资源,利用dingle-spa提供的first-mount事件来实现
* 2. string[] - 微应用名称列表,在第一个微应用挂载以后加载指定的微应用的静态资源
* 3. all - 主应用执行start以后直接开始 预加载所有微应用的静态资源
* 4. 自定义函数 - 返回两个微应用组成的数组,一个时关键微应用组成的数组,需要马上执行预加载的微应用,一个是普通的微应用组成的数组,在第一个微应用挂载以后预加载这些微应用的静态资源
* @param importEntryOpts
*/
export function doPrefetchStrategy(
apps: AppMetadata[],
prefetchStrategy: PrefetchStrategy,
importEntryOpts?: ImportEntryOpts,
) {
// 自定义函数,接收微应用名称组成的数组,然后从微应用列表中过滤出对应名称的微应用
const appsName2Apps = (names: string[]): AppMetadata[] => apps.filter((app) => names.includes(app.name));
if (Array.isArray(prefetchStrategy)) {
// 第二种预加载策略,在第一个微应用挂载之后加载其他指定的微饮用资源
prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
} else if (isFunction(prefetchStrategy)) {
// 自定义函数,第四种预加载策略
(async () => {
// critical rendering apps would be prefetch as earlier as possible
const { criticalAppNames = [], minorAppsName = [] } = await prefetchStrategy(apps);
// 需要马上执行预加载的微应用
prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts);
prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts);
})();
} else {
switch (prefetchStrategy) {
case true:
// 第一种资源预加载策略: 利用single-spa 的finst-mount时间加载其他静态资源
prefetchAfterFirstMounted(apps, importEntryOpts);
break;
case 'all':
// 第三种资源预加载策略:预加载所有微应用资源
prefetchImmediately(apps, importEntryOpts);
break;
default:
break;
}
}
}
五、源码解析
1. 源码结构
├── apis.ts // API相关方法
├── effects.ts
├── error.ts
├── errorHandler.ts
├── globalState.ts // 通信
├── index.ts
├── interfaces.ts
├── loader.ts // 加载应用核心方法
├── prefetch.ts // 预加载
├── sandbox // 沙箱
├── utils.ts
版权归原作者 波吉也有烦恼 所有, 如有侵权,请联系我们删除。