探讨 Vue Micro-Frontends (微前端) 架构的实现方案,例如基于 Webpack Module Federation 或 Qiankun。

各位好,我是你们今天的主讲人,咱们今天聊聊Vue微前端这事儿。这年头,单体应用动不动就大到没朋友,改个小bug,整个项目重新部署,简直是噩梦。微前端就像是给你的巨石阵(单体应用)来了个乾坤大挪移,把它拆成一堆小模块,各自为政,互不干扰,部署效率嗖嗖的!

微前端?听起来挺玄乎,到底是个啥?

简单来说,微前端就是把一个大型前端应用拆分成多个小型、自治的应用,这些应用可以由不同的团队独立开发、测试和部署。每个小应用就像一个独立的乐高积木,可以灵活组合,最终拼成一个完整的应用。

为什么要用微前端?

  • 独立开发和部署: 各个团队可以独立开发、测试和部署自己的微应用,互不影响,减少了代码冲突和部署风险。
  • 技术栈无关: 每个微应用可以使用不同的技术栈,可以根据业务需求选择最适合的技术。
  • 增量升级: 可以逐步迁移旧应用到新的技术栈,不用一次性重构整个应用。
  • 代码复用: 可以将公共组件或模块抽离成共享库,供多个微应用使用。
  • 团队自治: 各个团队可以拥有更大的自主权,可以更快地响应业务需求。

Vue 微前端,怎么玩?

Vue 作为前端界的扛把子之一,当然也少不了微前端的解决方案。目前比较流行的方案主要有以下两种:

  1. Webpack Module Federation: Webpack 5 带来的神器,可以让你像引用本地模块一样引用远程模块,简单粗暴,性能杠杠的。
  2. Qiankun: 蚂蚁金服开源的微前端框架,基于 single-spa,功能强大,生态完善,适合复杂的微前端场景。

方案一:Webpack Module Federation (MF)

Webpack MF 的核心思想是:共享模块。它允许不同的 Webpack 构建应用共享模块,从而实现微前端的集成。

实战演练:

咱们来创建一个简单的例子,包含一个基座应用(app-main)和两个微应用(app-vue1app-vue2)。

1. 基座应用 (app-main):

  • 安装依赖:

    mkdir app-main && cd app-main
    npm init -y
    npm install vue vue-router webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
  • webpack.config.js:

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { ModuleFederationPlugin } = require('webpack').container;
    const path = require('path');
    
    module.exports = {
        mode: 'development',
        devtool: 'source-map',
        entry: './src/index.js',
        output: {
            path: path.resolve(__dirname, 'dist'),
            publicPath: 'http://localhost:8080/', // 注意这里的publicPath,很重要
        },
        devServer: {
            port: 8080,
            historyApiFallback: true,
        },
        module: {
            rules: [
                {
                    test: /.vue$/,
                    use: 'vue-loader'
                },
                {
                    test: /.css$/,
                    use: [
                        'vue-style-loader',
                        'css-loader'
                    ]
                },
                {
                    test: /.js$/,
                    exclude: /node_modules/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env']
                        }
                    }
                }
            ]
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: './public/index.html'
            }),
            new ModuleFederationPlugin({
                name: 'app_main', // 必须唯一
                remotes: {
                    app_vue1: 'app_vue1@http://localhost:8081/remoteEntry.js', // 引入远程模块
                    app_vue2: 'app_vue2@http://localhost:8082/remoteEntry.js',
                },
                shared: ['vue', 'vue-router'] // 共享依赖
            })
        ],
        resolve: {
            extensions: ['.vue', '.js'],
            alias: {
                'vue': 'vue/dist/vue.esm-bundler.js'
            }
        },
    };
  • src/index.js:

    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    
    const app = createApp(App);
    app.use(router);
    app.mount('#app');
  • src/App.vue:

    <template>
        <div>
            <h1>Main App</h1>
            <router-link to="/vue1">Vue 1</router-link> |
            <router-link to="/vue2">Vue 2</router-link>
            <router-view></router-view>
        </div>
    </template>
    
    <script>
    export default {
        name: 'App',
    }
    </script>
  • src/router/index.js:

    import { createRouter, createWebHistory } from 'vue-router';
    
    const routes = [
        {
            path: '/',
            redirect: '/vue1'
        },
        {
            path: '/vue1',
            name: 'Vue1',
            component: () => import('app_vue1/VueApp') // 动态引入远程模块
        },
        {
            path: '/vue2',
            name: 'Vue2',
            component: () => import('app_vue2/VueApp') // 动态引入远程模块
        }
    ];
    
    const router = createRouter({
        history: createWebHistory(),
        routes
    });
    
    export default router;
  • public/index.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Main App</title>
    </head>
    <body>
        <div id="app"></div>
    </body>
    </html>

