微前端 qiankun@2.10.5 源码分析(一)
前言
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. – Micro Frontends
微前端架构具备以下几个核心价值:
技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
独立运行时
每个微应用之间状态隔离,运行时状态不共享微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。-- qiankun 官网
哈哈,其实目前我自己公司团队也存在上面说的一些问题,希望能够通过源码的分析研究从中得到一些灵感,对现有项目进行一些改造,打造符合自己的微前端生态。
安装
这里用的是 qiankun@2.10.5
版本。
执行以下命令安装 qiankun
源码:
$ git clone https://github.com/umijs/qiankun.git
$ cd qiankun
安装并运行:
$ yarn install
$ yarn examples:install
$ yarn examples:start
打开 http://localhost:7099
看效果:
开始
第一步:初始化应用
找到 examples/main/index.js
文件的第 15 行:
/**
* Step1 初始化应用(可选)
*/
render({loading: true});
const loader = (loading) => render({loading});
可以看到,调用了 render
方法,然后创建了一个 loader
,我们重点看一下 render
方法。
找到 examples/main/render/VueRender.js
文件:
import Vue from 'vue/dist/vue.esm';
function vueRender({ loading }) {
return new Vue({
template: `
<div id="subapp-container">
<h4 v-if="loading" class="subapp-loading">Loading...</h4>
<div id="subapp-viewport"> Vue 应用挂载节点 </div>
</div>
`,
el: '#subapp-container',
data() {
return {
loading,
};
},
});
}
let app = null;
export default function render({ loading }) {
if (!app) {
app = vueRender({ loading });
} else {
app.loading = loading;
}
}
可以看到,导出了一个 render
方法,在 render
方法中创建了一个 Vue
实例,这里有一个 id="subapp-viewport"
的 div
节点,这个就是应用的挂载节点,后面会用到。
如果这个时候我们执行 render
方法的话,页面会是一个 loading
状态,我们可以试试看。
修改一下 examples/main/index.js
文件:
import 'zone.js'; // for angular subapp
import './index.less';
/**
* 主应用 **可以使用任意技术栈**
* 以下分别是 React 和 Vue 的示例,可切换尝试
*/
import render from './render/VueRender';
//
/**
* Step1 初始化应用(可选)
*/
render({loading: true});
const loader = (loading) => render({loading});
保存看效果:
很简单,就不具体解释啦!
第二步:注册子应用
找到 examples/main/index.js
文件的第 23 行:
registerMicroApps(
[
{
name: 'react16', // 应用名称
entry: '//localhost:7100', // 应用入口文件
container: '#subapp-viewport', // 应用挂载节点
loader, // 应用加载器
activeRule: '/react16', // 应用路由匹配规则
},
{
name: 'vue',
entry: '//localhost:7101',
container: '#subapp-viewport',
loader,
activeRule: '/vue',
},
...
],
{
beforeLoad: [
(app) => {
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
},
],
beforeMount: [
(app) => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
},
],
afterUnmount: [
(app) => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
},
],
},
);
可以看到,这里注册了很多个子应用,我们重点看一下这个 registerMicroApps
方法。
找到 src/apis.ts
文件的第 59 行:
export function registerMicroApps<T extends ObjectType>(
apps: Array<RegistrableApp<T>>,
lifeCycles?: FrameworkLifeCycles<T>,
) {
// 过滤未注册过的应用,防止多次注册
const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));
microApps = [...microApps, ...unregisteredApps];
// 遍历每一个未注册的应用
unregisteredApps.forEach((app) => {
const { name, activeRule, loader = noop, props, ...appConfig } = app;
// 注册应用(SPA)
registerApplication({
name,
app: async () => {
loader(true);
await frameworkStartedDefer.promise;
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,
});
});
}
ok,其实我们可以看到,在 registerMicroApps
方法中主要就是调用 registerApplication
方法去注册了每一个应用,而这里的 registerApplication
方法是 single-spa 库的方法,先上一张 single-spa 库的流程图(没了解过 single-spa 库也没关系,后面我们会详细分析它的源码的):
从上面流程图中我们可以知道,当 single-spa 匹配到路由信息后,会渲染对应的子应用,接着就会调用子应用的
app
方法对子应用进行渲染。
我们可以回到 src/apis.ts
文件的 registerApplication
方法:
// 注册应用
registerApplication({
name,
app: async () => {
// 修改页面状态为 loading
loader(true);
// 等待 start 方法的调用
await frameworkStartedDefer.promise;
// 加载当前子应用,获取子应用的 mount 方法
const { mount, ...otherMicroAppConfigs } = (
// 调用 loadApp 加载子应用
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
前面我们说了,当 single-spa 匹配到路由信息后,会渲染对应的子应用,接着就会调用子应用的
app
方法对子应用进行渲染。
可以看到,在 app
方法又调用了一个叫 loadApp
的方法,loadApp
很重要!!!我们后面用到的时候再具体分析。
第三步:设置默认进入的子应用
找到 examples/main/index.js
文件的第 103 行:
/**
* Step3 设置默认进入的子应用
*/
setDefaultMountApp('/react16');
找到 src/effects.ts
文件的 setDefaultMountApp
方法:
export function setDefaultMountApp(defaultAppLink: string) {
// 当调用 spa 的 start 方法后,如果没有匹配到任何子应用的话,会调用该事件
window.addEventListener('single-spa:no-app-change', function listener() {
// 获取 spa 的所有渲染过的应用
const mountedApps = getMountedApps();
// 如果从未渲染过任何子应用的话就将当前路径指向默认路径
if (!mountedApps.length) {
navigateToUrl(defaultAppLink);
}
window.removeEventListener('single-spa:no-app-change', listener);
});
}
可以看到,如果从未渲染过任何子应用的话就将当前路径指向默认路径,我们这里传入的是 /react16
,我们可以测试一下。
当我们访问 http://localhost:7099/
地址的时候,qiankun 会自动的将我们的路径改为我们设置的默认路径 http://localhost:7099/react16
:
ok,我们继续往下看!
第四步:启动应用
找到 examples/main/index.js
文件的第 108 行:
/**
* Step4 启动应用
*/
start();
找到 src/apis.ts
文件中的 start
方法:
export function start(opts: FrameworkConfiguration = {}) {
frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts };
const { prefetch, urlRerouteOnly = defaultUrlRerouteOnly, ...importEntryOpts } = frameworkConfiguration;
// 预加载所有子应用(默认开启)
if (prefetch) {
doPrefetchStrategy(microApps, prefetch, importEntryOpts);
}
// 根据当前浏览器环境判断是否是需要降级
frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration);
// 启动应用(urlRerouteOnly = true:仅路由发生变换的时候才触发自定义 popstate 事件)
startSingleSpa({ urlRerouteOnly });
// 已经调用了 started 标志
started = true;
// start 调用准备完毕回调
frameworkStartedDefer.resolve();
}
可以看到,这里主要调用了 single-spa 库的 startSingleSpa
方法启动应用,最后一行有执行
准备完毕回调:
// start 调用准备完毕回调
frameworkStartedDefer.resolve();
ok,其实当我们调用了 single-spa 库的 startSingleSpa
方法的时候, single-spa 就会根据当前路由去匹配需要渲染的子应用,会调用子应用的 app
方法。
还记得我们在“第二步(注册子应用)”中的 registerMicroApps
方法?
找到 src/apis.ts
文件的第 59 行:
export function registerMicroApps<T extends ObjectType>(
apps: Array<RegistrableApp<T>>,
lifeCycles?: FrameworkLifeCycles<T>,
) {
// 过滤未注册过的应用,防止多次注册
const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));
microApps = [...microApps, ...unregisteredApps];
// 遍历每一个未注册的应用
unregisteredApps.forEach((app) => {
const { name, activeRule, loader = noop, props, ...appConfig } = app;
// 注册应用
registerApplication({
name,
app: async () => {
// 修改页面状态为 loading
loader(true);
// 等待 start 方法的调用
await frameworkStartedDefer.promise;
// 加载当前子应用,获取子应用的 mount 方法
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,
});
});
}
可以看到,又回到了这里的 app
方法了,接着又调用了 loadApp
方法去加载子应用。
小伙伴们可以先停下来回顾一下 qiankun 的创建和启动步骤,下节见啦~