微前端之背景与实践

微前端最早于2016年在 Micro-Frontends 被提出,并建⽴了早期的微前端模型。微前端的命名和能⼒和微服务有类似之处,微服务与微前端,都是希望将某个单⼀的单体应⽤,转化为多个可以独⽴运⾏、独⽴开发、独⽴部署、独⽴维护的服务或者应⽤的聚合,从⽽满⾜业务快速变化及分布式多团队并⾏开发的需求。如康威定律(Conway’s Law)所⾔,设计系统的组织,其产⽣的设计和架构等价于组织间的沟通结构;微服务与微前端不仅仅是技术架构的变化,还包含了组织⽅式、沟通⽅式的变化。微服务与微前端原理和软件⼯程,⾯向对象设计中的原理同样相通,都是遵循单⼀职责(Single Responsibility)、关注分离(Separation of Concerns)、模块化(Modularity)与分⽽治之(Divide & Conquer)等基本的原则。

课程目标

  1. 微前端如何解决业务场景的痛点,以 qiankun 为例接⼊微前端。
  2. 微前端的核⼼实现原理,从0到1实现简单的微前端框架。

知识要点

微前端的背景

微前端是什么

⼀种类似于微服务的架构,是⼀种由独⽴交付的多个前端应⽤组成整体的架构⻛格,将前端应⽤分解成⼀些更⼩、更简单的能够独⽴开发、测试、部署的应⽤,⽽在⽤户看来仍然是内聚的单个产品。

现代web应⽤⾯临的问题

  • DX(developer experience)

    • 多个系统在⼀个仓库应⽤中,不同⼦应⽤独⽴SPA模式
    • 系统分为多个仓库,独⽴上线部署,采⽤MPA模式
  • UX(user experience)

    • 性能体验
    • ⻚⾯跳转和⽤户体验问题

解决⽅案-微前端

微前端的意义

微前端的主要特点

  • 低耦合:当下前端领域,单⻚⾯应⽤(SPA)是⾮常流⾏的项⽬形态之⼀,⽽随着时间的推移以及应⽤功能的丰富,单⻚应⽤变得不再单⼀⽽是越来越庞⼤也越来越难以维护,往往是改⼀处⽽动全身,由此带来的发版成本也越来越⾼。微前端的意义就是将这些庞⼤应⽤进⾏拆分,并随之解耦,每个部分可以单独进⾏维护和部署,提升效率。
  • 不限技术栈:在不少的业务中,或多或少会存在⼀些历史项⽬,这些项⽬⼤多以采⽤⽼框架类似(Backbone.js,Angular.js 1)的B端管理系统为主,介于⽇常运营,这些系统需要结合到新框架中来使⽤还不能抛弃,对此我们也没有理由浪费时间和精⼒重写旧的逻辑。⽽微前端可以将这些系统进⾏整合,在基本不修改来逻辑的同时来同时兼容新⽼两套系统并⾏运⾏。

微前端解决的问题

  • 业务领域的代码库不够独立和高度可重用
  • 相同的产品功能由多个团队开发/产品功能难以保持统一
  • 新的产品理念无法在不同的应用中快速复用/实现
  • 快速迭代新子业务/干净移除将被淘汰的子业务
  • 提升构建效率
  • 改善交付效率
  • 架构渐进升级
  • 子团队的独立性

微前端的价值

微前端的方案

微前端应该具备哪些能力

⼀些可以实现微前端的⽅案

  • 使⽤ HTTP 服务器的路由来重定向多个应⽤

  • 在不同的框架之上设计通讯、加载机制,诸如 Single-SPA qiankun icestark

  • 通过组合多个独⽴应⽤、组件来构建⼀个单体应⽤

    • 微前端之构建时⽅案(Module Federation EMP)
  • iframe。使⽤ iframe 及⾃定义消息传递机制

  • 使⽤纯 Web Components 构建应⽤
  • 结合 Web Components 构建

