single-spa 原理

核心 API

registerApplication

  1. single-spa 注册应用
  2. 执行 load 加载生命周期
  3. 仅执行应用的加载阶段

start

  1. 执行 load 加载阶段、boostrap 启动阶段、mount 挂载阶段
  2. 加载至挂载整套流程

single-spa 流程和生命周期图

single-spa 流程图

reroute 方法

getAppChanges

根据 app 状态和启动规则找到待卸载、待加载、待挂载的 apps

export function getAppChanges() {
  const appsToUnload = [],
    appsToUnmount = [],
    appsToLoad = [],
    appsToMount = [];

  // We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
  const currentTime = new Date().getTime();

  apps.forEach((app) => {
    const appShouldBeActive =
      app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);

    switch (app.status) {
      case LOAD_ERROR:
        if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
          appsToLoad.push(app);
        }
        break;
      case NOT_LOADED:
      case LOADING_SOURCE_CODE:
        if (appShouldBeActive) {
          appsToLoad.push(app);
        }
        break;
      case NOT_BOOTSTRAPPED:
      case NOT_MOUNTED:
        if (!appShouldBeActive && getAppUnloadInfo(toName(app))) {
          appsToUnload.push(app);
        } else if (appShouldBeActive) {
          appsToMount.push(app);
        }
        break;
      case MOUNTED:
        if (!appShouldBeActive) {
          appsToUnmount.push(app);
        }
        break;
      // all other statuses are ignored
    }
  });

  return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}

loadApps 仅加载阶段

执行 registerApplication 时的流程
started 标识为 false
1.仅加载应用
2.加载完成后执行 callCapturedEventListeners

const loadPromises = appsToLoad.map(toLoadPromise);
Promise.all(loadPromise).then(() => { // ... })

toLoadPromise
1 状态 LOADING_SOURCE_CODE -> NOT_BOOTSTRAPPED
2 执行 app.loadApp 方法
3 为 app 增加 bootstrap、mount、unmount、unload 方法

function toLoadPromise() {
    return Promise.resolve().then(() => {
        app.status = LOADING_SOURCE_CODE

        return (app.loadPromise = Promise.resolve()
            .then(() => {
                const loadPromise = app.loadApp(getProps(app));
                return loadPromise.then((val) => {
                  app.status = NOT_BOOTSTRAPPED;
                  app.bootstrap = flattenFnArray(appOpts, "bootstrap");
                  app.mount = flattenFnArray(appOpts, "mount");
                  app.unmount = flattenFnArray(appOpts, "unmount");
                  app.unload = flattenFnArray(appOpts, "unload");
                  
                  delete app.loadPromise;
                  return app
                })
            })
    })
}

flattenFnArray
返回 Promise.resolve() 链, 通过 promise.then 逐个执行应用的生命周期函数(可为数组)

export function flattenFnArray(appOrParcel, lifecycle) {
  let fns = app[life] || []
  fns = Array.isArray(fns) ? fns : [fns]
  if (fns.length === 0) fns = [() => Promise.resolve()]

  return (props) => fns.reduce((promise, fn) => promise.then(() => fn(props)), Promise.resolve())
}

performAppChanges 加载、启动、挂载阶段

执行 start 时的流程
started 标识为 true
1.先卸载应用
2.加载应用
3.挂载应用
4.卸载应用完执行 callCapturedEventListeners

performAppChanges

// ..
      // appsToLoad 需加载的应用,先加载再启动、挂载
      const loadThenMountPromises = appsToLoad.map((app) => {
        return toLoadPromise(app).then((app) =>
          tryToBootstrapAndMount(app, unmountAllPromise)
        );
      });
      // appsToMount 需启动挂载的 app
      const mountPromises = appsToMount
        .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
        .map((appToMount) => {
          return tryToBootstrapAndMount(appToMount, unmountAllPromise);
        });
      return unmountAllPromise 
        .catch((err) => {
          callAllEventListeners();
          throw err;
        })
        .then(() => { // 卸载完毕 执行 callAllEventListeners,无需等待启动、挂载
          callAllEventListeners();
          return Promise.all(loadThenMountPromises.concat(mountPromises))
            .catch((err) => {
              pendingPromises.forEach((promise) => promise.reject(err));
              throw err;
            })
            .then(finishUpAndReturn);
        });

tryToBootstrapAndMount
在应用加载期间如果出现了某种延迟直接切换了另一个路由,那么之前路由的就不该被挂载。
所以进行了两次 shouldBeActive 判断

function tryToBootstrapAndMount(app, unmountAllPromise) {
  if (shouldBeActive(app)) { // 判断 app 是否需激活
    return toBootstrapPromise(app).then((app) => // 执行启动方法
      unmountAllPromise.then(() => // 卸载完后再次判断是否激活,执行挂载函数
        shouldBeActive(app) ? toMountPromise(app) : app
      )
    );
  } else {
    return unmountAllPromise.then(() => app);
  }
}

navigation-events

1.捕获 hashchange、popstate 事件
2.为 history API: pushState、replaceState 打补丁

执行时机

应用启动时待 vue-router 等必须监听的事件执行后

目的

1.浏览器地址改变时先执行 rerouter 进行子应用切换 2.保存导航事件的监听函数,待子应用切换完毕后执行,保证执行顺序正确

捕获路由导航事件

const routingEventsListeningTo = ["hashchange", "popstate"]; // 需捕获的事件

function urlReroute() { // 传入 url 参数执行 reroute 
    reroute([], arguments); 
}

window.addEventListener('hashchange', urlReroute);
window.addEventListener('popstate', urlReroute);
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function(eventName, fn) {
    // capturedEventListeners 中没有 fn
    if (routingEventsListeningTo.indexOf(eventName) >= 0 && !capturedEventListeners[eventName].some(listener => listener == fn)) {
        capturedEventListeners[eventName].push(fn); // 存入 capturedEventListeners 中待 rerouter 中执行 
        return;
    }
    return originalAddEventListener.apply(this, arguments)
}
window.removeEventListener = function(eventName, fn) {
    if (routingEventsListeningTo.indexOf(eventName) >= 0) {
        capturedEventListeners[eventName] = capturedEventListeners[eventName].filter(l => l !== fn);
        return;
    }
    return originalRemoveEventListener.apply(this, arguments)
}

history API 打补丁

当 pushState、replaceState history API 调用时是不会触发 popstate 事件的,需要进行一层处理

需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)
popstate_event

简单实现

function patchedUpdateState(updateState,methodName){
    return function(){
        const urlBefore = window.location.href;
        updateState.apply(this,arguments); // 执行 pushState | replaceState
        const urlAfter = window.location.href;

        if(urlBefore !== urlAfter){ // 触发 popstate 事件
            urlReroute(new PopStateEvent('popstate'));
        }
    }
}


window.history.pushState = patchedUpdateState(window.history.pushState,'pushState');
window.history.replaceState = patchedUpdateState(window.history.replaceState,'replaceState');

执行 listeners

等到 rerouter 中执行

export function callCapturedEventListeners(eventArguments) {
  if (eventArguments) {
    const eventType = eventArguments[0].type;
    if (routingEventsListeningTo.indexOf(eventType) >= 0) {
      capturedEventListeners[eventType].forEach((listener) => {
        try {
          listener.apply(this, eventArguments);
        } catch (e) { // 应用的事件错误不应该中断 single-spa 的执行。
          setTimeout(() => {
            throw e;
          });
        }
      });
    }
  }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容