JS Micro-Frontend (微前端) 架构:框架无关、应用隔离与通信机制

各位听众朋友们,大家好!我是今天的主讲人,很高兴能和大家一起聊聊JS微前端架构,这个听起来高大上,但其实也没那么神秘的技术。今天咱们不搞虚的,直接上干货,用最接地气的方式,把微前端这事儿掰开了揉碎了讲明白。

开场白:微前端,你到底是个啥?

首先,咱们得搞明白,啥是微前端?简单来说,就是把一个大型前端应用拆分成多个小型、自治的应用,这些小应用可以由不同的团队开发、部署和维护,最终组合成一个完整的用户界面。

你可以把它想象成乐高积木,每个积木(微应用)都是独立制作的,可以单独更换,但最终拼起来还是一个完整的城堡(整体应用)。

为什么要搞微前端?

你可能会问,为啥要这么折腾?好好的一个应用,拆它干啥?原因有很多:

  • 团队自治: 各个团队可以独立开发、测试和部署自己的微应用,互不干扰,提高开发效率。想象一下,一个团队在搞React,另一个在搞Vue,互不耽误,岂不美哉?
  • 技术栈无关: 每个微应用可以选择最适合自己的技术栈,不再受限于整个应用的统一技术选型。比如,你想用Svelte写个小模块,完全没问题!
  • 增量升级: 可以逐步升级应用,而不是一次性重构整个应用。这对于大型、遗留应用来说,简直是救命稻草。
  • 独立部署: 每个微应用可以独立部署,降低了部署风险,提高了发布频率。出问题了,只回滚一个小应用,影响范围可控。
  • 更好的可维护性: 小型应用更容易维护和调试,代码量少,逻辑清晰。

微前端架构的核心要素

微前端架构的核心在于:框架无关、应用隔离和通信机制。

  1. 框架无关性:

    这意味着每个微应用可以使用不同的前端框架(React、Vue、Angular等),甚至可以混用。为了实现这一点,我们需要一些技巧,比如使用Web Components或者自定义元素。

    • Web Components: Web Components 是一套浏览器原生支持的技术,允许我们创建可重用的自定义 HTML 元素。它们与框架无关,可以在任何支持 Web Components 的浏览器中使用。

      <!-- my-element.js -->
      <script>
      class MyElement extends HTMLElement {
        constructor() {
          super();
          this.attachShadow({ mode: 'open' });
          this.shadowRoot.innerHTML = `
            <style>
              :host {
                display: block;
                border: 1px solid black;
                padding: 10px;
              }
            </style>
            <h1>Hello from My Element!</h1>
            <p>This is a Web Component.</p>
          `;
        }
      }
      customElements.define('my-element', MyElement);
      </script>
      <!-- index.html -->
      <!DOCTYPE html>
      <html>
      <head>
        <title>Web Component Example</title>
      </head>
      <body>
        <my-element></my-element>
      </body>
      </html>
    • 自定义元素: 你也可以不使用 shadow DOM,直接用 JavaScript 创建自定义元素。但这通常不如 Web Components 规范强大和安全。

  2. 应用隔离:

    每个微应用应该运行在自己的沙箱环境中,避免样式和JavaScript冲突。

    • Shadow DOM: Web Components 的 Shadow DOM 提供了一种封装样式和行为的方式,可以有效地隔离微应用。

    • iframe: iframe 是最简单的隔离方式,但它也有一些缺点,比如通信比较麻烦,性能也相对较差。

    • JavaScript沙箱: 可以通过一些技术手段(比如Proxy)创建一个JavaScript沙箱,限制微应用的访问权限。

  3. 通信机制:

    微应用之间需要进行通信,比如共享数据、触发事件等。

    • 自定义事件 (Custom Events): 微应用可以通过派发自定义事件来通知其他微应用。

      // 微应用A
      const event = new CustomEvent('my-event', { detail: { message: 'Hello from A!' } });
      window.dispatchEvent(event);
      
      // 微应用B
      window.addEventListener('my-event', (event) => {
        console.log(event.detail.message); // 输出: Hello from A!
      });
    • Shared Storage (localStorage/sessionStorage): 可以使用 localStorage 或 sessionStorage 共享一些简单的数据。但是要注意,这可能会导致命名冲突。

    • 消息队列 (Message Queue): 可以使用消息队列服务(比如RabbitMQ、Redis)来实现更复杂的微应用通信。

    • 状态管理工具 (Redux/Vuex): 可以使用全局状态管理工具来共享状态。但是要注意,这可能会导致微应用之间的耦合。

