如何基于「qiankun」搭建微前端应用?

2,079 阅读7分钟

知识储备篇

什么是微前端( or 微前端架构 )?

定义

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

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

简单来说就是将大型企业级web应用拆分成一个个小型工程,也就是“微应用”,再通过主应用将微应用串联整合起来,实现所有应用页面的展示。

特点 & 优势

  • 技术栈无关

主框架不限制接入应用的技术栈,微应用具备完全自主权

  • 独立开发、独立部署

微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

  • 增量升级

在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

  • 独立运行时

每个微应用之间状态隔离,运行时状态不共享

核心价值

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

为什么选择微前端?

在开发一个企业级是中后台项目时,生命周期会相对较长,通常随着需求增加以及功能的不断完善,我们的项目会越来越庞大,此时我们的应用就会演变成一个巨石应用。随之而来就导致项目难维护,新人上手成本高。

梳理巨石应用的弊端如下:

  • 项目庞大引用依赖包多,打包上线慢;

  • 项目初期野蛮生长过度,代码难以维护,产品迭代举步维艰;

  • toB应用对于客户定制化需求,很难快速响应迭代;

所以,当我们有一个相对独立且完整的新需求,往往是由一个新团队来开发,并希望可支持独立开发&部署,最终期望有入口接入原应用,并和原应用优雅的融合,

Why Not Iframe?

确立了微前端架构后,当我们开始思考实现方案时,首先想到的就是iframe,那么我们可以来盘点一下iframe的优势和使用痛点。

优势(隔离)

js和css隔离,不需要考虑微应用和主应用之间的js和css冲突

使用痛点(难以突破的隔离)

  1. 无状态的URL,主应用浏览器刷新 iframe url 状态丢失,后退前进按钮无法控制iframe。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中。
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢,每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
  5. iframe高度不能自适应。

问题解决方案

  • 问题1:监听URL变化,js控制iframe的URL
  • 问题2: 无法解决,除非通过父子应用通信,实现一套“弹框 UI 的数据协议”,由父应用展示弹窗。
  • 问题3: 父子应用通信。
  • 问题4: 可以睁一只眼闭一只,暂不解决
  • 问题5: 无法解决

这些痛点解决起来很麻烦,有点强行解决了也会是父子应用耦合度很高,这使开发者不能专注业务逻辑,并给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。

什么是「乾坤(qiankun)」?

承接着上面提到的iframe,那么除此之外能否有一个相对无痛的微应用解决方案呢?

有,那就是qiankun。qiankun介绍

简介

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

特点

  • 📦 基于 single-spa 封装,提供了更加开箱即用的 API。

  • 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。

  • 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。

  • 🛡 样式隔离,确保微应用之间样式互相不干扰。

  • 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。

  • ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

  • 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。

核心API

其实qiankun的API很少,3分钟就能get,下面分两个场景来说明:

也可以在这里查看详细的文档:qiankun.umijs.org/zh/api

场景一:路由激活

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react app', // 微应用的名称,微应用之间必须确保唯一
    entry: '//localhost:7100',// 微应用入口,表示微应用html的访问地址
    container: '#yourContainer',// 微应用的容器节点的选择器或者 Element 实例
    activeRule: '/yourActiveRule',// 微应用的激活规则,匹配到这个前缀时会激活当前应用
  },
  {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);

start();// 启动 qiankun。有prefetch,sandbox的可选参数

场景二:手动加载

import { loadMicroApp } from 'qiankun';

loadMicroApp({
  name: 'app', // 微应用的名称,微应用之间必须确保唯一
  entry: '//localhost:7100', // 微应用入口,表示微应用的访问地址
  container: '#yourContainer', // 微应用的容器节点的选择器或者 Element 实例
});

由上面可以看出,使用乾坤的过程中,就是需要告诉乾坤需要加载的应用名字是什么,去哪里请求到页面,放到主应用的哪个元素下。

项目实战篇

基于qiankun(乾坤)搭建微前端项目

接下来分别从主应用和子应用两个方面来说明项目如何搭建,需要注意的是主应用和子应用都可以选择各自不同的技术栈,这不影响微前端架构的搭建,具体如下:

主应用

1. 安装 qiankun

$ yarn add qiankun # 或者 npm i qiankun -S

2. 在主应用中注册微应用

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react app', // app name registered
    entry: '//localhost:7100',
    container: '#yourContainer',
    activeRule: '/yourActiveRule',
  },
  {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);

start();

or 手动加载一个微应用

import { loadMicroApp } from 'qiankun';

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

比如,直接在某组件的生命周期中执行loadMicroApp,以引入一个微应用,示例如下:

import { FC, useState, useEffect } from 'react';
import { loadMicroApp } from 'qiankun';

const MicroAppA: FC = () => {
    const [app, setApp] = useState({})
    useEffect(() => {
        const a = loadMicroApp({
            name: 'social-relationship',
            entry: '//localhost:3001',
            container: '#reactContainer',
        });
        setApp(a);
        return () => {
            (app as any).unmount && (app as any).unmount();
        }
    }, [])

    return <div>
        <div id="reactContainer"></div>
    </div>
}

export default MicroAppA

微应用( React项目 )

1.创建项目

npx create-react-app my-app
cd my-app
npm start

引入react-router-dom,编写自己的业务页面代码。

2.在 src 目录新增 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef  
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