2. 微应用 1 (app-vue1):

  • 安装依赖:

    mkdir app-vue1 && cd app-vue1
    npm init -y
    npm install vue vue-router webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
  • webpack.config.js:

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { ModuleFederationPlugin } = require('webpack').container;
    const path = require('path');
    
    module.exports = {
        mode: 'development',
        devtool: 'source-map',
        entry: './src/index.js',
        output: {
            path: path.resolve(__dirname, 'dist'),
            publicPath: 'http://localhost:8081/', // 注意这里的publicPath,很重要
        },
        devServer: {
            port: 8081,
            historyApiFallback: true,
        },
        module: {
            rules: [
                {
                    test: /.vue$/,
                    use: 'vue-loader'
                },
                {
                    test: /.css$/,
                    use: [
                        'vue-style-loader',
                        'css-loader'
                    ]
                },
                {
                    test: /.js$/,
                    exclude: /node_modules/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env']
                        }
                    }
                }
            ]
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: './public/index.html'
            }),
            new ModuleFederationPlugin({
                name: 'app_vue1', // 必须唯一
                filename: 'remoteEntry.js', // 导出入口文件
                exposes: {
                    './VueApp': './src/App.vue' // 暴露模块
                },
                shared: ['vue', 'vue-router'] // 共享依赖
            })
        ],
        resolve: {
            extensions: ['.vue', '.js'],
            alias: {
                'vue': 'vue/dist/vue.esm-bundler.js'
            }
        },
    };
  • src/index.js:

    import { createApp } from 'vue';
    import App from './App.vue';
    
    const app = createApp(App);
    app.mount('#app');
  • src/App.vue:

    <template>
        <div>
            <h2>Vue 1 App</h2>
            <p>This is the Vue 1 micro-frontend.</p>
        </div>
    </template>
    
    <script>
    export default {
        name: 'App',
    }
    </script>
  • public/index.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Vue 1 App</title>
    </head>
    <body>
        <div id="app"></div>
    </body>
    </html>

3. 微应用 2 (app-vue2):

  • 代码结构和配置与 app-vue1 类似,只需要修改以下几点:
    • 端口号改为 8082
    • webpack.config.js 中的 name 改为 'app_vue2'
    • src/App.vue 的内容改成 Vue2 相关的。

启动应用:

分别启动 app-vue1app-vue2app-main

cd app-vue1 && npm run serve
cd app-vue2 && npm run serve
cd app-main && npm run serve

打开 http://localhost:8080,你就能看到基座应用,并且可以通过路由切换到两个微应用。

Webpack MF 的优点:

  • 简单易用: 配置简单,学习成本低。
  • 性能优秀: 共享模块,减少了重复加载。
  • 无框架限制: 可以与其他框架集成。

Webpack MF 的缺点:

  • 依赖管理: 需要仔细管理共享依赖,避免版本冲突。
  • 构建复杂: 需要配置多个 Webpack 构建。
  • 路由管理: 需要手动处理路由跳转。

方案二:Qiankun

Qiankun 是一个基于 single-spa 的微前端框架,它提供了一套完整的微前端解决方案,包括应用注册、路由管理、生命周期管理等。

实战演练:

咱们还是用上面的例子,用 Qiankun 来实现微前端。

1. 基座应用 (app-main):

  • 安装依赖:

    mkdir app-main && cd app-main
    npm init -y
    npm install vue vue-router qiankun --save
    npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
  • webpack.config.js:

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const path = require('path');
    
    module.exports = {
        mode: 'development',
        devtool: 'source-map',
        entry: './src/index.js',
        output: {
            path: path.resolve(__dirname, 'dist'),
            publicPath: '/',
        },
        devServer: {
            port: 8080,
            historyApiFallback: true,
            headers: {
                'Access-Control-Allow-Origin': '*', // 允许跨域
            },
        },
        module: {
            rules: [
                {
                    test: /.vue$/,
                    use: 'vue-loader'
                },
                {
                    test: /.css$/,
                    use: [
                        'vue-style-loader',
                        'css-loader'
                    ]
                },
                {
                    test: /.js$/,
                    exclude: /node_modules/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env']
                        }
                    }
                }
            ]
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: './public/index.html'
            }),
        ],
        resolve: {
            extensions: ['.vue', '.js'],
            alias: {
                'vue': 'vue/dist/vue.esm-bundler.js'
            }
        },
    };
  • src/index.js:

    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    import { registerMicroApps, start } from 'qiankun';
    
    const app = createApp(App);
    app.use(router);
    app.mount('#app');
    
    // 注册微应用
    registerMicroApps([
        {
            name: 'app-vue1',
            entry: '//localhost:8081', // 微应用入口
            container: '#container', // 容器
            activeRule: '/vue1', // 激活规则
        },
        {
            name: 'app-vue2',
            entry: '//localhost:8082', // 微应用入口
            container: '#container', // 容器
            activeRule: '/vue2', // 激活规则
        },
    ]);
    
    // 启动 Qiankun
    start();
  • src/App.vue:

    <template>
        <div>
            <h1>Main App</h1>
            <router-link to="/vue1">Vue 1</router-link> |
            <router-link to="/vue2">Vue 2</router-link>
            <div id="container"></div> <!-- 微应用容器 -->
        </div>
    </template>
    
    <script>
    export default {
        name: 'App',
    }
    </script>
  • src/router/index.js:

    import { createRouter, createWebHistory } from 'vue-router';
    
    const routes = [
        {
            path: '/',
            redirect: '/vue1'
        },
        {
            path: '/vue1',
            name: 'Vue1',
            // component: () => import('app_vue1/VueApp')  // 不要在这里引入微应用
        },
        {
            path: '/vue2',
            name: 'Vue2',
            // component: () => import('app_vue2/VueApp')  // 不要在这里引入微应用
        }
    ];
    
    const router = createRouter({
        history: createWebHistory(),
        routes
    });
    
    export default router;
  • public/index.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Main App</title>
    </head>
    <body>
        <div id="app"></div>
    </body>
    </html>

