微前端

cenweilings@163.com Lv2

参考链接:

可能是你见过最完善的微前端解决方案

深入调研了微前端,还是iframe最香

qiankun官方文档

什么是微前端?

微前端是一种软件架构,可以将前端应用拆解成一些更小的能够独立开发部署的微型应用,然后在将这些微型应用组合使其成为整体的架构模式。

微前端类似于组件架构,但不同的是,组件不能够独立构建和发布,但是微前端中的应用是可以的。

微前端架构与框架无关,每个微应用都可以使用不同的框架。

微前端

特性 单体前端 微前端
代码库 单个大型代码库 多个独立代码库
团队结构 集中式团队 分布式独立团队
技术栈 统一技术栈 混合技术栈
部署 整体部署 独立部署
开发速度 后期变慢 持续快速
复杂度 高度耦合 解耦独立

微前端的价值

微前端架构具备以下几个核心价值:

  • 技术栈无关 主框架不限制接入应用的技术栈,子应用具备完全自主权
  • 独立开发、独立部署 子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 独立运行时 每个子应用之间状态隔离,运行时状态不共享

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

应用架构如下:

微前端架构

Stitching layer 作为主框架的核心成员,充当调度者的角色,由它来决定在不同的条件下激活不同的子应用。因此主框架的定位则仅仅是:导航路由 + 资源加载框架

single-spa是什么

好的,我们来详细解释一下 single-spa 是什么。

核心定义

single-spa 是一个用于构建【微前端】架构的 JavaScript 框架。 你可以把它理解为一个顶层的路由器和应用程序加载器

它的核心思想是:将一个大型的单页前端应用(SPA)拆分成多个小型、独立、可以并行开发交付的“微应用”。然后,single-spa 负责在运行时根据一定的规则(通常是当前的 URL)动态地加载、展示、卸载这些微应用。


一个简单的比喻

想象一下你的浏览器是一个舞台:

  • 传统单体应用: 只有一个庞大的剧团表演一整部冗长的戏剧。要更换一个演员(修改一个功能),需要整个剧团停下来重新排练(整个应用重新构建部署)。
  • single-spa 微前端: 舞台(浏览器)本身是空的。有一个导演(single-spa)。导演手里有一个节目单(根配置),上面写着什么节目(微应用)在什么时刻(例如,URL 是 /settings 时)上台表演。
    • 当报幕员说“下一个节目是《用户设置》”时(用户访问了 /settings),导演就喊:“《用户设置》剧组,上台!”(加载并挂载 React 微应用)。
    • 节目表演完了(用户离开了 /settings),导演就喊:“《用户设置》剧组,下台!”(卸载 React 微应用)。
    • 下一个节目是《商品列表》,另一个完全不同的剧组(Vue 微应用)就上台表演。

single-spa 就是这个导演,它自己不表演具体内容,但它负责协调所有剧组(微应用)的上场和下场时机。


为什么需要 single-spa?(解决的问题)

  1. 技术栈无关: 各个微应用可以使用不同的技术栈(React, Vue, Angular, Svelte 等)。团队可以自由选择或升级其技术栈,而不会影响其他团队。
  2. 独立开发、独立部署: 每个微应用都由独立的团队开发、测试和部署,大大提升了团队的自治度和发布效率。
  3. 增量升级: 允许你逐步重写一个老旧的巨型前端应用,可以一部分一部分地用新的框架替换,而不是一次性重写全部。
  4. 代码隔离: 应用之间实现了良好的代码和样式隔离(虽然需要一些额外规范),避免了全局污染和冲突。

single-spa 的核心工作原理

single-spa 通过定义一套生命周期协议来工作。每个微应用都必须对外暴露三个核心的函数:

  1. bootstrap: 应用首次加载时执行一次,用于初始化。
  2. mount: 当激活条件满足时(例如用户访问了该应用的路由),执行此函数。应用需要在这个函数里完成渲染,将组件挂载到 DOM 上。
  3. unmount: 当激活条件不再满足时(例如用户离开了该路由),执行此函数。应用需要在这个函数里完成清理工作,将自己从 DOM 上卸载。

single-spa 的“根配置”会注册所有微应用,并为每个应用定义一个 activity function(激活函数)。这个函数通常根据 window.location 进行判断。

javascript

1
2
3
4
5
6
7
8
9
10
11
12
// single-spa 根配置示例 (root-config.js)
import { registerApplication, start } from 'single-spa';

// 注册一个应用
registerApplication({
name: 'my-react-app', // 应用唯一名
app: () => System.import('my-react-app'), // 如何加载该应用的代码
activeWhen: (location) => location.pathname.startsWith('/react'), // 激活函数:当路径以 /react 开头时,加载这个应用
});

// 启动 single-spa
start();

流程如下:

  1. 用户访问 https://example.com/react
  2. single-spa 检查所有注册的应用,发现 my-react-appactiveWhen 函数返回 true
  3. single-spa 动态加载 my-react-app 的代码包。
  4. 依次执行该应用的 bootstrapmount 生命周期函数,应用被渲染到页面上。
  5. 用户导航到 /vuemy-react-appactiveWhen 返回 false
  6. single-spa 执行它的 unmount 函数,将其卸载。

主要优缺点

