如果你面临以下问题,可以考虑微前端

  1. 随着新业务的快速扩张,项目出现爆炸发展的问题,项目管理和协同开发的难度在不断上升。
  2. 随着项目文件越来越多,开发、构建、部署速度越来越慢,开发体验持续下降。
  3. 一个小改动却要全量打包,导致用户在一定时间内无法使用应用的其他功能,用户体验下降。
  4. 技术开发人员想要在新的项目中尝试不同的技术栈,一方面可摆脱老项目的沉重和技术陈旧问题,一方面可学习新的技术,提升自我,但产品要求无痛迁移(用户无感)、分批迁移(大版本迭代总可能出现奇奇怪怪的问题)或部分迁移(保留老项目部分内容,且所有新迭代都将在新项目中开发)。

那么什么是微前端?

微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。微前端不是单纯的前端框架或者工具,而是一套架构体系。

Why Not Iframe

关于qiankun

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

  • 技术栈无关
    主框架不限制接入应用的技术栈,微应用具备完全自主权
  • 独立开发、独立部署
    微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 增量升级
    在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 独立运行时
    每个微应用之间状态隔离,运行时状态不共享

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

qiankun实践微前端

以下使用vue项目为主应用、vue和react项目为子应用构建qiankun demo。

1. 创建一个新项目qiankun-demo

1
2
mkdir qiankun-demo
npm init

2. 在该目录内构建主应用main(vue项目)和子应用micro-vue和micro-react(vue项目和react项目)

我使用vue create和create-react-app创建的项目
并且为两个子应用配上了路由(vue-router和react-router-dom)

3. 配置主应用main

1
2
cd main
npm install --save qiankun

在主应用中注册qiankun,src目录下添加qiankun.js文件,配置qiankun的启动项,我的配置如下

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
import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
start,
initGlobalState,
setDefaultMountApp
} from 'qiankun'

export default function () {
// 注册子应用
registerMicroApps([
{
name: 'micro-vue', // 微应用的名称,微应用之间必须确保唯一
entry: '//localhost:2001', // 微应用的入口
container: '#subapp-viewport', // 微应用的容器节点的选择器或者 Element 实例(此处表示渲染于主应用的subapp-viewport容器内)
activeRule: '/vue', // 微应用的激活规则,浏览器 url 发生变化会调用 activeRule 里的规则,activeRule 任意一个返回 true 时表明该微应用需要被激活。
props: { // 主应用需要传递给微应用的数据
test: 'props to vue: 给vue子项目的props'
}
},
{
name: 'micro-react',
entry: '//localhost:2002',
container: '#subapp-viewport', // 可为子应用分配不同的container,本次将不示例
activeRule: '/react',
props: { // 主应用需要传递给微应用的数据
test: 'props to react: 给react子项目的props'
}
}
],
// LifeCycles,注册微应用的基础配置信息。当浏览器 url 发生变化时,会自动检查每一个微应用注册的 activeRule 规则,符合规则的应用将会被自动激活
{
beforeLoad: [
app => {
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
}
],
beforeMount: [
app => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
}
],
afterMount: [
app => {
console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name);
}
],
beforeUnmount: [
app => {
console.log('[LifeCycle] before unmount %c%s', 'color: green;', app.name);
}
],
afterUnmount: [
app => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
}
]
})

// 添加全局的未捕获异常处理器
addGlobalUncaughtErrorHandler((event) => {
const { message } = event
// 加载失败时提示
if (message && message.includes('died in status LOADING_SOURCE_CODE')) {
console.log('微应用加载失败,请检查应用是否可运行', event)
}
})

// 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法
const actions = initGlobalState({ msg: '通过globalstate,大家都可以用' })
actions.onGlobalStateChange((state, prev) => {
console.log('主应用获取到globalState Change', state, prev)
}, true)

// actions.setGlobalState(state)

setDefaultMountApp('/vue') // 设置主应用启动后默认进入的微应用

start()
}

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:

1
2
3
4
5
6
7
8
import { loadMicroApp } from 'qiankun';
loadMicroApp(
{
name: 'app',
entry: '//localhost:7100',
container: '#yourContainer',
}
);

在主应用main.js中增加以下内容,启动qiankun

1
2
import startQiankun from './qiankun'
startQiankun()

在App.vue中增加容器subapp-viewport用于子应用渲染,router-view用于主应用页面渲染(这意味着我们可以在主应用中自定义路由,除了/vue和/react的路径,其它均走主应用路由渲染)

1
2
<div id="subapp-viewport" v-show="!$route.name"></div>
<router-view v-show="$route.name"></router-view>

为解决开发环境跨域问题,可按以下内容配置vue.config.js

1
2
3
4
5
6
7
8
module.exports = {
devServer: {
port: 2000,
headers: {
'Access-Control-Allow-Origin': '*' // 解决跨域
}
}
}

4. 配置子应用micro-vue和micro-react(子应用不需要额外安装任何其他依赖即可接入 qiankun 主应用)

子应用配置主要有以下几个步骤:
a. 增加'Access-Control-Allow-Origin': '*'解决开发环境跨域问题
b. 在入口js中导出相应的生命周期钩子
c. 配置微应用的打包工具

  • 配置vue子应用micro-vue
    修改vue.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const name = require('./package').name;

    module.exports = {
    devServer: {
    port: 2001,
    headers: {
    'Access-Control-Allow-Origin': '*' // 解决开发环境跨域问题
    }
    },
    configureWebpack: { // 配置微应用打包工具,让主应用能正确识别微应用暴露出来的一些信息
    output: {
    // 微应用的包名,这里与主应用中注册的微应用名称一致${name}
    library: `${name}-[name]`,
    // 将你的 library 暴露为所有的模块定义下都可运行的方式
    libraryTarget: 'umd',
    // 按需加载相关,设置为 webpackJsonp_${name} 即可
    jsonpFunction: `webpackJsonp_${name}`
    }
    },
    }
    修改main.js,导出相应的生命周期钩子
    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
    /* eslint-disable no-undef */
    import Vue from 'vue'
    import App from './App.vue'
    import routerConfig from './router/index.js'
    import VueRouter from 'vue-router';

    let instance = null
    let router = null

    if (window.__POWERED_BY_QIANKUN__) {
    // 动态设置 webpack publicPath,防止资源加载出错
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
    }

    function render (props = {}) {

    if (props) {
    // Vue.prototype.$app = props // 将props挂在prototype上
    }


    Vue.config.productionTip = false

    Vue.use(VueRouter)
    router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/',
    mode: 'history',
    routes: routerConfig
    })

    // 由于主应用渲染在#app上,为防止换用,增加判断,若使用了qiankun,则将子应用渲染在container下的#app中,若没有使用,则直接渲染在body下的#app中
    // 也可以为当前子应用定制一个不同名的渲染位置,但要同时修改子应用index.html和app.vue的id并且保证不跟主应用的id冲突
    const { container } = props

    console.log(props)
    instance = new Vue({
    router,
    render: h => h(App)
    }).$mount(container ? container.querySelector('#app') : '#app')
    }

    /**
    * 不存在主应用时可直接单独运行
    */
    if (!window.__POWERED_BY_QIANKUN__) {
    render()
    }

    /**
    * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
    * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
    */
    export async function bootstrap () {
    console.log('[vue] vue app bootstraped')
    }

    /**
    * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
    */
    export async function mount (props) {
    console.log('[vue] vue app mount', props.test)
    props.onGlobalStateChange && props.onGlobalStateChange((state, prev) => {
    console.log(state, prev)
    }, true);
    props.setGlobalState && props.setGlobalState({ msg: 'vue子应用修改globalstate' })
    render(props)
    }

    /**
    * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
    */
    export async function unmount () {
    console.log('[vue] vue app unmount')
    instance.$destroy()
    instance.$el.innerHTML = ''
    instance = null
    router = null
    }

  • 配置react子应用micro-react
    安装rescripts,用于修改webpack配置
    1
    npm i -D @rescripts/cli
    修改package.json
    1
    2
    3
    4
    5
    "scripts": {
    "start": "rescripts start",
    "build": "rescripts build",
    "test": "rescripts test"
    }
    在package.json同级目录下增加.rescriptsrc.js文件,内容如下
    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
    const { name } = require('./package');
    const path = require('path');
    const resolve = (dir, sourceDir = __dirname) => path.join(sourceDir, dir);


    module.exports = {
    webpack: config => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';

    config.resolve.alias = {
    '@': resolve('src'),
    }
    return config;
    },

    devServer: config => {
    config.headers = {
    'Access-Control-Allow-Origin': '*',
    };
    // config.historyApiFallback = true;

    // config.hot = false;
    // config.watchContentBase = false;
    // config.liveReload = false;

    return config;
    }
    };
    在package.json同级目录下增加.env文件,内容如下,其中PORT=2002WDS_SOCKET_PORT=2002指定运行的端口号,BROWSER=none表示运行该项目时不会默认打开浏览器
    1
    2
    3
    PORT=2002
    WDS_SOCKET_PORT=2002
    BROWSER=none
    WDS_SOCKET_PORT=2002一定要加,不加主应用会崩,报以下错误
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    webpackHotDevClient.js:60 WebSocket connection to 'ws://localhost:2000/sockjs-node' failed: Error during WebSocket handshake: Unexpected response code: 200
    ./node_modules/react-dev-utils/webpackHotDevClient.js @ webpackHotDevClient.js:60
    __webpack_require__ @ bootstrap:856
    fn @ bootstrap:150
    1 @ index.js:29
    __webpack_require__ @ bootstrap:856
    checkDeferredModules @ bootstrap:45
    webpackJsonpCallback @ bootstrap:32
    eval @ universalModuleDefinition:11
    webpackUniversalModuleDefinition @ universalModuleDefinition:9
    eval @ universalModuleDefinition:10
    eval @ main.chunk.js:784
    geval @ index.js?50b2:162
    exec @ index.js?50b2:179
    schedule @ index.js?50b2:217
    schedule @ index.js?50b2:222
    schedule @ index.js?50b2:222
    eval @ index.js?50b2:228
    eval @ index.js?50b2:227
    修改index.js内容,导出相应的生命周期钩子
    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
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    // import reportWebVitals from './reportWebVitals';


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

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

    export async function mount (props) {
    console.log('[react16] props from main framework', props);
    props.onGlobalStateChange && props.onGlobalStateChange((value, prev) => {
    console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev);
    }, true);
    props.setGlobalState && props.setGlobalState({ msg: 'react16' });
    render(props);
    }

    export async function unmount (props) {
    const { container } = props;
    ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
    }

    // ReactDOM.render(
    // <React.StrictMode>
    // <RouterConfig></RouterConfig>
    // </React.StrictMode>,
    // document.getElementById('root')
    // );
    // If you want to start measuring performance in your app, pass a function
    // to log results (for example: reportWebVitals(console.log))
    // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
    // reportWebVitals();

    5. 使用npm-run-all一键安装运行所有项目

    在qiankun-demo目录下,运行以下代码
    1
    npm install --save-dev npm-run-all
    修改qiankun-demo/package.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    "scripts": {
    "install": "npm-run-all --serial install:main install:micro-react install:micro-vue",
    "serve": "npm-run-all --parallel serve:*",
    "install:main": "cd main && rm -rf node_modules && npm install",
    "install:micro-react": "cd micro-react && rm -rf node_modules && npm install",
    "install:micro-vue": "cd micro-vue && rm -rf node_modules && npm install",
    "serve:main": "cd main && npm run serve",
    "serve:micro-react": "cd micro-react && npm run start",
    "serve:micro-vue": "cd micro-vue && npm run serve",
    "test": "echo \"Error: no test specified\" && exit 1"
    }
    接下来只需运行npm install即可安装主应用和子应用的package.json中定义的包,只需运行npm run serve即可同时运行主应用和子应用

    6. 自动读取子应用路由配置,生成sidebar

    一般内部系统都有一个左侧菜单栏,用于页面导航,但是由于我们有多个子应用,如果自己维护左侧菜单栏,则每次子应用增肌新页面时,都要手动在左侧菜单栏的配置文件中增加该页面的导航,十分麻烦。
    我们希望能够自动读取子应用的路由配置文件,在每次npm run serve时生成整合后的菜单栏配置文件。
    首先,修改qiankun-demo/package.json的serve运行语句
    1
    "serve": "node ./bin/start.js && npm-run-all --parallel serve:*"
    然后,在qiankun-demo目录下增加bin文件夹,在bin文件夹下增加start.js文件,其内容如下
    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
    // nodejs对es6的支持并不好,像class, import这些关键字,react的jsx都无法使用。但是babel可以将这些都转化为es5。此处讲讲如何在node端使用babel。

    // babel-register可以理解成一个小插件,将es6的东西转成es5,github地址:https://github.com/babel/babel/tree/master/packages/babel-register。

    // 使用很简单,只需要在文件中加入
    require('babel-register') ({
    presets: [ 'env' ]
    });

    const fs = require('fs');
    const path = require('path');
    const config = [
    {
    name: 'micro-vue',
    activeRule: '/vue'
    }, {
    name: 'micro-react',
    activeRule: '/react',
    }
    ];

    let content = [];

    const createSideMenu = () => {
    const tips = `// ${
    (new Date()).toLocaleString()
    }从bin/start.js自动生成`;

    config.forEach(item => {
    const appRouterConfig = require(path.join(__dirname, `../${item.name}/src/router/appRouterConfig.js`));
    content = content.concat(appRouterConfig.default.map(ele => ({ name: ele.name, path: item.activeRule + ele.path, meta: ele.meta })));
    })
    fs.writeFileSync(path.join(__dirname, '../main/src/sideMenu.js'), tips + '\n' + `export default ${JSON.stringify(content, null, 2)}\n`.replace(/\"/g, "\'"));
    }
    createSideMenu();

    7. 其他常见问题

  • 如何在 Node.js 中使用 import / export 的三种方法
    1
    2
    3
    4
    5
    6
    7
    8
    // nodejs对es6的支持并不好,像class, import这些关键字,react的jsx都无法使用。但是babel可以将这些都转化为es5。此处讲讲如何在node端使用babel。

    // babel-register可以理解成一个小插件,将es6的东西转成es5,github地址:https://github.com/babel/babel/tree/master/packages/babel-register。

    require('babel-register') ({
    presets: [ 'env' ]
    })

  • 解决vue-router报NavigationDuplicated: Avoided redundant navigation to current location 的问题
  • react项目每次启动不打开默认浏览器
    1
    BROWSER=none

    8. 项目地址

【参考】
记一次 微前端 qiankun 项目 实践 !!! 防踩坑指南
体验微前端(qiankun)
微前端原理和实战(single-spa qiankun)
qiankun 微前端实践总结(二)
qiankun 微前端应用实践与部署(三)
qiankun 微前端应用实践与部署(二)
qiankun 微前端方案实践及总结
基于qiankun落地部署微前端爬”坑“记

感谢您的阅读,本文由 Astar 版权所有。如若转载,请注明出处:Astar(http://example.com/2020/12/28/%E5%BE%AE%E5%89%8D%E7%AB%AF%E6%A1%86%E6%9E%B6qiankun%E5%AE%9E%E8%B7%B5/
计算机网络基础篇(汇总)
react16在ie11下运行