热知识~,乾坤是基于 single-spa 实现的微前端框架,而 single-spa 用到 SystemJS 作为主要的模块加载工具
在进一步剖析乾坤源码之前,我们先来了解下 SystemJS 和 single-spa 的工作原理
SystemJS 和 single-spa
- SystemJS: 允许在浏览器环境中动态加载微应用的模块,处理模块的导入导出
- single-spa: 提供了微前端的核心架构和生命周期管理,确保各个微应用能够独立运行和协作。 通过路由劫持机制实现子应用的动态加载,并利用 SystemJS 作为模块加载器来管理各个子应用的导入与导出。为了确保子应用能够与主应用无缝集成,子应用需要遵循特定的接入协议,即暴露固定的生命周期方法:
bootstrap
、mount
和unmount
SystemJS
是一个可运行于浏览器端的模块加载器,让我们可以在浏览器中使用 ES6 import/export
语法
我们可以通过 systemjs-importmap
指定依赖库的地址,也可以在 script标签里 System.import('./index.js')
直接导入某个模块,具体语法可以参考下面代码
注意 🙌,模块导入是一个异步过程,返回的是一个 Promise 对象,可以配合 then 来使用
html
<body>
<h3>主应用,也叫基座,用来加载子应用的 webpack importMap</h3>
<div id="root"></div>
<!-- 可以在浏览器使用 ES6 的 import/export 语法, 通过 systemjs-importmap 指定依赖库的地址 -->
<script type="systemjs-importmap">
{
"imports": {
"react-dom": "https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js",
"react": "https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js"
}
}
</script>
<script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js"></script>
<script>
// 直接加载子应用, 导入打包后的包来进行加载, 采用是 system规范
System.import('./index.js')
</script>
</body>
SystemJS 和 single-spa 没有任何关系,只是它的 in-browser import/export 和 single-spa 倡导的 in-browser run time 相符合,所以 single-spa 将其作为主要的导入导出工具 (并不是必须的!!!)
甚至在一些现代浏览器中,我们可以借助 importmap 实现 _import axios from 'axios'_
导入功能💯
typescript
<script type="importmap">
{
"imports": {
"axios": "https://cdn.jsdelivr.net/npm/axios@0.20.0/dist/axios.min.js"
}
}
</script>
<script type="module">
import axios from 'axios'
</script>
但在低版本浏览器中,我们就需要借助于一些 "Polyfill"来实现 import/export 了。SystemJS 就是解决这个问题的
我们也可以用 Webpack 动态引入,甚至可能比 SystemJS 更好用💯
typescript
import(/* webpackChunkName: "index" */ './index.js').then(moduleA => {
moduleA.doSomething();
});
single-spa
single-spa 通过路由劫持实现应用的加载(采用SystemJS),提供应用间公共组件加载以及公共业务逻辑处理。子应用需要遵循特定的接入协议,即暴露固定的生命周期钩子(bootstrap、mount、unmount)💯
无沙箱机制,需要实现自己的JS沙箱以及CSS沙箱
index.html(主应用)
负责声明资源路径
javascript
<script type="systemjs-importmap">
{
"imports": {
"@burc/root-config": "//localhost:9000/burc-root-config.js",
"@burc/react":"//localhost:3000/react.js",
"@burc/vue":"//localhost:4000/js/app.js"
}
}
</script>
main.js(主应用)
负责注册子应用和启动主应用 Application
javascript
import {
registerApplication,
start
} from "single-spa";
registerApplication({
name: "@burc/react", // 不重名即可
app: () =>
System.import('@burc/react'),
activeWhen: (location) => location.pathname.startsWith('/react'),
});
registerApplication({
name: "@burc/vue", // 不重名即可
app: () =>
System.import('@burc/vue'),
activeWhen: (location) => location.pathname.startsWith('/vue'),
});
start({
urlRerouteOnly: true,
});
sing-spa 只做了两件事: 一是提供生命周期概念,负责调度子应用的生命周期;二是劫持 url 变化事件,url 变化时匹配对应子应用,执行生命周期流程
Root Config: 指主应用的 index.html + main.js。HTML 负责声明资源路径,JS 负责注册子应用和启动主应用
Application: 子应用要暴露 bootstrap,mount,unmount 三个生命周期(接入协议)
registerMicroApps(注册子应用)
用于注册子应用的基础配置信息。当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule
规则,符合规则的应用将会被自动激活
详细参数可查看乾坤官网 - registerMicroApps(apps, lifeCycles?), 其基本语法如下:
javascript
import { registerMicroApps, start } from 'qiankun';
registerMicroApps(
[
{
name: 'reactApp', // 微应用的名称,微应用之间必须确保唯一
entry: '//localhost:40000', // 微应用的入口
activeRule: '/react', // 微应用的激活规则,当路径以 /react 为前缀时启动
container: '#container', // 微应用的容器节点的选择器或者 Element 实例
loader, // loading 状态发生变化时会调用的方法
props: { userInfo:{ name: 'burc', password: 'xxxxxx'} }, // 主应用需要传递给微应用的数据
},
{
name: 'vueApp',
entry: '//localhost:20000', // 默认react启动的入口是10000端口
activeRule: '/vue', // 当路径是 /react的时候启动
container: '#container', // 应用挂载的位置
loader,
props: { userInfo:{ name: 'burc', password: 'xxxxxx'}},
},
],
{
beforeLoad() { },
beforeMount() { },
afterMount() { },
beforeUnmount() { },
afterUnmount() { },
},
)
start()
registerMicroApps 注册子应用,乾坤源码对应 qiankun/blob/master/src/apis.ts
name: 微应用之间必须确保唯一,标识,用于区分不同的微应用
javascript
import { registerApplication, start as startSingleSpa } from 'single-spa';
export function registerMicroApps<T extends ObjectType>(
apps: Array<RegistrableApp<T>>, // 本次要注册的应用
lifeCycles?: FrameworkLifeCycles<T>, // 自己编写的生命周期
) {
// 拿到没有被注册过的应用,name 属性就是用来区分不同的应用的
const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));
// 最新要注册的应用
microApps = [...microApps, ...unregisteredApps];
// 循环注册未注册的应用
unregisteredApps.forEach((app) => {
// appConfig 应用的配置
const { name, activeRule, loader = noop, props, ...appConfig } = app;
// 注册应用的逻辑采用的是 single-spa 的 registerApplication (路由劫持也是在 single-spa 内部实现)
registerApplication({
name,
app: async () => {
loader(true);
await frameworkStartedDefer.promise; // 等待调用 start 方法,frameworkStartedDefer.resolve()
// loadApp方法返回的是一个函数 (loadApp())(), 沙箱的处理
const { mount, ...otherMicroAppConfigs } = (
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();
return {
// 返回的是应用的接入协议
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
// 1. 目前不会执行app方法,会等待路径匹配后执行app方法
// 2. 执行app方法时,也会等待调用start方法, await frameworkStartedDefer.promise
});
}
注册应用的底层逻辑采用的是 single-spa 的 registerApplication
方法
监听路由变化,匹配对应的子应用 (single-spa内部执行) ,路径匹配成功后执行 app
方法,然后等待调用下文的 start 方法,再去加载子应用 loadApp()
下文的 start 方法:
typescript
import { start as startSingleSpa } from 'single-spa';
export function start(opts: FrameworkConfiguration = {}) {
...
frameworkStartedDefer.resolve(); // 调用成功的promise
}
loadApp(加载子应用)
乾坤源码对应 qiankun/blob/master/src/loader.ts
- 通过 importEntry 加载解析子应用的入口HTML文件,获取 template模版 以及 js脚本执行器 execScripts 等
- 对 template 模板进行处理,
<qiankun-head-head>
替换<head>
,添加data-name
、data-version
等属性 - 创建 css沙箱,实现 影子dom沙箱 和 作用域css沙箱
- 创建 js沙箱,这里存在兼容性的降级操作,多应用代理沙箱 -> 单应用代理沙箱 -> 快照沙箱
- 在 js沙箱环境中执行脚本执行器 execScripts(),这里用 js沙箱的代理对象 代替了 全局对象window
importEntry(加载HTML)
通过 importEntry 加载解析子应用的入口HTML文件,获取解析后的html文件、并且拿到js脚本的执行器、和额外的js脚本
typescript
import { importEntry } from 'import-html-entry'
// 获取解析后的html文件、并且拿到js脚本的执行器、和额外的js脚本
const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);
乾坤相比于 single-spa 有两大特色,一个是实现了 JS 和 CSS沙箱机制;
另一个就是使用 import-html-entry 实现了 HTML entry,而 single-spa 只能是 JS entry 的形式来加载子应用
- JS Entry。 通常将子应用的所有资源打包成一个入口文件,在 single-spa 的很多样例中就使用了这种方式
- HTML Entry。 子应用构建输出的是一个 HTML 文件,主应用通过加载这个 HTML 文件完成子应用的加载
import-html-entry到底干了些什么?
[import-html-entry](https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fimport-html-entry "https://www.npmjs.com/package/import-html-entry"),它可以从指定的 URL 加载解析 HTML 文件,返回值如下:
* **template:** 是注释掉了 js脚本,并将外部css样式转化为内部css样式之后的 html
* **assetPublicPath:** 静态资源的公共路径
* **execScripts:** *Promise\<\>* ,执行js脚本的函数(包括内部脚本和外部脚本)
* \*\*getExternalScripts:\*\**Promise\<\>* Scripts URL from template,返回 html 文件的所有js脚本
* \*\*getExternalStyleSheets:\*\**Promise\<\>* StyleSheets URL from template,返回 html 文件的外部css样式表

**template模版,是注释掉了 js脚本,并将外部css样式转化为内部css样式之后的 html**

**getExternalScripts,*Promise\<\>*,返回 html 文件的所有js脚本**

**getExternalStyleSheets,*Promise\<\>*,返回 html 文件的外部css样式表**

#### getDefaultTplWrapper(处理template)
对 template 模板进行处理,`