我一直觉得,微前端这玩意儿,很多团队是“为了微前端而微前端”,最后搞出来的东西比巨石应用还难维护。但如果你真遇到了组织协作和发布节奏上的硬瓶颈,那垂直微前端(Vertical Micro Frontend)确实是个值得认真考虑的架构方向。尤其是当这个架构碰上了 Cloudflare 这套全球边缘网络,很多以前在微前端体系里让人头疼的资源调度、隔离和分发问题,反而有了新的解法。
今天我就结合自己实际趟过的一条路,聊聊怎么在 Cloudflare 平台上把垂直微前端跑起来。这篇不是纯概念科普,更多是我在选型、搭建、部署和排坑过程中的一手记录。如果你是负责公司中台前端、核心业务前端,或者是想折腾一套真正能落地的多团队协作方案,这篇应该能给你不少参考。
1. 垂直微前端的底层逻辑与适用场景
1.1 垂直微前端和水平微前端的本质区别
先理清一个概念,很多人一聊微前端就是“把一个页面拆成 header、菜单、内容区”,按 UI 区域横向切分,这叫水平微前端。垂直微前端不一样,它是按“业务能力”和“用户旅程”来切的。比如一个电商后台,订单管理的整套流程,从列表查询、详情展示、售后处理到数据导出,全流程都归一个团队负责。这个团队交付的不只是一个页面组件,而是一条完整的垂直业务链路。
在 Cloudflare 的体系里,这种垂直切分天然契合 Workers 的部署模型。每个垂直业务域可以独立部署成单独的 Worker 或者 Pages 项目,互不干扰。这种模式下,团队自治不再是口号,而是真正落到了基础设施层面。以前我们做水平切分,组件版本升级要考虑关联影响,牵一发动全身;垂直切分后,订单团队只需要关注订单域自己的版本迭代,只要对外暴露的接口契约没变,内部随便重构。
1.2 什么情况下才应该选择垂直微前端
这里我不建议为了炫技或者追求架构时髦去上垂直微前端,尤其是中小团队或者业务边界模糊的项目。但这几类情况就很适合:
- 多团队开发同一体系,且团队间代码仓库完全隔离,只有接口层面的协作。比如平台型公司里,交易、营销、风控各自为战,协同仅限于联调。
- 业务模块的生命周期差异巨大,有的模块每天发版几次,有的模块半年才动一次。部署频率差异过大时,水平微前端会迫使用户每次加载全部模块代码,浪费性能。
- 需要精细化权限隔离和故障隔离的场景。比如某个垂直应用崩溃了,不能拖垮整个壳子。
从 Cloudflare 的角度看,垂直微前端可以充分利用它的边缘部署特性。不同业务模块可以按区域、按用户群体、按版本策略分别灰度,这个灵活性比传统的单机部署强太多。
1.3 Cloudflare 生态能带来哪些关键价值
Cloudflare 并不是一个简单的 CDN,它提供的 Workers、Pages、KV、Durable Objects 以及各种集成能力,组合起来相当于一个边缘应用平台。在垂直微前端架构里,Cloudflare 的价值体现在三个层面:
第一,静态资源分发天然快。每个子应用打包产物可以独立部署,Cloudflare 的全球网络自动做缓存和加速,用户不管在哪个区域,拿到最近节点的资源。
第二,运行时逻辑放在边缘。公共的容器壳子(Host Application)可以运行在 Worker 上,根据用户请求动态决定加载哪个垂直应用、走哪个版本,这个能力传统服务器需要自己搭建路由层,而 Cloudflare 原生支持。
第三,基础设施成本低且弹性好。不需要为每个子应用单独准备一套服务器集群,Workers 的按请求计费模式让中小团队也能用得起企业级的分发网络。
不过,也有个现实问题——Cloudflare 在国内的访问稳定性一般,如果你的主要用户在国内,可能需要考虑备选方案或者做一层自建回源。这个属于边界条件,后面会单独说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 在 Cloudflare 上构建垂直微前端的整体架构方案
2.1 架构模式:边缘路由 + 静态托管 + 动态加载
我最终落地的架构方案可以概括成三层:边缘路由层、应用注册层、业务子应用层。
边缘路由层,运行在 Cloudflare Worker 上的一个入口脚本,负责接收所有前端请求。它会根据请求的路径前缀(比如 /order/* 走订单域,/marketing/* 走营销域),决定返回哪个子应用的 HTML 文档和资源路径。
应用注册层,是一份集中式的应用清单,存在 Cloudflare KV 里。这份清单记录着每个子应用的名称、版本号、资源入口地址、依赖共享策略。子应用发布新版本后,只需要更新 KV 里的记录,不用动路由代码。
业务子应用层,每个独立业务域构建后的静态资源,部署到 Cloudflare Pages 或者 Worker Static Assets。子应用之间完全隔离,构建部署互不依赖。
整体数据流大概是这样的:用户浏览器发起请求 → Cloudflare CDN 边缘节点拦截 → 执行边缘路由 Worker → 查询 KV 中当前版本清单 → 返回对应子应用的 HTML 和静态资源。
2.2 子应用之间怎么做运行时隔离
微前端绕不开的一个话题就是隔离。垂直微前端由于每个业务域本身就是整套页面,如果走 iframe 方案,隔离最彻底,但体验和通信成本都比较高;如果走 Web Components 或者 JavaScript 沙箱方案,体验好,但要自己处理样式冲突和全局变量污染。
我个人的选择是:容器壳子负责渲染框架级别的公共依赖(React、ReactDOM),子应用打包时把 React 标记为 external,运行时从全局变量 window.React 获取。这个方案也常被称为运行时共享依赖,能有效控制子应用的包体大小。
子应用内部的状态、路由、样式全部封装在自己的 Shadow DOM 或者 CSS 命名空间内。这块需要团队统一约定,从工程上强制。比如用 @scope/order 作为 class 前缀,或者在构建配置里通过 PostCSS 插件给所有类名自动加前缀。
还有一点,垂直微前端里子应用免不了跟容器通信,比如用户登录状态、菜单的展开收起。我建议不要直接用全局事件满天飞,而是通过容器注入的 hostApi 对象来调用,这样可管控可追踪。
2.3 依赖共享策略:什么该共享,什么该隔离
依赖共享是微前端里最容易翻车的点。共享太多,子应用之间耦合太深;共享太少,每个子应用都打一份 React,体积爆炸。
我的经验性结论是:
- 运行时框架(React/Vue)值得共享,且建议固定大版本。共享框架能显著降低资源体积。前提是团队有升级纪律,不要你升 18 我还在 16。
- UI 组件库不一定共享。如果你的团队用的都是公司内部封装的组件库,统一版本后可以共享;如果各自引第三方库,老实打成自己的包。
- 工具函数库(lodash、dayjs)不建议共享,收益不明显,混乱风险大。
- 业务状态管理绝对隔离。子应用之间的状态不能放一个全局 store 里,否则边界瞬间模糊。
在 Cloudflare 的实现里,共享依赖的处理方式通常是在容器壳子的入口 HTML 里用 import map 或者手动 script 标签加载公共库,子应用构建时将这些库排除打包。
3. 核心实现细节:从静态托管到边缘路由搭建
3.1 创建基础设施:Pages 项目和 Worker 路由准备
我先假设你已经有一个 Cloudflare 账号,并且域名也托管在 Cloudflare。如果没有,先去把域名接入,这一步对后续的 Worker 路由配置至关重要。
第一步,为每个子应用创建一个 Pages 项目。Pages 可以直接连接 Git 仓库,push 之后自动构建部署,也可以直接用 Wrangler CLI 手动上传构建产物。我建议如果你没有太复杂的构建需求,用 Git 集成就行;如果构建流水线比较特殊,用 Wrangler 更灵活。
以订单子应用为例:
bash复制# 安装 wrangler,Cloudflare 官方 CLI
npm install -g wrangler
# 登录
wrangler login
# 在项目根目录构建
npm run build
# 直接部署到 pages
wrangler pages deploy dist --project-name=order-app
Pages 项目创建好之后,Cloudflare 会分配一个默认的域名,比如 order-app.pages.dev。这个域名就是你的子应用静态资源的物理地址。生产环境建议绑定自己的自定义域名,比如 assets.example.com/order,通过路由规则拼到 Pages 项目上,这样资源路径更可控,也方便后端做统一的跨域配置。
第二步,创建边缘路由 Worker。这个 Worker 相当于所有前端请求的统一入口,我的建议是新建一个单独的 Worker 叫 host-router,专门负责路由分发。通过 Cloudflare 控制台或者 wrangler 配置它的路由规则,把 example.com/* 的请求全部指向这个 Worker。
bash复制# 初始化 worker
wrangler init host-router
接下来,给它配置 KV 命名空间,用来存储应用版本注册信息。
bash复制wrangler kv namespace create APP_REGISTRY
执行后 Cloudflare 会返回一个 id,把它填到 wrangler.toml 里:
toml复制name = "host-router"
main = "src/index.js"
compatibility_date = "2024-11-01"
[[kv_namespaces]]
binding = "APP_REGISTRY"
id = "你拿到的id"
3.2 编写边缘路由 Worker:动态识别请求并返回对应子应用入口
路由 Worker 的核心职责是:收到网页请求后,根据 URL 路径判断应该返回哪个子应用的 HTML 文档。
这里有一个细节需要想清楚:子应用是采用将 HTML 直接返回给浏览器的方式,还是采用把子应用嵌到容器壳子里渲染的方式。前者适合子应用完全独立、切换时整页跳转的模式;后者适合 SPA 路由平滑切换的模式。垂直微前端更多是后者,所以我实现了两种模式的兼容,下面给出一个基础版本:
javascript复制// 子应用注册表,生产环境建议放 KV
const APPS = {
'/order': {
name: 'order-app',
entry: 'https://order-app.pages.dev',
basePath: '/order',
},
'/marketing': {
name: 'marketing-app',
entry: 'https://marketing-app.pages.dev',
basePath: '/marketing',
},
};
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const pathname = url.pathname;
// 静态资源直接尝试从子应用获取
if (pathname.includes('/assets/')) {
const app = findAppByAssetPath(pathname);
if (app) {
const assetUrl = app.entry + pathname.replace(app.basePath, '');
return fetch(assetUrl);
}
}
// 页面请求返回子应用 HTML
const matchedApp = findAppByPath(pathname);
if (matchedApp) {
const html = await fetch(matchedApp.entry + '/index.html');
return new Response(html.body, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'X-App-Name': matchedApp.name,
},
});
}
// 默认返回容器壳子
return fetch('https://host-container.pages.dev/index.html');
},
};
function findAppByPath(pathname) {
const keys = Object.keys(APPS).sort((a, b) => b.length - a.length);
for (const key of keys) {
if (pathname.startsWith(key)) {
return APPS[key];
}
}
return null;
}
function findAppByAssetPath(pathname) {
// 根据静态资源特征匹配
return Object.values(APPS).find((app) => pathname.startsWith(app.basePath + '/assets/'));
}
这版代码虽然能跑通,但有两个优化点。第一,子应用注册表不要硬编码在代码里,改成从 KV 读取,这样发布新版本不需要重新部署 Worker。第二,页面请求和静态资源请求都要加上缓存策略。HTML 不建议缓存,或者只做短缓存,保证发版后用户能拿到最新入口;静态资源因为带了 hash,可以放心用长缓存。
3.3 容器的设计:加载子应用的关键机制——import map 与生命周期
容器壳子是所有子应用共同依赖的宿主,它的核心能力是动态加载子应用 JS 并管理生命周期。这一层我建议用系统级 JS(SystemJS)来做模块加载。
具体做法:
- 容器壳子自己是一个 React 应用,但打包产物不包含 React,只包含壳子代码。
- 子应用构建产物格式设置为 SystemJS module 格式。Webpack 5 和 Vite 都支持这个配置。
- 容器壳子加载子应用时,通过动态 import 请求子应用的入口 JS(比如
order-app.system.js),这个请求会打到 Worker 路由,再由 Worker 转发到 Pages 项目。
容器壳子的加载伪代码大致是这样:
javascript复制async function loadApp(appName, entryUrl) {
const module = await System.import(entryUrl);
if (module && typeof module.mount === 'function') {
const unmount = await module.mount({
container: document.getElementById('app-container'),
hostApi: window.hostApi,
});
mountedApps[appName] = unmount;
}
}
function unmountApp(appName) {
if (typeof mountedApps[appName] === 'function') {
mountedApps[appName]();
}
}
子应用的入口文件需要暴露 mount 和 unmount 两个函数,这是契约的一部分。mount 负责渲染应用到指定容器,返回一个用于清理的函数;unmount 在应用切换时被调用,用来销毁应用、移除全局监听器、清空副作用。
3.4 构建配置:SystemJS 与版本 hash 的配合
子应用的打包配置我以 Vite 为例。在 vite.config.js 里设置:
javascript复制import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
base: 'https://order-app.pages.dev/assets/',
build: {
rollupOptions: {
output: {
format: 'system',
entryFileNames: 'order-app.system.js',
assetFileNames: (assetInfo) => {
return `assets/[name]-[hash][extname]`;
},
},
external: ['react', 'react-dom'],
},
},
});
注意 base 必须设置成子应用在边缘的完整资源前缀。否则子应用内部加载图片或者 chunk 时,相对路径会指向容器壳子的域名,那就 404 了。
外部化 react 和 react-dom 后,子应用的包体会小很多,加载速度也会快。代价是容器壳子必须保证在加载任何子应用之前,已经把这个两个库挂到了全局。
3.5 应用注册与版本切换:通过 KV 实现动态流量调度
这里是我觉得 Cloudflare 平台比传统服务器方案更顺手的地方。发布一个新版本子应用,不需要重新构建任何网关层,只需要更新 KV 里的 entry 地址。
我们可以这样设计注册表结构:
code复制key: app:order
value: {
"version": "20241101",
"entry": "https://order-app.pages.dev",
"active": true
}
于是 Worker 的逻辑可以改成每次请求都去 KV 读取:
javascript复制const cacheKey = `app:${appName}`;
const cached = await env.APP_REGISTRY.get(cacheKey, 'json');
这里要注意 KV 的读取延迟。KV 的读有缓存,但毕竟是网络请求,如果每次请求都穿透到 KV 会浪费资源。建议在 Worker 里做一层内存缓存,并设置 TTL。Worker 实例的全局变量在短时间内多次执行时是复用的,直接放 globalThis 或者模块顶层变量就行。
灰度发布怎么做?可以给 KV 里加一个 weight 字段,比如 10% 流量走新版本:
javascript复制if (Math.random() < app.weight / 100) {
return fetch(app.newEntry + '/index.html');
}
return fetch(app.entry + '/index.html');
这种流量分配虽然粗糙,但够用,尤其在业务侧已经切好了用户灰度标识的情况下。
4. 子应用的接入规范与生命周期管理
4.1 团队需要遵守的接入约定
垂直微前端能不能跑起来,技术架构只占一半,另一半是团队能不能遵守契约。我把必须约定的内容分成几类:
- 路由约定:每个子应用有自己的 base path,内部路由不能出现越界的绝对路径。比如订单应用的所有路由都在
/order下面,子应用内部跳转/order/detail没问题,但不能跳/marketing/campaign,跨域跳转要通过容器提供的导航 API。 - 样式约定:所有样式加作用域标记。Vite 构建时可以使用
css.preprocessorOptions配合插件实现 class 前缀注入。团队内部风格不用完全一致,但前缀必须统一。 - 数据约定:子应用不允许自己去 fetch 一个和容器没关系的域名。所有 API 请求走容器注入的
hostApi.fetch,这样方便统一鉴权、统一错误处理。 - 事件约定:子应用内部事件不要直接用
window.addEventListener,如果需要监听全局事件,要通过hostApi.onGlobalEvent注册,卸载时统一自动清理。
4.2 生命周期设计:mount、unmount、更新、错误兜底
生命周期是整个微前端运行时的骨架,设计时我反复强调一句话:子应用要能死得干净,也要能活得好好的。
mount 阶段,容器传入 container 节点和 hostApi。子应用在这个阶段创建根组件、挂载路由、初始化状态。不能在这个阶段做同步的网络请求(除了必要的基础信息获取),否则切换应用时白屏时间过长。
unmount 阶段,容器会调用子应用暴露的卸载函数。这里最容易被忽略的是定时器、全局事件监听、ResizeObserver 实例。不清理干净,切换几次应用之后整个页面就会变得卡顿,而且内存占用只增不减。
错误兜底也很关键。mount 执行时子应用可能抛出异常,或者加载的 JS 文件 404,容器要捕获这些异常并显示友好的错误页,而不是整个壳子白屏。可以在容器里加一个 window.addEventListener('unhandledrejection') 和 window.addEventListener('error') 的兜底。
另外,应用更新是个容易被遗忘的场景。用户长时间停留在页面上,子应用发版后,用户打开的旧版本依然在运行。我建议实现一个轮询机制,定期检查 KV 里的版本号和当前加载的版本号,不同则弹提示让用户刷新页面。这个功能可以做成容器的通用能力,子应用不用各自实现。
4.3 常见动静资源的加载顺序与性能优化
资源加载顺序决定了首屏体验。垂直微前端模式下,一个路径的首屏需要加载以下内容:
- 容器壳子的 HTML 和 JS。
- 公共依赖 React、ReactDOM。
- 子应用的入口 JS 和首屏相关的 CSS。
- 业务异步 chunk。
在 Cloudflare 的架构里,我可以把这四步归并为:浏览器请求一个 URL,Worker 返回对应子应用的 HTML,HTML 里引用容器壳子的资源。这个逻辑上多了一次跳转,但实际性能损耗很低,因为 Cloudflare 内部同区域响应极快,而且 HTML 可以通过短缓存缓存住。
为了进一步优化,我建议把公共依赖的 script 标签直接内联到 HTML 里,减少一次 RTT。用 Cloudflare Workers 去做 HTML 重写,动态往 HTML 里插入 script 标签,这个就是另一个高级玩法的范畴了,后面有机会单独展开。
子应用内部的异步 chunk 分开请求,数量控制在个位数,不要让 Vite 把一堆小文件拆得到处都是。合理的做法是把路由级别或功能模块级别的 chunk 保持在固定数量,然后在子应用入口的 system.js 里用 System.import 按需加载。
5. 在 Cloudflare 平台部署与发布的完整流程
5.1 发布流程设计:开发、预发、生产三级环境
刚开始跑这套架构的时候,我们只有一套生产环境,每次联调都要提心吊胆。后来花了一个下午把环境拆成了三级,效率提升立竿见影。
开发环境:每个子应用可以直接连自己的本地开发服务跑,容器壳子通过路由配置指向本地端口,这个不依赖 Cloudflare。Cloudflare 上单独配一个 dev.example.com,指向开发版 Pages 项目。
预发环境:staging.example.com,跑的是独立的 Pages 项目,和生产的 KV 隔离,子应用和壳子都用预发版本。所有联调和验收在预发做,需要真实边缘环境验证的,也是走预发域名。
生产环境:example.com,走正式 Pages 项目和正式 KV。
这样设计的关键是三个环境必须有独立的应用注册表。不然你在预发改一个 KV 配置,生产跟着遭殃。好在 KV 的 namespace 可以建多个,互不干扰,这个成本很低。
5.2 通过 GitHub Actions 自动部署子应用并更新注册表
人工发布到生产实在太容易出错了,尤其是要改 KV 这件事,一不小心就点错环境。我把自动发布做进了 CI 流程,整个流程大概是:
第一步,子应用仓库 push 到 main 分支,GitHub Actions 触发构建。
第二步,构建产物用 wrangler 部署到 Pages 项目。
yaml复制- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages deploy dist --project-name=order-app
第三步,获取本次部署生成的版本标识,更新 KV 里的 entry 地址。如果版本支持回滚,就把上一个 entry 存到一个 history 字段里。
bash复制wrangler kv key put --binding=APP_REGISTRY "app:order" \
--value='{"version":"20241101","entry":"https://order-app.pages.dev"}' \
--namespace-id=你生产环境的namespace-id
这里有个容易踩的坑:wrangler 命令里的 --binding 在不同版本里行为不一样,有时候需要在命令后面再加 --preview false 才会操作生产环境的 KV,不然默认操作的是 preview 命名空间。这个坑我踩过,害我一次线上验证了一晚上。
5.3 静态资源缓存策略:hash 文件长缓存,HTML 短缓存
Cloudflare 的缓存策略对性能影响极大。我的建议是:
- 带 hash 的静态资源(JS、CSS、图片、字体),缓存时间设为 1 年。文件名变了就是新资源,不需要担心更新不到。可以配置 Pages 项目下的
_headers文件:
code复制/assets/*
Cache-Control: public, max-age=31536000, immutable
index.html不缓存,或者只缓存 60 秒。因为 HTML 是入口,它里面引用的资源路径会随版本变化。如果 HTML 被缓存太久,用户拿到旧入口,引用新资源也就无从谈起。
code复制/index.html
Cache-Control: public, max-age=60
另外,Cloudflare 默认会对静态资源做自动缓存,但如果你开了 Worker 路由,需要确保 Worker 返回的响应头里带了合适的 Cache-Control,否则可能被 Workers 的默认策略影响。
6. 安全隔离与故障排查实战
6.1 子应用之间的安全边界:Cloudflare 的零信任策略应用
垂直微前端架构里,每个子应用是一个独立实体,安全边界可以借助 Cloudflare 的能力做得很干净。
首先,子应用之间的网络访问隔离。Pages 项目默认自带一个 *.pages.dev 域名,如果这个域名被直接访问,会绕过容器壳子的鉴权逻辑。需要在 Pages 项目设置里配置访问策略,直接用 Cloudflare Access 做零信任管控,只允许通过壳子域名跳转过来的流量访问。
其次,子应用 API 请求的鉴权。容器壳子通过 hostApi.fetch 统一附加 JWT 或 Cookie,子应用内部不用关心凭证怎么来。这样一个业务域被攻破,攻击者拿到的也只是当前域的数据权限,横向扩散风险被控制住。
最后,第三方脚本隔离。如果你的某个子应用要嵌入第三方表单或者支付脚本,尽量单独开一个沙箱 iframe,避免和主应用共享 DOM。这块不建议偷懒。
6.2 典型故障排查:从边缘路由到子应用加载的完整追踪链路
搭建过程中我遇到的最典型问题,归纳一下,基本集中在三类。
第一类是子应用页面 404。通常是因为 base 配置不对,子应用构建产物里的资源路径指向容器域名,而不是 Pages 域名。排查思路:打开浏览器 Network 面板,先看 HTML 是否加载成功,再看 HTML 内部引用的 JS/CSS 请求路径是否和预期一致。如果不一致,回查 Vite 的 base 配置。
第二类是子应用能加载但白屏。这大概率是 SystemJS 加载失败或者共享依赖没注册。先在浏览器 Console 看有没有 “Shared dependencies” 相关报错,有的话检查容器壳子的 import map 是否覆盖了子应用所有 external 依赖。
第三类是切换应用后页面某些功能失效。这是典型的生命周期没清理干净。检查子应用 unmount 时是否销毁了全局事件监听、是否移除了手动添加的 DOM 节点、是否清空了定时器。可以在卸载函数里加日志,把完全清理后的状态打出来,定位问题会比较快。
6.3 如何利用 Logpush 和实时日志做端到端监控
Cloudflare 的免费版自带 Workers 的少数日志,但生产环境建议用 Logpush,把 Worker 的日志发送到 R2 存储、S3 或者第三方日志平台。这样你就能看到边缘路由 Worker 的每次请求、每个响应码、每次 KV 读取的耗时。
我的做法是:在 Worker 里对每个请求记录一条结构化日志,包含路径、匹配到的子应用名、入口 URL、请求耗时、缓存命中状态。这样一旦用户反馈页面异常,我们能从日志里反推出到底哪一环出了问题——是入口返回了 404,还是子应用资源响应超时。
还有一个非常实用的技巧:在 Worker 响应头里加一个自定义头 X-Micro-App,值为当前匹配到的子应用名。排查问题时,直接在浏览器 DevTools 里看响应头,立刻知道当前请求被分发到了哪个应用,不用去猜。
7. 进阶玩法:基于 Cloudflare 的全局灰度与容灾切换
7.1 按比例灰度发布与按用户维度定向发布
前面说了用 weight 做简单的比例灰度,但如果要做得更精细,建议引入用户维度定向发布。
实现思路是:子应用注册表里增加一个 targetUsers 字段,标识白名单用户。Worker 在请求里解析用户 ID 或者用户组,如果命中白名单就返回新版本入口,否则返回旧版本入口。
javascript复制const userId = request.headers.get('X-User-Id');
const targetUsers = app.targetUsers || [];
if (targetUsers.includes(userId)) {
return fetch(app.newEntry + '/index.html');
}
这里需要和容器壳子的登录体系打通,确保用户 ID 能从请求头或者 Cookie 里拿到。Cloudflare 的 Worker 可以很方便地处理 Cookie 解析,不需要额外引入服务端。
灰度期间要关注子应用的错误率数据,如果新版本导致错误率飙升,立即把 KV 里的 entry 改回旧版本,整个过程不需要重新部署代码,秒级生效。这就是把注册表和业务逻辑解耦带来的好处。
7.2 多区域部署与容灾切换思路
Cloudflare 的节点遍布全球,但某些区域的服务可用性会受到网络环境影响。如果在多个区域有业务容灾诉求,我建议按区域维护多套 Pages 项目和 KV。
比如,北美和欧洲各有一套生产环境,入口域名不同,数据存储独立。每个区域的 Worker 路由配置指向对应区域的子应用项目。一旦某个区域入口异常,可以将域名级路由切换到另一个区域。
注意,多区域部署时,KV 的读取策略也要做区域化。Cloudflare KV 是全局强一致的读最终一致,正常情况问题不大,但如果你设置了多区域的写操作,需要注意 KV 的写入延迟,尽量避免同时写入多个 namespace 带来的竞态问题。
7.3 自动扩容与成本控制:Workers 的大规模请求处理
这套架构能支撑多大的流量?Cloudflare Workers 免费计划每天 10 万次请求,付费计划按量付费,且请求量上限极高。对于大多中小团队,成本完全在可控范围内。
为了控制成本,建议在 Worker 里对静态资源请求和 HTML 请求做区分。静态资源请求可以直接用 Cloudflare 的 Cache API 缓存响应,命中缓存就不回源,也不消耗 Worker 的 CPU 时间。HTML 请求则走 KV 查询和 fetch,消耗相对较高,但可通过短缓存减少重复请求。
code复制Cost per request:
静态资源命中缓存 ≈ 0
HTML 请求(KV 查询 + fetch)≈ 0.5ms CPU
如果请求量真的很大,可以加一层 Cloudflare 的 Custom Hostnames 或者专门的 CDN 缓存规则,把大部分静态流量从 Worker 里剥离出去,Worker 只负责入口请求和动态逻辑。
8. 踩坑实录与实操建议
8.1 我踩过的五个让人头疼的坑
第一个坑是 wrangler pages deploy 的默认环境问题。这个命令在部分版本里如果不指定 --branch,会默认把构建产物部署到 preview 分支对应的临时域名,而不是生产域名。看起来像是部署成功了,实际上线上一直跑的旧版。解决方案是 CI 里显式指定 --branch=main,或者直接使用 Pages 的 Git 集成,避免手动部署。
第二个坑是 KV 的 get 方法默认返回字符串,不是对象。如果你直接 JSON.parse 解析一个不存在的 key,会报错,所以初始化时要做容错处理,或者提前把注册表初始化好。
第三个坑是 SystemJS 格式构建下,代码分割的懒加载 chunk 路径容易出错。Vite 构建 System 格式时,动态 import 的 chunk 路径依赖 base 配置。如果不设置绝对路径,chunk 会以相对路径请求,一旦入口 URL 和资源 URL 不在同一层级,就会出现加载不了 chunk 的情况。
第四个坑是子应用之间的样式隔离远比想象中难。不要指望 Shadow DOM 能解决所有问题,有时候第三方组件库渲染的弹层和 Tooltip 是挂在 body 上,Shadow DOM 根本管不到。最终的体感是:样式隔离靠约定、靠构建工具加前缀、靠 code review,单一技术方案很难完美。
第五个坑是边缘网络环境下,fetch 子应用 HTML 时如果子应用挂了,Worker 会持续尝试直到超时。需要给 fetch 设置 AbortController 超时,比如 3 秒超时就返回兜底页面,避免用户无限等待。
8.2 给新上手团队的几条实操建议
不要一开始就追求完美的基础设施。可以先让一个子应用跑通全流程,把容器壳子、边缘路由、KV 注册表、构建部署这四件事都摸熟了,再逐步接入第二个、第三个子应用。
微前端的性能优化优先级远比功能开发高。因为它天然多了一层请求转发,如果不在缓存、preload、共享依赖上下足功夫,首屏体验极大概率不如原来的单体应用。
团队规范不能只靠文档,要靠工具链强制。子应用的脚手架模板、构建配置、代码规范检查全部统一,从源头上杜绝乱七八糟的接入方式。宁可前期多花点时间做脚手架,也不要后期给每个团队擦屁股。
最后也是最重要的,微前端不是银弹。如果你的核心矛盾是代码太乱、质量太差,上微前端只会把乱和差分散到更多仓库。架构调整必须服务于组织协作方式和业务演进节奏,这一点想清楚了,技术方案怎么做都不会太偏。
在我自己实际维护这套系统几个月之后,最明显的体感是发布频率从原来的每周一次集中发版,变成了每个子应用按自己的节奏随时发版,团队之间的等待时间少了,冲突也少了很多。当然代价是基础设施的复杂度确实上来了,但只要容器和路由这层稳定,下层的自由度就非常大。
