简单记录学习single-spa的过程(2)

1,215 阅读12分钟

接上回

详细记录学习single-spa的过程(1)我们一起学习了single-spa的一个简单的注册中心的项目结构以及,如何通过System.js 跟 single-spa 进行注册,应用挂载。这一次我们呢来讲讲single-spa中一个普通的应用项目是怎么样的~

官方案列

这里我们还是要回到上次的github列表: github列表

这一次我们选择下载的是rate-dogs这个项目,项目下载下来以后整体的文件结构如下:

rate-dogs文件结构

在我仔细的观察整个项目以后,发现几处与我们平常的vue-cli初始化出来的项目有些不同

main.js

import "./set-public-path";
import Vue from "vue";
import singleSpaVue from "single-spa-vue";

import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;

// 没有直接通过vue对象初始化 而是使用singleSpaVue将Vue对象以及其初始化option包裹起来
const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: h => h(App),
    router
  }
});

export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

set-public-path.js

import { setPublicPath } from "systemjs-webpack-interop";

// 目前不知道设置这个地址有什么含义
setPublicPath("@vue-mf/rate-dogs");

router/index.js

import Vue from "vue";
import VueRouter from "vue-router";
import RateDogs from "../components/rate-dogs.vue";

Vue.use(VueRouter);

const routes = [{ path: "/rate-doggos", component: RateDogs }];

