前端 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 看作用:

微前端 qiankun@2.10.5 源码分析(一)

开端

第一步:初始化运用

找到 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});

保存看作用:

微前端 qiankun@2.10.5 源码分析(一)

很简单,就不具体解释啦!

第二步:注册子运用

找到 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 库也不要紧,后边咱们会详细分析它的源码的):

微前端 qiankun@2.10.5 源码分析(一)

从上面流程图中咱们能够知道,当 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

微前端 qiankun@2.10.5 源码分析(一)

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