§ Fox3.0前端框架-微前端
§ 前言
在企业的toB的领域中,存在着系统多技术栈不统一的情况。在运维方面无疑是增加了维护成本,同样从使用者的角度来看,不同操作风格的系统,也提高了学习成本、降低了工作效率。如何解决上述问题?如果我们使用同样的技术框架,重新开发吧把所有系统整合,那无疑能到达技术栈、操作风格双统一的目标。但是如此庞大的前端工程同时给系统升级和运维带来巨大的困难。 我们如何才能兼得鱼和熊掌呢?引人微前端的理念,将庞大的整体拆成可控的小块,并明确它们之间的依赖关系,无疑是一个很好的解决方案。微前端(Micro Front-end)的关键优势如下:
- 统一技术栈
- 代码库分离、每个库更小,更内聚、可维护性更高
- 松耦合、自治的团队可扩展性更好
- 渐进地升级、更新甚至重写部分前端功能,成为了可能
§ 统一技术栈
微前端架构提供了统一的技术框架、开发规范
§ 简单、松耦合的代码库
比起一整块的前端代码库,微前端架构下的代码库倾向于分离、更小/简单、更容易开发。此外,更重要的是避免模块间不合理的隐式耦合造成的复杂度上升。通过界定清晰的应用边界来降低意外耦合的可能性,增加子应用间逻辑耦合的成本,促使开发者明确数据和事件在应用程序中的流向
§ 独立部署
独立部署的能力在微前端体系中至关重要,能够缩小变更范围,进而降低相关风险。因此,每个微前端都应具备有自己的持续交付流水线(包括构建、测试并部署到生产环境),并且要能独立部署,不必过多考虑其它代码库和交付流水线的当前状态。
§ 增量升级
由于在微前端体系中各个子系统的代码、部署解耦。所以我们可以方便得对某个系统进行升级改造,而不会出现整体风险。意味着我们能够对产品功能进行低风险的局部替换。
§ 异构系统集成
微前端系统,同时具备继承不同技术栈前端系统的能力,能够在一定的规则下,把异构系统集成在一起,提供类似统一门户的功能。
§ 团队自治
除代码库及发布周期上的解耦之外,微前端还有助于形成完全独立的团队,由不同团队各自负责一块产品功能从构思到发布的整个过程,团队能够完全拥有为客户提供价值所需的一切,从而快速高效地运转。为此,应该围绕业务功能纵向组建团队,而不是基于技术职能划分。最简单的,可以根据最终用户所能看到的内容来划分,比如将应用中的每个页面作为一个微前端,并交给一个团队全权负责。与基于技术职能或横向关注点(如样式、表单、校验等)组织的团队相比,这种方式能够提升团队工作的凝聚力。
§ 原理
微前端(Micro Front-end),就是将一个巨无霸(Monolith)的前端工程拆分成一个一个的小工程。别小看这些小工程,它们也是“麻雀虽小,五脏俱全”,完全具备独立的开发、运行能力。整个系统就将由这些小工程协同合作,实现所有页面的展示与交互。其原理就是通过监听URL的变化,并通过匹配规则加载不同的子系统。

从上图可以看出,微前端工程可分为两类:主工程(portal 工程),子工程(micro工程)。protal工程故名思义是入口工程,用户打开浏览器,首次进入我们的页面时,不管是什么 URL,首先加载的就是portal。portal工程再根据路由、匹配对应的 URL、并加载对应的micro工程。
§ 架构
下面分别对protal和micro工程进行说明
§ protal工程
protal工程为入口工程,工程包括了公共库、login、frame这三部分

protal集成了fox-router,通过匹配路由跳转时候的route,从而动态加载对应的micro工程:
//before filter
fox.router.beforeEach(async (to, from, next, session) => {
if (!session.routeModel) {
//匹配
let app = microAppRouter.match(to.path)
if (app) {
microAppRouter.redirect(app, next)
return
}
}
//默认情况
next()
})
2
3
4
5
6
7
8
9
10
11
12
13
§ micro工程
micro可以独立开发、运行、部署,同样也可以集成在protal中,micro的组成如下:

§ 代码库
protal工程和micro都建议建立独立的git库。

