如何设计一个 Vue 微前端(Micro-Frontends)架构,并处理子应用之间的通信、路由隔离和状态共享?

各位朋友,大家好!我是你们的老朋友,今天咱们来聊聊Vue微前端这事儿,保证让你听完之后,感觉像打通了任督二脉一样,思路清晰,下笔如有神!

开场白:微前端,前端的“分久必合,合久必分”?

话说天下大势,分久必合,合久必分。前端架构也一样,从最初的刀耕火种,到后来的模块化、组件化,再到现在的微前端,这简直就是一部前端架构的演进史诗啊!

微前端,顾名思义,就是把一个原本巨大的前端应用拆分成多个小型、自治的应用,每个应用可以独立开发、独立部署、独立运行。就像把一艘航空母舰拆成几艘巡洋舰,虽然单艘船的火力不如航母,但灵活性大大提升了。

第一部分:微前端架构设计,别让你的“航母”变成“泰坦尼克”!

微前端架构有很多种实现方式,各有优缺点,咱们先来盘点一下:

  1. Iframe方案:最简单粗暴的朋友

    • 优点: 天然隔离,技术栈无关,兼容性好。
    • 缺点: 体验差,路由同步困难,通信复杂,性能损耗大。

    Iframe就像在一个页面里开辟了一个新的世界,两个世界之间互不干扰,但也正是这种完全的隔离,导致了通信和体验上的问题。想象一下,你要在一个Iframe里的按钮点击后,改变父页面的标题,那得费多大劲儿啊!

    适用场景:老旧系统改造,对体验要求不高,或者确实需要强隔离的场景。

  2. Web Components方案:组件化的极致追求

    • 优点: 真正的组件化,封装性好,复用性高。
    • 缺点: 学习成本高,兼容性问题,生态不完善。

    Web Components就像乐高积木,每个积木都是一个独立的组件,可以自由组合。但是,Web Components的坑也不少,polyfill、shadow DOM、事件穿透等等,都需要仔细研究。

    适用场景:对组件化要求高,有长期技术投入的团队。

  3. Webpack Module Federation方案:模块共享的理想主义者

    • 优点: 代码共享,性能好,开发体验好。
    • 缺点: 技术栈绑定,依赖管理复杂,安全性问题。

    Module Federation就像一个共享的代码仓库,每个应用都可以从中获取需要的模块。但是,这也意味着你的所有应用都必须使用相同的技术栈(Webpack),而且依赖管理也变得非常复杂。

    适用场景:技术栈统一,对性能要求高,开发团队协作紧密的场景。

  4. Single-SPA方案:路由劫持的魔法师

    • 优点: 技术栈无关,灵活,可渐进式改造。
    • 缺点: 路由劫持,有一定的性能损耗,需要手动处理生命周期。

    Single-SPA就像一个路由代理,它可以拦截所有的路由请求,并根据路由规则将请求转发到不同的子应用。这种方式可以实现技术栈无关,而且可以渐进式地改造现有应用。

    适用场景:技术栈多样,需要渐进式改造现有应用,对灵活性要求高的场景。

结论:没有最好的方案,只有最合适的方案!

方案 优点 缺点 适用场景
Iframe 天然隔离,技术栈无关,兼容性好 体验差,路由同步困难,通信复杂,性能损耗大 老旧系统改造,对体验要求不高,或者确实需要强隔离的场景
Web Components 真正的组件化,封装性好,复用性高 学习成本高,兼容性问题,生态不完善 对组件化要求高,有长期技术投入的团队
Webpack MF 代码共享,性能好,开发体验好 技术栈绑定,依赖管理复杂,安全性问题 技术栈统一,对性能要求高,开发团队协作紧密的场景
Single-SPA 技术栈无关,灵活,可渐进式改造 路由劫持,有一定的性能损耗,需要手动处理生命周期 技术栈多样,需要渐进式改造现有应用,对灵活性要求高的场景

第二部分:Single-SPA实战,手把手教你打造微前端“航空母舰”!

咱们就以Single-SPA为例,手把手教你打造一个简单的Vue微前端架构。

1. 准备工作:安装Node.js、npm或yarn,并创建一个空的文件夹。

2. 创建根应用(Root Config):负责加载和注册子应用。

*   安装Single-SPA:

    ```bash
    npm install single-spa --save
    ```