const router = new VueRouter({
  // 普通使用的是hash模式 
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

export default router;

基于这几个区别,我们又来到了官方文档看对于这几个内容的讲解,除了singleSpaVue(...)函数的入参,大致提及了一下,其他内容基本没有说明。于是我全局阅读了一下文档内容,文档写的比较多的是一下几个点:

  • single-spa团队对于微前端的理解(当然这可能是一个公认理解,小编不了解罢了)
  • 对于root, application, parcel这三个模块的简单介绍 以及 出入参数介绍
  • 一些构建应用的简单推荐配置 或者 工具使用
  • 关于主流框架(react,vue,angular等)在single-spa应用的Api介绍

读完了这一份文档以后呢,笔者产生了这么主要有几个疑问(可能是本人较菜,没能看出来):

  • Parcels的概念有点模糊,不够清晰
  • 对于不同框架如何渲染的,没有具体说明
  • props的实现,路由的实现(好奇)

准备

那么没有办法,只能直接从代码层面去下手看看作者到底是怎么做的,我们首先要做的事情是吧我们现在已知的东西给列举出来。

首先现根据目前看到现在为止知道整体的关系上会分成三种不同的模块:

  • root-config(注册中心)
  • application(微应用)
  • parcel(包裹) // 目前暂不知道包裹是做什么使用

Api能力: 我们通过官方的Api得知微应用会具有通过路由进行渲染,以及一些跳转控制函数,props这一类的能力,以及对外暴露的Api。

应用拆分: 有三种不同的应用拆分方式,目前笔者文章DEMO中更多的应该是提到的动态加载模块 的方式

开始

拉取

// 拉取源码
git clone https://github.com/single-spa/single-spa.git
// 进入文件
cd single-spa

文件结构

如下:

├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs
├── examples
│   └── README.md
├── jest-browser.config.js
├── jest-node.config.js
├── node-spec
│   └── nodejs.spec.js
├── package.json
├── rollup.config.js(rollup打包配置)
├── spec
├── src
│   ├── applications(应用相关)
│   │   ├── app-errors.js
│   │   ├── app.helpers.js
│   │   ├── apps.js
│   │   └── timeouts.js
│   ├── devtools
│   │   └── devtools.js
│   ├── jquery-support.js
│   ├── lifecycles(生命周期相关)
│   │   ├── bootstrap.js
│   │   ├── lifecycle.helpers.js
│   │   ├── load.js
│   │   ├── mount.js
│   │   ├── prop.helpers.js
│   │   ├── unload.js
│   │   ├── unmount.js
│   │   └── update.js
│   ├── navigation(导航相关)
│   │   ├── navigation-events.js
│   │   └── reroute.js
│   ├── parcels(parcel相关)
│   │   └── mount-parcel.js
│   ├── single-spa.js(入口文件)
│   ├── start.js
│   └── utils(工具文件)
│       ├── assign.js
│       ├── find.js
│       └── runtime-environment.js
├── typings
│   ├── single-spa.d.ts
│   └── single-spa.test-d.ts
└── yarn.lock

首先通过rollup.config.js来确认打包的入口文件:

... // 其他代码
{
 // 入口文件
 input: "./src/single-spa.js",
 ... 其他配置
}

./src/single-spa.js下面主要是一些api的暴露

export { start } from "./start.js";
export { ensureJQuerySupport } from "./jquery-support.js";
export {
  setBootstrapMaxTime,
  setMountMaxTime,
  setUnmountMaxTime,
  setUnloadMaxTime,
} from "./applications/timeouts.js";
export {
  registerApplication,
  getMountedApps,
  getAppStatus,
  unloadApplication,
  checkActivityFunctions,
  getAppNames,
  pathToActiveWhen,
} from "./applications/apps.js";
export { navigateToUrl } from "./navigation/navigation-events.js";
export { triggerAppChange } from "./navigation/reroute.js";
export {
  addErrorHandler,
  removeErrorHandler,
} from "./applications/app-errors.js";
export { mountRootParcel } from "./parcels/mount-parcel.js";

export {
  NOT_LOADED,
  LOADING_SOURCE_CODE,
  NOT_BOOTSTRAPPED,
  BOOTSTRAPPING,
  NOT_MOUNTED,
  MOUNTING,
  UPDATING,
  LOAD_ERROR,
  MOUNTED,
  UNMOUNTING,
  SKIP_BECAUSE_BROKEN,
} from "./applications/app.helpers.js";

import devtools from "./devtools/devtools";
import { isInBrowser } from "./utils/runtime-environment.js";

if (isInBrowser && window.__SINGLE_SPA_DEVTOOLS__) {
  window.__SINGLE_SPA_DEVTOOLS__.exposedMethods = devtools;
}

我们挂载到注册中心,渲染到根节点用到的api主要是start(...)registerApplication(...)那么我们就这两个API看下去吧。根据调用顺序我们先看registerApplication(...)函数这边。

registerApplication

/src/application/apps.js

...
// 应用列表
const apps = [];
...
export function registerApplication(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
  /*
  *  参数校验 以及 格式化入参数
  */ 
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );
  ... 某些代码
  apps.push(
    // Object.assign 的垫片方法
    assign(
      {
        loadErrorTime: null,
        status: NOT_LOADED,
        parcels: {},
        devtools: {
          overlays: {
            options: {},
            selectors: [],
          },
        },
      },
      registration
    )
  );
  // 浏览器环境下的话
  if (isInBrowser) {
    ensureJQuerySupport();
    // 刷新路由的函数
    reroute();
  }
}

这里要注意的点可能就是两个地方:

  • 一个是apps这个应用列表的数组, 后续的状态查询,应用名称查询,状态修改,移除注册,移除挂载,都是基于这个apps的数组操作
  • reroute(...)这个函数可能跟页面渲染有关,跟着看进去
// 这里为了大家的阅读方便我合并了几个js文件的内容

// 全部的应用状态
export const NOT_LOADED = "NOT_LOADED";
export const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE";
export const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED";
export const BOOTSTRAPPING = "BOOTSTRAPPING";
export const NOT_MOUNTED = "NOT_MOUNTED";
export const MOUNTING = "MOUNTING";
export const MOUNTED = "MOUNTED";
export const UPDATING = "UPDATING";
export const UNMOUNTING = "UNMOUNTING";
export const UNLOADING = "UNLOADING";
export const LOAD_ERROR = "LOAD_ERROR";
export const SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN";

  
// 这里会遍历刚提到的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 (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 };
}
// 渲染 与 重渲染
export function reroute(pendingPromises = [], eventArguments) {
 ... // 队列等待

  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
  let appsThatChanged;

  if (isStarted()) {
	...
  } else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }
  // 拉取所有应用的模块
  function loadApps() {
    return Promise.resolve().then(() => {
      const loadPromises = appsToLoad.map(toLoadPromise);
      return (
        Promise.all(loadPromises)
          .then(callAllEventListeners)
          // there are no mounted apps, before start() is called, so we always return []
          .then(() => [])
          .catch((err) => {
            callAllEventListeners();
            throw err;
          })
      );
    });
  }
  ... 
}
// 拉取对应的应用模块,并对四个生命周期进行初始化
export function toLoadPromise(app) {
  return Promise.resolve().then(() => {
    if (app.loadPromise) {
      return app.loadPromise;
    }

	// 判断是否为已经在模块
    if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
      return app;
    }

    app.status = LOADING_SOURCE_CODE;

    let appOpts, isUserErr;

    return (app.loadPromise = Promise.resolve()
      .then(() => {
        const loadPromise = app.loadApp(getProps(app));
		... // 错误判断
        return loadPromise.then((val) => {
          appOpts = val;

          ... // 错误判断
          
		  ... // 调试判断

		  // appOpts
          app.status = NOT_BOOTSTRAPPED;
          // 这里做了封装,去针对四个生命周期的promise递归调用,其感觉有点像是tapable中的AsyncSeriesHook的feel
          app.bootstrap = flattenFnArray(appOpts, "bootstrap");
          app.mount = flattenFnArray(appOpts, "mount");
          app.unmount = flattenFnArray(appOpts, "unmount");
          app.unload = flattenFnArray(appOpts, "unload");
          app.timeouts = ensureValidAppTimeouts(appOpts.timeouts);

          delete app.loadPromise;

          return app;
        });
      })
      .catch((err) => {
		... // 错误处理
      }));
  });
}