⚠️注意:如果使用ts,需要加一行注释避免构建报错。

  • __POWERED_BY_QIANKUN__ 代表是否是以微服务架构乾坤启动的,如果是通过主应用启动并访问到子应用,这个参数会是true;
  • __INJECTED_PUBLIC_PATH_BY_QIANKUN__代表由乾坤主应用注入的公共路径publicPath,其实就是在主应用代码中配置的entry,经过赋值之后就可以从__webpack_public_path__取到主应用的entry了;(ps: 这个值在一个iframe页面中,设置src的属性值时有用到)

3.设置 history 模式路由的 base

<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>

⚠️注意:此处的/app-react,需要和主应用协议好,主应用如果是希望在/app-react路由下展示微应用,需要这样设置,具体场景如下:

主应用中/app-react/koc这个路径的定义有讲究,可以说是完全取决于微应用,微应用必须定义了basename, 且存在/koc这个路由。主应用休想用/app-react/test这个path去匹配和展示微应用的/koc;

4.入口文件 index.js 修改

为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。

在接入qiankun时暴露出几个生命周期钩子函数,在不使用qiankun时,直接render;

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'));
}

5.修改 webpack 配置

安装插件 @rescripts/cli,当然也可以选择其他的插件,例如 react-app-rewired

npm i -D @rescripts/cli

根目录新增 .rescriptsrc.js, 配置构建出口的名称和一些参数,proxy设置允许跨域等;

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;

    return config;
  },
};

修改 package.json

-   "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"

微应用( vue项目 )

vue项目的改造方式可参考官网文章:

qiankun.umijs.org/zh/guide/tu…

微应用隔离解决方案

样式隔离

背景

最初我们在接入主应用时遇到的问题是,微应用和主应用同时使用了antd或者是同一个class类名,就出现了一个问题,当加载了子应用后,主应用的样式受到了影响;

思考

所以使用微应用的时候,我们需要考虑样式隔离问题,我们可以思考到的三个不同场景,以及期望应该是如下:

  • 主应用样式不影响某个微应用样式
  • 某个微应用样式不能影响主应用样式
  • 某个微应用的样式不能影响其他微应用的样式

当然在qiankun设计的时候也考虑到了样式隔离的问题,并给我们提供一些提示,官方文档对此的说明如下:

官网链接:API 说明

方案

这里我们只考虑简单的单实例场景来看看这几个配置方案的作用:

sandbox的值

效果

问题

true(默认)

子应用之间隔离

主应用和子应用没有隔离

{ experimentalStyleIsolation: true }

子应用之间隔离 + 子应用不影响主应用

( 子应用样式加了特殊选择器,来限制样式所控制的范围 )

主应用会影响子应用

(比如:主应用有 layout:10px!important ,子应用也有这个layout 类名还是会影响)

{ strictStyleIsolation: true }

子应用之间隔离 + 主子应用互不影响

(基于ShadowDOM的实现,将子应用容器内部的内容包裹一个ShadowDOM)

  1. 会引入其他问题,官网还没有提供一个完整的改造方式;

  2. 开发者对ShadowDOM的使用熟练程度有限,有风险

考虑到以上几点,最终我们的方案为:antd加自定义前缀,其他样式通过css module 方式开发

antd自定义前缀

antd的默认前缀是"antd",如果主应用和子应用都使用这个UI框架的话,会出现互相影响,所以我们修改每个子应用的前缀用来区分和避免影响;

接下来说一下如何进行antd前缀的自定义,其方式参见下面的步骤:

  1. 我们安装 craco (一个对 create-react-app 进行自定义配置的社区解决方案,antd推荐使用),并修改 package.json 里的 scripts 属性。

    $ yarn add @craco/craco

    /* package.json */ "scripts": {

    • "start": "react-scripts start",
    • "build": "react-scripts build",
    • "test": "react-scripts test",
    • "start": "craco start",
    • "build": "craco build",
    • "test": "craco test", }
  2. 然后在项目根目录创建一个 craco.config.js 用于修改默认配置。

    /* craco.config.js */ module.exports = { // ... };

  3. 自定义主题需要用到类似 less-loader 提供的 less 变量覆盖功能。我们可以引入 craco-less 来帮助加载 less 样式和修改变量,需要把 src/App.css 文件修改为 src/App.less,然后修改样式引用为 less 文件。

    /* src/App.js */

    • import './App.css';
    • import './App.less';

    /* src/App.less */

    • @import '~antd/dist/antd.css';
    • @import '~antd/dist/antd.less';
  4. 安装 craco-less 并修改 craco.config.js 文件如下。

    $ yarn add craco-less

    const CracoLessPlugin = require('craco-less');

    module.exports = { plugins: [ { plugin: CracoLessPlugin, options: { lessLoaderOptions: { lessOptions: { modifyVars: { '@primary-color': '#1DA57A' }, javascriptEnabled: true, }, }, }, }, ], };

当然,除了@primary-color ,也可以修改配置其他的less变量,具体可以参见antd文档。

Css module使用

Css module 特点就是可以让每个组件中引入的css样式文件,仅作用于当前组件而不是作用于全局,原理是解析时将进入的模块css封装在一个哈希值类名下,这个类名是独一无二的也就实现了模块之间的隔离;

什么是css module? & 如何使用?

www.ruanyifeng.com/blog/2016/0…

js隔离

目前,并没有遇到每个子应用内部的变量会互相污染的情况,子应用没有对window这样的变量有自己的处理,只是有的场景取了主应用window的值。如果遇到有污染的情况,有人提出在子应用加载的钩子里将window进行快照,然后在卸载的钩子里重新赋值以达到隔离效果。

相关文章资料