微前端

微前端

技术圈是比较爱炒概念的,中台、微服务、DDD一时都变成比较热门的话题,微前端作为微服务的一种自然也免不了俗。在我看来,大多数情况下大多数公司都用不上微前端。当然微前端不是什么新技术,只是为了解决实际问题的一种方法。微前端这三个字听起来不明所以,实际上只是将项目打散,变成若干小项目的合集,使用一种方案使得在多个项目之前平滑切换的方法。

为什么需要微前端

前面说过,大多数情况下我们是不需要微前端的,通过清晰的组件划分,部分情况下使用 iframe 嵌入就能解决绝大部分问题。但是微前端对下面的问题解决起来更有优势:

  1. 迭代日积月累导致维护困难
    一个生命周期超长的软件,必然产生出一个体积庞大的软件。软件会囊括各种交错复杂的业务逻辑,阅读维护起来特别困难。前端项目体积滋长还会使编译时间越来越长,本地开发热更新时电脑吃力,卡机。

  2. 技术栈多样性
    公司在逐步扩大业务,永远有新的业务需要上线,永远有新的技术栈在尝试。庞大的单体软件很难同时运行多个技术栈,很难在业务中尝试使用新的技术,举步维艰。

  3. 跨团队开发
    若同一项目交给多个团队同时开发,由于是同一项目同一仓库,团队开发的资源并没有隔离经常会导致代码冲突,互相影响业务功能,造成开发风险。

技术方案

  1. iframe 嵌套多个子项目
  2. MPA 多子项目之前通过链接跳转
  3. 整合多个子项目资源,由主项目动态导航不同的子项目

这里重点介绍第三种

Single-SPA

这种方案的重点是加载子应用资源,并根据路由进行资源导航,实现的方式市面上的工具各有不同
比较知名的开源方案有:

  1. Single-SPA
  2. Qiankun
  3. icestark

qiankun 使用了 single-SPA 的路由系统,且实现了一个 sandbox,用来隔离 js 运行环境。icestark 是飞冰团队的微前端方案,整体思路和前两者也比较接近。这里以 single-SPA 介绍其实现思路。

Single-SPA 将项目分为了主应用和子应用,主应用负责子应用导航,加载 JS、挂载应用等功能,而我们的主要业务代码放在子应用内。

主应用注册子应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { registerApplication, start } from "single-spa";
import * as isActive from "./activity-functions";

registerApplication(
"@react-mf/navbar",
// System.import 是 system.js 提供的异步导入 js 模块的方法
() => System.import("@react-mf/navbar"),
isActive.navbar
);

registerApplication(
"@react-mf/people",
() => System.import("@react-mf/people"),
isActive.people
);

registerApplication(
"@react-mf/planets",
() => System.import("@react-mf/planets"),
isActive.planets
);

start();

子应用导出示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "./set-public-path";
import React from "react";
import ReactDOM from "react-dom";
import singleSpaReact from "single-spa-react";
import Root from "./root.component";

const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: Root
});

export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;

上面的两个代码示例介绍了 single-spa 的使用方法。那 single-spa 怎么实现微前端的呢?整体思路为:

  1. 子应用导出生命周期函数
    子应用需导出 bootstrapmountunmountupdate(可选)函数,用于在 single-spa 生命周期中调用来挂载和卸载子应用

  2. single-spa 提供 registerApplication 注册子应用。
    该方法第二个参数 applicationOrLoadingFn 用于异步导入子应用的资源包,示例中使用了 system.js 提供的 import 方法导入,当然别的异步导入方法也是支持的,只要返回一个 Promise 就行。
    该方法第三个参数 activityFn 用于确认是否激活该子应用。这是 single-spa 路由系统的关键,只有激活状态的子应用才会 mount。
    方法调用后会生成一个 app 对象并将 name, applicationOrLoadingFn, activityFn 等参数挂载到 app。

  3. 调用 reroute 方法挂载应用
    如果子应用还未调用 start 方法则会调用 loadApps 将所有激活状态(通过 activityFn)的子应用的 load 下来,并将子应用生命周期方法挂到 app 对象中。
    否则,调用 performAppChanges。该方法会先后调用需激活的子应用的 bootstrap、mount 方法完成子组件的挂载。

  4. 监听 hashchange, popstate 事件拦截路由变化
    路由变化时 single-spa 会重新调用 reroute 决定哪些子应用需要 unmount,哪些应用需要 mount。
    不仅如此,还会重写 window.addEventListenerwindow.removeEventListener 拦截所有 hashchangepopstate 的事件注册,
    使事件处理器在子应用在 single-spa 处理完子应用的 load、挂载或卸载之后才被调用。

Single-SPA 与 Qiankun

  1. qiankun 实现了一个 JS sandbox,避免子应用之间的环境污染

    实现逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const rawWindow = window;
    const fakeWindow = Object.create(null) as Window;

    const sandbox: WindowProxy = new Proxy(fakeWindow, {
    set(_: Window, p: PropertyKey, value: any): boolean {
    // 省略...
    },

    get(_: Window, p: PropertyKey): any {
    // 省略...
    },

    has(_: Window, p: string | number | symbol): boolean {
    return p in rawWindow;
    },
    });

    qiankun 通过 Proxy 拦截了 fakeWindow 对象,在使用他们编写的插件 import-html-entry load 子应用 js 时将其作为子应用的 window,并且每个子应用生成的对象都不同。这样,就实现了每个子应用的环境独立,避免变量污染。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // get the entry html content and script executor
    const { template: appContent, execScripts, assetPublicPath } = await importEntry(entry, {
    // compose the config getTemplate function with default wrapper
    getTemplate: flow(getTemplate, getDefaultTplWrapper(appName)),
    ...settings,
    });

    //省略部分代码...

    // get the lifecycle hooks from module exports
    let { bootstrap: bootstrapApp, mount, unmount } = await execScripts(jsSandbox);
  2. JS Entry vs HTML Entry
    qiankun 采用 HTML Entry 作为资源注入方式,single-SPA 采用 JS Entry 方式注入。

    JS Entry 方式要求主应用必须给子应用提供一个挂载的 DOM 节点;子应用需要将资源(js、css 等)打包到一个文件,或者打包时将所有子应用的资源路径单独保存配置,在主应用引用;

    HTML Entry 方式则不需要单独提供挂载点,也不需要单独处理资源加载的问题,无论是挂载点还是资源都在 HTML 中,都能一次性全部解析到。qiankun 拉取和解析 html 还是使用 import-html-entry 完成的。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×