优点:

  • 强大的解耦和能力: 真正实现了前端应用的拆分和团队自治。
  • 框架无关: 是整合不同技术栈应用的理想选择。
  • 懒加载: 提升首次加载速度,按需加载应用。

缺点:

  • 复杂度高: 引入了分布式系统的复杂性,需要解决依赖共享、样式隔离、通信机制等问题。
  • 开发体验: 调试和配置比单体应用更复杂。通常需要类似 single-spa 官方的 create-single-spa 工具链来简化流程。
  • 依赖管理: 需要谨慎处理共享库(如 react, vue)以避免重复打包。

与 Module Federation 的区别

Webpack 5 的 Module Federation(模块联邦) 是另一种实现微前端的热门技术。它们不是互斥的,甚至可以结合使用:

  • single-spa 主要解决的是路由和应用生命周期管理的问题(“什么时候加载哪个应用”)。
  • Module Federation 主要解决的是应用之间代码共享和依赖处理的问题(“如何共享一个 React 实例,避免重复加载”)。

一个常见的模式是:使用 single-spa 作为路由器,使用 Module Federation 来加载微应用和共享依赖

总结

**single-spa 是一个成熟、强大的微前端解决方案,它通过一套生命周期协议,将多个独立开发、独立部署的微应用组合成一个完整的单体体验。它非常适合大型企业级应用、需要整合不同技术栈或进行增量迁移的项目。**虽然它引入了一定的复杂性,但它为解决前端单体膨胀问题提供了非常系统化的思路和工具。

html entry和js entry的概念是什么

好的,这是一个非常核心的微前端概念。**js-entry****html-entry** 是微前端中两种不同的应用打包和加载方式,它们代表了两种不同的设计哲学和实现技术。


核心概念对比

特性 js-entry (JavaScript 入口) html-entry (HTML 入口)
入口文件 一个 JavaScript 文件 (e.g., app.js
)
一个 HTML 文件 (e.g., index.html
)
内容 主要包含 JavaScript 逻辑和组件 包含 HTML、**<script>**
**<link>**
**<style>**
加载方式 框架(如 single-spa)加载并执行 JS 文件 框架通过 fetch 获取 HTML,解析出其中的 JS/CSS 并执行
样式处理 需要手动管理。通常需要约定(如 CSS Modules, CSS-in-JS)或工具来避免冲突。 自动处理。HTML 中的 <link>
<style>
标签会被自动插入到 <head>
中。
沙箱隔离 通常需要额外的库(如 qiankun
的沙箱)来实现 JS 和 CSS 的运行时隔离。
天然更易于实现HTML级别的沙箱(例如,创建一个 Shadow DOM 来包裹整个微应用的内容)。
代表性方案 原始的 single-spa 方案 qiankun(基于 single-spa)、Module Federation(某种程度上)

深入理解

1. js-entry (JavaScript 入口)

这是 single-spa 早期和官方推荐的方式。它的理念是:一个微应用本质上是一个 JavaScript 模块,这个模块导出了 bootstrap, mount, unmount 等生命周期函数。

如何工作?

  1. 主应用(容器)根据路由规则,判断需要加载微应用 A。
  2. 主应用通过 System.import()import() 动态加载微应用 A 的 入口 JS 文件(例如 https://a.com/app.js)。
  3. 这个 JS 文件被执行,并返回一个包含生命周期函数的对象。
  4. 主应用依次调用微应用的 bootstrapmount 函数。
  5. mount 函数中,微应用用自己的逻辑将组件渲染到主应用提供的 DOM 容器中。

示例代码 (single-spa 配置):

javascript

1
2
3
4
5
6
registerApplication({
name: 'my-app',
// 这里是一个 js-entry,指向一个 JS 文件
app: () => System.import('https://a.com/js/app.js'),
activeWhen: '/app'
});

优点:

  • 概念清晰,符合“应用即模块”的理念。
  • 打包输出干净,通常是一个或多个 JS 包。

缺点:

  • 样式处理麻烦:你需要自己在微应用的 JS 代码里引入 CSS(如 import './app.css'),并小心全局样式冲突。
  • 需要额外配置:为了实现资源加载和隔离,需要复杂的 Webpack 配置和额外的库。

2. html-entry (HTML 入口)

这种方式的理念是:一个微应用是一个完整的、可以独立运行的“页面”。主应用只需要加载这个页面的 HTML 文件,剩下的资源(JS, CSS)都由这个 HTML 文件自己声明。

如何工作?

  1. 主应用根据路由规则,判断需要加载微应用 B。
  2. 主应用通过 fetch 请求微应用 B 的入口 HTML 文件(例如 https://b.com/index.html)。
  3. 主应用解析这个 HTML 文件,提取出其中的 **<script>**** **<link>** **标签
  4. 主应用手动创建这些 <script><link> 元素,并将它们插入到主文档的 <head> 中,从而加载并执行微应用的 JS 和 CSS。
  5. 同时,主应用通常会提供一个隔离的沙箱环境(如 Shadow DOM),将微应用的整个 DOM 结构渲染在其中。

示例概念(qiankun 的做法):

javascript

1
2
3
4
5
6
7
8
9
10
11
12
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
{
name: 'vue-app',
// 这里是一个 html-entry,指向一个 HTML 文件
entry: '//localhost:7101', // 会默认加载这个地址下的 index.html
container: '#container',
activeRule: '/vue',
},
]);
start();

