likes
comments
collection
share

微前端理论和实践

作者站长头像
站长
· 阅读数 16

微服务的主要思路

  • 将应用分解为小的、互相连接的微服务,一个微服务完成某个特定功能 。
  • 每个微服务都有自己的业务逻辑和适配器,不同的微服务,可以使用不同的技术去实现。
  • 使用统一的网关进行调用。

微服务的主要思路是化繁为简,通过更加细致的划分,使得服务内部更加内聚,服务之间耦合性降低,有利于项目的团队开发和后期维护。把微服务的概念应用到前端, 前端微服务/微前端服务就诞生了,简称其为微前端。

微前端是什么

微前端理论和实践 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

WEB应用面临的问题

  • DX(developer experience)
    • 业务领域的代码库不够独立和高度可重用
    • 相同的产品功能由多个团队开发 / 产品功能难以保持统一
    • 新的产品理念无法在不同的应用中快速复用 / 实现
    • 快速迭代新子业务 / 干净移除将被淘汰的子业务
  • UX(user experience)
    • 性能体验
    • 页面跳转和用户体验问题
  • 多个系统在一个仓库应用中,不同子应用独立SPA模式
  • 系统分为多个仓库,独立上线部署,采用MPA模式

任何新技术的产生都是为了解决现有场景和需求下的技术痛点,微前端也不例外:

  1. 拆分和细化:当下前端领域,单页面应用(SPA)是非常流行的项目形态之一,而随着时间的推移以及应用功能的丰富,单页应用变得不再单一而是越来越庞大也越来越难以维护,往往是改一处而动全身,由此带来的发版成本也越来越高。微前端的意义就是将这些庞大应用进行拆分,并随之解耦,每个部分可以单独进行维护和部署,提升效率。
  2. 整合历史系统:在不少的业务中,或多或少会存在一些历史项目,这些项目大多以采用老框架类似(Backbone.js,Angular.js 1)的B端管理系统为主,介于日常运营,这些系统需要结合到新框架中来使用还不能抛弃,对此我们也没有理由浪费时间和精力重写旧的逻辑。而微前端可以将这些系统进行整合,在基本不修改来逻辑的同时来同时兼容新老两套系统并行运行。

微前端核心价值

  • 技术栈无关 主框架不限制接入应用的技术栈,子应用可自主选择技术栈
  • 独立开发/部署 各个团队之间仓库独立,单独部署,互不依赖
  • 增量升级 当一个应用庞大之后,技术升级或重构相当麻烦,而微应用具备渐进式升级的特性
  • 独立运行时 微应用之间运行时互不依赖,有独立的状态管理
  • 提升效率 应用越庞大,越难以维护,协作效率越低下。微应用可以很好拆分,提升效率

微前端应用具备的能力

微前端理论和实践

常见的微前端方案

iframe

优点

  1. 非常简单,无需任何改造
  2. 完美隔离,JS、CSS 都是独立的运行环境
  3. 不限制使用,页面上可以放多个 iframe 来组合业务

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。

缺点

  1. URL 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

Web Components

很多小伙伴对 Web Components不是很了解,它是由google推出的浏览器的原生组件,具体内容查看 链接 了解

优点

  • 复用性:不需要对外抛出加载和卸载的全局 API,可复用能力更强
  • 标准化:W3C 的标准,未来能力会得到持续升级
  • 插拔性:可以非常便捷的进行移植和组件替换

缺点

  • 兼容性:对于 IE 浏览器不兼容,需要通过 Polyfill 的方式进行处理
  • 学习曲线:相对于传统的 Web 开发,需要掌握新的概念和技术

基于webpack module federation

模块联邦主要是一种去中心化的思想,也可以用来做服务拆分, 实现原理比较复杂,主要涉及到以下几个方面:

模块接口定义

在需要共享的模块中,通过 module.exports 或 export 将需要共享的模块封装成一个模块接口,并将其在模块系统中注册。

共享模块的描述信息

在需要共享模块的应用程序中,通过使用 ModuleFederationPlugin 插件,将需要共享的模块的描述信息以 JSON 格式写入配置中。描述信息包括需要共享的模块名称、模块接口、提供共享模块的应用程序的 URL 等。

共享模块的加载

在需要使用共享模块的应用程序中,通过 webpack 的 container 远程加载共享模块的代码,并将其封装成一个容器。容器在当前应用程序中的作用是在容器中运行共享模块的代码,并按照描述信息将导出的模块接口暴露出来。容器本身是一个 JavaScript 运行时环境,它可以在需要使用共享模块的应用程序中被动或主动加载。

远程模块的执行

在容器中加载共享模块的代码后,容器需要将其执行,并将执行过程中产生的模块接口导出。为了实现这个目的,容器会利用 webpack 打包时在编译过程中生成的一个特殊的运行时代码,即 remoteEntry.js,通过 script 标签远程加载到当前应用程序中。在这个特殊的运行时代码中,会封装一些与容器通信的方法,例如 remote 方法,可以用于按需加载模块、获取模块接口等。 综上,webpack-module-federation 基于这些原理,实现了多个独立的应用程序之间的模块共享和远程加载,从而可以实现高度解耦、可扩展的架构。

主流微前端框架

single-spa

single-spa只是实现了加载器、路由托管。沙箱隔离并没有实现。single-spa只做了两件事情:

  • 加载微应用(加载方法还得用户自己来实现)
  • 管理微应用的状态(初始化、挂载、卸载)

qiankun

qiankun 是一个基于 single-spa微前端 实现库,阿里系开源的微前端框架。 qiankun对single-spa方案进行完善,主要的完善点:

  • 子应用资源由 js 列表修改进为一个url,大大减轻注册子应用的复杂度
  • 实现应用隔离,完成js隔离方案 (window工厂) 和css隔离方案 (类vue的scoped)
  • 增加资源预加载能力,预先子应用html、js、css资源缓存下来,加快子应用的打开速度,通过 import-html-entry 包解析 HTML 获取资源路径,然后对资源进行解析、加载