*   创建`index.html`:

    ```html
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Micro-Frontends Demo</title>
    </head>
    <body>
        <div id="root"></div>
        <script src="https://cdn.jsdelivr.net/npm/single-spa/lib/umd/single-spa.min.js"></script>
        <script>
            // 注册子应用
            singleSpa.registerApplication(
                'app1', // 子应用名称
                () => import('./app1/app1.js'), // 子应用加载函数
                location => location.pathname.startsWith('/app1') // 子应用激活函数
            );

            singleSpa.registerApplication(
                'app2',
                () => import('./app2/app2.js'),
                location => location.pathname.startsWith('/app2')
            );

            // 启动Single-SPA
            singleSpa.start();
        </script>
    </body>
    </html>
    ```

*   **代码解读:**

    *   `singleSpa.registerApplication`:注册子应用,需要三个参数:
        *   `name`:子应用名称,必须唯一。
        *   `loadingFunction`:加载子应用的函数,通常使用`import()`动态加载。
        *   `activityFunction`:激活子应用的函数,当满足条件时,子应用会被激活。
    *   `singleSpa.start()`:启动Single-SPA,开始监听路由变化。

3. 创建子应用(Sub Applications):独立的Vue应用。

*   创建`app1`和`app2`文件夹。
*   在每个文件夹中,使用`vue-cli`创建Vue项目:

    ```bash
    cd app1
    vue create . --default
    cd ../app2
    vue create . --default
    ```

*   修改`app1/src/main.js`:

    ```javascript
    import Vue from 'vue'
    import App from './App.vue'
    import singleSpaVue from 'single-spa-vue';

    Vue.config.productionTip = false

    const vueLifecycles = singleSpaVue({
        Vue,
        appOptions: {
            el: '#vue-app', // 挂载点
            render: h => h(App)
        }
    });

    export const bootstrap = vueLifecycles.bootstrap;
    export const mount = vueLifecycles.mount;
    export const unmount = vueLifecycles.unmount;
    ```

*   修改`app1/public/index.html`:

    ```html
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <link rel="icon" href="<%= BASE_URL %>favicon.ico">
        <title><%= htmlWebpackPlugin.options.title %></title>
      </head>
      <body>
        <noscript>
          <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
        </noscript>
        <div id="vue-app"></div>
        <!-- built files will be auto injected -->
      </body>
    </html>
    ```

*   **代码解读:**

    *   `single-spa-vue`:Single-SPA官方提供的Vue集成插件,可以方便地将Vue应用转换为Single-SPA子应用。
    *   `bootstrap`、`mount`、`unmount`:子应用的生命周期函数,Single-SPA会根据子应用的激活状态调用这些函数。
    *   挂载点:在`appOptions`中指定挂载点,这里我们使用`#vue-app`。

*   `app2`的配置也类似,只需要修改挂载点和路由即可。

4. 配置Webpack:让根应用能够加载子应用。

*   在根目录下创建一个`webpack.config.js`:

    ```javascript
    const path = require('path');

    module.exports = {
        mode: 'development',
        entry: './index.html',
        output: {
            filename: 'index.js',
            path: path.resolve(__dirname, 'dist'),
        },
        devServer: {
            static: {
                directory: path.join(__dirname, '/'),
            },
            compress: true,
            port: 9000,
            historyApiFallback: true, // 解决刷新404问题
        },
        module: {
            rules: [
                {
                    test: /.html$/i,
                    loader: "html-loader",
                },
            ],
        },
    };
    ```
  • 修改app1app2的vue.config.js文件,添加以下内容,是为了将子应用打包成umd格式

    module.exports = {
    configureWebpack: {
    output: {
      library: 'app1', // 对应singleSpa.registerApplication的name
      libraryTarget: 'umd',
    },
    },
    devServer: {
    port: 8081, // 修改端口,避免冲突
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
    },
    };
    • 代码解读:

      • webpack-dev-server:提供本地开发服务器,方便调试。
      • historyApiFallback:解决刷新404问题,Single-SPA需要通过路由来控制子应用的激活状态。

5. 运行:启动根应用和子应用。

*   分别启动`app1`和`app2`:

    ```bash
    cd app1
    npm run serve
    cd ../app2
    npm run serve
    ```

*   启动根应用:

    ```bash
    npm install webpack webpack-cli webpack-dev-server html-loader --save-dev
    npx webpack serve --config webpack.config.js
    ```

*   在浏览器中访问`http://localhost:9000/app1`和`http://localhost:9000/app2`,就可以看到两个子应用了。

第三部分:子应用通信,让你的“巡洋舰”协同作战!

