微前端 之 icestark 源码阅读
fengshi123 opened this issue · comments
一、前言
在之前《微前端探索》文章中,我们分析了微前端框架 single-spa 和 qiankun 的一些源码、原理等,本文再对社区另外一个微前端框架 icestark 做一个简单整理分析,帮助各位想探索微前端的同学有个感性的认识。
二、 源码目录
icestark 源码的主要目录结构如下所示,主要包括 packages 和 src 两个目录,相关总结整理如下,我们不会一行一行去讲解代码,如果你对某一块很感兴趣,可以自己去 github 上面 clone 代码库,源码还是非常简单易懂的。
三、重要代码整理
1、子应用状态管理
我们学习 single-spa 的时候总结到 single-spa 是一个状态机,负责管理各个子应用的状态。所以 icestark 必然存在这个子应用的状态管理,相关的实现在 src/apps.ts 文件中,其包括以下几个主要过程/方法:
- registerMicroApp:注册子应用的方法,将子应用添加到全局 microApps 数组变量中;
- createMicroApp:创建子应用的方法,包括加载子应用的资源等;
- mountMicroApp:挂载子应用的方法,将子应用挂载到容器中;
- unmountMicroApp:卸载子应用的方法,将子应用从容器中卸载;
- unloadMicroApp:unload 子应用的方法,包括移除子应用的资源等;
- removeMicroApp:移除子应用的方法,将子应用从全局 microApps 数组中移除;
以上方法实现的思路还是比较简单的,定义个全局 microApps 数组变量来保存子应用,然后每个子应用分别有个对应的 status 变量来表示该子应用的状态是怎样的,然后进行下一步操作时会根据当前的状态进行对应的下一步操作,以下我们挑选个最“复杂”的方法 createMicroApp 来看下,我们对代码进行了相关注释,还是比较简单的,就不再赘述了。
export async function createMicroApp(app: string | AppConfig, appLifecyle?: AppLifecylceOptions) {
const appConfig = getAppConfigForLoad(app, appLifecyle);
const appName = appConfig && appConfig.name;
if (appConfig && appName) {
if (appConfig.status === NOT_LOADED || appConfig.status === LOAD_ERROR ) {
// 如果该子应用的当前状态是未加载或加载失败,执行以下逻辑
if (appConfig.title) document.title = appConfig.title;
// 更新子应用的状态
updateAppConfig(appName, { status: LOADING_ASSETS });
let lifeCycle: ModuleLifeCycle = {};
try {
// 加载子应用资源
lifeCycle = await loadAppModule(appConfig);
// in case of app status modified by unload event
if (getAppStatus(appName) === LOADING_ASSETS) {
// 更新子应用配置
updateAppConfig(appName, { ...lifeCycle, status: NOT_MOUNTED });
}
} catch (err){
// 出错,更新子应用配置
updateAppConfig(appName, { status: LOAD_ERROR });
}
if (lifeCycle.mount) {
// 进行子应用挂载
await mountMicroApp(appConfig.name);
}
} else if (appConfig.status === UNMOUNTED) {
// 如果当前的子应用是卸载状态执行以下逻辑
if (!appConfig.cached) {
// 加载 js/css 资源
await loadAndAppendCssAssets(appConfig.appAssets || { cssList: [], jsList: []});
}
// 进行挂载
await mountMicroApp(appConfig.name);
} else if (appConfig.status === NOT_MOUNTED) {
// 如果当前的子应用是没有挂载状态,则进行挂载
await mountMicroApp(appConfig.name);
} else {
console.info(`[icestark] current status of app ${appName} is ${appConfig.status}`);
}
// 返回创建的子应用的信息
return getAppConfig(appName);
} else {
console.error(`[icestark] fail to get app config of ${appName}`);
}
return null;
}
2、路由劫持
我们都知道 react、vue、angular 等单应用路由劫持的实现都是:history 路由通过监听 popstate 事件、hash 路由通过监听 hashchange 路由来实现的,那么 icestark 的路由劫持是怎么做的呢,嗯哼,他们也没有变出花来,也是一样的实现原理,代码位置在 src/start.js 文件中,相关源码如下所示
const hijackHistory = (): void => {
// 监听对应的路由事件,urlChange 为事件回调函数
window.addEventListener('popstate', urlChange, false);
window.addEventListener('hashchange', urlChange, false);
};
3、沙箱隔离
在微前端容器中,存在多个子应用共有一个 window 对象的情况,如果不进行隔离,可能多个子应用之间会存在互相影响的情况。icestark 基于 Proxy 为每个子应用启用了一个沙箱环境,代码位置在 packages/icestark-sandbox/src/index.js 文件中,相关代码实现如下所示
createProxySandbox(injection?: object) {
const { propertyAdded, originalValues, multiMode } = this;
const proxyWindow = Object.create(null) as Window;
const originalWindow = window;
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
const originalSetInerval = window.setInterval;
const originalSetTimeout = window.setTimeout;
// hijack addEventListener
proxyWindow.addEventListener = (eventName, fn, ...rest) => {
const listeners = this.eventListeners[eventName] || [];
listeners.push(fn);
return originalAddEventListener.apply(originalWindow, [eventName, fn, ...rest]);
};
// hijack removeEventListener
proxyWindow.removeEventListener = (eventName, fn, ...rest) => {
const listeners = this.eventListeners[eventName] || [];
if (listeners.includes(fn)) {
listeners.splice(listeners.indexOf(fn), 1);
}
return originalRemoveEventListener.apply(originalWindow, [eventName, fn, ...rest]);
};
// hijack setTimeout
proxyWindow.setTimeout = (...args) => {
const timerId = originalSetTimeout(...args);
this.timeoutIds.push(timerId);
return timerId;
};
// hijack setInterval
proxyWindow.setInterval = (...args) => {
const intervalId = originalSetInerval(...args);
this.intervalIds.push(intervalId);
return intervalId;
};
const sandbox = new Proxy(proxyWindow, {
set(target: Window, p: PropertyKey, value: any): boolean {
target[p] = value;
},
get(target: Window, p: PropertyKey): any {
const targetValue = target[p];
if (targetValue) {
// case of addEventListener, removeEventListener, setTimeout, setInterval setted in sandbox
return targetValue;
}
},
has(target: Window, p: PropertyKey): boolean {
return p in target || p in originalWindow;
},
});
this.sandbox = sandbox;
}
4、通信
icestark 提供了 event/store 通信,其无非是实现了个简单的 EventEmit 实例,代码位置在 packages/icestark-data/src/event.js 文件中,相关源代码实现逻辑如下,我们进行了些代码注释,就不再赘述。
class Event implements Hooks {
eventEmitter: object;
constructor() {
this.eventEmitter = {};
}
// 事件触发
emit(key: string, ...args) {
const keyEmitter = this.eventEmitter[key];
// 执行事件注册的回调方法
keyEmitter.forEach(cb => {
cb(...args);
});
}
// 事件监听
on(key: string, callback: (value: any) => void) {
if (!this.eventEmitter[key]) {
this.eventEmitter[key] = [];
}
// 将事件回调方法放入数组中
this.eventEmitter[key].push(callback);
}
// 取消注册
off(key: string, callback?: (value: any) => void) {
if (callback === undefined) {
this.eventEmitter[key] = undefined;
return;
}
this.eventEmitter[key] = this.eventEmitter[key].filter(cb => cb !== callback);
}
has(key: string) {
const keyEmitter = this.eventEmitter[key];
return isArray(keyEmitter) && keyEmitter.length > 0;
}
}
四、总结
经过以上代码整理分析,我们可以看到,其实 icestark 跟 single-spa、qiankun 这些微前端框架做的事情/原理大同小异,icestark 自己去实现了子应用的状态管理,然后也去实现了沙箱、通信等这些辅助功能。
辛苦整理良久,还望手动点赞鼓励~
博客 github地址为:github.com/fengshi123/blog ,汇总了作者的所有博客,欢迎关注及 star ~