微前端架构学习笔记

发布于:2024-12-18 ⋅ 阅读:(178) ⋅ 点赞:(0)

前言

之前遇到过一个需求,有两个项目分别由两个不同的部门负责,不同技术栈,不同代码仓库:

  • A 项目是官网,负责展示产品亮点等信息,有多个入口可以进入 B 项目中的不同页面。
  • B 项目是业务线,负责处理具体的功能,可以跳转到 A 项目中登录/注册等。

现在想在这样跨域的两个项目之间,仅通过两个客户端,如何传递客户从 A 项目中上传的文件,使得 B 项目可以接收文件再处理后续的业务逻辑。

因为并未引入微前端方案,最终通过 传递文件的 DataURL + IndexedDB 的方式来实现。

如果引入了微前端的方案,那么这个问题就很好解决。

跑题一点,每个文件都可以生成 DataURL 和 BlobURL,在这篇文章中不展开讲了,之后会更新一篇文章记录两者的使用。

最终决定使用 DataURL 的原因是,涉及到链接跳转的动作,会导致 BlobURL 丢失。

案例

        假设现在有一个电商系统,是用 JQuery 框架编写的,分别有商品管理、用户中心、物流管理...等,现在想开展新的业务(比如新增一个订单管理),但是又想使用现在流行的 Vue、React 等框架。如何可以既保留现有业务的运行,又可以在想加新业务的时候就加呢? 

没引入微前端前:

如果想修改里面任意一个模块的内容,在修改之后,即使其他模块的代码没有改动,都需要再将整个应用进行打包。 

引入微前端后:

 如果想修改里面任意一个模块的内容,在修改之后,只需要将该模块进行打包。 


 1.什么是微前端?

        微前端(Micro Frontends)是一种将前端应用程序拆分成多个较小、独立、可管理的部分的架构方法。

图片来源于micro-app官网 

微前端借鉴微服务架构(Micro Services),将一个前端应用程序拆分成多个子应用,子应用可以分别独立开发、测试、部署和维护。

微前端平台

single-spa

将多个独立的单页面应用组合到一个父应用中的微前端框架

特点
  • 无框架限制

        对于 React,可以使用 single-spa-react

        对于 Vue,可以使用 single-spa-vue

        对于 Angular,可以使用 single-spa-angular

  • 可以与其他微前端框架(如 qiankun)结合使用
  • 主应用与子应用无耦合,每个子应用都可以独立加载和卸载
缺点
  • 配置较为繁琐,需要开发者手动管理每个子应用的生命周期和加载

        要求子应用暴露三个生命周期方法(bootstrapmountunmount),并且需要对子应用的入口进行适当的修改。

适用场景
  • 需要多种前端技术栈共存的项目
  • 可以与现有的应用逐步迁移到微前端架构中
以 Vue3.0 为例,借用 single-spa-vue
npm install vue@next single-spa-vue
目录结构
my-micro-frontend/
├── dist/                      # 构建后的输出
├── public/
│   └── index.html             # 主页面,包含单页面应用入口
├── src/
│   ├── main.ts                # 主应用,加载子应用
│   ├── vue-app.ts             # 子应用生命周期方法
│   ├── vue-app.vue            # Vue 3 组件
├── tsconfig.json              # TypeScript 配置
├── webpack.config.js (or vite.config.ts)
└── package.json
配置子应用
// vue-app.vue
<template>
  <div id="app">
    <h1>Vue 3 Micro Frontend</h1>
    <p>This is a micro frontend app rendered using Vue 3 and Single-spa!</p>
  </div>
</template>

<script setup lang="ts">
/**
 * 这个组件通过 single-spa 在主应用中渲染
 */
</script>

        在 vue-app.ts 中,使用 singleSpaVue 暴露 bootstrapmountunmount 方法。

// vue-app.ts
import { createApp, App as VueApp } from "vue";
import { singleSpaVue } from "single-spa-vue";
import App from "./App.vue";

// 定义 Single-spa 生命周期方法的类型
const vueLifecycles = singleSpaVue({
  createApp,
  appOptions: {
    render: (h: any) => h(App), // 渲染根组件
  },
});

