fengshi123/blog

微前端 之 icestark 源码阅读

fengshi123 opened this issue · 0 comments

一、前言

在之前《微前端探索》文章中,我们分析了微前端框架 single-spa 和 qiankun 的一些源码、原理等,本文再对社区另外一个微前端框架 icestark 做一个简单整理分析,帮助各位想探索微前端的同学有个感性的认识。

二、 源码目录

icestark 源码的主要目录结构如下所示,主要包括 packages 和 src 两个目录,相关总结整理如下,我们不会一行一行去讲解代码,如果你对某一块很感兴趣,可以自己去 github 上面 clone 代码库,源码还是非常简单易懂的。
1

三、重要代码整理

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 ~