Mirco-App

使用qiankun搭建微前端项目实战

采用 React 作为主应用基座,接入 Vue 技术栈的子应用和 React 技术栈的完整项目 tdesign-react-starter

主应用

使用 create-react-app 生成一个 React 的项目,初始化主应用,将它作为 qiankun 主应用基座。

  1. 创建微应用容器 - 用于承载微应用,渲染显示微应用;
  2. 注册微应用 - 设置微应用激活条件,微应用地址等等;
  3. 启动 qiankun;

安装依赖

npm i react-router-dom -S
npm i antd -S
npm i qiankun -S

注册微应用

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

import {
  registerMicroApps,
  start,
  initGlobalState,
} from 'qiankun';

// 传值相关逻辑
const state = { id: 1 };
const actions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

registerMicroApps([
  {
    name: 'react1App',
    entry: '//localhost:3003',
    container: '#micro-box',
    activeRule: '/app-react1',
  },
  {
    name: 'vue2App',
    entry: '//localhost:5173',
    container: '#micro-box',
    activeRule: '/app-vue2',
  },
]);
start();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

菜单逻辑代码

// src/app.js
import { BrowserRouter as Router, Link } from 'react-router-dom';
import { Menu } from 'antd';

const Menus = [
  {
    key: 'app-vue1',
    label: <Link to='/app-react1'>react微应用1</Link>,
  },
  {
    key: 'app-vue2',
    label: <Link to='/app-vue2'>vue微应用2</Link>,
  },
];

function App() {
  return (
    <Router>
      <div style={{ display: 'flex' }}>
        <Menu
          style={{
            width: 200,
            height: '100vh',
          }}
          theme='dark'
          mode='inline'
          items={Menus}
        />
        <div
          id='micro-box'
          style={{ flex: 1 }}
        />
      </div>
    </Router>
  );
}

export default App;

实现效果

微前端理论和实践

微应用react1App

微应用react1App 使用的是tdesign-react-starter,可以按照 官网 安装项目。

安装依赖

npm i vite-plugin-qiankun -S

配置微应用

在主应用注册好了微应用后,我们还需要对微应用进行一系列的配置。首先,我们在入口文件 main.js 中,导出 qiankun 主应用所需要的三个生命周期钩子函数(这里需要注意的是React18版本的卸载函数和vite的qiankun插件):

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import store from 'modules/store';
import App from 'layouts/index';
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';

import 'tdesign-react/es/style/index.css';

import './styles/index.less';

const env = import.meta.env.MODE || 'development';
const baseRouterName = env === 'site' ? '/starter/react/' : '';

let root: ReactDOM.Root | null = null;

const renderApp = (props) => {
  const container = props?.container;
  const app = container ? container?.querySelector('#app')! : document.getElementById('app')!;
  root = ReactDOM.createRoot(app);
  root.render(
    <Provider store={store}>
      <BrowserRouter basename={baseRouterName}>
        <App />
      </BrowserRouter>
    </Provider>,
  );
};
renderWithQiankun({
  mount(props) {
    renderApp(props);
  },
  bootstrap() {
    console.log('bootstrap');
  },
  unmount() {
    root.unmount();
  },
});

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

配置文件

加入vite-plugin-qiankun的配置,注释掉react方法。

import path from 'path';
import { loadEnv } from 'vite';
import { viteMockServe } from 'vite-plugin-mock';
import react from '@vitejs/plugin-react';
import svgr from '@honkhonk/vite-plugin-svgr';
import qiankun from 'vite-plugin-qiankun';

const CWD = process.cwd();

export default (params) => {
  const { mode } = params;
  const { VITE_BASE_URL } = loadEnv(mode, CWD);

  return {

    /**
     * 省略其他配置文件
    **/

    base: VITE_BASE_URL,
    devServer: {
      headers: {
        'Access-Control-Allow-Origin': '*',
      },
    },
    plugins: [
      svgr(),
      // react(), // 这里一定要注释,否则会报错
      qiankun('react1App', {
        useDevMode: true,
      }),
      mode === 'mock' &&
      viteMockServe({
        mockPath: './mock',
        localEnabled: true,
      }),
    ]
  }
}

实现效果

注意:里面的配置需要根据自己的需求改一下。 微前端理论和实践

微应用vue2App

vue这个就简单用vite初始化搭建一下。

安装依赖

npm i vite-plugin-qiankun -S

注册微应用

和之前的react微应用一样,导出 qiankun 主应用所需要的三个生命周期钩子函数:

// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import {
  renderWithQiankun,
  qiankunWindow,
} from 'vite-plugin-qiankun/dist/helper';

let app;

const render = () => {
  app = createApp(App);
  app.mount('#box');
};

const initQianKun = () => {
  renderWithQiankun({
    mount(props) {
      props.onGlobalStateChange((state, prev) => {
        // state: 变更后的状态; prev 变更前的状态
        console.log(state, prev);
      });
      props.setGlobalState({ id: 2 });
      const { container } = props;
      render(container);
    },
    bootstrap() {},
    unmount() {
      app.unmount();
    },
  });
};

qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render();

配置文件

import { fileURLToPath, URL } from 'node:url';

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';
import qiankun from 'vite-plugin-qiankun'

export default defineConfig({
  devServer: {
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
  plugins: [
    vue(),
    Components({
      resolvers: [VantResolver()],
    }),
    qiankun('vue2App', {
      useDevMode: true
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
});

实现效果

微前端理论和实践

参考链接