微前端架构

一、前端(架构)发展史

最初,前端是没有架构的,因为功能简单的代码没有架构可言。通过操作DOM就能完成的工作,不需要复杂的设计模式和代码管理机制,也就不需要架构来支撑起应用。前端开发的发展历史分为以下几个阶段:

● 古典时期。由后端渲染出前端 HTML,用 Table 布局,用 CSS 进行简单的辅助。
● 动效时期。前端开始编写一些简单的 JavaScript 脚本来做动画效果,如轮播广告。
● Ajax 异步通信时期。2005年,Google 在诸多 Web 应用中使用了异步通信技术如 Google 地图,开启了 Web 前端的一个新时代。

一旦前端应用需要从后端获取数据,就意味着前端应用在运行时是动态地渲染内容的,这便是 Model(模型)UI 层解耦。jQuery 能够提供 DOM 操作方法和模板引擎等。这时的开发人员需要做下面两个事情:

● 动态生成 HTML。由后端返回前端所需要的 HTML,再动态替换页面的 DOM 元素。早期的典型架构如 jQuery Mobile,事先在前端写好模板与渲染逻辑,用户的行为触发后台并返回对应的数据,来渲染文件。
● 模板分离。由后端用 API 返回前端所需要的 JSON 数据,再由前端来计算生成这些 HTML。前端的模板不再使用 HTML,而是使用诸如 Mustache 这样的模板引擎来渲染 HTML。

由于 HTML 的动态生成、模板的独立与分离,前端应用开始变得复杂。后端的 MVC 架构进一步影响了前端开发,便诞生了一系列早期的 MVC 框架,如 Backbone,Knockout,等等。

与此同时,在 Ryan Lienhart Dahl 等人开发了 Node.js 之后,前端的软件工程便不断地改善:

● 更好的构建工具。诞生了诸如 Grunt 和 Gulp 等构建工具。
● 包管理。产生了用于前端的包管理工具 Bower 和 NPM。
● 模块管理。也出现了 AMD、Common.js 等不同的模块管理方案。

随着单页面应用的流行,前后端分离架构也成为行业内的标准实践。由此,前端进入了一个新的时代,要考虑的内容也越来越多:

● API 管理,采用了诸如 Swagger的API 管理工具,各式的 Mock Server 也成为标准实践。
● 大前端,由前端来开发跨平台移动应用框架,采用诸如 Ionic、React Native、Flutter 等框架。
● 组件化,前端应用从此由一个个细小的组件结合而成,而不再是一个大的页面组件。

系统变得越来越复杂,架构在前端的作用也变得越来越重要。MVC 满足不了开发人员的需求,于是采用了组件化架构。

而组件化 + MV* 也无法应对大型的前端应用。

微前端架构_第1张图片
微前端便又出现在我们的面前,它解决了以下问题:

● 跨框架。在一个页面上运行,可以同时使用多个前端框架。
● 应用拆分。将一个复杂的应用拆解为多个微小的应用,类似于微服务。
● 遗留系统迁移。让旧的前端框架,可以直接嵌入现有的应用运行。

复杂的前端应用发展了这么久,也出现了一系列需要演进的应用——考虑重写、迁移、重构,等等。

二、什么是微前端架构

微前端不是单纯的前端框架或者工具,而是一套架构体系,这个概念最早在 2016 年底被提出,可以参考在 Google 上搜索 Micro-Frontends, 排名靠前的 https://micro-frontends.org 的博客文章,提出了早期的微前端模型。

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. – Micro Frontends

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

微前端的实现意味着对前端应用的拆分。拆分应用的目的并不只是为了在架构上好看,它还可以提升开发效率。比如10万行的代码拆解成10个项目,每个项目1万行代码,要独立维护每个项目就会容易得多。而我们只需要实现应用的自治,即实现应用的独立开发和独立部署,就可以在某种程度上实现微前端架构的目的。

三、微前端的核心价值

  1. 拆分巨型应用

