我们是怎么在项目中落地 Qiankun

开发 项目管理
随着互联网的快速发展,公司业务发展也随之增长,这个时候一个系统共存多个子应用的需求也就应运而生了。微前端作为近几年很火的架构,解决的就是这类问题。

背景

由于业务增长,团队拆分,我们需要将原有系统的一部分模块(Vue实现)迁移到另外一个系统(React)中。但两个系统技术栈不同,导致重构成本变大,但业务又希望在短期内看到效果,后面可以增量的重构。

要求是对用户无感知的,真正将两个系统融合到一起。

经过技术调研,我们决定用微前端的方式实现。

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这跟我们现在的情况是相符的。它具有如下的特点:

  • 技术栈无关。主框架不限制接入应用的技术栈,微应用具备完全自主权
  • 独立开发、独立部署。微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 增量升级。在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 独立运行时。每个微应用之间状态隔离,运行时状态不共享

图片

技术选型

微前端是一种类似微服务的架构,目标是将单一的单体应用变成由多个小型应用聚合为一的应用。

经过调研,我们有以下的实现方案。

iframe

优点:

  • 提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决

缺点:

  • url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用
  • UI 不同步,DOM 结构不共享
  • 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果
  • 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程

缺点层面,暂时是无法满足业务的要求的,所以我们没有采取这种方案。

qiankun

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

它有以下的特性:

  • 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
  • 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  • 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  • 🛡 样式隔离,确保微应用之间样式互相不干扰。
  • 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  • ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
  • 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。

以上基本能满足我们的要求。

webpack Module Federation

webpack 5 的支持的特性。

单页应用的每个页面都是在单独的构建中从容器暴露出来的。主体应用程序(application shell)也是独立构建,会将所有页面作为远程模块来引用。通过这种方式,可以单独部署每个页面。在更新路由或添加新路由时部署主体应用程序。主体应用程序将常用库定义为共享模块,以避免在页面构建中出现重复。

优点:

  • 能够共享常用库(我们的项目比较特殊,主框架分别为 Vue 和 React,所以能共享的更多的是一些 moment.js / lodash / axios 这类工具库)

缺点:

  • 需要使用 webpack
  • 需要升级 webpack 5

qiankun 有一个缺点就是模块共享,如果能够和 webpack module federation 一起解决这个问题是一个不错的实践。但旧项目是基于 webpack4 构建,升级存在一定的风险,固没有采用这个方案。

其他框架

  • micro-app[1]。京东零售。micro-app是京东零售推出的一款微前端框架,它基于类WebComponent进行渲染,从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。GitHub Star 数[2]- 2.5k
  • emp[3]。欢聚时代。基于下一代构建实现微前端解决方案,结合了 webpack5 和 Module Federation。GitHub Star 数[4]- 2.7k
  • single-spa。qiankun 就是基于这个进行开发,做了一些优化,比如 开箱即用、HTML Entry。GitHub star 数[5]-11k

qiankun GitHub star 数[6]-12.4k。可以看到 qiankun 的社区也是非常活跃的,综上,我们最终选择拥抱 qiankun。

qiankun 主应用改造

我们的主应用主技术栈是 React, 第一步是安装:

yarn add qiankun

第二步是设置路由,这里的 path 需要有一个特殊的前缀,用于激活子应用,这里我们统一称为 ``/vueApp`,这个后面还会用到,大家请记住。

第三步添加渲染入口:

const ChargingContainer = () => <section id="micro-app-container" />;

第四步注册微应用,通过 qiankun 的 registerMicroApps 注册,name 微应用名称(这个后面也会用到,这里我就叫 vueAppName),entry 代表的微应用入口。container ,微应用的容器节点的选择器或者 Element 实例,就是第三步中的渲染入口中声明的。activeRule 是微应用的激活规则,支持数组,这里设置的就是我们第二步上面提到的 /vueApp。

import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
start,
} from 'qiankun';
import { initAppGlobalState } from './action';
import { CHARGING_ACTIVE_RULES } from './constant';

const apps = [
{
name: 'vueAppName', // app name registered
entry: `//localhost:9528/appVue`,
container: '#micro-app-container',
activeRule: `/${CHARGING_ACTIVE_RULES}`,
},
];