这里reroute会通过getAppChanges(...)方法返回的数组appsToLoad通过loadApps(...)进行遍历加载模块,并进行四个生命周期的格式化,至此registerApplication(...)方法的调用链就结束了。总结起来,整个调用链路实际上就是格式化了registerApplication(...)入参数,以及进行模块拉取,并且对于整个模块进行了格式化就结束了。

目前我们知道这里的应用还没有进行挂载,但是对应的模块可能已经拉取到本地。

start

另一边我们完成registerApplication的调用链阅读后,我们看到start(...)方法 /src/start.js

import { reroute } from "./navigation/reroute.js";
import { formatErrorMessage } from "./applications/app-errors.js";
import { setUrlRerouteOnly } from "./navigation/navigation-events.js";
import { isInBrowser } from "./utils/runtime-environment.js";

let started = false;

// 开始挂载节点
export function start(opts) {
  started = true;
  if (opts && opts.urlRerouteOnly) {
    setUrlRerouteOnly(opts.urlRerouteOnly);
  }
  if (isInBrowser) {
    reroute();
  }
}

// 返回状态
export function isStarted() {
  return started;
}

// 注册的超时检查
if (isInBrowser) {
  setTimeout(() => {
    if (!started) {
		... // 警告
    }
  }, 5000);
}

这里我们看到实际上还是调用了reroute(...)唯一的区别只是设置started = true, 我们再回来看到reroute(...)这一边

// 这里为了大家的阅读方便我合并了几个js文件的内容

// 是否在正在挂载中
let appChangeUnderway = false,
  peopleWaitingOnAppChange = [];
  
export function reroute(pendingPromises = [], eventArguments) {
  // 如果正在进行变化就挂载起来本次的一个请求队列,并在完成后执行 
  if (appChangeUnderway) {
    return new Promise((resolve, reject) => {
      peopleWaitingOnAppChange.push({
        resolve,
        reject,
        eventArguments,
      });
    });
  }

  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
  let appsThatChanged;

  if (isStarted()) {
    appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  } else {
	...// 拉取模块
  }

  
  function performAppChanges() {
    return Promise.resolve().then(() => {
      // https://github.com/single-spa/single-spa/issues/545
      // 派发事件before-no-app-change或before-app-change
      window.dispatchEvent(
        new CustomEvent(
          appsThatChanged.length === 0
            ? "single-spa:before-no-app-change"
            : "single-spa:before-app-change",
          getCustomEventDetail(true)
        )
      );
      // 派发事件before-routing-event
      window.dispatchEvent(
        new CustomEvent(
          "single-spa:before-routing-event",
          getCustomEventDetail(true)
        )
      );
      // 移除
      // 检查是否有应用注册了unload生命周期(如果有就调用unload函数) 
      const unloadPromises = appsToUnload.map(toUnloadPromise);
	  // 卸载 卸载万以后再次检查有没有要移除的应用
      const unmountUnloadPromises = appsToUnmount
        .map(toUnmountPromise)
        .map((unmountPromise) => unmountPromise.then(toUnloadPromise));

      const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
      
      const unmountAllPromise = Promise.all(allUnmountPromises);
	  // 执行刚刚合并的两个异步
      unmountAllPromise.then(() => {
        // 派发事件before-mount-routing-event
        window.dispatchEvent(
          new CustomEvent(
            "single-spa:before-mount-routing-event",
            getCustomEventDetail(true)
          )
        );
      });

      /* We load and bootstrap apps while other apps are unmounting, but we
       * wait to mount the app until all apps are finishing unmounting
       */
      const loadThenMountPromises = appsToLoad.map((app) => {
        // 这个同registerApplication中的toLoadPromise
        return toLoadPromise(app).then((app) =>
          tryToBootstrapAndMount(app, unmountAllPromise)
        );
      });

      /* These are the apps that are already bootstrapped and just need
       * to be mounted. They each wait for all unmounting apps to finish up
       * before they mount.
       */
      const mountPromises = appsToMount
        .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
        .map((appToMount) => {
          return tryToBootstrapAndMount(appToMount, unmountAllPromise);
        });
      return unmountAllPromise
        .catch((err) => {
          callAllEventListeners();
          throw err;
        })
        .then(() => {
          /* Now that the apps that needed to be unmounted are unmounted, their DOM navigation
           * events (like hashchange or popstate) should have been cleaned up. So it's safe
           * to let the remaining captured event listeners to handle about the DOM event.
           */
          callAllEventListeners();

          return Promise.all(loadThenMountPromises.concat(mountPromises))
            .catch((err) => {
              pendingPromises.forEach((promise) => promise.reject(err));
              throw err;
            })
            .then(finishUpAndReturn);
        });
    });
  }
  // 完成挂载后的回调函数
  function finishUpAndReturn() {
    const returnValue = getMountedApps();
    pendingPromises.forEach((promise) => promise.resolve(returnValue));
    ... // 事件触发
    appChangeUnderway = false;
	// 检查事件队列中是否还有等待的事件等待触发
    if (peopleWaitingOnAppChange.length > 0) {
      const nextPendingPromises = peopleWaitingOnAppChange;
      peopleWaitingOnAppChange = [];
      reroute(nextPendingPromises);
    }

    return returnValue;
  }

  function callAllEventListeners() {
 	... // 先不用关注
  }

  function getCustomEventDetail(isBeforeChanges = false) {
	... // 先不用关注
  }
}

