引言

前一段时间, 正好在做微前端的接入和微前端治理平台的相干事项。 而咱们以后应用的微前端框架则是 qiankun, 他是这样介绍本人的:

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮忙大家能更简略、无痛的构建一个生产可用微前端架构零碎。

所以本文基于 single-spa 源码, 来介绍 single-spa

以后应用版本 5.9.4

启动

在官网 demo 中, 要运行此框架须要做的是有这四步:

  1. 筹备好子利用的文件, 须要抛出一些生命周期函数
  2. 一个子利用 app1 的加载函数(_能够是 import 异步加载, 也能够是 ajax/fetch 加载_)
  3. 注册子利用
  4. 启动程序

app1.js:

export function bootstrap(props) {    //初始化时触发}export function mount(props) {    // 利用挂载结束之后触发}export function unmount(props) {    // 利用卸载之后触发}

main.js:

import * as singleSpa from 'single-spa'const name = 'app1';const app = () => import('./app1/app1.js'); // 一个加载函数const activeWhen = '/app1'; // 当路由为 app1 时, 会触发微利用的加载// 注册利用singleSpa.registerApplication({name, app, activeWhen});// 启动singleSpa.start();

文件构造

single-spa 的文件构造为:

├── applications│   ├── app-errors.js│   ├── app.helpers.js│   ├── apps.js│   └── timeouts.js├── devtools│   └── devtools.js├── jquery-support.js├── lifecycles│   ├── bootstrap.js│   ├── lifecycle.helpers.js│   ├── load.js│   ├── mount.js│   ├── prop.helpers.js│   ├── unload.js│   ├── unmount.js│   └── update.js├── navigation│   ├── navigation-events.js│   └── reroute.js├── parcels│   └── mount-parcel.js├── single-spa.js├── start.js└── utils    ├── assign.js    ├── find.js    └── runtime-environment.js

registerApplication

咱们先从注册利用开始看起

function registerApplication(    appNameOrConfig,    appOrLoadApp,    activeWhen,    customProps) {    // 数据整顿, 验证传参的合理性, 最初整顿失去数据源:    // {    //      name: xxx,    //      loadApp: xxx,    //      activeWhen: xxx,    //      customProps: xxx,    // }    const registration = sanitizeArguments(        appNameOrConfig,        appOrLoadApp,        activeWhen,        customProps    );        // 如果有重名,则抛出谬误, 所以 name 应该是要放弃惟一值    if (getAppNames().indexOf(registration.name) !== -1)        throw Error('xxx'); // 这里省略具体谬误        // 往 apps 中增加数据    // apps 是 single-spa 的一个全局变量, 用来存储以后的利用数据    apps.push(        assign(            {                // 预留值                loadErrorTime: null,                status: NOT_LOADED, // 默认是 NOT_LOADED , 也就是待加载的状态                parcels: {},                devtools: {                    overlays: {                        options: {},                        selectors: [],                    },                },            },            registration        )    );        // 判断 window 是否为空, 进入条件    if (isInBrowser) {        ensureJQuerySupport(); // 确保 jq 可用        reroute();    }}

reroute

reroutesingle-spa 的外围函数, 在注册利用时调用此函数的作用, 就是将利用的 promise 加载函数, 注入一个待加载的数组中 等前面正式启动时再调用, 相似于 ()=>import('xxx')

次要流程: 判断是否合乎加载条件 -> 开始加载代码

export function reroute(pendingPromises = [], eventArguments) {    if (appChangeUnderway) { //  一开始默认是 false        // 如果是 true, 则返回一个 promise, 在队列中增加 resolve 参数等等        return new Promise((resolve, reject) => {            peopleWaitingOnAppChange.push({                resolve,                reject,                eventArguments,            });        });    }        const {        appsToUnload,        appsToUnmount,        appsToLoad,        appsToMount,    } = getAppChanges();    // 遍历所有利用数组 apps , 依据 app 的状态, 来分类到这四个数组中    // 会依据 url 和 whenActive 判断是否该 load    // unload , unmount, to load, to mount        let appsThatChanged,        navigationIsCanceled = false,        oldUrl = currentUrl,        newUrl = (currentUrl = window.location.href);        // 存储着一个闭包变量, 是否曾经启动, 在注册步骤中, 是未启动的    if (isStarted()) {        // 省略, 以后是未开始的    } else {        // 未启动, 间接返回 loadApps, 他的定义在下方        appsThatChanged = appsToLoad;        return loadApps();    }        function cancelNavigation() {        navigationIsCanceled = true;    }        // 返回一个 resolve 的 promise    // 将须要加载的利用,  map 成一个新的 promise 数组    // 并且用 promise.all 来返回    // 不论胜利或者失败, 都会调用 callAllEventListeners 函数, 进行路由告诉    function loadApps() {        return Promise.resolve().then(() => {            // toLoadPromise 次要作用在甲方有讲述, 次要来定义资源的加载, 以及对应的回调            const loadPromises = appsToLoad.map(toLoadPromise);                        // 通过 Promise.all 来执行, 返回的是 app.loadPromise            // 这是资源加载            return (                Promise.all(loadPromises)                .then(callAllEventListeners)                // there are no mounted apps, before start() is called, so we always return []                .then(() => [])                .catch((err) => {                    callAllEventListeners();                    throw err;                })            );        });    }}

toLoadPromise

注册流程中 reroute 中的次要执行函数
次要性能是赋值 loadPromiseapp, 其中 loadPromise 函数中包含了: 执行函数、来加载利用的资源、定义加载结束的回调函数、状态的批改、还有加载谬误的一些解决

export function toLoadPromise(app) {    return Promise.resolve().then(() => {        // 是否反复注册 promise 加载了        if (app.loadPromise) {            return app.loadPromise;        }        // 刚注册的就是 NOT_LOADED 状态        if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {            return app;        }                // 批改状态为, 加载源码        app.status = LOADING_SOURCE_CODE;                let appOpts, isUserErr;                // 返回的是 app.loadPromise        return (app.loadPromise = Promise.resolve()        .then(() => {            // 这里调用的了 app的 loadApp 函数(由内部传入的), 开始加载资源            // getProps 用来判断 customProps 是否非法, 最初传值给 loadApp 函数            const loadPromise = app.loadApp(getProps(app));            // 判断 loadPromise 是否是一个 promise            if (!smellsLikeAPromise(loadPromise)) {                // 省略报错                isUserErr = true;                throw Error("...");            }            return loadPromise.then((val) => {                // 资源加载胜利                app.loadErrorTime = null;                                appOpts = val;                                let validationErrMessage, validationErrCode;                                // 省略对于资源返回后果的判断                // 比方appOpts是否是对象, appOpts.mount appOpts.bootstrap 是否是函数, 等等                // ...                                // 批改状态为, 未进入疏导                // 同时将资源后果的函数赋值, 以备前面执行                app.status = NOT_BOOTSTRAPPED;                app.bootstrap = flattenFnArray(appOpts, "bootstrap");                app.mount = flattenFnArray(appOpts, "mount");                app.unmount = flattenFnArray(appOpts, "unmount");                app.unload = flattenFnArray(appOpts, "unload");                app.timeouts = ensureValidAppTimeouts(appOpts.timeouts);                                // 执行结束之后删除 loadPromise                delete app.loadPromise;                                return app;            });        })        .catch((err) => {            // 报错也会删除 loadPromise            delete app.loadPromise;            // 批改状态为 用户的传参报错, 或者是加载出错            let newStatus;            if (isUserErr) {                newStatus = SKIP_BECAUSE_BROKEN;            } else {                newStatus = LOAD_ERROR;                app.loadErrorTime = new Date().getTime();            }            handleAppError(err, app, newStatus);                        return app;        }));    });}

start

注册完利用之后, 最初是 singleSpa.start(); 的执行

start 的代码很简略:

// 一般来说 opts 是不传什么货色的function start(opts) {    // 次要作用还是将标记符 started设置为 true 了    started = true;    if (opts && opts.urlRerouteOnly) {        // 应用此参数能够人为地触发事件 popstate        setUrlRerouteOnly(opts.urlRerouteOnly);    }    if (isInBrowser) {        reroute();    }}

reroute

上述曾经讲过注册时 reroute 的一些代码了, 这里会疏忽已讲过的一些货色

function reroute(pendingPromises = [], eventArguments) {    const {        appsToUnload,        appsToUnmount,        appsToLoad,        appsToMount,    } = getAppChanges();    let appsThatChanged,        navigationIsCanceled = false,        oldUrl = currentUrl,        newUrl = (currentUrl = window.location.href);        if (isStarted()) {        // 这次开始执行此处        appChangeUnderway = true;        // 合并状态须要变更的 app        appsThatChanged = appsToUnload.concat(            appsToLoad,            appsToUnmount,            appsToMount        );        // 返回 performAppChanges 函数        return performAppChanges();    }}

performAppChanges

在启动后,就会触发此函数 performAppChanges, 并返回后果
本函数的作用次要是事件的触发, 包含自定义事件和子利用中的一些事件

  function performAppChanges() {    return Promise.resolve().then(() => {        // 触发自定义事件, 对于 CustomEvent 咱们再下方详述        // 以后事件触发 getCustomEventDetail        // 次要是 app 的状态, url 的变更, 参数等等        window.dispatchEvent(            new CustomEvent(                appsThatChanged.length === 0                    ? "single-spa:before-no-app-change"                    : "single-spa:before-app-change",                getCustomEventDetail(true)            )        );                // 省略相似事件                // 除非在上一个事件中调用了 cancelNavigation, 才会进入这一步        if (navigationIsCanceled) {            window.dispatchEvent(                new CustomEvent(                    "single-spa:before-mount-routing-event",                    getCustomEventDetail(true)                )            );            // 将 peopleWaitingOnAppChange 的数据从新执行 reroute 函数 reroute(peopleWaitingOnAppChange)              finishUpAndReturn();            // 更新 url            navigateToUrl(oldUrl);            return;        }                // 筹备卸载的 app        const unloadPromises = appsToUnload.map(toUnloadPromise);                // 执行子利用中的 unmount 函数, 如果超时也会有报警        const unmountUnloadPromises = appsToUnmount        .map(toUnmountPromise)        .map((unmountPromise) => unmountPromise.then(toUnloadPromise));                const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);                const unmountAllPromise = Promise.all(allUnmountPromises);                // 所有利用的卸载事件        unmountAllPromise.then(() => {            window.dispatchEvent(                new CustomEvent(                    "single-spa:before-mount-routing-event",                    getCustomEventDetail(true)                )            );        });                // 执行 bootstrap 生命周期, tryToBootstrapAndMount 确保先执行 bootstrap        const loadThenMountPromises = appsToLoad.map((app) => {            return toLoadPromise(app).then((app) =>                tryToBootstrapAndMount(app, unmountAllPromise)            );        });                // 执行 mount 事件        const mountPromises = appsToMount        .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)        .map((appToMount) => {            return tryToBootstrapAndMount(appToMount, unmountAllPromise);        });        // 其余的局部不太重要, 可省略    });}

CustomEvent

CustomEvent 是一个原生 API, 这里略微介绍下

在某些场景中, 咱们会常常做出一些模仿点击的行为, 比方这样:

<button id="submit" onclick="alert('Click!');">btn</button><script>    const btn = document.getElementById('submit');    btn.click()</script>

通过 CustomEvent 也能实现这种事件:

<button id="submit" onclick="alert('Click!');">btn</button><script>    const btn = document.getElementById('submit');    btn.dispatchEvent(new CustomEvent('click'))    // 应用 btn.dispatchEvent(new Event('click')) 也是一样的    // 区别在于 CustomEvent 能够传递自定义参数</script>

不仅是浏览器原生的事件,如'click','mousedown','change','mouseover','mouseenter'等能够触发,任意的自定义名称的事件也是能够触发的

document.body.addEventListener('测试自定义事件', (ev) => {    console.log(ev.detail)})document.body.dispatchEvent(new CustomEvent('测试自定义事件', {    detail: {        foo: 1    }}))

整体流程

  1. 在正式环境应用 registerApplication 来注册利用
  2. 这时候在 single-spa 外部会将注册的信息, 初始化加载函数
  3. 应用 url 进行匹配, 是否要加载, 如果须要加载, 则归类
  4. 如果匹配上, 开始加载利用的文件 (即便还没应用 start)
  5. 最初应用 start, 开始发送各类事件, 调用利用的各类生命周期办法

这里用一个简略的图来阐明下:

总结

single-spa 无疑是微前端的一个重要里程碑,在大型利用场景下, 可反对多类框架, 抹平了框架间的微小交互老本

他的外围是对子利用进行治理,但还有很多工程化问题没做。比方JavaScript全局对象笼罩、css加载卸载、公共模块治理要求只下载一次等等性能问题

这又促成了其余的框架的诞生, 比拟闻名的就是 qiankunIsomorphic Layout Composer

而这些就是另一个话题了。

援用

  • https://zh-hans.single-spa.js.org/docs/getting-started-overview
  • https://zhuanlan.zhihu.com/p/344145423
  • https://www.zhangxinxu.com/wordpress/2020/08/js-customevent-p...
  • https://juejin.cn/post/7054454791803502628