/**
* 注册微应用
* 第一个参数 - 微应用的注册信息
* 第二个参数 - 全局生命周期钩子
*/
registerMicroApps(apps, {
// qiankun 生命周期钩子 - 微应用加载前
beforeLoad: (app: any) => {
// eslint-disable-next-line
console.log('before load', app.name);
return Promise.resolve();
},
// qiankun 生命周期钩子 - 微应用挂载后
afterMount: async(app: any) => {
// eslint-disable-next-line
console.log('after mount', app.name);
await initAppGlobalState();
return Promise.resolve();
},
});

// 导出 qiankun 的启动函数
export default start;

第五步 qiankun 中的 start 函数,用来启动 qiankun。它可以通过 Options 传参开启一些有用的功能,比如 prefetch 预加载,sandbox 开启沙箱等。导出 start 在 App.ts 中启动即可。这里需要注意的 start 启动函数的时机,需要在微应用入口渲染完成之后才调用。

registerMicroApps 和 start 的图示(来自网络)。

图片

qiankun 注册微应用

我们微应用的主技术栈是 Vue。

在主应用注册好了微应用后,我们还需要对微应用进行一系列的配置。

第一步,我们在 Vue 的入口文件 main.js 中,导出 qiankun 主应用所需要的三个生命周期钩子函数(相关功能在代码注释中说明),代码实现如下:

let instance = null;
let ownRouter = router;

/**
* 渲染函数
* 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
*/
function render(props) {
// eslint-disable-next-line
console.log('render 子应用');
if (props) {
// 注入 actions 实例
actions.setActions(props);
}
ownRouter = router;
// 挂载应用
instance = new Vue({
router: ownRouter,
store,
i18n,
render: h => h(App) // 需要用render的方式渲染
}).$mount('#pms-app');
}

// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render();
}

/**
* bootstrap 只会在微应用初始化的时候调用一次
* 下次微应用重新进入时会直接调用 mount 钩子
* 不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化
* 比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
// eslint-disable-next-line
console.log('pmsMicroApp bootstraped');
}

/**
* 应用每次进入都会调用 mount 方法
* 通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
// eslint-disable-next-line
console.log('pmsMicroApp mount', props);

props.onGlobalStateChange((curState) => {
store.dispatch('InitUserInfo', curState.store);
setRequest(curState.createRequest);
render(props);
});
}

/**
* 应用每次 切出/卸载 会调用的方法
* 通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
// eslint-disable-next-line
console.log('pmsMicroApp unmount');
instance.$destroy();
instance = null;
ownRouter = null;
}

另外,需要注意的是,需要在 main.js 入口中 import './public-path'; 否则会导致资源加载 404,比如主应用是 http://a.com/,微应用是 http://b.com,假如不设置的话,会以 http://a.com/1.js 访问微应用静态资源,会产生错误。public-path.js 如下:

图片

// 设置动态配置路径
// 解决路由异构的问题:https://www.jianshu.com/p/5f99acb6aa10
if (window.__POWERED_BY_QIANKUN__ && process.env.NODE_ENV === 'development') {
// 动态设置 webpack publicPath,防止资源加载出错
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

这里解释一下,因为在开发环境中,两个是不同的域名,所以需要设置 __webpack_public_path__,我们线上还是使用同一个域名(后面部署的环节会讲到),所以非开发环境不需要设置 __webpack_public_path__。

第二步,我们还需要修改一下路由,因为之前添加了一个前缀 /vueApp,所以我们在路由中设置 base(我们使用的是 Vue Router 的 history 模式,这里没试过 hash 模式):

new Router({
base: window.__POWERED_BY_QIANKUN__ ? '/vueApp' : '/',
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
});

第三步,修改 webpack 构建打包配置,使 main.js 导出的生命周期钩子函数可以被 qiankun 识别。先是 devServer,要使微应用能够被 fetch 并配置相应的跨域请求头,解决开发环境的跨域问题:

devServer: {
// 关闭主机检查,使微应用可以被 fetch
disableHostCheck: true,
// 配置跨域请求头,解决开发环境的跨域问题
headers: {
"Access-Control-Allow-Origin": "*",
},
// ...
}

还需要配置导出方式,导出方式设置为:umd,就将我们的 library 暴露为所有的模块都可以运行的方式了(webpack 4 不支持对于 libraryTarget 设置为  module-ES Module。webpack 5 支持,但也还是实验阶段)。另外这个 library 设置的是微应用的包名,这里与主应用中注册的微应用名称一致。

output: {
//
// 微应用的包名,这里与主应用中注册的微应用名称一致
library: "vueAppName",
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
// 按需加载相关,设置为 webpackJsonp_pmsMicroApp 即可
jsonpFunction: `webpackJsonp_pmsMicroApp`,
},

至此,我们的微前端就搭建完成。

qiankun 通信

官方提供了 initGlobalState[7] 方法用于注册 MicroAppStateActions 实例用于通信。其使用的就是发布-订阅模式。

图片

  • setGlobalState:设置 globalState - 设置新的值时,内部将执行 浅检查,如果检查到 globalState 发生改变则触发通知,通知到所有的观察者函数。
  • onGlobalStateChange:注册观察者函数 - 响应 globalState 变化,在 globalState 发生改变时触发该观察者函数。
  • offGlobalStateChange:取观察者函数 - 该实例不再响应 globalState 变化。

图片

offGlobalStateChange:取观察者函数 - 该实例不再响应 globalState 变化。

主应用

在主应用中,通过 initGlobalState 和 setGlobalState 设置通信信息:

import { initGlobalState, MicroAppStateActions } from 'qiankun';

let globalState: any = {};
const actions: MicroAppStateActions = initGlobalState(globalState);
export async function initAppGlobalState() {
await actions.setGlobalState(globalState);
}

子应用

在子应用中,设置 Action 类,并将 onGlobalStateChange,setGlobalState 映射到类方法中,导出类实例。

// micro-app-vue/src/shared/actions.js
function emptyAction() {
console.warn("Current execute action is empty!");
}

class Actions {
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction
};
/**
* 设置 actions
*/
setActions(actions) {
this.actions = actions;
}
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange(...args);
}
setGlobalState(...args) {
return this.actions.setGlobalState(...args);
}
}

const actions = new Actions();
export default actions;

在挂载子应用的时候,会调用 render 方法。这时可以获取到相关的 props,并传给 action 实例:

if (props) {
// 注入 actions 实例
actions.setActions(props);
}

在需要使用的地方, 通过 onGlobalStateChange 监听获取:

actions.onGlobalStateChange(state => {
console.log('state: ', state);
}, true);

CSS 隔离

qiankun 加载子项目 css 样式机制大体为:挂载子应用时将子应用的 css 样式以 style 标签的形式插入并做快照,卸载子应用时再将快照内的 style 样式删除。

所以在加载子应用期间,若未开启 css 沙箱隔离,后加载的这些样式,可能会对整个系统的样式产生影响,对此,qiankun 提供了两种 css 沙箱功能,可以将子应用的样式包裹在沙箱容器内部,以此来达到样式隔离的目的。

qiankun 严格沙箱

在加载子应用时,添加 strictStyleIsolation: true 属性,实现形式为将整个子应用放到 Shadow DOM 内进行嵌入,完全隔离了主子应用

缺点:

  • 子应用的弹窗、抽屉、popover 因找不到主应用的 body 会丢失,或跑到整个屏幕外
  • 主应用不方便去修改子应用的样式

实验性沙箱

在加载子应用时,添加 experimentalStyleIsolation: true 属性,实现形式类似于 vue 中 style 标签中的 scoped 属性,qiankun 会自动为子应用所有的样式增加后缀标签,如:div[data-qiankun-microName]

缺点:

  • 子应用的弹窗、抽屉、popover因插入到了主应用的body,所以导致样式丢失或应用了主应用了样式。相关issue[8]
使用 postcss-selector-namespace

在子应用中,配置 postcss 插件,给子应用添加类前缀:

const postcssLoader = {
loader: 'postcss-loader',
options: {
// exclude: /node_modules/,
sourceMap: options.sourceMap,
plugins: [
selectorNamespace({ namespace: '.vueapp' }),
]
}
}

还是会存在上面插入 body 中的样式没有成功的问题,需要特殊处理。

主应用使用 CSS module,有特殊的情况特殊处理

上面也提到,子应用离开的时候,会销毁子应用的 style,而处于子应用的时候,我们页面大部分是子应用的 UI,所以我们尽可能保证主应用对子应用的无影响(主应用使用 CSS Module)。假如子应用对主应用有影响,我们就进行特殊处理。

因为我们主应用和子应用使用的框架是不一样的,所以冲突还比较少,所以目前使用这种方式。

部署

我们采用的是主应用和微应用都部署到同一个服务器(同一个 IP 和端口)的方式。将主应用部署在一级目录,微应用部署在二级目录。

需要注意:上面提到我们在路由中加了前缀 /vueApp,也是通过这个进行激活子应用。但是 activeRule 不能和微应用的真实访问路径一样,否则在主应用页面刷新会直接变成微前端应用页面。所以我们这里的二级目录名称为 microApp,跟 vueApp 区分开(只是举例说明)。

这里提到的微应用的真实访问路径就是微应用的 entry,我们设置为 ***/microApp/,然后子应用构建的时候,配置 webpack 构建时的 publicPath 为 microApp。

举例:

└── html/                     # 根文件夹
|
├── microApp/ # 存放微应用的文件夹
├── index.html # 主应用的index.html
├── css/ # 主应用的css文件夹
├── js/ # 主应用的js文件夹

主应用设置 entry 和 activeRules:

const apps = [
{
// ...
entry: `//localhost:9528/microApp`,
activeRule: `vueApp`,
},
];

子应用路由设置:

base: window.__POWERED_BY_QIANKUN__ ? '/vueApp/' : '/microApp/',

子应用 publicPath 配置为:/microApp/

总结

随着互联网的快速发展,公司业务发展也随之增长,这个时候一个系统共存多个子应用的需求也就应运而生了。微前端作为近几年很火的架构,解决的就是这类问题。

qiankun 作为一个相对成熟的微前端解决方案,目前社区活跃,开箱即用,并且提供较为完备的功能,比如样式隔离、JS 沙箱、预加载等。

本文记录了 qiankun 在我们业务中的落地时间,整体而言,使用相对简单,能够满足我们业务需求,问题大部分能够在网上找到答案。如果跟我们有一样的业务场景,qiankun 是一个的不错选择。

责任编辑:武晓燕 来源: 前端杂货铺
相关推荐

2021-09-09 08:02:46

项目Bundleless架构

2022-06-02 11:11:43

AI深度学习

2024-01-31 16:36:53

2023-07-14 13:34:34

StableDiffusion模型

2021-05-07 11:25:29

项目网关流量

2017-04-24 11:40:26

大数据制造企业

2019-04-04 12:59:03

微服务企业数字化

2022-06-21 14:18:06

RBACTienChin项目

2020-05-06 09:25:10

微前端qiankun架构

2018-03-20 05:33:17

2020-03-18 13:10:09

华为

2020-03-18 14:22:52

华为生态大学Σco时间区块链

2024-05-24 10:01:12

2012-07-13 09:35:58

PHP

2012-07-13 09:38:15

项目代码

2009-06-29 15:51:48

Spring容器

2020-01-21 21:48:07

架构平滑上云机房迁移

2017-07-20 13:11:46

Code ReviewPR评审

2015-03-21 06:41:50

oauth2.0监控宝

2010-12-31 16:22:04

Analysis Se
点赞
收藏

51CTO技术栈公众号