2. 微应用 1 (app-vue1):

  • 安装依赖:

    mkdir app-vue1 && cd app-vue1
    npm init -y
    npm install vue vue-router --save
    npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
  • webpack.config.js:

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const path = require('path');
    
    module.exports = {
        mode: 'development',
        devtool: 'source-map',
        entry: './src/index.js',
        output: {
            path: path.resolve(__dirname, 'dist'),
            publicPath: 'http://localhost:8081/', // 注意这里的publicPath,很重要
            library: `app-vue1`,
            libraryTarget: 'umd',
            jsonpFunction: `webpackJsonp_app_vue1`,
        },
        devServer: {
            port: 8081,
            historyApiFallback: true,
            headers: {
                'Access-Control-Allow-Origin': '*', // 允许跨域
            },
        },
        module: {
            rules: [
                {
                    test: /.vue$/,
                    use: 'vue-loader'
                },
                {
                    test: /.css$/,
                    use: [
                        'vue-style-loader',
                        'css-loader'
                    ]
                },
                {
                    test: /.js$/,
                    exclude: /node_modules/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env']
                        }
                    }
                }
            ]
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: './public/index.html'
            }),
        ],
        resolve: {
            extensions: ['.vue', '.js'],
            alias: {
                'vue': 'vue/dist/vue.esm-bundler.js'
            }
        },
    };
  • src/index.js:

    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    
    let instance = null;
    
    function render(props = {}) {
      const { container } = props;
      instance = createApp(App);
      instance.use(router);
      instance.mount(container ? container.querySelector('#app') : '#app');
    }
    
    // 独立运行时
    if (!window.__POWERED_BY_QIANKUN__) {
      render();
    }
    
    export async function bootstrap(props) {
      console.log('[vue] vue app bootstraped', props);
    }
    
    export async function mount(props) {
      console.log('[vue] props from main framework', props);
      render(props);
    }
    
    export async function unmount(props) {
      console.log('[vue] props from main framework', props);
      instance.unmount();
      instance = null;
    }
  • src/App.vue:

    <template>
        <div>
            <h2>Vue 1 App</h2>
            <p>This is the Vue 1 micro-frontend.</p>
        </div>
    </template>
    
    <script>
    export default {
        name: 'App',
    }
    </script>
  • src/router/index.js

    import { createRouter, createWebHistory } from 'vue-router';
    
    const routes = [
      {
        path: '/',
        redirect: '/vue1'
      },
      {
        path: '/vue1',
        name: 'Vue1',
        component: {
          template: '<div><h1>Vue1 Content</h1></div>'
        }
      }
    ];
    
    const router = createRouter({
      history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue1' : '/'),
      routes
    });
    
    export default router;
  • public/index.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Vue 1 App</title>
    </head>
    <body>
        <div id="app"></div>
    </body>
    </html>

3. 微应用 2 (app-vue2):

  • 代码结构和配置与 app-vue1 类似,只需要修改以下几点:
    • 端口号改为 8082
    • webpack.config.js 中的 libraryjsonpFunction 改为 'app-vue2''webpackJsonp_app_vue2'
    • 修改路由history的base

启动应用:

分别启动 app-vue1app-vue2app-main

cd app-vue1 && npm run serve
cd app-vue2 && npm run serve
cd app-main && npm run serve

打开 http://localhost:8080,你就能看到基座应用,并且可以通过路由切换到两个微应用。

Qiankun 的优点:

  • 功能强大: 提供了一套完整的微前端解决方案。
  • 生态完善: 社区活跃,文档丰富。
  • 兼容性好: 可以与其他框架集成。

Qiankun 的缺点:

  • 学习成本高: 配置复杂,需要学习 Qiankun 的 API。
  • 性能损耗: 基于 iframe 或 shadow DOM,有一定的性能损耗。
  • 侵入性强: 需要修改微应用的入口文件。

总结:

特性 Webpack Module Federation Qiankun
易用性 简单 复杂
性能 优秀 较好
功能 基础 强大
侵入性
适用场景 简单的微前端场景 复杂的微前端场景
技术栈无关 支持 支持

选哪个?

  • 如果你的项目比较简单,只需要简单的模块共享,那么 Webpack Module Federation 是一个不错的选择。
  • 如果你的项目比较复杂,需要完整的微前端解决方案,那么 Qiankun 可能更适合你。

微前端的坑:

  • 状态管理: 微应用之间的状态如何共享?
  • 通信: 微应用之间如何通信?
  • UI 统一: 如何保证微应用的 UI 风格一致?
  • 版本管理: 如何管理微应用的版本?
  • 部署: 如何部署微应用?
  • 权限管理: 如何管理微应用的权限?

这些问题需要根据具体的项目情况进行选择和实现。

好了,今天的 Vue 微前端讲座就到这里,希望对大家有所帮助。记住,微前端不是银弹,不要为了用而用,要根据实际情况选择合适的方案。祝大家早日成为微前端大师!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注