优点:

  • 对旧项目更友好:很多老项目(如 jQuery、Backbone 项目)本身就是以 HTML 为中心开发的,改造为 html-entry 更容易。
  • 资源加载自动化:无需在 JS 中手动引入资源,HTML 中声明的所有资源都会被自动加载。
  • 更强大的隔离能力:更容易与 Shadow DOM 结合,实现真正的样式和作用域隔离。

缺点:

  • 需要解析 HTML:主应用需要具备解析 HTML 并提取资源的能力,实现起来比直接 import 一个 JS 文件更复杂。
  • 可能加载多余资源:如果 HTML 中声明了多个入口,需要更精细的解析策略来避免加载未使用的资源。

总结与类比

概念 类比
**js-entry** 就像 “组装电脑”。主应用(你)买来各个零件(JS 逻辑、手动引入的 CSS),自己动手组装成一台完整的电脑(微应用)。控制力强,但过程繁琐。
**html-entry** 就像 “购买品牌整机”。主应用(你)直接买来一台戴尔电脑(HTML 文件),插上电(插入 DOM)就能用。开箱即用,非常方便,但内部细节被封装了。

现代趋势:

  • 纯粹的 js-entry 由于其配置的复杂性,在直接使用时体验并不友好。
  • qiankun 这类框架通过实现 html-entry 模式,极大地改善了开发者体验,降低了微前端的上手门槛。
  • Webpack Module Federation 提供了另一种更现代化的 js-entry,它通过复杂的依赖共享机制,解决了 js-entry 的许多痛点(如共享库重复加载),可以看作是 js-entry 的终极进化形态。

因此,选择哪种方式取决于你的技术栈和框架:

  • 使用 原始的 single-spa,你通常需要处理 js-entry
  • 使用 qiankun,你享受的是 html-entry 带来的便利。
  • 使用 Module Federation,你体验的是高级的、基于依赖共享的 js-entry

路由分发可以实现微前端架构吗

当然可以。路由分发是实现微前端架构最主要、最常用也是最经典的方式。它被认为是微前端架构的“第一种模式”,并且非常有效。

核心思想

通过路由(URL)的变化,来决定在页面容器中加载、展示和卸载哪个微前端应用。 主应用(通常称为“容器应用”或“根应用”)充当一个路由器编排者的角色,而各个微应用则是独立开发、部署的功能模块。


它是如何工作的?

  1. 一个路由规则集:主应用中维护一个配置表,将特定的 URL 路径模式(如 /app1/*, /settings, /dashboard)映射到对应的微应用。
  2. 监听路由变化:主应用监听 popstatehashchange 等浏览器路由事件。
  3. 匹配与决策:当路由发生变化时(用户点击链接或手动输入 URL),主应用根据配置表判断当前应该激活哪个微应用。
  4. 加载与挂载:主应用动态获取该微应用的资源(JS, CSS),并将其挂载到页面中指定的 DOM 容器(如 <div id="micro-app-container">)中。
  5. 卸载与清理:当用户离开当前路由时,主应用会卸载之前的微应用,并进行必要的清理工作(如清除事件监听器、内存释放等),然后加载新的微应用。

webpack5的Module Federation可以详细说说嘛

好的,我们来深入、详细地探讨 Webpack 5 的 Module Federation(模块联邦)。它不仅仅是微前端的一种实现方式,更是一种革命性的架构思想。

一、核心概念:它是什么?

Module Federation (MF) 的核心目标是:允许一个 JavaScript 应用在运行时动态地从另一个应用加载代码并共享依赖

你可以把它想象成在浏览器端实现了类似 Node.js 的 requireimport 机制,但不是在本地文件系统,而是在网络上的不同独立应用之间

它打破了传统的应用隔离边界,允许应用彼此成为“模块提供者”和“模块消费者”。


二、为什么要用 Module Federation?(解决的核心痛点)

  1. 彻底解决依赖重复打包
    • 传统微前端:如果主应用和微应用都使用了 React、Vue 等相同的库,这些库的代码会被分别打包到各自的 bundle 中。用户浏览器会多次下载和执行相同的库代码,导致体积膨胀和性能下降。
    • MF:可以指定共享依赖。React 等库只加载一次,所有应用都使用同一份实例。这解决了最重要的“依赖地狱”问题。
  2. 更彻底的应用拆分与团队自治
    • 它允许将应用拆分成更细粒度的“模块”或“组件”,而不仅仅是“页面”或“应用”。
    • 团队可以独立开发、部署一个按钮、一个表单、一个页面,并让其他团队直接消费。
  3. 运行时动态集成
    • 代码集成发生在运行时(Runtime),而非构建时(Build-time)。这意味着:
      • 你可以进行 A/B 测试,动态切换不同版本的组件。
      • 可以独立部署某个模块,而无需重新部署整个应用。
      • 主应用甚至不需要提前知道所有可能被加载的微应用。

三、核心角色与配置

MF 中有两个关键角色:

  1. Host (宿主模块/消费者)
    • 它是一个使用方应用,在运行时从其他地方(Remote)导入并执行代码。
    • 它的配置使用 remotes 属性。
    • 主应用,消费其他远程模块
    • 容器应用,集成各个微前端模块
    • 通常是用户直接访问的入口应用
  2. Remote (远程模块/提供者)
    • 它是一个提供方应用,将其内部的某些模块暴露给外部使用。
    • 它的配置使用 exposes 属性。
    • 微前端子应用,提供特定功能模块
    • 可以独立开发、部署和运行

一个应用可以同时是 Host Remote

配置详解 (webpack.config.js)

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Remote 应用的配置 (提供模块的应用,端口3001)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
// ...其他配置
plugins: [
new ModuleFederationPlugin({
name: 'app1', // 必填,这个应用的唯一名称,是消费者调用时的标识
filename: 'remoteEntry.js', // 必填,对外暴露的入口文件清单名
exposes: { // 暴露哪些模块给外部使用
'./Button': './src/components/Button', // 键:供外部使用的别名;值:本地模块路径
'./App': './src/App',
},
shared: { // 共享的依赖库
react: {
singleton: true, // 确保只使用一个单例版本
requiredVersion: '^18.2.0' // 需要的版本
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0'
},
},
}),
],
};

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Host 应用的配置 (消费模块的应用,端口3002)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
// ...其他配置
plugins: [
new ModuleFederationPlugin({
name: 'app2',
remotes: { // 声明要消费哪些远程应用
// 格式: "name@url/remoteEntry.js"
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
shared: { // 共享依赖的配置必须与Host一致,才能成功共享
react: {
singleton: true,
requiredVersion: '^18.2.0'
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0'
},
},
}),
],
};

四、如何使用?

app2 (Host) 的代码中,你可以像导入本地模块一样动态导入 app1 (Remote) 暴露的模块:

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在 app2 的 React 组件中
import React, { Suspense } from 'react';

// 使用动态导入语法。`app1` 是配置中remotes的key,`./Button` 是app1暴露的模块名
const RemoteButton = React.lazy(() => import('app1/Button'));

function App() {
return (
<div>
<h1>我是 App2</h1>
<Suspense fallback={<div>Loading Button...</div>}>
{/* 这个Button组件来自另一个独立应用 app1! */}
<RemoteButton />
</Suspense>
</div>
);
}

export default App;

五、工作流程(魔法是如何发生的?)

  1. 构建阶段
    • Host (app1) 构建时,会生成一个 remoteEntry.js 文件。这个文件是一个清单(Manifest),记录了 exposes 了哪些模块以及如何获取它们。
    • Remote (app2) 构建时,会意识到 import('app1/Button') 是一个远程模块,不会将其打包到自己的 bundle 中。
  2. 运行时阶段
    • 浏览器加载 app2
    • 当执行到 import('app1/Button') 时,Webpack 运行时就会去检查配置。
    • 它发现 app1 对应 http://localhost:3001/remoteEntry.js,于是动态加载这个 remoteEntry.js 文件。
    • remoteEntry.js 告诉 app2 如何从 app1 的服务器上获取 Button 组件的代码块。
    • Webpack 加载这个代码块,并** **app2** 共享的 **react** **react-dom** **实例一起执行它
    • 组件渲染成功。

六、与 single-spa 等方案的对比

特性 Module Federation single-spa (传统方式)
核心能力 模块共享与依赖管理 应用生命周期管理与路由
集成方式 运行时动态导入 运行时加载(但依赖需各自处理)
依赖处理 卓越。自动共享库,避免重复。 困难。需要额外手动配置或工具解决。
打包输出 生成 remoteEntry.js
清单文件
生成应用的入口 JS 文件
粒度 非常细。可以共享单个组件、函数、工具库。 较粗。通常以整个“应用”或“页面”为单元。
关系 互补。可以结合使用:用 single-spa 做路由和生命周期管理,用 MF 来加载组件和共享依赖。

总结

Webpack 5 的 Module Federation 是一项颠覆性的技术。它不再是简单地将应用机械地拼凑在一起,而是真正实现了应用间代码的“无缝融合”与“依赖共享”。

它最适合的场景包括:

  • 大型平台型产品:需要多个团队独立开发不同功能模块。
  • 微前端架构:是目前实现微前端最先进、最优雅的方案之一。
  • 跨应用组件/工具库共享:避免维护多个版本的通用组件。

它代表了前端架构从“单体”到“分布式”演进的重要方向,是每一位前端架构师都应该深入理解的技术。

web component

好的,我们来全面、深入地探讨 Web Components。

一、核心概念:它是什么?

Web Components 是一套浏览器原生支持的技术集合,允许你创建可重用的、封装的自定义 HTML 元素(类似于 <div><button>,但是是你自己定义的),并在任何现代浏览器中使用它们,无需任何外部框架(如 React、Vue、Angular)。

它的核心目标是为 Web 开发提供真正的组件化模型,并解决代码重用和封装的问题。


二、为什么需要 Web Components?(解决的问题)

  1. 框架无关性 (Framework-Agnostic)
    • 你用 Web Components 编写的组件可以在任何 HTML 页面中使用,无论这个页面用的是 React、Vue、Angular、jQuery 还是纯 JavaScript。它提供了最大程度的可移植性
  2. 原生封装 (Native Encapsulation)
    • Shadow DOM 提供了强大的样式和行为封装。组件内部的样式不会泄漏到外部,外部的样式也不会渗透到组件内部(除非特意允许)。这彻底解决了 CSS 全局污染的问题。
  3. 长期稳定性 (Longevity)
    • 作为Web 平台标准,它由浏览器厂商直接实现和维护,不像前端框架那样有生命周期(例如,AngularJS 到 Angular 的断代升级)。你写的组件在未来很多年内都能继续工作。
  4. 生态系统互操作性 (Interoperability)
    • 它可以在任何框架中被当作普通的 HTML 元素使用,成为了连接不同技术栈应用的“桥梁”。