微前端架构_第2张图片

  1. 遗留系统增量迁移

迁移是一项非常耗时且艰难的任务,比如有一个管理系统使用 AngularJS 开发维护已经有三年时间,但是随时间的推移和团队成员的变更,无论从开发成本还是用人需求上,AngularJS 已经不能满足要求,于是团队想要更新技术栈,想在其他框架中实现新的需求,但是现有项目怎么办?直接迁移是不可能的,在新的框架中完全重写也不太现实。

使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求, 然后再使用微前端架构将旧的项目和新的项目进行整合。这样既可以使产品得到更好的用户体验, 也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。

不重写原有系统,同时抽出人力来开发新的业务,这对业务人员来说,是一个相当有吸引力的特性,而且对技术人员来说,也是一件相当不错的事情。人生苦短,请尽量不重写。

  1. 独立发布

在目前的单页应用架构中,使用组件构建用户界面,应用中的每个组件或功能开发完成或者bug修 复完成后,每次都需要对整个产品重新进行构建和发布,任务耗时操作上也比较繁琐。

在使用了微前端架构后,可以将不能的功能模块拆分成独立的应用,此时功能模块就可以单独构建单独发布了,构建时间也会变得非常快,应用发布后不需要更改其他内容应用就会自动更新,这意味着你可以进行频繁的构建发布操作了。

微前端架构_第3张图片

  1. 允许单个团队做出技术决策

因为微前端构架与框架无关,当一个应用由多个团队进行开发时,每个团队都可以使用自己擅长的 技术栈进行开发,也就是它允许适当的让团队决策使用哪种技术,从而使团队协作变得不再僵硬。

  1. 聚合前端应用,更利于用户体验

四、微前端也不是银弹

微前端帮助开发者解决了实际的问题,但是对于每个业务来说,是否适合使用微前端,以及是否正确的使用微前端,还是需要遵循以下一些原则:

  1. 微前端最佳的使用场景是一些 B 端的管理系统,既能兼容集成历史系统,也可以将新的系统集成进来,并且不影响原先的交互体验。
  2. 整体的微前端不仅仅是只将系统集成进来,而是整个微前端体系的完善,这其中就包括:

● 基座应用和微应用的自动部署能力。
● 微应用的配置管理能力。
● 本地开发调试能力。
● 线上监控和统计能力等等。

只有将整个能力体系搭建完善,才能说是整个微前端体系流程的完善。

除此之外,它也有一系列的缺点:

● 应用的拆分基础依赖于基础设施的构建,一旦大量应用依赖于同一基础设施,那么维护变成了一个挑战。
● 拆分的粒度越小,便意味着架构变得复杂、维护成本变高。
● 技术栈一旦多样化,便意味着技术栈混乱

当发现使用微前端反而使效率变低,简单的变更复杂那就说明微前端并不适用。

五、如何实现微前端

从技术实践上,微前端架构可以采用以下几种方式进行:

(1)路由分发式。通过 HTTP 服务器的反向代理功能,将请求路由到对应的应用上。

(2)前端微服务化。在不同的框架之上设计通信和加载机制,以在一个页面内加载对应的应用。

(3)微应用。通过软件工程的方式,在部署构建环境中,把多个独立的应用组合成一个单体应用。

(4)微件化。开发一个新的构建系统,将部分业务功能构建成一个独立的 chunk 代码,使用时只需要远程加载即可。

(5)前端容器化。将 iframe 作为容器来容纳其他前端应用。

(6)应用组件化。借助于 Web Components 技术,来构建跨框架的前端应用。

实施的方式虽然多,但都是依据场景而采用的。在有些场景下,可能没有合适的方式;在有些场景下,则可以同时使用多种方案。

六、微前端架构模式

从微前端应用间的关系来看分为两种:基座模式(管理式)、自组织式,分别对应两种不同的架构模式:

● 基座模式。通过一个主应用来管理其他应用。设计难度小、方便实践,但是通用度低。
● 自组织模式。应用之间是平等的,不存在相互管理的模式。设计难度大,不方便实施,但是通用度高。

就当前而言,基座模式实施起来比较方便,方案上也是蛮多的。

不论哪种方式,都需要提供一个查找应用的机制,在微前端中称为服务的注册表模式。和微服务架构相似,不论哪种微前端方式,都需要有一个应用注册表的服务,它可以是一个固定值的配置文件,如JSON文件,或者是一个可动态更新的配置,又或者是一种动态的服务。

它主要做以下一些事情:

● 应用发现。让主应用可以寻找到其他应用。
● 应用注册。即提供新的微前端应用,向应用注册表注册的功能。
● 第三方应用注册。即让第三方应用接入系统中。
● 访问权限等相关配置。

应用在部署的时候,可以在注册表服务中注册。如果基于注册表来管理应用,那么使用基座模式来开发就比较方便。

中心化:基座模式

在这种模式的微前端架构中,基座承担了微前端应用的基础与技术核心。基座模式,是由一个主应用和一系列业务子应用构成的系统,并由这个主应用来管理其他子应用,包括从子应用的生命周期管理到应用间的通信机制。

基座模式中的主应用,类似于 API Gateway 的概念,它作为系统的统一入口,负责将对应的请求指向对应的服务。子应用,则是负责各个子模块的业务实现,其架构如图所示。

微前端架构_第4张图片
这个主应用,既可以只带有单纯的基座功能,也可以带有业务功能。它所处理的业务功能指的是核心部分的业务功能,如:

● 用户的登录、注册管理。
● 系统的统一鉴权管理。
● 导航菜单管理。
● 路由管理。
● 数据管理。
● 通信代理。
● ……

作为应用的基础核心,它还需要:

● 维护应用注册表。在应用注册表上表明系统有多少个服务、能否找到对应的应用等。
● 管理其他子应用。如在何时加载应用、何时卸载应用等。

要实现这种模式的微前端架构,只需要设计好对应的应用加载机制即可,因此在实施的时候也比较方便。

去中心化:自组织模式

去中心化自组织模式指的是,系统内部各子系统之间能自行按照某种规则形成一定的结构或功能。采用这种模式可以使系统内的各种前端应用,都各自拥有一个小型的基座管理功能,也相当于每个应用都可以是基座。

在采用基座模式时,用户要想访问A应用需要先加载主应用,然后才能加载A应用。采用自组织模式时,用户想要访问A应用则只访问A应用,不需要加载主应用,这也因此使它拥有了更高的自主性。

不过多数时候,我们并不需要自组织模式的微前端架构,因为它设计起来复杂、拥有大量的重复代码。

七、微前端框架方案

目前业界已经有不少框架来帮助开发者轻松的集成微前端架构,其中比较主流的有:

● Single-Spa:最早的微前端框架(2018年),兼容多种前端技术栈,核心只做路由劫持和应用加载。
○ 本身没有处理样式隔离、JavaScript 执行隔离
● Qiankun:诞生于2019年,基于 Single-Spa,阿里系开源微前端框架。
○ single-spa + sandbox + import-html-entry
○ 本身解决了样式隔离、JavaScript 执行隔离
○ 接入简单
● 2020年 webpack5 模块联邦
● 2020 年 EMP 基于 Module Federation(模块联邦),接入成本低,解决第三方依赖包的问题
● Icestark:阿里飞冰微前端框架,兼容多种前端技术栈。

八、qiankun

qiankun 介绍

微前端架构_第5张图片

● qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
● 使用简单
○ 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
○ HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
● 功能完备:几乎包含所有构建微前端系统时所需要的基本能力。
○ 样式隔离,确保微应用之间样式互相不干扰。
○ JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
○ ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
○ umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
● 生产可用:qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 qiankun 打磨的更加成熟完善。目前 qiankun 已在蚂蚁内部服务了超过 200+ 线上应用,在易用性及完备性上,绝对是值得信赖的。
● 架构思想:中心化基座模式的微前端典型代表,由一个主应用和一系列业务子应用构成的系统,并由这个主应用来管理其他子应用,包括从子应用的生命周期管理到应用间的通信机制。

九、qiankun 主应用

主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。

这里以 Vue 2 应用为例演示主应用的搭建。

1、使用 Vue CLI 创建 Vue 2 应用

mkdir qiankun-examples

cd qiankun-examples

vue create main

Vue CLI v4.5.15
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router
? Choose a version of Vue.js that you want to start the project with 2.x
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

cd main

npm run serve

2、安装 qiankun

npm i qiankun -S # 或者 yarn add qiankun

3、注册微应用并启动

import { registerMicroApps, start } from 'qiankun'

registerMicroApps([
  {
    name: 'reactApp',
    entry: '//localhost:3000',
    container: '#container',
    activeRule: '/app-react',
  },
  {
    name: 'vueApp',
    entry: '//localhost:8080',
    container: '#container',
    activeRule: '/app-vue',
  },
  {
    name: 'angularApp',
    entry: '//localhost:4200',
    container: '#container',
    activeRule: '/app-angular',
  },
])

// 启动 qiankun
start()

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

十、qiankun 微应用

微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。

微应用分为有 webpack 构建和无 webpack 构建项目,有 webpack 的微应用(主要是指 Vue、React、Angular)需要做的事情有:

  1. 新增 public-path.js 文件,用于修改运行时的 publicPath。什么是运行时的 publicPath ?。

注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。

  1. 微应用建议使用 history 模式的路由,需要设置路由 base,值和它的 activeRule 是一样的。
  2. 在入口文件最顶部引入 public-path.js,修改并导出三个生命周期函数。
  3. 修改 webpack 打包,允许开发环境跨域和 umd 打包。

主要的修改就是以上四个,可能会根据项目的不同情况而改变。例如,你的项目是 index.html 和其他的所有文件分开部署的,说明你们已经将构建时的 publicPath 设置为了完整路径,则不用修改运行时的 publicPath (第一步操作可省)。

无 webpack 构建的微应用直接将 lifecycles 挂载到 window 上即可。

1、React 微应用

以 create react app 生成的 react 16 项目为例,搭配 react-router-dom 5.x。

  1. 在 src 目录新增 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 设置 history 模式路由的 base:
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
  1. 入口文件 index.js 修改,为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。
import './public-path';import React from 'react';import ReactDOM from 'react-dom';import App from './App';
function render(props) {  const { container } = props;  ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));}
if (!window.__POWERED_BY_QIANKUN__) {  render({});}
export async function bootstrap() {  console.log('[react16] react app bootstraped');}
export async function mount(props) {  console.log('[react16] props from main framework', props);  render(props);}
export async function unmount(props) {  const { container } = props;  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));}
  1. 修改 webpack 配置
    安装插件 @rescripts/cli,当然也可以选择其他的插件,例如 react-app-rewired。
    根目录新增 .rescriptsrc.js:
    修改 package.json:
npm i -D @rescripts/cli
const { name } = require('./package')

module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`
    config.output.libraryTarget = 'umd'
    config.output.jsonpFunction = `webpackJsonp_${name}`
    config.output.globalObject = 'window'

    return config
  },

  devServer: (_) => {
    const config = _

    config.headers = {
      'Access-Control-Allow-Origin': '*'
    }
    config.historyApiFallback = true
    config.hot = false
    config.watchContentBase = false
    config.liveReload = false
    config.port = 3000

    return config
  }
}
-   "start": "react-scripts start",
+   "start": "rescripts start",
-   "build": "react-scripts build",
+   "build": "rescripts build",
-   "test": "react-scripts test",
+   "test": "rescripts test",
-   "eject": "react-scripts eject"
  1. 解决使用 create-react-app 创建的 React 微应用 socket 连接导致基座报错问题。
    安装 cross-env 用来设置跨操作系统的环境变量。
    在启动命令中设置开发环境中热更新请求的端口号。
npm i -D cross-end
- "start": "rescripts start",
+ "start": "cross-env WDS_SOCKET_PORT=3000 rescripts start",

2、Vue 2 微应用

以 vue-cli 3+ 生成的 vue 2.x 项目为例,vue 3 版本等稳定后再补充。

  1. 在 src 目录新增 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 入口文件 main.js 修改,为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。
import './public-path';import Vue from 'vue';import VueRouter from 'vue-router';import App from './App.vue';import routes from './router';import store from './store';
Vue.config.productionTip = false;
let router = null;let instance = null;function render(props = {}) {  const { container } = props;  router = new VueRouter({    base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',    mode: 'history',    routes,  });
  instance = new Vue({    router,    store,    render: (h) => h(App),  }).$mount(container ? container.querySelector('#app') : '#app');}
// 独立运行时if (!window.__POWERED_BY_QIANKUN__) {  render();}
export async function bootstrap() {  console.log('[vue] vue app bootstraped');}export async function mount(props) {  console.log('[vue] props from main framework', props);  render(props);}export async function unmount() {  instance.$destroy();  instance.$el.innerHTML = '';  instance = null;  router = null;}
  1. 打包配置修改(vue.config.js)
const { name } = require('./package');
module.exports = {
  devServer: {
  // 主要 允许跨域********************
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

3、Angular 微应用

以 Angular-cli 9 生成的 angular 9 项目为例,其他版本的 angular 后续会逐渐补充。

  1. 在 src 目录新增 public-path.js 文件,内容为:
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 设置 history 模式路由的 base,src/app/app-routing.module.ts 文件:
+ import { APP_BASE_HREF } from '@angular/common';

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  // @ts-ignore
+  providers: [{ provide: APP_BASE_HREF, useValue: window.__POWERED_BY_QIANKUN__ ? '/app-angular' : '/' }]
})
  1. 修改入口文件,src/main.ts 文件。
import './public-path';import { enableProdMode, NgModuleRef } from '@angular/core';import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';import { AppModule } from './app/app.module';import { environment } from './environments/environment';
if (environment.production) {  enableProdMode();}
let app: void | NgModuleRef<AppModule>;async function render() {  app = await platformBrowserDynamic()    .bootstrapModule(AppModule)    .catch((err) => console.error(err));}if (!(window as any).__POWERED_BY_QIANKUN__) {  render();}
export async function bootstrap(props: Object) {  console.log(props);}
export async function mount(props: Object) {  render();}
export async function unmount(props: Object) {  console.log(props);  // @ts-ignore  app.destroy();}
  1. 修改 webpack 打包配置
    先安装 @angular-builders/custom-webpack 插件,注意:angular 9 项目只能安装 9.x 版本,angular 10 项目可以安装最新版。
    在根目录增加 custom-webpack.config.js ,内容为:
    修改 angular.json,将 [packageName] > architect > build > builder 和 [packageName] > architect > serve > builder 的值改为我们安装的插件,将我们的打包配置文件加入到 [packageName] > architect > build > options。
npm i @angular-builders/custom-webpack@9.2.0 -D
const appName = require('./package.json').name;
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  output: {
    library: `${appName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${appName}`,
  },
};
- "builder": "@angular-devkit/build-angular:browser",
+ "builder": "@angular-builders/custom-webpack:browser",
  "options": {
+    "customWebpackConfig": {
+      "path": "./custom-webpack.config.js"
+    }
  }
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "@angular-builders/custom-webpack:dev-server",
  1. 解决 zone.js 的问题
    在父应用引入 zone.js,需要在 import qiankun 之前引入。
    将微应用的 src/polyfills.ts 里面的引入 zone.js 代码删掉。
    在微应用的 src/index.html 里面的 标签加上下面内容,微应用独立访问时使用。
- import 'zone.js/dist/zone';
<!-- 也可以使用其他的CDN/本地的包 -->
<script src="https://unpkg.com/zone.js" ignore></script>
  1. 修正 ng build 打包报错问题,修改 tsconfig.json 文件,参考 issues/431
- "target": "es2015",
+ "target": "es5",
+ "typeRoots": [
+   "node_modules/@types"
+ ],
  1. 为了防止主应用或其他微应用也为 angular 时, 会冲突的问题,建议给 加上一个唯一的 id,比如说当前应用名称。
    src/index.html :
    src/app/app.component.ts :
- <app-root></app-root>
+ <app-root id="angular9"></app-root>
- selector: 'app-root',
+ selector: '#angular9 app-root',

当然,也可以选择使用 single-spa-angular 插件,参考 single-spa-angular 的官网 和 angular demo

(补充)angular7 项目除了第 4 步以外,其他的步骤和 angular9 一模一样。angular7 修改 webpack 打包配置的步骤如下:

除了安装 angular-builders/custom-webpack 插件的 7.x 版本外,还需要安装 angular-builders/dev-server。

npm i @angular-builders/custom-webpack@7 -D
npm i @angular-builders/dev-server -D

在根目录增加 custom-webpack.config.js ,内容同上。

修改 angular.json, [packageName] > architect > build > builder 的修改和 angular9 一样, [packageName] > architect > serve > builder 的修改和 angular9 不同。

- "builder": "@angular-devkit/build-angular:browser",
+ "builder": "@angular-builders/custom-webpack:browser",
  "options": {
+    "customWebpackConfig": {
+      "path": "./custom-webpack.config.js"
+    }
  }
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "@angular-builders/dev-server:generic",

十一、非 webpack 构建的微应用

一些非 webpack 构建的项目,例如 jQuery 项目、jsp 项目,都可以按照这个处理。

接入之前请确保你的项目里的图片、音视频等资源能正常加载,如果这些资源的地址都是完整路径(例如 https://qiankun.umijs.org/logo.png),则没问题。如果都是相对路径,需要先将这些资源上传到服务器,使用完整路径。

接入非常简单,只需要额外声明一个 script,用于 export 相对应的 lifecycles。例如:

  1. 声明 entry 入口
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Purehtml Example</title>
</head>
<body>
  <div>
    Purehtml Example
  </div>
</body>

+ <script src="//yourhost/entry.js" entry></script>
</html>
  1. 在 entry.js 里声明 lifecycles
const render = ($) => {  $('#purehtml-container').html('Hello, render with jQuery');  return Promise.resolve();};
((global) => {  global['purehtml'] = {    bootstrap: () => {      console.log('purehtml bootstrap');      return Promise.resolve();    },    mount: () => {      console.log('purehtml mount');      return render($);    },    unmount: () => {      console.log('purehtml unmount');      return Promise.resolve();    },  };})(window);

你也可以直接参照 examples 中 purehtml 部分的代码

同时,你也需要开启相关资源的 CORS,具体请参照此处

十二、Vite 构建的微应用

目前没有很好的支持,具体可以参考这个官方仓库 issue。

社区还有一个封装好的 vite 插件 vite-plugin-qiankun,可以试试。

十三、手动加载微应用

https://qiankun.umijs.org/zh/api#手动加载微应用

如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:

import { loadMicroApp } from 'qiankun';

loadMicroApp({
  name: 'app',
  entry: '//localhost:7100',
  container: '#yourContainer',
});

十四、统一加载多个应用

多个应用的手动启动比较麻烦,这里我们可以通过 npm scripts 来快速启动多个应用。

  1. 在所有应用的父目录下配置 npm scripts 启动脚本:
cd qiankun-examples

npm init -y
"scripts": {
	"start": "npm run start:main && npm run start:app1 && npm run app2 && npm run app3",
  "start:main": "cd main && npm start",
  "start:app1": "cd app1 && npm start",
  "start:app2": "cd app2 && npm start",
  "start:app3": "cd app3 && npm start"
}
  1. 最后执行 qiankun-examples 目录中的 npm start 就可以把所有应用启动起来了。

使用 npm-run-all

使用 npm-run-all 可以把诸如 npm run clean && npm run build:css && npm run build:js && npm run build:html 的一长串命令通过 glob 语法简化成 npm-run-all clean clean build:* 这样精致小巧的模样。此外 npm-run-all 还支持并行运行多个任务。

  1. 在所有应用的父目录安装 npm-run-all
npm i -D npm-run-all

配置 npm scripts

"scripts": {
	"start": "npm-run-all --parallel start:*",
  "start:main": "cd main && npm start",
  "start:app1": "cd app1 && npm start",
  "start:app2": "cd app2 && npm start",
  "start:app3": "cd app3 && npm start"
}

启动所有应用

npm start

需要注意如果脚本退出时返回空值,所有其它子进程都会被 SIGTERM 信号中断,可以用--continue-on-error 参数禁用行为。

npm-run-all 的其它用法示例:

# 这段命令首先按顺序执行 clean lint 两个脚本,然后同时执行 watch:html 和 watch:js 的任务。
npm-run-all clean lint --parallel watch:html watch:js

# 顺序运行 a 和 b;
# 然后同时运行 c 和 d;
# 再依次运行 e 和 f;
# 最后同时执行 g, h, i
npm-run-all a b --parallel c d --sequential e f --parallel g h i
# 或者
npm-run-all a b --parallel c d --serial e f --parallel g h i

# 你可以使用 Glob 通配符来匹配任务名称,方便指定多个名称相似的任务,和标准语法不同的是分隔符由 / 改为 : 以适应需要。
npm-run-all --parallel watch:*

# 匹配分隔符,所有以 watch: 开头的脚本都会被运行。
npm-run-all --parallel watch:**

# 在脚本名称后使用双引号包裹来提供参数,甚至还支持用占位符,延迟到运行命令时再提供参数。
npm-run-all build "start-server -- --port {1}" -- 8080

更多使用方式参考 npm-run-all 官方稳定。

十五、加载过程的生命周期

registerMicroApps 方法的第二个参数可以配置加载子应用过程的生命周期钩子。

type Lifecycle = (app: RegistrableApp) => Promise<any>;

● beforeLoad - Lifecycle | Array - 可选,加载加载子应用前
● beforeMount - Lifecycle | Array - 可选,开始渲染子应用前
● afterMount - Lifecycle | Array - 可选,已完成子应用的渲染
● beforeUnmount - Lifecycle | Array - 可选,开始卸载子应用前
● afterUnmount - Lifecycle | Array - 可选,子应用已完成卸载

比如我们利用这些接口给子应用加入加载动画等功能。

npm i nprogress

代码如下

import NProgress from "nprogress";
import "nprogress/nprogress.css";
import {
  registerMicroApps,
  addGlobalUncaughtErrorHandler,
  start,
} from "qiankun";

registerMicroApps(apps, {
  // qiankun 生命周期钩子 - 微应用加载前
  beforeLoad: (app: any) => {
    // 加载微应用前,加载进度条
    NProgress.start();
    console.log("before load", app.name);
    return Promise.resolve();
  },
  // qiankun 生命周期钩子 - 微应用挂载后
  afterMount: (app: any) => {
    // 加载微应用前,进度条加载完成
    NProgress.done();
    console.log("after mount", app.name);
    return Promise.resolve();
  },
});

十六、全局异常捕获

/**
 * 添加全局的未捕获异常处理器
 */
addGlobalUncaughtErrorHandler((event: Event | string) => {
  console.error(event);
  const { message: msg } = event as any;
  // 加载失败时提示
  if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
    message.error("微应用加载失败,请检查应用是否可运行");
  }
});

你可能感兴趣的