微前端

微前端

写在前面

中台部分业务模块多,单个仓库存放代码量多,阅读困难,难以维护,所以按照业务区分来拆分微前端,方便维护。本文介绍微应用的基本应用,并在最后介绍基本api。

什么是微前端

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

核心价值

  • 技术栈无关

    • 主框架不限制应用技术栈接入,微应用完全具备自主权
  • 独立开发、独立部署

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

    • 在面对各种复杂场景时候,很难对一个已经存在的系统进行全量的升级或者重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 独立运行时

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

主应用

在对应的微应用仓库运行项目,在主应用注册相关微应用配置即可。

主应用不限制技术展 只需提供一个容器DOM,然后start即可

安装qiankun

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

1、主应用 配置文件添加微应用注册配置

注册微应用并start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/micro/config.ts
import { registerMicroApps, start } from 'qiankun'
const apps = [
{
name: 'subapp',// 注意跟微应用pageage.json name一致
entry: '//localhost:3000/',// 微应用地址入口
container: '#subapp-container',// 微应用加载容器
activeRule: '/subapp-admin',// 微应用路由入口,注意子应用的所有路由都须加上相应的路由前缀规则
},
{
name: 'subapp_admin',
entry: '//localhost:4000/',
container: '#subapp-container',
activeRule: '#/subapp-admin-account',
},
]
registerMicroApps(apps)
// 启动qiankun
start()

这里我们是拆分中台应用,所以使用的语言都为vue,所以向微应用传递store、router、BaseRequest(axios请求封装),并且使用prefetchApps手动预加载指定的微应用静态资源

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
import { registerMicroApps, prefetchApps } from 'qiankun'
import store from '@/store'
import router from '@/router'
import BaseRequest from '@/utils/http'
const apps = [
{
name: 'subapp',// 注意跟微应用pageage.json name一致
entry: '//localhost:3000/',// 微应用地址入口
container: '#subapp-container',// 微应用加载容器
activeRule: '/subapp-admin',// 微应用路由入口,注意子应用的所有路由都须加上相应的路由前缀规则
props: {
store,
router,
BaseRequest,
},
},
{
name: 'subapp_admin',
entry: '//localhost:4000/',
container: '#subapp-container',
activeRule: '#/subapp-admin-account',
props: {
store,
router,
BaseRequest,
},
},
]
registerMicroApps(apps)
// 手动预加载指定的微应用静态资源
prefetchApps(apps)

2、微应用 项目配置

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

这里只介绍有webpack构建且用vue框架时如何应用

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

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

2.微应用建议使用history模式的路由,需要设置路由base,值和它的activeRule是一样

3.在入口文件最顶部引入public-path.js,修改并导出三个生命周期函数

4.修改webpack打包,允许开发环境跨域和umd打包

vue微应用

1.在src目录新增 public-path.js 文件

1
2
3
4
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

2.这里在vue中使用hash路由

3.在入口文件最顶部引入public-path.js,修改并导出三个生命周期函数

  • 在微应用的入口文件中导出相应的生命周期钩子

以Vue3.x的入口文件为例:src/main.ts

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import Vue, { createApp } from 'vue'
import { createRouter, createWebHashHistory, Router } from 'vue-router'
import App from './App.vue'
import store from './store/index'
import routes from './router/index'
import './public-path.ts'

let instance: Vue.App<Element>
let router: Router
const routerBase = '/subapp-account'
const __qiankun__ = window.__POWERED_BY_QIANKUN__
interface IRenderProps {
container: Element | string
store?: any
[propname: string]: any
}
/**
* @name 渲染函数
* @param props 主应用下发的props
* @description 分为微前端应用和独立应用渲染
*/
function render(props: IRenderProps) {
const { container, store: mainStore } = props
window.$mainStore = mainStore
// 查看主应用router
router = createRouter({
history: createWebHashHistory(__qiankun__ ? `${routerBase}/` : '/'),
routes
})
instance = createApp(App)
instance
.use(router)
.use(store)
.mount(
container instanceof Element
? (container.querySelector('#app') as Element)
: (container as string)
)
}

function setupStore(props: IRenderProps) {
props.onGlobalStateChange(
(value: any, prev: any) =>
console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
true
)
props.setGlobalState &&
props.setGlobalState({
ignore: props.name,
user: {
name: props.name
},
routes
})
}
// 独立运行时
if (!__qiankun__) {
render({ container: '#app' })
}
// 暴露主应用生命周期钩子
export async function bootstrap() {
console.log('subapp bootstraped')
}

/**
* @name 创建微应用
* @param props 主应用下发的props
*/
export async function mount(props: any) {
console.log('mount subapp')
setupStore(props)
render(props)
}

/**
* @name 卸载微应用
*/
export async function unmount() {
console.log('unmount college app')
instance.unmount()
}

4.修改webpack打包,允许开发环境跨域和umd打包

除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack

const packageName = require('./package.json').name;


module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
output: {

library: `${packageName}-[name]`,

libraryTarget: 'umd',// 把微应用打包成 umd 库格式

jsonpFunction: `webpackJsonp_${packageName}`,
// webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
},
};

配置gitlab CI

// 参考subapp-admin gitlab-ci.yml

微应用单独编译打包

这里只做简单解释 具体需要先了解 gitlab ci/cd 极狐GitLab CI/CD 入门 | 极狐GitLab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
stages:
- build-211-dev
- build-211-sit
- build-211-uat
- build-211-pro
variables: # 变量 根据脚本执行添加 在script中应用
project: admin
targetpage: admin-sg
proRep: admin-pro