// 处理每一个应用的卸载
export function toUnmountPromise(appOrParcel, hardFail) {
  return Promise.resolve().then(() => {
    if (appOrParcel.status !== MOUNTED) {
      return appOrParcel;
    }
    appOrParcel.status = UNMOUNTING;

	// 统一处理在每一个app中的parcels
    const unmountChildrenParcels = Object.keys(
      appOrParcel.parcels
    ).map((parcelId) => appOrParcel.parcels[parcelId].unmountThisParcel());

    let parcelError;

    return Promise.all(unmountChildrenParcels)
      .then(unmountAppOrParcel, (parcelError) => {
        // There is a parcel unmount error
		... // 卸载parcels相关的一些错误处理
      })
      .then(() => appOrParcel);

    function unmountAppOrParcel() {
      // We always try to unmount the appOrParcel, even if the children parcels failed to unmount.
      return reasonableTime(appOrParcel, "unmount")
        .then(() => {
          // The appOrParcel needs to stay in a broken status if its children parcels fail to unmount
          if (!parcelError) {
            appOrParcel.status = NOT_MOUNTED;
          }
        })
        .catch((err) => {
			... // 错误处理
        });
    }
  });
}
// 执行application或者pracel中对应的生命周期的方法
export function reasonableTime(appOrParcel, lifecycle) {
  ... // 错误参数初始化
  return new Promise((resolve, reject) => {
    let finished = false;
    let errored = false;

    appOrParcel[lifecycle](getProps(appOrParcel))
      .then((val) => {
        finished = true;
        resolve(val);
      })
      .catch((val) => {
        finished = true;
        reject(val);
      });

    ... // 错误处理 以及 超时处理
}

// 去初始化app以及挂载节点
function tryToBootstrapAndMount(app, unmountAllPromise) {
  // 校验appWhen
  if (shouldBeActive(app)) {
    return toBootstrapPromise(app).then((app) =>
      unmountAllPromise.then(() =>
        shouldBeActive(app) ? toMountPromise(app) : app
      )
    );
  } else {
    return unmountAllPromise.then(() => app);
  }
}

// 初始化app节点
export function toBootstrapPromise(appOrParcel, hardFail) {
  return Promise.resolve().then(() => {
    // 是否已经拉取 为  等待初始化状态
    if (appOrParcel.status !== NOT_BOOTSTRAPPED) {
      return appOrParcel;
    }

    appOrParcel.status = BOOTSTRAPPING;

    return reasonableTime(appOrParcel, "bootstrap")
      .then(() => {
        appOrParcel.status = NOT_MOUNTED;
        return appOrParcel;
      })
      .catch((err) => {
		... // 异常处理
      });
  });
}

// 挂载
export function toMountPromise(appOrParcel, hardFail) {
  return Promise.resolve().then(() => {
    // 判断是否初始化完成为可挂载状态
    if (appOrParcel.status !== NOT_MOUNTED) {
      return appOrParcel;
    }
    ...
    return reasonableTime(appOrParcel, "mount")
      .then(() => {
        appOrParcel.status = MOUNTED;
		... // 事件派发
        return appOrParcel;
      })
      .catch((err) => {
		... // 异常处理
      });
  });
}


这一块代码虽然比较长,但是还是相对清晰的,基本上就是针对不同状态的应用 分别进行了 移除,卸载,拉取,初始化,挂载等操作,之前我们的对于 对于不同框架如何渲染的,没有具体说明? 这个问题实际上已经可以在上方代码看出点端倪了。在toMountPromise(...)实际上还是通过调用appOrParcel自身所挂载的mount方法进行渲染的,那么其实我们现在再看到rate-dogs项目下的main.js的代码。

import "./set-public-path";
import Vue from "vue";
import singleSpaVue from "single-spa-vue";

import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;

// 没有直接通过vue对象初始化 而是使用singleSpaVue将Vue对象以及其初始化option包裹起来
const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: h => h(App),
    router
  }
});
// toBootstrapPromise函数调用的bootstrap方法
export const bootstrap = vueLifecycles.bootstrap;
// toMountPromise函数调用的mount方法
export const mount = vueLifecycles.mount;
// toUnmountPromise函数调用的unmount方法
export const unmount = vueLifecycles.unmount;

那么我们初始化,挂载,卸载的关键点,就是在这个singleSpaVue(...)函数包裹后所返回的函数中,那么我们一起康康这里面到底发生了什么?

singleSpaVue

拉取

// 拉取源码
git clone https://github.com/single-spa/single-spa-vue.git
// 进入文件
cd single-spa-vue

文件结构

.
├── LICENSE
├── README.md
├── package.json
├── rollup.config.js           // rollup 打包配置
├── src
│   ├── single-spa-vue.js      // 主文件
│   └── single-spa-vue.test.js // 单元测试
├── types
│   └── single-spa-vue.d.ts
└── yarn.lock

分析

那么文件结构比较简单,我们直接看到single-spa-vue.js文件即可

single-spa-vue.js

// 转译的垫片库,应该是安全相关的原因
import "css.escape";

const defaultOpts = {
  // required opts
  Vue: null,
  appOptions: null,
  template: null
};

export default function singleSpaVue(userOpts) {
  if (typeof userOpts !== "object") {
    throw new Error(`single-spa-vue requires a configuration object`);
  }

  const opts = {
    ...defaultOpts,
    ...userOpts
  };

  ... // 错误判断
  
  // Just a shared object to store the mounted object state
  // key - name of single-spa app, since it is unique
  let mountedInstances = {};
  // 返回对应的生命周期
  return {
    bootstrap: bootstrap.bind(null, opts, mountedInstances),
    mount: mount.bind(null, opts, mountedInstances),
    unmount: unmount.bind(null, opts, mountedInstances),
    update: update.bind(null, opts, mountedInstances)
  };
}