三、技术构成(四大核心技术)

Web Components 主要由四项技术标准组成,它们可以单独使用,但组合在一起威力最大。

1. Custom Elements(自定义元素)

一套 JavaScript API,允许你定义自定义元素及其行为

  • 如何定义:通过继承 HTMLElement 类来创建一个新的元素类。
  • 生命周期回调
    • connectedCallback: 当元素首次被插入到 DOM 时调用。
    • disconnectedCallback: 当元素从 DOM 中移除时调用。
    • adoptedCallback: 当元素被移动到新的文档时调用。
    • attributeChangedCallback: 当元素的被观察属性(在 observedAttributes 中定义)发生变化时调用。

示例:定义一个简单的自定义元素

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyButton extends HTMLElement {
constructor() {
super();
// 初始化逻辑
}

connectedCallback() {
this.innerHTML = `<button>Click Me!</button>`;
this.addEventListener('click', () => {
alert('Button clicked!');
});
}

// 定义需要监听的属性
static get observedAttributes() {
return ['disabled'];
}

// 当disabled属性变化时触发
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'disabled') {
console.log(`disabled changed from ${oldValue} to ${newValue}`);
}
}
}

// 向浏览器注册这个新元素,标签名必须包含连字符 `-`
customElements.define('my-button', MyButton);

在 HTML 中使用

html

2. Shadow DOM(影子 DOM)

一套用于将封装的、“影子”的 DOM 树附加到元素的 API。这是实现样式和行为封装的关键。

  • Shadow Root:Shadow DOM 的根节点。
  • 模式
    • open: 可以通过 JavaScript 从外部访问(例如 element.shadowRoot)。
    • closed: 外部无法访问,封装性更强。
  • 作用域:在 Shadow DOM 内部定义的样式和脚本只在这个范围内有效,与外部隔离。

示例:为自定义元素添加 Shadow DOM

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MyCard extends HTMLElement {
constructor() {
super();
// 附加一个打开的 Shadow Root
const shadow = this.attachShadow({ mode: 'open' });

// 创建模板
const template = document.createElement('template');
template.innerHTML = `
<style>
/* 这些样式只在这个卡片内有效,不会影响外部 */
.card {
border: 1px solid #ccc;
padding: 16px;
border-radius: 8px;
font-family: sans-serif;
}
h2 { color: blue; margin-top: 0; }
</style>
<div class="card">
<h2><slot name="title">Default Title</slot></h2>
<p><slot name="content">Default content...</slot></p>
</div>
`;

// 克隆模板内容并添加到 Shadow Root
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('my-card', MyCard);

在 HTML 中使用

html

1
2
3
4
5
<my-card>
<!-- 使用 slot 将外部内容投影到 Shadow DOM 中的指定位置 -->
<span slot="title">My Awesome Title</span>
<span slot="content">This is some fantastic content.</span>
</my-card>

3. HTML Templates(HTML 模板)

<template><slot> 元素允许你编写在页面加载时不会立即渲染的标记模板。这些模板可以被后续的 JavaScript 激活和使用。

  • <template>:内容不会被浏览器解析、渲染或执行,直到被 JavaScript 提取并使用。
  • <slot>:在 Web Component 内部充当占位符,允许用户在使用组件时传入自己的自定义内容(如上例所示)。

4. ES Modules(ES 模块)

现代 JavaScript 的模块标准,为在 Web 上包含和重用 JS 代码提供了官方机制。它使得导入和导出 Web Components 变得简单。

html

1
2
3
<!-- 在 HTML 中导入并使用一个定义好的 Web Component -->
<script type="module" src="./my-component.js"></script>
<my-component></my-component>

四、Web Components 的优点与缺点

优点:

  1. 浏览器原生:无需编译,无需复杂的构建工具。
  2. 高度封装:Shadow DOM 解决了 CSS 和 DOM 的隔离问题。
  3. 框架无关:极高的互操作性和可移植性。
  4. 永久可用:作为 Web 标准,具有极长的生命周期和向后兼容性。

缺点:

  1. 生态相对年轻:虽然基础功能强大,但缺少像 React/Vue 那样庞大的生态系统(状态管理、路由、测试工具等)。
  2. 数据流管理较弱:原生不支持类似 React 的 Props 向下传递、事件向上冒泡的父子组件通信范式,需要自己实现(通常用 Custom Events)。
  3. SSR (服务器端渲染) 支持差:自定义元素可以在服务器上渲染,但其动态行为和客户端激活(Hydration)非常复杂,远不如 Next.js/Nuxt.js 成熟。
  4. 开发体验 (DX):原始的开发方式比较繁琐,需要写很多样板代码。但社区有诸如 LitStencil 等库来极大地改善体验。

五、Web Components 与微前端和框架

  • 在微前端中:Web Components 是实现技术栈无关微前端的理想载体。每个团队可以用自己熟悉的技术(React, Vue)开发组件,但最终发布为一个封装好的 Web Component(例如,将一个 React 组件包装成 <team-product-card>),由主应用统一调度。
  • 与框架的关系
    • 不是替代关系,而是互补关系
    • 框架 (React/Vue) 擅长解决: 复杂的 UI 状态管理、数据流、高效的 DOM 差分更新。它们提供了优秀的开发体验完整的解决方案
    • Web Components 擅长解决: 跨框架重用、底层 UI 组件库、集成遗留应用、长期稳定的基础组件。它们提供了浏览器级别的互操作性

总结来说,Web Components 是 Web 平台的底层基础,为构建可长期存在、高度封装、框架无关的组件提供了强大的原生能力。 虽然它在开发复杂应用时不如现代框架那么方便,但其独特优势使其在特定场景(如微前端、跨技术栈组件库、大型企业级应用)中是不可替代的选择。

qiankun实现样式和js隔离的原理

好的,我们来深入剖析 qiankun 实现样式和 JavaScript 隔离的原理。这是 qiankun 的核心能力之一,也是它相比原始 single-spa 更易用、更强大的关键。

总体思想

qiankun 的隔离策略可以概括为:“重样式隔离,轻 JS 沙箱”。它通过在应用加载和卸载时动态地操作样式和 JavaScript 执行环境来模拟一个隔离的沙箱环境。


一、样式隔离 (CSS Isolation)

qiankun 提供了三种主要的样式隔离方案,根据场景不同自动或手动启用。

1. 严格样式隔离 (Scoped CSS)

原理: 为每个微应用包裹一个 Shadow DOM

  • 如何工作:
    1. 当 qiankun 挂载一个微应用时,它会创建一个 Shadow Root,并将其作为该应用的容器。
    2. 微应用的所有 DOM 结构都被渲染在这个 Shadow DOM 内部。
    3. Shadow DOM 的特性天然实现了样式的封装:内部的样式不会影响外部,外部的样式也不会影响内部(除非使用 ::part:host 等特定语法)。
  • 优点: 隔离性最强,是浏览器原生的完美隔离方案。
  • 缺点:
    • 某些第三方库(特别是弹窗类)可能会因为无法正确定位到 Shadow DOM 外部而出现问题。
    • 微应用内的样式完全无法影响主应用,反之亦然,有时这可能不符合设计需求。
  • 启用方式:start 函数中配置 { strictStyleIsolation: true }

javascript

1
2
3
4
import { start } from 'qiankun';
start({
strictStyleIsolation: true, // 启用严格样式隔离
});

2. 实验性样式隔离 (CSS Scoped)

原理: 一种更宽松的隔离方式,使用 运行时动态样式表重写

  • 如何工作:
    1. qiankun 会劫持微应用运行时动态添加样式标签(**<style>**,** **<link>**)** 的行为。
    2. 当微应用插入一个新的样式标签时,qiankun 会将其内容抓取过来。
    3. 使用 CSS 规则重写器(例如 postcss 插件)为所有 CSS 选择器添加一个特殊的前缀。这个前缀通常基于微应用的名称或一个特定属性。
    4. 将重写后的 CSS 内容插入到 document.head 中。
    5. 同时,qiankun 会为微应用的容器元素添加上一步中使用的相同属性

示例:

- 微应用有一个样式规则:`.button { color: red; }`
- qiankun 将其重写为:`[data-qiankun="my-app"] .button { color: red; }`
- 同时,微应用的容器 `<div>` 会获得属性:`<div data-qiankun="my-app">...</div>`
- 这样,样式规则就只会在这个容器内生效。
  • 优点: 比 Shadow DOM 兼容性更好,允许微应用样式影响其容器内的任何元素(包括动态 append 到 body 的弹窗)。
  • 缺点: 是运行时重写,有一定性能开销,且是实验性功能。
  • 启用方式:start 函数中配置 { experimentalStyleIsolation: true }

javascript

1
2
3
start({
experimentalStyleIsolation: true, // 启用实验性样式隔离
});

3. 动态样式表加载/卸载 (最常见的默认行为)

如果不开启上述两种隔离,qiankun 默认采用一种更简单但有效的策略。

  • 原理:
    1. 加载时: qiankun 通过 fetch 获取微应用的 HTML 入口,解析出所有的 <style><link> 标签。
    2. 将这些样式标签直接插入到** ****document.head** 中。
    3. 卸载时: qiankun 会记录所有由该微应用添加的样式标签,并在卸载微应用时直接将这些标签从 DOM 中移除
  • 优点: 实现简单,性能好。
  • 缺点: 不是真正的隔离。如果多个微应用有相同选择器的样式规则,后加载的会覆盖先加载的(因为后加载的样式表在更后面,优先级更高)。这依赖于开发者的约定。
  • 这是 qiankun 的默认行为,对于很多应用来说已经足够。

二、JavaScript 隔离 (JS Sandbox)

qiankun 的 JS 沙箱的核心目标是:防止微应用在全局环境(**window**)上留下永久的污染,并在应用切换时恢复和清理环境。它主要模拟了三个环境的隔离:

1. 快照沙箱 (SnapshotSandbox) - 用于单实例场景

原理: 在应用加载前后对全局 window 对象进行“拍照”和“diff”,适用于同一时间只能有一个微应用活跃的浏览器环境。

  • 工作流程:
    1. 激活沙箱 (mount): 将当前 window 的所有属性拍一个快照windowSnapshot),存起来。
    2. 微应用运行: 微应用可以任意修改 window
    3. 失活沙箱 (unmount):
      • 将当前的 window 和之前存的 windowSnapshot 进行对比,得到修改的差异(modifyPropsMap)。
      • 还原现场: 遍历差异,将 window 上的属性恢复到拍快照时的状态。
      • 记录污染: 将微应用修改的差异保存起来。
    4. 再次激活: 将之前保存的差异(modifyPropsMap重新应用到** **window** **,让微应用感觉自己的修改一直都在。
  • 优点: 兼容性极好,支持所有浏览器。
  • 缺点: 无法支持多个微应用同时运行(多实例),因为共用一个全局 window

2. 代理沙箱 (ProxySandbox) - 用于多实例场景(主流)

原理: 使用 ES6 的 Proxy 为每个微应用创建一个假的、隔离的 window 对象。

  • 工作流程:
    1. qiankun 为每个微应用创建一个空的 fakeWindow 对象。
    2. Proxy 代理这个 fakeWindow
    3. 当微应用操作 window 时:
      • 读操作:优先从 fakeWindow 里读,如果读不到,则 fallback 到真正的全局 window(这样可以共享 document, location 等全局对象)。
      • 写操作:所有对属性的新增和修改都只作用于 fakeWindow 上,完全不会污染真正的全局** ****window**
    4. 微应用的所有代码都在这个代理的上下文中执行(通过 with 语句或 eval 改写)。
  • 示例:

javascript

1
2
3
// 微应用代码
window.myGlobalVar = 123; // 写入的是沙箱的 fakeWindow,真 window 不受影响
console.log(document.title); // 读取的是真 window 的属性
  • 优点:
    • 真正的隔离,多个微应用可以同时运行,每个都有自己独立的 window 空间。
    • 对微应用无感知,无需修改代码。
  • 缺点: 依赖 ES6 Proxy,无法在低版本浏览器(如 IE)中使用。

3. 遗留沙箱 (LegacySandbox) - 已逐渐被代理沙箱取代

原理类似于快照沙箱,但性能优化更好。它只记录微应用修改过的属性,在卸载时还原这些属性。同样不支持多实例。


总结:qiankun 的隔离策略

隔离类型 技术方案 优点 缺点 适用场景
样式隔离 Shadow DOM 原生,隔离最彻底 兼容性问题,弹窗等组件可能异常 需要最强隔离的新应用
动态样式表重写 (实验性) 兼容性好,支持弹窗 运行时开销,实验性功能 需要兼容弹窗的复杂应用
动态加载/卸载 (默认) 简单,性能好 非真正隔离,依赖约定 大多数简单应用,样式冲突风险低
JS 隔离 快照沙箱 (SnapshotSandbox) 兼容所有浏览器 不支持多实例 单实例模式,需兼容 IE 等老浏览器
代理沙箱 (ProxySandbox) 真正隔离,支持多实例,对应用无感知 依赖 Proxy
,不兼容 IE
现代浏览器下的主流选择

qiankun 会自动选择最佳策略:在支持 Proxy 的浏览器中默认使用代理沙箱 (多实例)动态样式加载/卸载。如果不支持 Proxy,则降级到快照沙箱 (单实例)

通过这种组合策略,qiankun 在绝大多数场景下为微前端应用提供了足够安全、稳定且高性能的隔离环境,极大地简化了微前端的落地复杂度。

qiankun是如何实现父子应用通信的呢

好的,qiankun 的通信机制是其核心功能之一,它设计得非常灵活,提供了多种方式来实现主应用(父应用)和微应用(子应用)之间的通信。

qiankun 并没有强制使用某一种通信方式,而是提供了一个轻量的 API(**initGlobalState** 作为推荐方案,同时也完全支持任何其他流行的状态管理库(如 Redux, Mobx, Vuex)或浏览器原生 API。


一、官方推荐方案:使用 initGlobalState (Actions 通信)

这是 qiankun 官方提供的通信方式,适用于大多数场景。它的核心思想是一个简单的发布-订阅模式

1. 在主应用中初始化状态并下发通信方法

主应用是整个状态的管理中心。

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// main-app/src/actions.js
import { initGlobalState } from 'qiankun';

// 1. 初始化全局状态
const initialState = {
user: {
name: '张三',
token: 'abc123'
},
theme: 'light'
};

// 2. 创建 Actions 实例
const actions = initGlobalState(initialState);

// 3. 监听状态变化(可选,主应用自己也可以监听)
actions.onGlobalStateChange((state, prevState) => {
// state: 变更后的新状态
// prevState: 变更前的旧状态
console.log('主应用监听到状态变化: ', state, prevState);
});

// 4. 定义一个更新状态的方法
export const setGlobalState = (newState) => {
// 按层级合并状态
actions.setGlobalState(newState);
};

// 5. 将 actions 暴露出去,供微应用使用
export default actions;

2. 在微应用中获取并操作状态

微应用需要从生命周期函数中获取到 props,其中就包含了通信方法。

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// micro-app/src/main.js (入口文件)
let microAppActions; // 用来保存通信方法

// 导出 qiankun 生命周期函数
export async function mount(props) {
console.log('微应用挂载了', props);
// 1. 从 props 中获取主应用下发的 actions
microAppActions = props;

// 2. 监听全局状态变化
props.onGlobalStateChange((state, prevState) => {
console.log('微应用监听到状态变化: ', state, prevState);
// 状态变化后,更新微应用自己的视图
renderApp(state);
});

// 3. 首次挂载,获取当前全局状态并渲染应用
renderApp(props.getGlobalState());
}

export async function unmount() {
// 卸载时取消监听(qiankun 内部会自动清理,但显式取消是好习惯)
microAppActions.offGlobalStateChange();
}

// React/Vue 应用的渲染函数
function renderApp(state) {
ReactDOM.render(<App globalState={state} actions={microAppActions} />, document.getElementById('app'));
}

3. 在微应用组件中具体使用

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// micro-app/src/App.jsx
import React from 'react';

function App({ globalState, actions }) {
const { user, theme } = globalState;

const handleChangeTheme = () => {
// 更新全局状态
actions.setGlobalState({
theme: theme === 'light' ? 'dark' : 'light'
});
};

return (
<div className={`app ${theme}`}>
<h1>微应用页面</h1>
<p>用户名: {user.name}</p>
<button onClick={handleChangeTheme}>切换主题</button>
</div>
);
}

export default App;

initGlobalState API 说明:

  • setGlobalState(state: object): 设置新的全局状态,会自动与旧状态进行浅合并
  • onGlobalStateChange(callback: function): 注册监听器,状态变化时触发。
  • offGlobalStateChange(): 取消监听。
  • getGlobalState(): 获取当前全局状态。

二、其他通信方案

qiankun 是框架无关的,因此你也可以选择任何你熟悉的通信方式。

1. 使用 CustomEvent (浏览器原生事件)

原理: 利用浏览器原生的 window.dispatchEventwindow.addEventListener 进行通信。

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 主应用 - 发送事件
window.dispatchEvent(new CustomEvent('main-app-event', {
detail: { // 通过 detail 传递数据
type: 'CHANGE_THEME',
payload: 'dark'
}
}));

// 微应用 - 接收事件
window.addEventListener('main-app-event', (event) => {
const { type, payload } = event.detail;
// 处理事件...
});

// 微应用 - 发送事件(同理)
window.dispatchEvent(new CustomEvent('micro-app-event', {
detail: { message: 'Hello from micro app' }
}));

优点: 原生支持,非常简单。
缺点: 数据传递能力较弱(只能同步),缺乏状态管理能力,事件需要全局唯一命名以免冲突。

2. 使用 Redux/Mobx/Vuex 等状态库

原理: 主应用和微应用共享同一个状态库实例

  • 步骤:
    1. 主应用创建一个 Redux Store 或其他状态库实例。
    2. 将这个 Store 通过 window 对象或者微应用的 props 暴露给微应用。
    3. 微应用连接到这个全局的 Store,进行状态的读取和分发 Action。

javascript

1
2
3
4
5
6
7
8
9
// 主应用 - 创建并暴露 store
import { createStore } from 'redux';
const globalStore = createStore(/* ... */);
window.__MAIN_APP_STORE__ = globalStore; // 挂到全局

// 微应用 - 获取并使用 store
const store = window.__MAIN_APP_STORE__;
store.dispatch({ type: 'AN_ACTION' });
const state = store.getState();

优点: 功能强大,可以处理复杂的业务逻辑和状态流。
缺点: 紧密耦合,主应用和微应用必须使用同一种状态库,且版本需要兼容。

3. 通过 URL 或 Query Parameters 通信

原理: 通过改变 URL 的查询参数来传递简单信息。

javascript

1
2
3
4
5
6
7
8
9
10
// 主应用改变URL
const newUrl = `${window.location.pathname}?theme=dark&userId=123`;
history.pushState(null, '', newUrl);

// 微应用监听URL变化
window.addEventListener('popstate', () => {
const urlParams = new URLSearchParams(window.location.search);
const theme = urlParams.get('theme');
// ...使用参数
});

优点: 非常简单,状态可被收藏和分享。
缺点: 只适合传递少量简单数据。


总结与选择建议

方案 优点 缺点 适用场景
官方** ****initGlobalState** 官方推荐,简单轻量,满足大部分通信需求 功能相对简单,不适合极其复杂的场景 绝大多数微前端通信场景的首选
CustomEvent 浏览器原生,非常简单 功能弱,只能传简单数据,易产生事件名冲突 简单的父子通知、触发动作
Redux/Vuex 等 功能强大,适合复杂状态管理 耦合度高,主子和微应用必须使用同一种状态库 大型复杂应用,且技术栈统一
URL Parameters 实现简单,状态可分享 传递数据量有限,类型受限 过滤条件、简单配置等

最佳实践建议:

  1. 优先使用官方的** ****initGlobalState**,它能覆盖 90% 的微前端通信需求。
  2. 对于简单的、一次性的动作触发(如“刷新列表”、“显示通知”),可以辅以 CustomEvent
  3. 只有在主应用和所有微应用技术栈统一且非常复杂时,才考虑使用共享状态库
  4. 通信的设计应遵循最小化原则,尽量减少主应用和微应用之间的耦合,让微应用保持最大的独立性。
  • Title: 微前端
  • Author: cenweilings@163.com
  • Created at : 2024-01-15 00:00:00
  • Updated at : 2025-09-27 15:11:56
  • Link: https://blog-git-main-cenweilings-projects.vercel.app/2024/01/15/微前端/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments