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

2,829 阅读5分钟

接上回

我们目前还剩下两个问题:

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

Parcels

官网文档

我们先来看看官方文档是这么叙述Parcels:

Parcels是single-spa的一个高级特性。在对single-spa的注册相关api有更多了解之前,请尽量避免使用该特性。一个single-spa 的 parcel,指的是一个与框架无关的组件,由一系列功能构成,可以被应用手动挂载,无需担心由哪种框架实现。Parcels 和注册应用的api一致,不同之处在于parcel组件需要手动挂载,而不是通过activity方法被激活。

一个parcel可以大到一个应用,也可以小至一个组件,可以用任何语言实现,只要能导出正确的生命周期事件即可。在 single-spa 应用中,你的SPA可能会包括很多个注册应用,也可以包含很多parcel。通常情况下我们建议你在挂载parcel时传入应用的上下文,因为parcel可能会和应用一起卸载。

注意点:

  • 框架无关
  • 手动挂载
  • 正确导出生命周期
  • 可以挂载在根节点 或者 应用节点

开始分析

翻阅文档后 parcel进行注册相关的方法有两个,分别是mountRootParcel(...)mountParcel(...)。根据刚刚的注意点中的第四条可以区分出mountRootParcel(...)是将Parcel注册在根应用(注册中心)。而mountParcel(...)应该是用来注册在应用节点中~,接下来我们就根据这两个方法看进去吧。

mountRootParcel(...)方法参数如下:

  • parcelConfig
// parcel 的实现
const parcelConfig = {
  bootstrap() {
    // 初始化
    return Promise.resolve()
  },
  mount() {
    // 使用某个框架来创建和初始化dom
    return Promise.resolve()
  },
  unmount() {
    // 使用某个框架卸载dom,做其他的清理工作
    return Promise.resolve()
  }
}

parcelConfig 要求我们必须实现生命周期函数(bootstrapmountunmount)和可选生命周期函数(update),这个地方三个必须实现的生命周期函数有点雷同Application。我们在看一下另一个参数parcelProps(...)

  • parcelProps
// parcelProps
const parcelProps = {
	domElement, // 渲染的DOM元素
    ... // custProps
}

parcelProps从文档中看由两部分组成,一部分是渲染的DOM元素,另一部分是props的相关数据。

猜想

其实我们之前也提到了,single-spa实际上只是一个状态跟指挥中心,至于怎么实现内部过程的都是通过,应用本身本身的一个接口去做实现的。那么Parcels有没有可能就是官方给我们提供的一个应用内应用的模块呢?

验证

首先我们来看mountRootParcel(...)里面是怎么实现的。

/parcels/mount-parcel.js

import {
  validLifecycleFn,
  flattenFnArray,
} from "../lifecycles/lifecycle.helpers.js";
import {
  NOT_BOOTSTRAPPED,
  NOT_MOUNTED,
  MOUNTED,
  LOADING_SOURCE_CODE,
  SKIP_BECAUSE_BROKEN,
  toName,
} from "../applications/app.helpers.js";
import { toBootstrapPromise } from "../lifecycles/bootstrap.js";
import { toMountPromise } from "../lifecycles/mount.js";
import { toUpdatePromise } from "../lifecycles/update.js";
import { toUnmountPromise } from "../lifecycles/unmount.js";
import { ensureValidAppTimeouts } from "../applications/timeouts.js";
import { formatErrorMessage } from "../applications/app-errors.js";

let parcelCount = 0;
const rootParcels = { parcels: {} };

// This is a public api, exported to users of single-spa
// 
export function mountRootParcel() {
  return mountParcel.apply(rootParcels, arguments);
}

export function mountParcel(config, customProps) {
  const owningAppOrParcel = this;

  // Validate inputs
  if (!config || (typeof config !== "object" && typeof config !== "function")) {
		... // 错误处理
  }

  if (config.name && typeof config.name !== "string") {
		... // 错误处理
  }

  if (typeof customProps !== "object") {
		... // 错误处理
  }

  if (!customProps.domElement) {
		... // 错误处理
  }

  const id = parcelCount++;

  const passedConfigLoadingFunction = typeof config === "function";
  const configLoadingFunction = passedConfigLoadingFunction
    ? config
    : () => Promise.resolve(config);

  // Internal representation
  const parcel = {
    id,
    parcels: {},
    status: passedConfigLoadingFunction
      ? LOADING_SOURCE_CODE
      : NOT_BOOTSTRAPPED,
    customProps,
    // 获取当前挂载的应用名称,如果为根节点 那应该是空
    parentName: toName(owningAppOrParcel),
    // 卸载的封装
    unmountThisParcel() {
      if (parcel.status !== MOUNTED) {
		... // 错误处理
      }
	  // toUnmountPromise 同之前卸载 Application 的卸载 都是通过 reasonableTime 去调用对应的生命周期方法
      return toUnmountPromise(parcel, true)
        .then((value) => {
          if (parcel.parentName) {
            delete owningAppOrParcel.parcels[parcel.id];
          }

          return value;
        })
        .then((value) => {
          resolveUnmount(value);
          return value;
        })
        .catch((err) => {
          parcel.status = SKIP_BECAUSE_BROKEN;
          rejectUnmount(err);
          throw err;
        });
    },
  };

  // We return an external representation
  let externalRepresentation;

  // Add to owning app or parcel
  owningAppOrParcel.parcels[id] = parcel;
  // 其实在这里就已经发起了整个的链式调用 后续的 load -》 bootstarp -》mount
  let loadPromise = configLoadingFunction();

  if (!loadPromise || typeof loadPromise.then !== "function") {
		... // 错误处理
  }

  loadPromise = loadPromise.then((config) => {
    if (!config) {
      throw Error(
		... // 错误处理
    }

    const name = config.name || `parcel-${id}`;

    if (!validLifecycleFn(config.bootstrap)) {
		... // 错误处理
    }

    if (!validLifecycleFn(config.mount)) {
		... // 错误处理
    }

    if (!validLifecycleFn(config.unmount)) {
		... // 错误处理
    }

    if (config.update && !validLifecycleFn(config.update)) {
		... // 错误处理
    }

	// flattenFnArray 这个之前已经提及过了 就是 Promise的嵌套操作
    const bootstrap = flattenFnArray(config, "bootstrap");
    const mount = flattenFnArray(config, "mount");
    const unmount = flattenFnArray(config, "unmount");
	
    parcel.status = NOT_BOOTSTRAPPED;
    parcel.name = name;
    parcel.bootstrap = bootstrap;
    parcel.mount = mount;
    parcel.unmount = unmount;
    parcel.timeouts = ensureValidAppTimeouts(config.timeouts);

    if (config.update) {
      parcel.update = flattenFnArray(config, "update");
      externalRepresentation.update = function (customProps) {
        parcel.customProps = customProps;

        return promiseWithoutReturnValue(toUpdatePromise(parcel));
      };
    }
  });

  // Start bootstrapping and mounting
  // The .then() causes the work to be put on the event loop instead of happening immediately
  // 加载
  const bootstrapPromise = loadPromise.then(() =>
	// 同之前的初始化
    toBootstrapPromise(parcel, true)
  );
  // 初始化
  const mountPromise = bootstrapPromise.then(() =>
  	// 同之前的挂载
    toMountPromise(parcel, true)
  );

  let resolveUnmount, rejectUnmount;

  const unmountPromise = new Promise((resolve, reject) => {
    resolveUnmount = resolve;
    rejectUnmount = reject;
  });

  externalRepresentation = {
    mount() {
      return promiseWithoutReturnValue(
        Promise.resolve().then(() => {
          if (parcel.status !== NOT_MOUNTED) {
			... // 抛出错误
          }

          // Add to owning app or parcel
          owningAppOrParcel.parcels[id] = parcel;

          return toMountPromise(parcel);
        })
      );
    },
    unmount() {
      return promiseWithoutReturnValue(parcel.unmountThisParcel());
    },
    getStatus() {
      return parcel.status;
    },
    loadPromise: promiseWithoutReturnValue(loadPromise),
    bootstrapPromise: promiseWithoutReturnValue(bootstrapPromise),
    mountPromise: promiseWithoutReturnValue(mountPromise),
    unmountPromise: promiseWithoutReturnValue(unmountPromise),
  };
  // 到这一步就返回了一个具有完整生命周期的配置
  return externalRepresentation;
}

function promiseWithoutReturnValue(promise) {
  return promise.then(() => null);
}

看完后发现其实在进入mountParcel(...)方法之后,在调用configLoadingFunction(...)之后就一连串的把load -》 bootstarp -》mount都给完成了~

实验

首先在之前的Vue案例的分别拉取navBarrate-dogsroot-config三个应用即可

root-config的改动如下

/src/index.ejs

  <!-- 修改此段 -->
  <% if (isLocal) { %>
    <script type="systemjs-importmap">
      {
        "imports": {
          "@vue-mf/root-config": "//localhost:9000/vue-mf-root-config.js",
          "@vue-mf/root-config/": "//localhost:9000/",
           // 在本地运行时使用,本地服务的应用地址
      	   // 本地的rate-dogs 服务
          "@vue-mf/rate-dogs-local": "https://localhost:8503/js/app.js",
     	   // 本地的navbar 服务
          "@vue-mf/navbar": "http://localhost:8501/js/app.js"
        }
      }
    </script>
  <% } %>

/src/vue-mf-root-config.js

... // 其他注册
// 注册本地的微应用(用于实验)
registerApplication({
  name: "@vue-mf/rate-dogs-local",
  app: () => System.import("@vue-mf/rate-dogs-local"),
  activeWhen: "/rate-doggos-local",
});

start();

navBar的改动如下

/src/App.vue

  ... // 其他代码
 <ul>
    <li>
      <router-link
        to="/rate-doggos"
        class="nav-link"
        active-class="active-nav-link"
        >Rate some doggos</router-link
      >
    </li>
    <!-- 增加此段 -->
    <li>
      <router-link
        to="/rate-doggos-local"
        class="nav-link"
        active-class="active-nav-link"
        >Rate some doggos local</router-link
      >
    </li>
    <!-- 增加此段 -->
  </ul>
  ... // 其他代码

rate-dogs的改动如下

/src/App.vue

  <template>
    <div class="container">
      <!-- 增加此段 -->
      <div>本地应用</div>
      <router-view />
    </div>
  </template>
  <style scoped>
  .container {
    margin-left: var(--navbar-width);
  }
  </style>

然后分别启动服务,我们来到浏览器输入链接http://localhost:9000/rate-doggos-local看到下面内容也就算是成功了~ 本地root-config

本地的应用接入,就已经算是完成了~。那么我们现在来测试Parcels的内容

首先在root-config/src/vue-mf-root-config.js加入下面的代码

... // 其他代码
start()


const domElement = document.createElement('div')
const parcelProps = {domElement, testPorps: 'try-parcel'}

domElement.classList = ['try-parcel']
// parcel 的实现
const parcelConfig = {
  bootstrap() {
    // 初始化
    return Promise.resolve().then(() => {
      console.log('bootstrap')
    })
  },
  mount() {
    return new Promise((resolve, reject) => {
      domElement.innerHTML = `mount`
      document.body.appendChild(domElement)
      resolve()
    })
  },
  unmount() {
    // 使用某个框架卸载dom,做其他的清理工作
    return Promise.resolve().then(() => {
      domElement.innerHTML = `unmount`
    })
  }
}
// 调用挂载
const parcel = mountRootParcel(parcelConfig, parcelProps)

并且在/src/index.ejs加入下列样式

  <style>
    .try-parcel{
      margin: 30px var(--navbar-width);
    }
  </style>

调用完成后 你就能得到一个这样的界面

Parcels注册后

相当于我们自定义的Parcels已经注册成功了~

验证

其实到这里我们就可以验证我们之前的猜想,Parcels其实就是一个自定义化的一个Application,可以通过自己的定义的一些方法来做页面渲染(比如部分组件),或者是一些其他的能力。

剩下的问题

props的实现,路由的实现(好奇)?

我们将把这个问题放到简单记录学习single-spa的过程(4)去讨论~