微前端破局:告别巨石应用

461 阅读10分钟

你是否还在为“巨石应用”或“跨技术栈集成”而头疼不已?微前端,或许正是你需要的解决方案。它借鉴微服务理念,将单体SPA(单页面应用)拆分为独立开发、独立部署、独立运行的微应用(小型SPA)。

一、Web架构演进

df057d64b72ae9efedb6cbefdcb06864.png

图1:Web应用架构演进示意图

Web应用架构的演进可以分为三个阶段:

  1. 单体应用

在前期的web应用中,前后端代码紧密耦合在一起(例如经典的JSP技术)。后端查询数据库,并把数据套入html模板返回给浏览器。这种架构的缺点显而易见:用户体验差(每次交互都需要刷新整个页面),更适合展示静态网页

  1. 前后端分离

AJAX技术的兴起,使得网页在不刷新html的情况下获取后端数据,并局部刷新页面。至此,前后端分离,前端更专注于用户交互并通过XMLHttpRequeat和后端数据交互,SPA成为这个阶段的典型代表。但是,随着业务发展和代码堆积,应用复杂度变得不可控,最终导致了难以维护的“巨石应用”。

  1. 微服务

后端应用根据业务领域拆分为多个独立的微服务,服务之间通过远程调用进行交互,从而实现复杂度可控。前端调用后端接口时,一般会经过API网关或BFF路由到具体的微服务。

面对业务需求膨胀带来的应用复杂度,后端通过微服务进行拆解分治,而前端还处于传统的SPA,又该如何解决这个问题呢?

二、前端困局

  • 巨石应用

用户体验需求的不断提升和前端技术的快速发展,使得SPA的复杂度呈指数级增长。这导致:代码修改牵一发而动全身,维护成本高昂;构建速度缓慢,资源加载延迟,严重影响用户体验(UX)和开发体验(DX)

  • 跨技术栈集成

在逐步重构历史系统时,新老应用需要共存;在整合历史应用时,不同技术栈的业务应用需要集成在一起。核心难点在于:不同技术栈,无法统一编码和部署

上面两个问题表面看似迥然不同,但“巨石应用”的拆分需求与“跨技术栈集成”的整合需求,其本质都是如何实现有效的“系统解耦”。因此,借鉴后端“微服务”的成功经验来解决前端问题,成为一种自然的思路。

三、微服务和微前端

微服务是一种应用架构,它将一个大型单体应用拆分为多个独立的小型服务,并通过轻量级的通信协议组织起来。重点在于独立二字,即独立开发、独立部署、独立运行。微服务具备以下优势:

  • 系统解耦:每个服务聚焦单一的业务功能,不互相依赖,系统复杂度可控。
  • 快速迭代:新增功能或修复bug时,只要更新对应的服务,不用覆盖整个系统。
  • 故障隔离:单个服务故障不会导致整个系统崩溃。
  • 多技术栈共存:不同服务可以用不同技术栈开发,解决了逐步重构的多技术栈共存难题。

微前端(Micro Frontends)正是借鉴了微服务的核心理念,将一个单体应用(SPA)拆分为多个“微应用”(SPA)。这些微应用同样遵循“独立开发、独立部署、独立运行”的原则,并通过路由分发机制(通常由“基座应用”负责)组织在一起,共同构成完整的用户界面

image.png 图2:微前端架构示意图

了解了什么是微前端,接下来我们将介绍几个主流的微前端框架,包括使用方法和各自的特点。

四、开山鼻祖:single-spa

作为微前端领域的开创者,single-spa 提出了一种革命性的架构:把微应用当做react/Vue组件进行管理。每个微应用需要导出挂载、卸载等生命周期函数,single-spa会根据url变化调用这些函数控制微应用的挂载和卸载。

1. 使用方法

  1. 注册微应用
import { registerApplication } from 'single-spa';

registerApplication({
  name: 'app1',
  app: () => import('src/app1/main.js'),
  activeWhen: '/app1',
  customProps: {
    some: 'value',
  }
);

singleSpa.start() // 启动基座应用

示例说明:当用户访问基座应用的 '/app1' 路由时,single-spa 会动态加载并执行 app1 的 main.js 入口文件,触发其 mount 生命周期函数,最终将 app1 的界面渲染到指定的 DOM 节点。

  1. 导出微应用
import React from 'react';
import ReactDOM from 'react-dom';
import App from './index.tsx'

export const bootstrap = () => {
// 可选:微应用首次挂载前执行一次,常用于初始化全局资源
}
export const mount = () => {
  ReactDOM.render(<App/>, document.getElementById('root'));
}
export const unmount = () => {
// 可选:微应用卸载时执行,用于清理资源(如事件监听、定时器、DOM元素等)
}

微应用要导出bootstrap、mount、unmount等生命周期函数,single-spa会根据url变化调用这些函数控制微应用的加载和卸载。

2. 局限性

single-spa实现了路由转发和微应用生命周期管理等核心功能,但是也存在以下局限性。

  • 基于js entry的资源加载模式

注册时指定微应用的入口js文件路径,如果文件名有变化,需要同步更新基座应用的配置。并且,默认是在编译时加载微应用资源,无法动态加载,违背“独立部署”的原则。另外,微应用只能打包成一个js文件,无法按需加载,影响性能。

  • 缺乏沙箱隔离机制

当有多个子应用时,存在js全局变量冲突和css冲突。,比如:微应用A声明了一个全局变量 window.a,这时候切换到微应用B,B也有一个全局变量window.a,如何保证访问到正确的值?这种冲突可能导致难以预测的行为或直接导致应用崩溃

五、更完善的框架:qiankun

qiankun 在 single-spa 的基础上进一步完善,提供了开箱即用的能力,是目前国内最流行的微前端解决方案之一。

1. 使用方法

  1. 在基座应用中注册微应用
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'app1',
    entry: 'http://domain1/app1',
    container: '#container',
    activeRule: '/app1',
  },
  {
    name: 'app2',
    entry: 'http://domain2/app2',
    container: '#container',
    activeRule: '/app2',
  },
]);

