2

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

 1 year ago
source link: https://developer.51cto.com/article/710588.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们是怎么在项目中落地 Qiankun-51CTO.COM

1d399cf74af1eac48d98eb2e4df4f2aa.jpg
我们是怎么在项目中落地 Qiankun
作者:Gopal 2022-06-02 08:30:55
随着互联网的快速发展,公司业务发展也随之增长,这个时候一个系统共存多个子应用的需求也就应运而生了。微前端作为近几年很火的架构,解决的就是这类问题。

由于业务增长,团队拆分,我们需要将原有系统的一部分模块(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 是一个的不错选择。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK