接上回
我们目前还剩下两个问题:
- 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 要求我们必须实现生命周期函数(bootstrap, mount 和 unmount)和可选生命周期函数(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案例的分别拉取navBar,rate-dogs,root-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看到下面内容也就算是成功了~
本地的应用接入,就已经算是完成了~。那么我们现在来测试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其实就是一个自定义化的一个Application,可以通过自己的定义的一些方法来做页面渲染(比如部分组件),或者是一些其他的能力。
剩下的问题
props的实现,路由的实现(好奇)?
我们将把这个问题放到简单记录学习single-spa的过程(4)去讨论~