微前端架构的几种常见模式

现在,我们来看看几种常见的微前端架构模式:

  1. 构建时集成 (Build-time Integration):

    这种模式是在构建时将所有微应用打包成一个完整的应用。

    • 优点: 简单,易于实现。
    • 缺点: 缺乏灵活性,每次修改都需要重新构建整个应用。
    • 适用场景: 比较小的项目,或者对性能要求不高的项目。
  2. 运行时集成 (Runtime Integration):

    这种模式是在运行时动态加载和渲染微应用。

    • 优点: 灵活性高,可以独立部署和更新微应用。
    • 缺点: 实现起来比较复杂,需要考虑应用隔离和通信。
    • 适用场景: 大型项目,或者需要频繁更新和部署的项目。

    运行时集成又可以细分为几种子模式:

    • 基于路由的集成 (Route-based Integration): 根据 URL 路由将请求转发到不同的微应用。

      // 伪代码
      const routes = {
        '/app1': 'http://localhost:3001/app1.js',
        '/app2': 'http://localhost:3002/app2.js',
      };
      
      function loadApp(route) {
        const script = document.createElement('script');
        script.src = routes[route];
        document.body.appendChild(script);
      }
      
      window.addEventListener('hashchange', () => {
        const route = window.location.hash.slice(1); // 获取 #/app1 之后的部分
        loadApp(route);
      });
    • 基于 Web Components 的集成 (Web Components-based Integration): 将微应用封装成 Web Components,然后在主应用中动态加载和渲染这些 Web Components。

      // 主应用
      import('./app1.js').then(() => {
        const app1 = document.createElement('app-1');
        document.getElementById('container').appendChild(app1);
      });
    • 基于 iframe 的集成 (Iframe-based Integration): 将每个微应用放到一个 iframe 中,然后在主应用中嵌入这些 iframe。

      <!-- 主应用 -->
      <iframe src="http://localhost:3001/app1.html"></iframe>
      <iframe src="http://localhost:3002/app2.html"></iframe>
  3. 边缘集成 (Edge Integration):

    这种模式是在边缘服务器(比如CDN)上进行微应用的组合。

    • 优点: 性能好,可以利用CDN的缓存能力。
    • 缺点: 实现起来比较复杂,需要对边缘服务器进行配置。
    • 适用场景: 对性能要求非常高的项目。

微前端框架和工具

市面上有很多微前端框架和工具,可以帮助我们更方便地实现微前端架构。这里列举几个比较流行的:

框架/工具 特点
Qiankun 基于 single-spa,支持多种框架,提供完善的应用隔离和通信机制。阿里出品,中文文档完善。
single-spa 一个微前端路由协调器,可以集成各种框架的应用。比较底层,需要自己实现应用隔离和通信。
Module Federation Webpack 5 的一个特性,允许不同的 Webpack 构建之间共享代码。可以实现非常灵活的微前端架构。
FrintJS 基于 RxJS 的微前端框架,强调响应式编程。
Piral 一个基于 React 的微前端框架,提供插件机制。

实战案例:用 Qiankun 实现一个简单的微前端应用

接下来,我们用 Qiankun 来实现一个简单的微前端应用,包含两个微应用:一个 React 应用和一个 Vue 应用。

  1. 创建主应用 (Main App):

    npx create-react-app main-app
    cd main-app
    npm install qiankun --save
    npm start

    修改 src/App.js:

    import React, { useEffect } from 'react';
    import { registerMicroApps, start } from 'qiankun';
    
    function App() {
      useEffect(() => {
        registerMicroApps([
          {
            name: 'react-app',
            entry: '//localhost:3001', // React 应用的地址
            container: '#container',
            activeRule: '/react',
          },
          {
            name: 'vue-app',
            entry: '//localhost:3002', // Vue 应用的地址
            container: '#container',
            activeRule: '/vue',
          },
        ]);
    
        start();
      }, []);
    
      return (
        <div>
          <h1>Main App</h1>
          <div id="container"></div>
        </div>
      );
    }
    
    export default App;
  2. 创建 React 微应用 (React App):

    npx create-react-app react-app
    cd react-app
    npm start

    修改 src/index.js:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    
    function render(props) {
      const { container } = props;
      ReactDOM.render(<App {...props} />, container ? container.querySelector('#root') : document.getElementById('root'));
    }
    
    if (!window.__POWERED_BY_QIANKUN__) {
      render({});
    }
    
    export async function bootstrap(props) {
      console.log('[react-app] bootstrap', props);
    }
    
    export async function mount(props) {
      console.log('[react-app] mount', props);
      render(props);
    }
    
    export async function unmount(props) {
      console.log('[react-app] unmount', props);
      const { container } = props;
      ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.getElementById('root'));
    }

    修改 public/index.html:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="theme-color" content="#000000" />
        <meta
          name="description"
          content="Web site created using create-react-app"
        />
        <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
        <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
        <title>React App</title>
      </head>
      <body>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root"></div>
        <div id="container"></div>
      </body>
    </html>

    修改 package.json:

    {
      "name": "react-app",
      "version": "0.1.0",
      "private": true,
      "dependencies": {
        "@testing-library/jest-dom": "^5.16.5",
        "@testing-library/react": "^13.4.0",
        "@testing-library/user-event": "^13.5.0",
        "react": "^18.2.0",
        "react-dom": "^18.2.0",
        "react-scripts": "5.0.1",
        "web-vitals": "^2.1.4"
      },
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject"
      },
      "eslintConfig": {
        "extends": [
          "react-app",
          "react-app/jest"
        ]
      },
      "browserslist": {
        "production": [
          ">0.2%",
          "not dead",
          "not op_mini all"
        ],
        "development": [
          "last 1 chrome version",
          "last 1 firefox version",
          "last 1 safari version"
        ]
      },
      "devServer": {
        "port": 3001,
        "headers": {
          "Access-Control-Allow-Origin": "*"
        }
      }
    }

    package.json 中添加 devServer 配置,允许跨域请求。

  3. 创建 Vue 微应用 (Vue App):

    vue create vue-app
    cd vue-app
    npm install
    npm start

    修改 src/main.js:

    import Vue from 'vue'
    import App from './App.vue'
    
    Vue.config.productionTip = false
    
    let instance = null;
    function render(props = {}) {
      const { container } = props;
      instance = new Vue({
        render: (h) => h(App),
      }).$mount(container ? container.querySelector('#app') : '#app');
    }
    
    if (!window.__POWERED_BY_QIANKUN__) {
      render();
    }
    
    export async function bootstrap() {
      console.log('[vue-app] bootstraped');
    }
    
    export async function mount(props) {
      console.log('[vue-app] mount', props);
      render(props);
    }
    
    export async function unmount() {
      console.log('[vue-app] unmount');
      instance.$destroy();
      instance.$el.innerHTML = '';
      instance = null;
    }

    修改 vue.config.js:

    module.exports = {
      devServer: {
        port: 3002,
        headers: {
          'Access-Control-Allow-Origin': '*',
        },
      },
      configureWebpack: {
        output: {
          library: 'vue-app',
          libraryTarget: 'umd',
        },
      },
    };

    vue.config.js 中配置 devServerconfigureWebpackdevServer 用于允许跨域请求,configureWebpack 用于配置输出格式为 UMD。

  4. 启动所有应用:

    分别启动主应用、React 微应用和 Vue 微应用。

    在浏览器中访问主应用 (http://localhost:3000),然后访问 http://localhost:3000/#/reacthttp://localhost:3000/#/vue,就可以看到 React 和 Vue 微应用被加载到主应用中了。

微前端的挑战与注意事项

微前端架构虽然有很多优点,但也带来了一些挑战:

  • 复杂性增加: 需要管理多个应用,增加了整体的复杂性。
  • 运维成本增加: 需要部署和维护多个应用,增加了运维成本。
  • 共享依赖: 如果多个微应用依赖同一个库的不同版本,可能会导致冲突。
  • 性能问题: 如果微应用加载和渲染速度慢,可能会影响用户体验。
  • SEO问题: 微前端架构可能会对搜索引擎优化产生影响,需要特别注意。

因此,在选择微前端架构之前,需要仔细评估项目的需求和团队的能力。

总结

微前端是一种非常有潜力的前端架构,可以帮助我们解决大型前端应用开发和维护的难题。但是,它也带来了一些挑战,需要我们认真对待。希望今天的讲座能帮助大家更好地理解微前端,并在实际项目中应用它。

今天的分享就到这里,谢谢大家!有问题欢迎提问。

发表回复

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