§ Fox3.0前端框架-微前端

§ 前言

在企业的toB的领域中,存在着系统多技术栈不统一的情况。在运维方面无疑是增加了维护成本,同样从使用者的角度来看,不同操作风格的系统,也提高了学习成本、降低了工作效率。如何解决上述问题?如果我们使用同样的技术框架,重新开发吧把所有系统整合,那无疑能到达技术栈、操作风格双统一的目标。但是如此庞大的前端工程同时给系统升级和运维带来巨大的困难。 我们如何才能兼得鱼和熊掌呢?引人微前端的理念,将庞大的整体拆成可控的小块,并明确它们之间的依赖关系,无疑是一个很好的解决方案。微前端(Micro Front-end)的关键优势如下:

  • 统一技术栈
  • 代码库分离、每个库更小,更内聚、可维护性更高
  • 松耦合、自治的团队可扩展性更好
  • 渐进地升级、更新甚至重写部分前端功能,成为了可能

§ 统一技术栈

微前端架构提供了统一的技术框架、开发规范

§ 简单、松耦合的代码库

比起一整块的前端代码库,微前端架构下的代码库倾向于分离、更小/简单、更容易开发。此外,更重要的是避免模块间不合理的隐式耦合造成的复杂度上升。通过界定清晰的应用边界来降低意外耦合的可能性,增加子应用间逻辑耦合的成本,促使开发者明确数据和事件在应用程序中的流向

§ 独立部署

独立部署的能力在微前端体系中至关重要,能够缩小变更范围,进而降低相关风险。因此,每个微前端都应具备有自己的持续交付流水线(包括构建、测试并部署到生产环境),并且要能独立部署,不必过多考虑其它代码库和交付流水线的当前状态。

§ 增量升级

由于在微前端体系中各个子系统的代码、部署解耦。所以我们可以方便得对某个系统进行升级改造,而不会出现整体风险。意味着我们能够对产品功能进行低风险的局部替换。

§ 异构系统集成

微前端系统,同时具备继承不同技术栈前端系统的能力,能够在一定的规则下,把异构系统集成在一起,提供类似统一门户的功能。

§ 团队自治

除代码库及发布周期上的解耦之外,微前端还有助于形成完全独立的团队,由不同团队各自负责一块产品功能从构思到发布的整个过程,团队能够完全拥有为客户提供价值所需的一切,从而快速高效地运转。为此,应该围绕业务功能纵向组建团队,而不是基于技术职能划分。最简单的,可以根据最终用户所能看到的内容来划分,比如将应用中的每个页面作为一个微前端,并交给一个团队全权负责。与基于技术职能或横向关注点(如样式、表单、校验等)组织的团队相比,这种方式能够提升团队工作的凝聚力。

§ 原理

微前端(Micro Front-end),就是将一个巨无霸(Monolith)的前端工程拆分成一个一个的小工程。别小看这些小工程,它们也是“麻雀虽小,五脏俱全”,完全具备独立的开发、运行能力。整个系统就将由这些小工程协同合作,实现所有页面的展示与交互。其原理就是通过监听URL的变化,并通过匹配规则加载不同的子系统。

3ec8381adcd56dc9c83036ff28af5296.png

从上图可以看出,微前端工程可分为两类:主工程(portal 工程),子工程(micro工程)。protal工程故名思义是入口工程,用户打开浏览器,首次进入我们的页面时,不管是什么 URL,首先加载的就是portal。portal工程再根据路由、匹配对应的 URL、并加载对应的micro工程。

§ 架构

下面分别对protal和micro工程进行说明

§ protal工程

protal工程为入口工程,工程包括了公共库、login、frame这三部分

5122a5f21047130efa2461d579673d15.png

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()
        })
1
2
3
4
5
6
7
8
9
10
11
12
13

§ micro工程

micro可以独立开发、运行、部署,同样也可以集成在protal中,micro的组成如下:

a84cf75e1bfeffdbc0802068a22e43bb.png

§ 代码库

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

8c95db234ce257ed4b3b22004075e03b.png

§ 微前端集成

目前支持集成前端框架为

  • 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/',
        },
    ],
}
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

§ 微应用配置说明

§ 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
1
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,
    },
})
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
§ 集成运行入口
//导入路由配置
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
1
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,
        }),
    ],
})
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

§ 异构微应用集成(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
    }
}
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
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'
    }
})
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

§ 远程vue微前端配置(非本地)

§ 微前端入口文件位置
http://localhost:52330/dist/micro-vue/index.js
1
§ 主工程上对应微前端配置
 {
            name: 'vue-micro',
            type: 'vue',
            matched: '/micro-vue/',
            module: 'http://localhost:52330/dist/micro-vue/index.js',
        },
1
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'
    }
})
1
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)
    },
}))

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

§ 异构微应用集成(多页面-MPA)

类似JSP、ASP之类的应用,目前只能通过iframe的方式集成。如果集成的系统,需求实现单点登陆功能,我们推荐使用令牌的方式,在加载对应的微应用的时候,会在url后面加上token,例如:url?token=007,该系统获取对应的token,进行校验即可。

最后更新于: 7/5/2022, 5:29:52 PM