微前端 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 的创建和发动过程,下节见啦~