start();

使用方法跟single-spa差异不大,但是entry不再是具体的js文件路径,而是微应用的入口URL(通常是其index.html的访问地址)。这是 qiankun 解决 single-spa js entry 限制的核心—引入html entry。qiankun会请求入口地址,获取入口html并解析html,然后构造请求去加载html中的script、style资源

  1. 导出微应用
import React from 'react';
import ReactDOM from 'react-dom';
import App from './index.tsx'

export const bootstrap = () => {}
export const mount = (props) => {
  ReactDOM.render(<App/>, props.container.getElementById('root'));
}
export const unmount = () => {}
  1. 微应用打包配置
const packageName = require('./package.json').name;  
  
module.exports = {  
output: {  
library: `${packageName}-[name]`,  
libraryTarget: 'umd',  
jsonpFunction: `webpackJsonp_${packageName}`,  
},  
};

微应用打包成一个umd库,并设置库名称为注册微应用时的name,,便于qiankun正确加载和执行打包后的代码。

2. 优点

  • 基于html entry的资源加载模式

在single-spa中要实现动态加载微应用资源,得借助SystemJS工具。

import { registerApplication } from 'single-spa';

registerApplication({
  name: 'app1',
  app: () => System.import('http://domain1/app1/main.js'),
  activeWhen: '/app1',
  customProps: {
    some: 'value',
  }
);

singleSpa.start() // 启动基座应用

只有实现了动态加载,微应用才能独立部署。另外也不再需要关注入口js文件,html entry能够根据入口地址自动加载html、js、css资源。这种方式更符合常规的Web应用加载模式,天然支持动态加载和独立部署。

  • js沙箱

qiankun通过js沙箱,解决了js全局变量冲突。具体是通过proxy代理window对象,为每个微应用创建一个window对象副本。当切换应用时,微应用通过proxy访问到对应的window对象副本。

  • css隔离

通过shadow dom实现css隔离,这是浏览器原生支持的。为每个微应用创建一个shadow dom的父节点,shadow dom内部的dom和css不会影响外部,外部也不会访问到内部的数据。

还有一种方案就是scoped css,在css选择器里加一个前缀,不同微应用的css选择器就不会冲突。qiankun更推荐使用此方案。

  • 应用状态管理

qiankun提供了设置全局状态和监听全局状态的方法,从而实现基座应用和微应用的状态管理。

主应用

import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

微应用

// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });

  props.setGlobalState(state);
}

六、qiankun避坑指南

  • 微应用资源跨域

默认情况下,浏览器用<script>和<style>加载js、css资源,不受同源策略限制。而qiankun是由框架构造XHR请求获取微应用的js、css资源,存在跨域问题(主应用和微应用的域名不一致),通过修改微应用的nginx配置即可。

server {
   listen 80;
   server_name app1.com;
   location / {
      add_header Access-Control-Allow-Origin main1.com; // 允许基座应用(main1.com)访问
   }
}
  • dialog组件无法找到body节点

qiankun支持两种沙箱方案,一种是shadow dom,虽然shadow dom可以实现彻底的css隔离,但也会导致微应用无法访问html、body等全局DOM节点,例如dialog、Modal等组件无法找到body节点,进而无法挂在到DOM上。所以一般推荐用另一种方案,即scoped css。

// shadow dom
start({
  sandbox: {
    strictStyleIsolation: true,
  }
})

// scoped css
start({  
  sandbox: {  
    experimentalStyleIsolationtrue,  
  }  
})
  • 微应用hash路由不生效

当基座应用使用history路由,微应用使用hash路由时,会出现在基座应用跳转微应用页面时路由不生效。因为在基座应用是使用的history api,不会触发hash事件。解决方案就是在基座应用路由微应用页面时加上触发hash事件的逻辑。

七、为什么不用iframe

iframe是浏览器原生技术,常用来在网页中嵌入另一个网页,这一点和微前端是不谋而合的。但是它的强隔离性也带来了一系列问题:

  • 刷新会丢失路由状态
  • dom割裂,iframe内部的弹窗无法全局展示
  • 跨域,客户端的登录态无法共享,子应用需要重新登录
  • 通信复杂,只能通过postmessage传递消息

因此,对于追求无缝用户体验、复杂交互和状态共享的现代 Web 应用,iframe 通常不是微前端的理想选择。

八、总结

微前端架构借鉴微服务的成功经验,将单体SPA拆分为独立的微应用(独立开发、独立部署、独立运行),解决了“巨石应用”和“跨技术栈集成”的难题。同时也介绍主流微前端框架(single-spa,qiankun,iframe)的相关实践。后续我们将继续探索微前端的核心实现原理,敬请期待。

参考资料

一个js库就把你的网页的底裤🩲都扒了——import-html-entry

single-spa官网

qiankun官网

微前端方案 qiankun 只是更完善的 single-spa

HTML Entry 源码分析

qiankun 2.x 运行时沙箱 源码分析

Garfish 微前端实现原理

在腾讯换了新部门,微前端 + 重构 Vue -> React 项目实战落地总结

微前端在小米 CRM 系统的实践

有赞美业微前端的落地总结

字节跳动是如何落地微前端的

前端微服务在字节跳动的打磨与应用

将微前端做到极致-无界方案

一文看透 Module Federation