function bootstrap(opts) {
  // 这里与官网Ecosystem/Vue 文档中的配置项产生呼应
  if (opts.loadRootComponent) {
    return opts.loadRootComponent().then(root => (opts.rootComponent = root));
  } else {
    return Promise.resolve();
  }
}
// 这里的opts, mountedInstances相当于是函数柯里化进来的,props是在single-spa调用的时候传进来的
function mount(opts, mountedInstances, props) {
  const instance = {};
  return Promise.resolve().then(() => {
    const appOptions = { ...opts.appOptions };
    // 渲染节点判断
    if (props.domElement && !appOptions.el) {
      appOptions.el = props.domElement;
    }

    let domEl;
    // 下面这块主要就是对于元素渲染DOM的获取或者创建
    if (appOptions.el) {
      if (typeof appOptions.el === "string") {
        domEl = document.querySelector(appOptions.el);
        if (!domEl) {
		 	... // 错误处理
        }
      } else {
        domEl = appOptions.el;
        if (!domEl.id) {
          domEl.id = `single-spa-application:${props.name}`;
        }
        appOptions.el = `#${CSS.escape(domEl.id)}`;
      }
    } else {
      const htmlId = `single-spa-application:${props.name}`;
      appOptions.el = `#${CSS.escape(htmlId)}`;
      domEl = document.getElementById(htmlId);
      if (!domEl) {
        domEl = document.createElement("div");
        domEl.id = htmlId;
        document.body.appendChild(domEl);
      }
    }

    appOptions.el = appOptions.el + " .single-spa-container";

    // single-spa-vue@>=2 always REPLACES the `el` instead of appending to it.
    // We want domEl to stick around and not be replaced. So we tell Vue to mount
    // into a container div inside of the main domEl
    
    if (!domEl.querySelector(".single-spa-container")) {
      const singleSpaContainer = document.createElement("div");
      singleSpaContainer.className = "single-spa-container";
      domEl.appendChild(singleSpaContainer);
    }

    instance.domEl = domEl;

    if (!appOptions.render && !appOptions.template && opts.rootComponent) {
      appOptions.render = h => h(opts.rootComponent);
    }

    if (!appOptions.data) {
      appOptions.data = {};
    }

    appOptions.data = { ...appOptions.data, ...props };
	// 创建并挂载Vue对象
    instance.vueInstance = new opts.Vue(appOptions);
    // 绑定作用域
    if (instance.vueInstance.bind) {
      instance.vueInstance = instance.vueInstance.bind(instance.vueInstance);
    }

	// 把创建的实例化对象给共享出去
    mountedInstances[props.name] = instance;

    return instance.vueInstance;
  });
}

function update(opts, mountedInstances, props) {
  return Promise.resolve().then(() => {
    const instance = mountedInstances[props.name];
    const data = {
      ...(opts.appOptions.data || {}),
      ...props
    };
    for (let prop in data) {
      instance.vueInstance[prop] = data[prop];
    }
  });
}

// 卸载 销毁实例,清除引用
function unmount(opts, mountedInstances, props) {
  return Promise.resolve().then(() => {
    const instance = mountedInstances[props.name];
    instance.vueInstance.$destroy();
    instance.vueInstance.$el.innerHTML = "";
    delete instance.vueInstance;

    if (instance.domEl) {
      instance.domEl.innerHTML = "";
      delete instance.domEl;
    }
  });
}

这一段代码结束其实我们的渲染相关的问题,基本上就已经明了了,实际上single-spa本身并不关心,Application是如何在页面上,初始化,挂载,更新,卸载,更多的是对每一个应用的状态进行一个把控,可以看成一个控制中心,只会发出指令,但具体执行还是落地到每一个接受命令者去完成,最终命令接受者返回自身的状态即可。那么除去Vue以外相信别singleSpa*** 也是类似的实现方式。笔者为了验证这个点,又看了几个对应的仓库,基本验证了笔者的想法。感兴趣的同学可以去康康代码,都是相对简单。

拓展

那么这样做有什么好处呢?我认为可能其中最大的好处就是拥有了一个较高的灵活度,目前前端领域的框架发展是相当迅速的,一个框架可能都还没有学习搞清楚搞明白就有可能过时了,这么做其实很大程度上避免了这个问题,并且在一些挂载 跟 卸载的节点做更多自定义的操作。比如去拓展一个single-spa-ext(Ext.js)插件或者在节点挂载的时候给定一个命名空间,让样式仅在此范围内生效等。这也都是笔者的YY

剩下的问题

  • Parcels的概念有点模糊,不够清晰?
  • props的实现,路由的实现(好奇)?

由于本期的篇幅已经有点小长,所以打算继续往推到简单记录学习single-spa的过程(3)中 请大家敬请期待。

以上内容纯属笔者个人看法,如有不同意见欢迎讨论,如有雷同纯属巧~