// 暴露 Single-spa 所需的生命周期方法
export const bootstrap: (props: any) => Promise<void> = vueLifecycles.bootstrap;
export const mount: (props: any) => Promise<void> = vueLifecycles.mount;
export const unmount: (props: any) => Promise<void> = vueLifecycles.unmount;
配置主应用

        主应用负责注册子应用并启动 single-spa,不需要直接渲染 Vue 应用。在主应用中不需要手动调用 createApp(App).mount('#app'),都由 single-spa-vue 管理。

// main.ts
import { registerApplication, start } from "single-spa";

// 注册 Vue 子应用
registerApplication(
  "vue-app", // 子应用名称
  () => import("./vue-app.ts"), // 动态导入子应用生命周期方法
  (location) => location.pathname.startsWith("/vue-app") // 激活条件
);

// 启动 single-spa
start();
配置 Webpack 或 Vite
// webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/main.ts",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
    publicPath: "/",
  },
  resolve: {
    extensions: [".ts", ".js", ".vue", ".json"],
    alias: {
      vue: "vue/dist/vue.esm-bundler.js",
    },
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: "ts-loader",
        exclude: /node_modules/,
      },
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
    ],
  },
  plugins: [
    new (require("vue-loader").VueLoaderPlugin)(),
  ],
  devServer: {
    historyApiFallback: true,
  },
};
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
  build: {
    target: "esnext",
    outDir: "dist",
    lib: {
      entry: "src/vue-app.ts",
      name: "VueApp",
      fileName: "vue-app",
      formats: ["es"],
    },
    rollupOptions: {
      external: ["vue"],
    },
  },
});
qiankun

基于 single-spa 的微前端实现库,提供了更易用的 API

特点
  • 无框架限制
  • 支持按需加载子应用,支持懒加载、动态加载
  • 提供沙箱机制,可以隔离不同微前端应用的全局状态
  • 支持使用插件系统来扩展功能
缺点
  • 封装了 single-spa,因此继承了对子应用的生命周期管理
适用场景
  • 适合已有单页面应用架构,并计划迁移或拆分成多个微前端应用的场景
MicroApp

基于 WebComponent 的微前端框架,使用原生浏览器支持的 Web Component 标准来封装和管理微前端应用

特点
  • 将每个微前端应用封装为 Web Component,子应用能够更好地与其他应用隔离
  • Web Component 是原生浏览器技术,MicroApp 不依赖任何前端框架
  • 支持自定义生命周期函数,能精细控制子应用的加载、渲染、销毁等过程
缺点
  • Web Component 在不同浏览器的兼容性可能会存在差异
  • 对开发者来说,需要一定的 Web Component 知识
适用场景
  • 适合希望轻量化的微前端架构,或者不希望依赖第三方框架的场景
  • 对应用的封装性和隔离性有较高要求的场景
Module federation

Webpack 5 引入的一个新特性,允许将一个应用的模块动态加载到另一个应用中。

特点
  • 能够共享模块,比如 React 和 Vue 等常用依赖,避免重复加载
  • 可以按需加载子应用,并支持运行时动态决定要加载哪些模块
  • 不依赖框架,是直接基于 Webpack 的构建系统
缺点
  • 配置复杂,需要对 Webpack 的运行机制有较深入的了解
  • 很少提供封装好的 API,开发者需要更多的手动管理
适用场景
  • 适合已经使用 Webpack 的项目,特别是对微前端的集成要求较高的场景

2.微前端的架构实现方式

1)基于 URL 路由

通过 URL 路由来加载不同的微前端子应用

        假设有一个电商平台,主应用是一个统一的首页展示,而不同的页面(如产品详情页、用户中心、购物车等)分别由不同的子应用负责。

  • 使用主应用中的路由管理器来控制子应用的加载和渲染。
  • 主应用的路由会根据 URL 地址来决定展示哪个子应用的页面。
// 主应用路由配置示例
const routes = [
  { path: '/products', component: ProductApp },
  { path: '/cart', component: CartApp },
  { path: '/user', component: UserApp }
];

2)基于 Web Components

使用 Web Components 技术来将不同的子应用封装成独立的组件。这样可以确保每个子应用的样式和功能相互隔离,避免冲突。

        假设有一个新闻网站,主应用展示统一的导航栏和布局,而每个新闻分类(如国内新闻、国际新闻、科技新闻)由不同的微前端子应用负责。

  • 每个子应用作为一个独立的 Web Component 封装,主应用直接通过 <my-product-app></my-product-app> 这样的标签来加载和渲染。
  • Web Components 提供了封装功能,避免了样式和脚本冲突。
基础写法
// 子应用:ProductApp 使用 Web Components 封装
class ProductApp extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<div>Product List</div>`;
  }
}

customElements.define('my-product-app', ProductApp);

// 主应用直接使用 Web Component
<my-product-app></my-product-app>
复杂结构写法
// 定义子应用组件 ProductApp
class ProductApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' }); // 使用 Shadow DOM,避免样式污染
  }

  connectedCallback() {
    this.render();
  }

  render() {
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        .product-list {
          color: #333;
          font-size: 16px;
        }
        .product-item {
          margin: 10px 0;
        }
      </style>
      <div class="product-list">
        <div class="product-item">Product 1</div>
        <div class="product-item">Product 2</div>
        <div class="product-item">Product 3</div>
      </div>
    `;

    // 将模板内容插入 Shadow DOM
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

// 注册自定义元素
customElements.define('my-product-app', ProductApp);
// 在主应用中
<my-product-app></my-product-app>

通过 Shadow DOM 进行样式隔离,也可以使用外部样式表。Shadow DOM 有两个主要好处:

  • 样式封装:子应用的样式不会影响到主应用或其他子应用。
  • DOM 隔离:确保子应用的内部结构和样式不会被外部应用干扰。
使用外部样式表
class ProductApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
  }

  render() {
    // 引入外部样式
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = 'styles.css'; // 外部 CSS 文件

    // 模板内容
    const template = document.createElement('template');
    template.innerHTML = `
      <div class="product-list">
        <div class="product-item">Product 1</div>
        <div class="product-item">Product 2</div>
        <div class="product-item">Product 3</div>
      </div>
    `;

    // 将样式和模板添加到 Shadow DOM
    this.shadowRoot.appendChild(link);
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

3)基于 JavaScript 动态加载

使用 JavaScript 动态加载工具(如 Webpack)来按需加载不同的子应用。

        假设有一个在线教育平台,主应用加载时并不加载所有课程内容,而是按需加载不同课程模块。

  • 在主应用中通过 import() 动态加载子应用的 JavaScript 模块。
// 使用 Webpack 动态加载子应用
const loadCourseApp = () => {
  import(/* webpackChunkName: "course-app" */ './course-app')
    .then(module => {
      module.renderCoursePage();
    })
    .catch(err => {
      console.error('Error loading course app:', err);
    });
};

4)基于 iFrame

每个子应用运行在一个独立的 iFrame 中,彼此之间无任何依赖。

        假设有一个跨域的企业管理平台,不同的部门使用不同的子系统(如人事管理、财务管理等),每个部门的系统是独立的。

  • 每个子应用运行在独立的 iFrame 中,iFrame 嵌入到主应用页面中。
  • 可以完全隔离不同的子系统,避免了跨域和样式冲突的问题。
<!-- 主应用使用 iFrame 嵌入子应用 -->
<iframe src="https://example.com/finance" width="100%" height="500px"></iframe>
<iframe src="https://example.com/hr" width="100%" height="500px"></iframe>

3.类单页应用

基于微前端架构,将两个不同的微前端模块(例如:文档管理模块和 AI 工具模块)聚合在一个页面中,通过路由动态加载它们。用户体验像单页应用,但实际上是多个独立模块通过微前端框架聚合而成的“类单页应用”。

技术栈

  • 使用 Vue 3React 18 分别构建两个微前端模块。
  • 使用 qiankun 作为微前端框架进行整合。

主应用(Aggregator)

目录结构
main-app/
├── src/
│   ├── main.js
│   ├── App.vue
│   ├── router.js
│   └── views/
│       ├── DocsView.vue
│       └── AIView.vue
├── public/
└── package.json
安装依赖 
npm install vue@next vue-router qiankun
代码部分
main.js
// main.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);

registerMicroApps([
  {
    name: 'document-management',
    entry: '//localhost:3001',
    container: '#micro-container',
    activeRule: '/docs',
  },
  {
    name: 'ai-tools',
    entry: '//localhost:3002',
    container: '#micro-container',
    activeRule: '/ai',
  },
]);

start();
app.mount('#app');
 router.js
// router.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  { path: '/', redirect: '/docs' }, // 默认跳转到文档管理
  { path: '/docs', component: () => import('./views/DocsView.vue') },
  { path: '/ai', component: () => import('./views/AIView.vue') },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;
 App.vue
<template>
  <div id="app">
    <nav>
      <router-link to="/docs">文档管理</router-link>
      <router-link to="/ai">AI 工具</router-link>
    </nav>
    <router-view></router-view>
    <div id="micro-container"></div> <!-- 微前端子应用挂载点 -->
  </div>
</template>

<script>
export default {
  name: 'App',
};
</script>
DocsView.vue
<template>
  <div>
    <h2>文档管理模块</h2>
    <div id="micro-container"></div> <!-- 子应用挂载点 -->
  </div>
</template>
AIView.vue
<template>
  <div>
    <h2>AI 工具模块</h2>
    <div id="micro-container"></div> <!-- 子应用挂载点 -->
  </div>
</template>

子应用1:文档管理模块(Vue 3)

目录结构
document-management/
├── src/
│   ├── main.js
│   ├── App.vue
│   └── router.js
├── public/
└── package.json
安装依赖
npm install vue@next vue-router qiankun
代码部分
main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { renderWithQiankun, qiankunWindow } from 'qiankun';

let app = null;

function render(props = {}) {
  const { container } = props;
  app = createApp(App).use(router);
  app.mount(container ? container.querySelector('#app') : '#app');
}

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render();
}

renderWithQiankun({
  mount(props) {
    render(props);
  },
  bootstrap() {},
  unmount() {
    app.unmount();
  },
});
router.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from './views/Home.vue';

const router = createRouter({
  history: createWebHistory('/docs'),
  routes: [
    { path: '/', component: Home },
  ],
});

export default router;
App.vue
<template>
  <div>
    <h3>文档管理子应用</h3>
    <router-view></router-view>
  </div>
</template>
Home.vue
<template>
  <div>
    <p>这里是文档管理的主页面</p>
  </div>
</template>

子应用 2:AI 工具模块(React 18)

目录结构
ai-tools/
├── src/
│   ├── index.js
│   ├── App.jsx
└── package.json
安装依赖
npm install react react-dom qiankun
代码部分
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { renderWithQiankun, qiankunWindow } from 'qiankun';

function render(props) {
  const { container } = props;
  ReactDOM.render(
    <App />,
    container ? container.querySelector('#root') : document.getElementById('root')
  );
}

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render({});
}

renderWithQiankun({
  mount(props) {
    render(props);
  },
  bootstrap() {},
  unmount(props) {
    const { container } = props;
    ReactDOM.unmountComponentAtNode(
      container ? container.querySelector('#root') : document.getElementById('root')
    );
  },
});
App.jsx
import React from 'react';

const App = () => {
  return (
    <div>
      <h3>AI 工具子应用</h3>
      <p>这里是 AI 工具模块。</p>
    </div>
  );
};

export default App;
运行方式
启动主应用:

监听 http://localhost:3000

cd main-app
npm run dev
启动子应用:

文档管理模块:监听 http://localhost:3001

cd document-management
npm run dev

AI 工具模块:监听 http://localhost:3002

cd ai-tools
npm start

访问主应用:

  • 打开 http://localhost:3000/docs 加载文档管理模块。
  • 打开 http://localhost:3000/ai 加载 AI 工具模块。

4.参考文章链接

https://tech.meituan.com/tags/%E5%BE%AE%E5%89%8D%E7%AB%AF.html

https://wujie-micro.github.io/doc/guide/

https://juejin.cn/post/7113503219904430111

https://developer.jdcloud.com/article/2898


网站公告

今日签到

点亮在社区的每一天
去签到