子应用之间需要通信,才能实现更复杂的功能。常见的通信方式有:

  1. Custom Events:事件驱动的通信方式

    • 优点: 简单易用,解耦性好。
    • 缺点: 只能传递简单数据,无法共享状态。

    Custom Events就像广播电台,一个应用可以发布一个事件,其他应用可以监听这个事件。

    • 示例:

      • app1中发布事件:

        window.dispatchEvent(new CustomEvent('app1-event', {
            detail: {
                message: 'Hello from App1!'
            }
        }));
      • app2中监听事件:

        window.addEventListener('app1-event', (event) => {
            console.log(event.detail.message); // 输出 "Hello from App1!"
        });
  2. Shared Global State:共享全局状态

    • 优点: 可以共享复杂数据,方便管理。
    • 缺点: 耦合性高,容易出现状态冲突。

    Shared Global State就像一个公共的仓库,所有应用都可以访问和修改其中的数据。

    • 示例:

      • 创建一个共享状态:

        window.sharedState = {
            user: {
                name: 'John Doe',
                age: 30
            }
        };
      • app1中修改状态:

        window.sharedState.user.name = 'Jane Doe';
      • app2中访问状态:

        console.log(window.sharedState.user.name); // 输出 "Jane Doe"
  3. Props:组件之间的属性传递

    • 优点: 数据流清晰,易于维护。
    • 缺点: 需要修改组件结构,不适合传递复杂数据。

    Props就像组件之间的桥梁,可以将数据从一个组件传递到另一个组件。

    • 示例:

      • 在根应用中,将数据传递给子应用:

        singleSpa.registerApplication(
            'app1',
            () => import('./app1/app1.js'),
            location => location.pathname.startsWith('/app1'),
            {
                userName: 'John Doe' // 通过props传递数据
            }
        );
      • app1中接收数据:

        import Vue from 'vue'
        import App from './App.vue'
        import singleSpaVue from 'single-spa-vue';
        
        Vue.config.productionTip = false
        
        const vueLifecycles = singleSpaVue({
            Vue,
            appOptions: {
                el: '#vue-app',
                render: h => h(App, {
                    props: {
                        userName: singleSpa.getMountedApps()[0].customProps.userName // 获取props
                    }
                })
            }
        });
        
        export const bootstrap = vueLifecycles.bootstrap;
        export const mount = vueLifecycles.mount;
        export const unmount = vueLifecycles.unmount;

第四部分:路由隔离,让你的“巡洋舰”各司其职!

路由隔离是微前端架构的关键,它可以确保每个子应用都有自己的路由空间,互不干扰。

  1. URL Prefixing:URL前缀

    • 原理: 为每个子应用分配一个URL前缀,例如/app1/app2
    • 优点: 简单易用,易于理解。
    • 缺点: URL结构不美观,用户体验较差。

    我们上面例子用的就是这种。

  2. Subdomain Routing:子域名路由

    • 原理: 为每个子应用分配一个子域名,例如app1.example.comapp2.example.com
    • 优点: URL结构美观,用户体验好。
    • 缺点: 需要配置DNS,部署复杂。
  3. Abstracted Routing:抽象路由

    • 原理: 使用一个统一的路由管理中心,将路由请求转发到不同的子应用。
    • 优点: 灵活,可定制性强。
    • 缺点: 实现复杂,需要维护一个路由管理中心。

第五部分:状态共享,让你的“巡洋舰”保持同步!

状态共享是微前端架构的难点,需要仔细设计,避免状态冲突。

  1. Global Event Bus:全局事件总线

    • 原理: 使用一个全局的事件总线,子应用可以通过事件总线发布和订阅事件。
    • 优点: 解耦性好,易于扩展。
    • 缺点: 容易出现事件冲突,难以追踪状态变化。
  2. Centralized State Management:集中式状态管理

    • 原理: 使用一个中心化的状态管理库,例如Redux、Vuex,所有子应用都可以访问和修改其中的状态。
    • 优点: 状态管理清晰,易于追踪状态变化。
    • 缺点: 耦合性高,需要所有子应用都使用相同的状态管理库。
  3. Shared Module:共享模块

    • 原理: 将共享的状态和逻辑封装成一个独立的模块,所有子应用都可以引入这个模块。
    • 优点: 代码复用性高,易于维护。
    • 缺点: 耦合性高,需要仔细设计模块接口。

总结:微前端,一种思维方式!

微前端不仅仅是一种技术架构,更是一种思维方式。它强调的是独立、自治、可组合。在设计微前端架构时,需要仔细考虑业务需求、团队结构、技术栈等因素,选择最合适的方案。

希望今天的讲解能帮助你更好地理解微前端,并在实际项目中应用它。记住,没有最好的方案,只有最合适的方案!

好了,今天的讲座就到这里,谢谢大家!下次有机会再和大家分享更多的技术心得。如果大家有什么问题,欢迎随时提问。

发表回复

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