build-211-dev:
stage: build-211-dev
only: # 触发条件
refs:
- /^dev$/ # dev分支
variables:
- $CI_COMMIT_MESSAGE =~ /\-\-build/ # commit 中添加 --build 触发
script: # 触发后执行脚本
- sh /home/gitlab/front-shell/projects/youxin-admin/admin.sh -b dev -d subapp-account -r admin-sg/subapp-account-sg
tags: # runner 名称 tag
- web-dev

常见api

registerMicroApps(apps, lifeCycles?)

  • 参数

    • apps - Array<RegistrableApp> - 必选,微应用的一些注册信息
    • lifeCycles - LifeCycles - 可选,全局的微应用生命周期钩子
  • 类型

    • RegistrableApp

      • name - string - 必选,微应用的名称,微应用之间必须确保唯一。

      • entry - string | { scripts?: string[]; styles?: string[]; html?: string } - 必选,微应用的入口。

        • 配置为字符串时,表示微应用的访问地址,例如 https://qiankun.umijs.org/guide/
        • 配置为对象时,html 的值是微应用的 html 内容字符串,而不是微应用的访问地址。微应用的 publicPath 将会被设置为 /
      • container - string | HTMLElement - 必选,微应用的容器节点的选择器或者 Element 实例。如container: '#root' 或 container: document.querySelector('#root')

      • activeRule - string | (location: Location) => boolean | Array<string | (location: Location) => boolean> - 必选,微应用的激活规则。

        • 支持直接配置字符串或字符串数组,如 activeRule: '/app1' 或 activeRule: ['/app1', '/app2'],当配置为字符串时会直接跟 url 中的路径部分做前缀匹配,匹配成功表明当前应用会被激活。
        • 支持配置一个 active function 函数或一组 active function。函数会传入当前 location 作为参数,函数返回 true 时表明当前微应用会被激活。如 location => location.pathname.startsWith('/app1')
  • LifeCycles

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

    • beforeLoad - Lifecycle | Array<Lifecycle> - 可选
    • beforeMount - Lifecycle | Array<Lifecycle> - 可选
    • afterMount - Lifecycle | Array<Lifecycle> - 可选
    • beforeUnmount - Lifecycle | Array<Lifecycle> - 可选
    • afterUnmount - Lifecycle | Array<Lifecycle> - 可选
  • 用法

    注册微应用的基础配置信息。当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活。

  • 示例

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

    registerMicroApps(
    [
    {
    name: 'app1',
    entry: '//localhost:8080',
    container: '#container',
    activeRule: '/react',
    props: {
    name: 'kuitos',
    },
    },
    ],
    {
    beforeLoad: (app) => console.log('before load', app.name),
    beforeMount: [(app) => console.log('before mount', app.name)],
    },
    );

prefetchApps(apps, importEntryOpts?)

  • 参数

    • apps - AppMetadata[] - 必选 - 预加载的应用列表
    • importEntryOpts - 可选 - 加载配置
  • 类型

    • AppMetadata
      • name - string - 必选 - 应用名
      • entry - string | { scripts?: string[]; styles?: string[]; html?: string } - 必选,微应用的 entry 地址
  • 用法

    手动预加载指定的微应用静态资源。仅手动加载微应用场景需要,基于路由自动激活场景直接配置 prefetch 属性即可。

  • 示例

    1
      

常见问题

1、Sass变量全局配置问题

场景:新项目安装node-sass、和sass-loder后,自动导入样式文件还是无法读取全局变量。

解决方案:还需安装sass-resources-loader 包

疑问:其他项目同样版本并无安装以上依赖照样可以运行。估计是Webpack版本兼容问题。

1
2
3
4
5
6
7
8
// vue.config.js
css: {
loaderOptions: {
sass: {
prependData: `@import "@/assets/styles/theme.scss";`,
},
},
},

2、Vue路由切换卡顿问题

解决方案:watch route.name 而不是整个route响应式对象

[Deprecation] ‘window.webkitStorageInfo’ is deprecated. Please use ‘navigator.webkitTemporaryStorage’ or ‘navigator.webkitPersistentStorage’ instead. · Issue #3112 · vuejs/vue-next

3、样式隔离问题

解决方案:添加命名空间

子服务使用element-ui: Failed to execute ‘getComputedStyle’ on ‘Window’: parameter 1 is not of type ‘Element’. · Issue #634 · umijs/qiankun

4、打包缓存问题

解决方案:强制刷新,打包添加md5命名

Uncaught TypeError: Cannot read property ‘call’ of undefined · Issue #959 · webpack/webpack

5、KeepAlive方案

解决方案:子应用使用KeepAlive,主应用控制需要缓存的组件列表

其他方案参考

[Feature Request] 主应用多页签切换不同子应用的页面状态保持 · Issue #361 · umijs/qiankun

6、Hash路由模式不触发unmont问题

解决方案:不同子项目才会触发,同子项目路由变更不触发

[Bug]似乎不支持hash路由 · Issue #118 · umijs/qiankun

7、子路由前缀相同时,路由切换报错

报错提示: https://single-spa.js.org/error/?code=31&arg=mount&arg=application&arg=subapp-market&arg=3000

解决方案:精确匹配路由规则即可

子应用路由前缀相同时,路由切换错误 #607