业界主流的微前端框架

  • single-spa :社区公认的主流⽅案,可以基于它做⼆次开发
  • qiankun :基于 single-spa 封装,增加 umi 特⾊,增加沙箱机制(JS、ShadowDOM 等)
  • icestark :类似于 single-spa 实现,React 技术栈友好,阿⾥的另⼀个轮⼦

基于 qiankun 的微前端实战

创建主应用基座

基坐主要实现微应⽤框架的初始化和注册等,通常没有具体的应⽤业务逻辑在⾥边

创建主应用
  • 使用 React 创建主应用
1
2
npx create-react-app main
cd main && npm i qiankun
  • 使用 Vue 创建主应用
1
2
3
npm install -g @vue/cli
vue create main
cd main && npm i qiankun
创建微应用容器

在主应⽤中创建微应⽤的承载容器,这个容器规定了微应⽤的显示区域,微应⽤将在该容器内渲染并显示。

注册微应用

构建好了主框架后,需要使⽤ 的 registerMicroApps ⽅法注册微应⽤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// index.js
import { registerMicroApps } from 'qiankun';

registerMicroApps([
{
name: 'vue1', // app name registered
entry: '//localhost:8080',
container: '#micro-container',
activeRule: '/vue1',
},
{
name: 'vue2',
entry: "http://localhost:8081/",
container: '#micro-container',
activeRule: '/vue2',
},
]);
启动微应用
1
2
3
4
// index.js
import { registerMicroApps, start } from 'qiankun';
// registerMicroApps
start();

接入微应用

qiankun 内部通过 import-entry-html 加载微应⽤,要求微应⽤需要导出⽣命周期钩⼦函数

  1. 导出相应的生命周期钩子
    微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
    * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
    */
    export async function bootstrap() {
    console.log('react app bootstraped');
    }

    /**
    * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
    */
    export async function mount(props) {
    ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
    }

    /**
    * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
    */
    export async function unmount(props) {
    ReactDOM.unmountComponentAtNode(
    props.container ? props.container.querySelector('#root') : document.getElementById('root'),
    );
    }

    /**
    * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
    */
    export async function update(props) {
    console.log('update props', props);
    }
  2. 配置微应用的打包工具
    除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const packageName = require('./package.json').name;

    module.exports = {
    output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${packageName}`,
    },
    };
接入 Vue 微应用

qiankun Doc

接入 React 微应用

qiankun Doc

补充知识点

DIY 微前端实现核心能力

应用注册

  • registerMicroApps

监听路由变化

  • history
  • hashHistory

生命周期

  • 主应用
    • beforeLoad:挂载子应用前
    • mounted:挂载子应用后
    • unmounted:卸载子应用
  • 子应用
    • bootstrap:首次应用加载触发,常用于配置子应用全局信息
    • mount:应用挂载时触发,常用于渲染子应用
    • unmount:用于卸载时触发,常用于销毁子应用

路由劫持

  • 路由变化时匹配子应用
  • 执行子应用生命周期
  • 加载子应用

资源加载

  • 加载样式表
  • 提取 js 代码
  • 执行 js 渲染

JS 沙箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
export class ProxySandbox {
proxy: any
running = false
constructor() {
const fakeWindow = Object.create(null)
const proxy = new Proxy(fakeWindow, {
set: (target: any, p: string, value: any) => {
if (this.running) {
target[p] = value
}
return true
},
get(target: any, p: string): any {
switch (p) {
case 'window':
case 'self':
case 'globalThis':
return proxy
}
if (
!window.hasOwnProperty.call(target, p) &&
window.hasOwnProperty(p)
) {
// @ts-ignore
const value = window[p]
if (typeof value === 'function') return value.bind(window)
return value
}
return target[p]
},
has() {
return true
},
})
this.proxy = proxy
}
active() {
this.running = true
}
inactive() {
this.running = false
}
}

预加载