§ 微前端集成
目前支持集成前端框架为
- Fox前端框架
- Vue类型前端框架
- 单页面前端框架
- 传统web系统
§ 微前端配置
{
sandboxKeys: ['fox', '$', 'Vue'],
apps: [{
name: 'fox-micro',
type: 'fox',
matched: '/micro/',
module: './static/js/micro-fox.js',
},
{
name: 'vue-micro',
type: 'vue',
matched: '/micro-vue/',
module: './static/js/micro-vue.js',
},
{
name: 'legacy-micro',
type: 'legacy',
matched: '/micro-legacy/',
module: './static/js/micro-legacy/index.js',
},
{
name: 'iframe-micro',
type: 'iframe',
matched: '/micro-iframe/',
module: 'http://ali.foxloader.cn:8011/wiki/',
},
],
}
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
§ 微应用配置说明
§ sandboxKeys 沙箱配置,如果配置对应的key,就能实现在不同微应用实现隔离
例如:
- 配置:sandboxKeys:['$']
- 微应用A,window.$
- 微应用B, window.$
上述两个微应用的$是隔离的,获取或者设置value,都不会相互覆盖。
§ apps 微应用配置
| 名称 | 默认值 | 说明 |
|---|---|---|
| name | 无 | 微应用名称(必须唯一) |
| type | fox | 微应用类型(fox,vue、legacy,iframe) |
| matched | 无 | 微应用匹配路由前缀 |
| module | 无 | 微应用对应的入口js路径,支持相对路径(./),绝对路径(http://、https://、file:///) |
| navigate | hash | 微应用导航方式(hash, program), 如果设置为program导航,那么对应的微应用入口文件也必须实现对应的navigate hook |
§ 同构微应用集成
这是指使用Fox前端架构的应用集成方法,对于每个微前端工程,都一个统一引入protal commons库,该公共库提供ui组件,工具类,入口等功能
§ protal commons
通过webpack脚本protal工程打包为commons库,提供给micro引用,为micro工程提供独立调试的能力。 protal commons库的引用方式有三种:
- 本地引用
- cdn引用
- npm引用
§ 本地引用
把protal commons放在micro工程内,通过import引用,而commons库的更新可以通过手动或git库更新
§ cnd引用
通过script方式在micro的index.html加入引用,更新方式比较简单,把最新的库更新到cnd服务器即可
§ npm引用
把commons库放在npm服务器上,通过micro工程中的package.json引入库。更新方式则是更新npm上对应的库。
§ micro工程打包
micro工程提供了两种打包方式,通过不同的webpack脚本实现。
§ 独立运行入口
//导入路由配置
import RouteConfig from './utils/commons/route-config-micro'
//导入fox
import { fox } from 'fox-commons'
//路由表
let routes = {}
//集成入口
if(process.env.APP_TYPE == 'module'){
//获取路由表
routes = RouteConfig.getRoutes()
}
//独立运行入口
else{
//安装路由
RouteConfig.install(fox)
}
export default routes
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
§ 独立运行脚本
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {
merge,
} = require('webpack-merge')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const vConsolePlugin = require('vconsole-webpack-plugin')
const baseConfig = require('./webpack.base.js')
// 导入环境配置
const env = require('../config/dev.env')
module.exports = merge(baseConfig, {
mode: 'development',
devtool: 'eval-source-map',
entry: {
config: './config/dev.config.js',
mock: './mock/index.js',
app: './src/main.js',
},
output: {
publicPath: '/',
path: path.resolve(__dirname, '../dist'),
filename: '[name].js',
},
optimization: {
usedExports: true, // treeShaking
splitChunks: {
chunks: 'all', // 当存在多个入口时,可以防止同一模块被引入多次, 例如同一个库被多次引入,则库文件会被单独打包出一份vendor
// 将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,是比较推荐的做法
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
runtimeChunk: 'single', // 将其设置为 single 来为所有 chunk 创建一个 runtime bundle (缓存方式)
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
// copy custom static assets
new CopyWebpackPlugin({
patterns: [{
from: path.resolve(__dirname, '../static'),
to: path.resolve(__dirname, '../dist/static'),
},
{
from: path.resolve(__dirname, '../src/assets/libs/static'),
to: path.resolve(__dirname, '../dist/static'),
},
],
}),
new vConsolePlugin({
filter: [], // 需要过滤的入口文件
enable: true // 生产版本应该设置为false
}),
new webpack.DefinePlugin({
'process.env': env,
}),
],
// developement server
devServer: {
contentBase: './dist',
// host配置,可让局部网内能访问
// host: '0.0.0.0',
// 启动gzip 压缩
compress: true,
// 端口号
port: 3000,
// 自动打开浏览器
open: true,
// hot
hot: true,
},
})
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
§ 集成运行入口
//导入路由配置
import RouteConfig from './utils/commons/route-config-micro'
//导入fox
import { fox } from 'fox-commons'
//路由表
let routes = {}
//集成入口
if(process.env.APP_TYPE == 'module'){
//获取路由表
routes = RouteConfig.getRoutes()
}
//独立运行入口
else{
//安装路由
RouteConfig.install(fox)
}
export default routes
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
§ 集成运行打包
const path = require('path')
const webpack = require('webpack')
const {
merge,
} = require('webpack-merge')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const baseConfig = require('./webpack.base.js')
// 导入环境配置
let env = {}
// 配置
let commons = {}
// 生产环境不包含源代码
if (process.env.NODE_ENV === 'production') {
commons = {
mode: 'production',
optimization: {
minimize: true,
usedExports: true, // treeShaking
},
}
// 导入环境变量
env = require('../config/prod.env')
} else { // 非生产环境生成源代码
commons = {
mode: 'development',
devtool: 'eval-source-map',
}
// 导入环境变量
env = require('../config/dev.env')
}
// 导入引入文件
let inputFileName = process.env.input
// 导出模块工程名称
let moduleName = process.env.module
module.exports = merge(baseConfig, commons, {
entry: {
[moduleName]: `./src/${inputFileName}`,
},
output: {
publicPath: './',
path: path.resolve(__dirname, '../dist'),
filename: '[name].js',
library: moduleName,
libraryTarget: 'amd'
},
externals: {
'fox-commons': '',
},
plugins: [
// copy custom static assets
new CopyWebpackPlugin({
patterns: [{
from: path.resolve(__dirname, '../static'),
to: path.resolve(__dirname, '../dist/static'),
},
{
from: path.resolve(__dirname, '../src/assets/libs/static'),
to: path.resolve(__dirname, '../dist/static'),
},
],
}),
new webpack.DefinePlugin({
'process.env': env,
}),
],
})
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
§ 异构微应用集成(VUE)
Fox微前端框架也可以集成vue技术栈的应用,当然该对应vue工程,也必须遵从规范进行适量的改造
§ vue微前端入口文件
§ hook事件列表
| 名称 | 说明 |
|---|---|
| install | 微应用装载成功后调用,参数(props, context) |
| uninstall | 微应用卸载成功后调用,参数(props, context) |
| mount | 微应用挂载到dom结构时调用,参数(props, context) |
| unmount | 微应用从dom结构移除时调用,参数(props, context) |
| active | 微应用激活时调用,参数(props, context) |
| deactive | 微应用失活时调用,参数(props, context) |
| navigate | 设置program导航的情况下,需要实现该方法,参数(props, context) |
如果微应用的路由导航比较复杂,不能简单的通过hash change事件来导航,那么就必须在MicroAppConfig配置上设置navigate为program,并在入口文件中实现navigate方法,主应用在匹配到该微应用的路由后,会把路由path传入navigate方法中,微应用根据path进行导航
§ hook事件参数
| 名称 | 说明 |
|---|---|
| props | 主工程往微应用注入的属性集合 |
| context | 微应用上下文 |
§ context微应用上下文
| 名称 | 说明 |
|---|---|
| global | 微应用数据沙箱 |
| require | 用于加载js、css资源 |
| emit | 微应用往主应用发送消息接口,参数(type:string, data:any) |
| on | 注册事件监听器,监听主应用的消息,参数(type:string, callback:{(...args:any[]):void}) |
| off | 注销事件监听器,参数(type:string, callback:{(...args:any[]):void}) |
§ 集成要求
- 子应用的路由path,都必须基于相同的前缀,而且最好和该微应用的名称保存一致,如来微应用vue-micro,那么它下面的所以路由path就应该微/vue-micro/*
- 子应用的路由跳转,尽量使用vue-router的方法,不要有太多的自定义改造
- 子应用中的资源加载路径,必须为相对路径,如./img/log.png,避免在集成在主应用中后,导致路径错误
§ 集成入口文件
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import routes from './router/index'
//vue 实例
let vueInstance = null
export default {
/**
* 装载处理
* @returns
*/
async install(props, context) {
console.info('intall vue micro app')
console.info('------ props -------')
console.info(props)
console.info('------ context -------')
console.info(context)
return
},
/**
* 卸载处理
* @param props
* @param context
*/
async uninstall(props, context) {
console.info('uninstall vue micro app')
console.info('------ props -------')
console.info(props)
console.info('------ context -------')
console.info(context)
return
},
/**
* app激活处理
*/
async active(props, context) {
console.info('active vue micro app')
console.info('------ props -------')
console.info(props)
console.info('------ context -------')
console.info(context)
return
},
/**
* app失活处理
*/
async deactive(props, context) {
console.info('deactive vue micro app')
console.info('------ props -------')
console.info(props)
console.info('------ context -------')
console.info(context)
return
},
/**
* 加载处理
* @param props
* @param context
*
*/
async mount(props, context) {
console.info('mount vue micro app')
console.info('------ props -------')
console.info(props)
console.info('------ context -------')
console.info(context)
//获取挂载点
let mountPoint = props.mountPoint
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'hash',
base: __dirname,
routes,
})
Vue.config.productionTip = false
//创建vue实例
vueInstance = new Vue({
router,
render: h => h(App),
})
vueInstance.$mount(mountPoint)
},
/**
* 卸载处理
* @param props
* @param context
*
* @returns
*/
async unmount(props, context) {
console.info('umount vue micro app')
console.info('------ props -------')
console.info(props)
console.info('------ context -------')
console.info(context)
return
}
}
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
§ 集成打包脚本
/* eslint-disable */
const path = require('path')
const {
merge
} = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
// 配置
let commons = {}
// 生产环境不包含源代码
if (process.env.NODE_ENV === 'production') {
commons = {
mode: 'production',
optimization: {
minimize: true,
usedExports: true, // treeShaking
},
}
} else { // 非生产环境生成源代码
commons = {
mode: 'development',
devtool: 'eval-source-map',
}
}
//导出模块工程名称
let inFileName = process.env.in || 'main-micro.js'
let moduleName = process.env.module
module.exports = merge(baseConfig, commons, {
entry: path.join(__dirname, `../src/${inFileName}`),
output: {
publicPath: './',
path: path.resolve(__dirname, '../dist'),
filename: `${moduleName}.js`,
library: moduleName,
libraryTarget: 'amd'
}
})
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
§ 远程vue微前端配置(非本地)
§ 微前端入口文件位置
http://localhost:52330/dist/micro-vue/index.js
§ 主工程上对应微前端配置
{
name: 'vue-micro',
type: 'vue',
matched: '/micro-vue/',
module: 'http://localhost:52330/dist/micro-vue/index.js',
},
2
3
4
5
6
§ 子工程的webpack编译配置(保证资源能访问)
module.exports = merge(baseConfig, commons, {
entry: path.join(__dirname, `../src/${inFileName}`),
output: {
publicPath: `http://localhost:52330/dist/${moduleName}/`,
path: path.resolve(__dirname, `../dist/${moduleName}`),
filename: `index.js`,
library: moduleName,
libraryTarget: 'amd'
}
})
2
3
4
5
6
7
8
9
10
publicPath上设置远程url路径
§ 异构微应用集成(单页面-SPA)
对于早期采用类似require.js,sea.js库的单页面应用,我们也提供了对应的集成方案,需要实现对应的集成入口文件,主工程会传入挂载点,单页面工程,必须把页面加载在指定的point下面
§ 入口文件
define(() => ({
// 安装函数
mount(props, context) {
let require = context.require
let mountPoint = props.mountPoint
window.history.replaceState({}, '', 'static/isomerism/fox/index.html')
if (window.fox) {
window.fox = {
$: window.fox.$,
}
}
require.ensure(['static/isomerism/fox/libs/fox/fox-v1.3.0.js',
'static/isomerism/fox/libs/lib/libs-v1.0.0.js',
'static/isomerism/fox/custom/main-phone.js'
], () => {
// 加载成功处理函数
}, {
jsPoint: mountPoint
})
},
// 卸载函数
unmount(props, context) {
let mountPoint = props.mountPoint
fox.router.unMount(mountPoint)
},
}))
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
§ 异构微应用集成(多页面-MPA)
类似JSP、ASP之类的应用,目前只能通过iframe的方式集成。如果集成的系统,需求实现单点登陆功能,我们推荐使用令牌的方式,在加载对应的微应用的时候,会在url后面加上token,例如:url?token=007,该系统获取对应的token,进行校验即可。