1. 为什么我在 Cloudflare 上做“垂直”微前端,而不是“水平”的
先讲清楚一个容易混淆的点。大部分团队聊微前端,默认指的是“水平拆分”——也就是按业务域拆成多个独立应用,每个应用占一个完整页面,比如订单页是一个应用、商品页是另一个应用,通过路由层做分发。这种方案的好处是团队边界清晰,但坏处也很明显:页面之间的跳转是整页刷新级别的体验,公共数据要跨应用传递,Case 一多就成了“搬砖式”协作。
而我这里说的“垂直微前端”,指的是在同一个页面里,把不同的页面区块(区块)拆给不同团队独立开发、独立部署。比如一个 Dashboard 页面,上面是用户信息卡片,中间是订单趋势图,底部是消息列表,这三个区块分别由三个团队维护,但在浏览器里它们拼在同一个 URL、同一个页面里——这就是垂直拆分。
为什么我会想到在 Cloudflare 上做这件事?两个原因:
第一,Cloudflare 的 Workers 和 Pages 天然适合做轻量级的服务端聚合层,而且它们对边缘缓存和静态资源的处理非常成熟,可以用来做“区块级别的路由分发”。第二,垂直微前端的一半复杂度不在浏览器端,而在构建部署和版本协调——Cloudflare 的 CI 集成、Worker 路由、环境变量注入恰好能把这块成本压得很低。
所以这篇文章的主题是:把 Cloudflare 当作一个“组合平台”来构建垂直微前端,而不是仅仅把它当成 CDN 用。我会把整个架构的拆解思路、技术选型、落地方案和踩坑记录都讲清楚。
适合谁来参考:如果你所在的前端团队正在纠结要不要上微前端,或者已经在用传统方案但是被跨团队协作折磨得够呛,那这篇文章能帮你看到另一条路。不适合谁:如果你是单团队、单应用、产品逻辑也不复杂,垂直微前端是正儿八经的过度设计,别套方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 垂直微前端的核心设计思路
2.1 垂直拆分的三个关键维度
在做技术选型之前,先搞清楚垂直拆分到底拆什么。我在实践中发现,有三个维度必须同时考虑清楚,否则后面一定会打架。
第一是视觉维度。 垂直拆分天然对应着页面在纵向上被切成不同的区块。每个区块的高度、边距、背景色、是否懒加载,这些信息必须由“区块协议”来定义,不能由各个子应用自己拍脑袋。否则拼出来的页面会像补丁摞补丁。
第二是数据维度。 区块之间通常是有关联的,比如订单趋势图需要知道顶部的用户城市信息。垂直微前端的核心难点就在这里:数据不能写死在各区块内部,而是要有一个跨区块的状态通道。这块我后面会展开讲。
第三是生命周期维度。 每个区块不会同时出现、同时消失。有的区块需要等某个事件触发后才加载,有的只需要展示一次然后销毁。这个生命周期谁来管理?是容器应用统一管理,还是各区块管理自己?如果设计不好,就会遇到一种很常见的病:页面初始加载 3 秒,但 90% 的区块根本不需要首屏出现。
2.2 为什么 Cloudflare 适合做区块分发层
传统微前端架构里,一般有一个主应用(容器)负责加载子应用,比如 qiankun、single-spa 都是在浏览器端做这件事。这些方案本身没问题,但有一个隐藏前提:所有子应用的构建产物必须能被主应用从同一个域名下访问到,否则跨域、Cookie 等问题会让你疯掉。
Cloudflare 的 Workers + Pages 组合,恰好把这个“同一域名”的问题在边缘层解决了。具体来说:
- 每个子应用可以部署到独立的 Pages 项目,拥有自己独立的预览链接和生产链接;
- 在容器应用所在的 Worker 里,按照 URL 路径或请求头做分发,把不同区块的静态资源请求代理到对应的 Pages 项目上;
- 所有请求最终都走容器应用自己的域名,所以不存在跨域问题,也不存在 Cookie 失效问题。
这在传统方案里通常要靠 Nginx 或者网关层配置一堆路由规则,而在 Cloudflare 上只需要一两段 Worker 代码。而且由于 Worker 在各边缘节点就近执行,静态资源的响应速度不会因为多了一层分发而明显变慢,实测大多数区域首包耗时只增加了 5ms 左右。
3. 技术选型:容器、通信、样式隔离这三件套
3.1 容器层:用 Worker 承担“胶水”职责
垂直微前端的容器应用,我建议用 Cloudflare Worker 来实现“请求的组合”,而不是在浏览器端用 JS 手动拼接。这两种思路的差别很本质:
- 浏览器端拼接(类似 iframe 或动态 script)的问题在于:首屏性能吃亏,而且各子应用加载的时序很难控制;
- 边缘端拼接的做法是:Worker 把页面骨架的 HTML 直接返回给浏览器,浏览器拿到的是一个完整页面,而每个区块的内容是通过 Worker 在边缘端代理加载的。
具体实现不复杂。骨架 HTML 可以是静态文件,放在 Worker 的静态资源目录里。区块区域用 <div id="block-a"></div> 占位,Worker 收到请求后,根据当前用户身份和环境变量,决定哪些区块需要在服务端渲染(SSR),哪些只需要在客户端异步加载。
我最初也想过用 iframe 来承载垂直区块,因为 iframe 在样式隔离上是无脑方案。但后来放弃了,原因有三:iframe 之间的通信太别扭,滚动条和高度自适应永远是个坑,而且移动端性能开销大。所以最终还是走了 Worker 渲染 + 客户端动态挂载的混合路子。
3.2 通信机制:别再折腾 EventBus 了
垂直微前端的跨区块通信,是团队最容易翻车的地方。很多团队上来就搞一个 EventBus 或者全局事件中心,一开始确实爽,等到事件多了就变成了蜘蛛网——你根本不知道谁在监听谁。
我建议的通信方案是:用“状态容器 + 选择器订阅”的方式,而不是用事件广播。每个区块不是监听一个“订单更新”事件然后自己去拉数据,而是把自己的数据源交给一个共享的 Store(我用的类似于 Redux Toolkit 的轻量切片机制),区块只关心自己关心的切片。
在 Cloudflare 这个场景下,这个 Store 的数据可以来源于两个方面:
- 服务端通过请求头或 HTML 里注入的
window.__INITIAL_DATA__传递基础数据; - 客户端各区块通过 API 获取的数据,写入同一个 Store,供其他区块读取。
这样设计的好处是数据流可追踪,调试的时候能清楚看到每个区块读取了哪些字段。坏处是需要团队成员统一心智,否则就会有人图省事直接操作 window.xxx,然后把你的架构变成一座危楼。
3.3 样式隔离:Shadow DOM 到底该不该用
垂直微前端里最头痛的问题之一就是样式隔离。水平微前端里每个应用占一整个页面,样式隔离的压力相对小;垂直微前端是多个应用挤在同一屏,Class 冲突几乎必然发生。
我试验过三种方案:
CSS Modules / CSS-in-JS:需要团队规范保障,约定所有类名统一前缀。成本最低,但一旦有人违反约定就会出事故,排查起来还特别费劲。
Scoped CSS(构建期加属性选择器):在小范围试点是可用的,但遇到全局样式互相覆盖的情况依然无能为力,比如 Reset 样式和组件库的全局变量。
Shadow DOM:隔离效果最好,但组件库兼容性问题多。比如很多 UI 库用到 document.body 下的弹层,放到 Shadow DOM 里就失效了;字体、颜色变量、CSS 变量也都要在 Shadow DOM 边界手动传递。
我的最终选择是混合策略:默认用 CSS Modules + 命名约定来管控,同时只有类似“第三方可视化大屏组件”这种高隔离需求的区块才使用 Shadow DOM。原因是 Shadow DOM 的成本高于收益,在绝大多数产品页面上没必要全覆盖。
4. 实操落地:基于 Cloudflare 的完整架构与代码实现
4.1 整体架构预览
先给一张我最终定型的架构描述,方便你对照后续的代码:
- 一个 Cloudflare Worker 作为容器,负责“页面骨架 + 区块路由”;
- 三个独立的 Cloudflare Pages 项目,分别对应三个垂直区块(header / main / footer);
- 各区块项目通过 Wrangler 的 CI 命令独立构建部署,版本号通过环境变量注入 Worker;
- Worker 根据请求路径和区域信息(比如国内 / 海外用户)决定区块版本,命中后返回对应的 HTML 片段;
- 浏览器端通过一个轻量级 Runtime 把 HTML 片段挂载到指定容器,并初始化区块的 Store。
这个架构的几大收益是:各个区块的独立部署、独立回滚、按区域灰度、性能追踪都能单独做。
4.2 Worker 端代码:区块路由与版本管理
Worker 的核心逻辑很简单,就是“请求进来,判断区块,返回 HTML 片段”。下面是我线上正在用的一个简化版实现:
javascript复制// worker.js
import { parse } from 'cookie';
// 区块版本映射,实际会通过环境变量注入
const BLOCK_VERSION = {
header: 'v1.2.0',
main: 'v2.5.3',
footer: 'v1.0.7'
};
const BLOCK_BASE_URL = {
header: 'https://header-block-project.pages.dev',
main: 'https://main-block-project.pages.dev',
footer: 'https://footer-block-project.pages.dev'
};
// 按区域 + Cookie 决定使用哪个灰度版本
function resolveVersion(block, country, cookies) {
if (cookies[`${block}_canary`] === '1') {
return 'canary-' + BLOCK_VERSION[block];
}
if (country === 'CN') {
return BLOCK_VERSION[block] + '-cn'; // 国内灰度版本
}
return BLOCK_VERSION[block];
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const country = request.headers.get('CF-IPCountry') || 'unknown';
const cookies = parse(request.headers.get('Cookie') || '');
if (url.pathname === '/') {
// 返回页面骨架 HTML
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<link rel="stylesheet" href="/assets/shell.css">
</head>
<body>
<div id="block-header" data-version="${resolveVersion('header', country, cookies)}"></div>
<div id="block-main" data-version="${resolveVersion('main', country, cookies)}"></div>
<div id="block-footer" data-version="${resolveVersion('footer', country, cookies)}"></div>
<script src="/runtime.js" defer></script>
</body>
</html>
`;
return new Response(html, {
headers: { 'content-type': 'text/html; charset=utf-8' }
});
}
if (url.pathname.startsWith('/block/')) {
// 代理区块资源,返回对应区块的 HTML 片段
const block = url.pathname.split('/')[2];
const version = resolveVersion(block, country, cookies);
const targetUrl = `${BLOCK_BASE_URL[block]}/${version}${url.pathname}`;
const blockResponse = await fetch(targetUrl, {
headers: {
'user-agent': request.headers.get('User-Agent') || ''
}
});
// 对区块响应做缓存
const response = new Response(blockResponse.body, blockResponse);
response.headers.set('cache-control', 'public, max-age=60');
return response;
}
// 其他静态资源直接代理
if (url.pathname.startsWith('/assets/')) {
return fetch(`https://container-assets.pages.dev${url.pathname}`);
}
return new Response('Not Found', { status: 404 });
}
};
你可能会问:为什么区块版本号是写死的?实际上在生产环境,这些版本号是由构建流水线在部署时自动更新的,不需要手动硬编码。Wrangler 支持在 wrangler.toml 里配置 [vars],我们会在 CI 里用命令行把最新版本号注入进去。
4.3 各区块的构建与部署产物约定
每个垂直区块本质上是一个独立的前端工程,但需要遵循一些产物约定,容器才能正常加载。我这边定了几条规则,并且把它写成了团队的开发规范:
- 每个区块构建后必须输出一个
block.html,这个文件是区块的入口描述文件,里面包含区块渲染所需的所有脚本和样式标签; - 构建产物中不包含 HTML 骨架,只有区块自身的 JS / CSS 和一份入口描述;
- 区块的 JS 入口暴露两个方法:
mount(container, props)和unmount(container),容器 Runtime 在挂载和销毁时会调用它们。
下面是一个简化版的区块入口示例:
javascript复制// header-block/src/index.js
import { createStore } from './store';
let store = null;
export function mount(container, props) {
// 利用 props.initialData 初始化区块数据
store = createStore(props.initialData || {});
container.innerHTML = renderHeader(store.getState());
}
export function unmount(container) {
// 清理事件监听、销毁定时器
store = null;
container.innerHTML = '';
}
这个约定和 single-spa / qiankun 的“生命周期钩子”思路是同一套,只不过我们不需要加载器,由容器 Worker 加一个轻量 Runtime 就能支持。
4.4 客户端 Runtime:把区块挂载到页面
Worker 返回骨架 HTML 后,浏览器需要用一个 Runtime 脚本去加载各区块的 block.html,解析里面的脚本和样式,按顺序挂载区块,并注入跨区块的 Store。
这个 Runtime 我设计得很薄,核心逻辑就三步:读取区块元素的 data-version 属性;按版本号请求 /block/{name}?version=x;拿到 HTML 片段后原地初始化。
javascript复制// runtime.js
async function loadBlock(name, container) {
const version = container.dataset.version;
const response = await fetch(`/block/${name}?version=${version}`);
const html = await response.text();
// 创建一个内部 document 来解析 HTML 片段
const doc = new DOMParser().parseFromString(html, 'text/html');
// 把样式插入到主文档
doc.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
const cloned = document.createElement('link');
cloned.rel = 'stylesheet';
cloned.href = link.href;
document.head.appendChild(cloned);
});
// 加载脚本
const scripts = Array.from(doc.querySelectorAll('script'));
await Promise.all(scripts.map((script) => {
if (script.src) {
return import(/* @vite-ignore */ script.src);
}
// 处理内联脚本
return eval(script.textContent);
}));
// 检查区块是否暴露 mount 方法
if (window[`__block_${name}`]?.mount) {
window[`__block_${name}`].mount(container, {
initialData: window.__INITIAL_DATA__?.[name] || {}
});
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-version]').forEach((container) => {
const name = container.id.replace('block-', '');
loadBlock(name, container);
});
});
这个过程里最容易踩的坑是脚本加载时序:区块 A 的脚本可能在区块 B 的脚本之前加载完,但 A 的挂载函数可能依赖 B 中定义的全局变量。我建议在 Runtime 里加上一个全局的依赖注册表,区块声明自己依赖哪些区块的全局变量,Runtime 保证依赖顺序。
4.5 数据与状态方案:跨区块的 Shared Store
各区块的数据流我最终采用了类似模块联邦的思路,但去掉了 Webpack 的耦合,直接用一个自定义的轻量 Store 实现。
具体做法是:在容器骨架 HTML 里提前注入 window.__INITIAL_DATA__,各区块在 mount 时读取属于自己的切片数据。当区块需要跨区块同步数据时(比如用户点了顶部“切换地区”,中间的趋势图要联动),通过一个统一的 Event Channel 来做发布订阅。
这个 Event Channel 我在生产环境里做了两个约束:
- 事件名必须采用“域/动作”的格式,比如
order/fetch-success,禁止出现随意取名; - 事件只能由“数据产生方”触发,其他区块只订阅自己不产生的事件,不允许反过来。
另外值得注意的事:不要把所有数据都塞进全局 Store。垂直微前端的性能瓶颈往往不是 JS 执行,而是数据请求的冗余。如果一个区块只需要自己的数据,就让它自己发请求,不要经过容器转发。
5. Cloudflare 平台能力的取舍与优化
5.1 Workers 大小受限,怎么处理大 chunk
Cloudflare Workers 的主脚本限制是 JavaScript Bundle 不超过 1MB(gzip 后),这个对于容器应用来说确实是个约束。按我们前面的架构,容器只做“骨架 + 区块路由”,不大可能超过 1MB。但如果你脑子一热想把整个页面的 SSR 逻辑也搬进去,迟早会撞墙。
我的处理方式是:把容器的职责压缩到最小,只做路由和聚合;所有涉及复杂计算的服务端逻辑都丢给 Cloudflare 的 Durable Objects 或者后端的 API 服务,容器只做代理转发。这样既保持 Worker 的轻量化,也让排错变得简单。
5.2 缓存策略:区块变化的传播延迟控制
Cloudflare 的缓存是分层的,默认情况下对 HTML 做缓存需要手动设置 Cache-Control,而且各边缘节点的缓存清空需要一定时间。垂直微前端里,区块更新是高频的,所以缓存策略特别重要。
我最终采用的策略如下:
- 对于区块的静态资源(JS/CSS),文件名带上内容哈希,缓存时间设成
max-age=31536000,也就是一年。这样版本更新后请求的是新哈希的文件,不会命中旧缓存; - 对于区块的入口描述文件
block.html,缓存时间设成 60 秒。这样区块发版后,最多 1 分钟就能全量生效,同时不会让 Worker 回源太频繁; - 在发版流程中,先更新 Pages 项目,再更新 Worker 里的版本号变量。顺序反了会出现“新版本号指向的文件还没部署完成”的情况。
注意:Cloudflare 的 Cache Purge 是按 URL 或 Cache-Tag 来做的,如果你在 Worker 里代理了多版本区块响应,一定要给每个版本设置独立的 Cache-Tag,否则发版时容易误伤其他版本。
5.3 灰度发布与 A/B 测试在边缘节点的落地
灰度发布的实现,本质上就是在 Worker 的 resolveVersion 函数里做分流。我这边支持两种分流维度:
- 按用户属性:通过 Cookie、请求头或
CF-IPCountry区分用户群; - 按随机流量:比如让某个区块的 v2 版本覆盖 10% 的流量,用于 A/B 测试。
随机流量的实现比较简单,在 Worker 里根据用户 IP 取模就行:
javascript复制function getBucket(ip) {
const hash = [...ip].reduce((acc, char) => acc + char.charCodeAt(0), 0);
return hash % 100;
}
需要注意的事项是:灰度分组必须在整个请求链路中保持一致,不能一个请求里某区块走了 v2,另一个区块走了 v3,否则页面会出现区块间数据不匹配的怪异问题。我的做法是计算一次分组 ID,然后在所有区块请求之间共享,具体实现是把分组结果写进 Cookie。
6. 部署与 CI/CD:少踩多线程构建的坑
6.1 三块职责的独立流水线设计
整个体系里有三条独立构建流水线,分别对应三个区块项目,外加一条容器项目的流水线。区块的流水线互不干扰,任何一条失败都不能阻塞其他区块发版。
每个区块的 CI 流程我设计成四个阶段:
- 安装依赖与单元测试;
- 构建产物生成(含
block.html); - 发布到 Cloudflare Pages;
- 更新容器 Worker 的版本号。
第四步非常关键,很多团队会漏掉。仅仅把代码发布到 Pages 还不够,容器 Workers 必须知道“新版本已经可用”,否则用户拿到的还是旧版本。这一步我是通过一个内部命令来实现的:
bash复制# 发布区块 v1.3.0,并将版本号同步到容器 Worker
wrangler pages deploy dist --project-name=header-block
wrangler secret put BLOCK_VERSION_HEADER --value "v1.3.0" --env=production
6.2 多线程构建下的产物一致性
在我们的实际体量中,三个区块并行构建是常态。最容易遇到的问题就是:构建时区差异和输出文件名冲突。
比如区块 A 和区块 B 都生成一个叫 charts.js 的文件,如果部署时没有做隔离,会互相覆盖。这个问题的解法有两个层次:
- 所有产物必须带内容哈希,比如
charts-1f2e3a.js,从根本上避免文件名冲突; - 每个区块的
block.html必须明确引用自己版本的产物,禁止引用相对路径而不带版本。
我在 CI 里强制加了检查脚本,一旦发现 block.html 里引用的脚本路径不含哈希,直接构建失败。宁可多费几秒检查,也不要上生产被文件名覆盖坑。
6.3 回滚与故障恢复的实操方法
边缘平台的回滚逻辑和传统服务器不太一样。传统服务器回滚是“把旧代码拉回来再启动”;Cloudflare 上是“把 Worker 的版本号切回旧值”。
所以我的回滚预案是:每个区块的版本号,是一个独立的变量。当出现线上问题时,运维不需要去重新部署任何代码,只需要把 Worker 对应的 BLOCK_VERSION_HEADER 切回上一个稳定版本即可。
这里有个容易忽视的点:版本号回滚了,但是 Edge Cache 里可能还有新版本的资源。为了避免用户在新旧版本之间来回横跳,回滚时我会主动调用 Cloudflare 的 Cache Purge,把对应版本号的资源全部清掉。这个动作最好做成自动化脚本,否则出现问题的时候手忙脚乱。
7. 常见问题与排查技巧实录
7.1 区块加载白屏:先看 Worker 日志,别着急改代码
排错顺序很重要。区块加载白屏,第一反应通常是去查区块的前端代码是不是报错了,但实际上问题很可能是 Worker 端返回的 HTML 片段不对,或者版本号指向了一个不存在的资源。
我的排错步骤是:
- 打开 Cloudflare Worker 的实时日志,确认
/block/{name}的请求是否返回 200; - 直接 curl
https://your-worker.example/block/header?version=xxx,看看返回的 HTML 片段格式对不对; - 再检查区块项目本地的
block.html是否包含了正确的 JS 引用。
如果以上三步都没问题,再考虑浏览器端脚本报错。很多“白屏”其实是资源加载 404,而不是 JS 异常,你把 DevTools 切到 Network 面板一眼就能看出来。
7.2 样式错乱:一定是两个伪类或全局样式打架
我用过几个微前端方案,最后得出的结论是:样式错乱 90% 是因为团队成员“忍不住”写了覆盖全局样式的代码。比如某区块里写了 button { background: blue },则所有区块里的按钮都会受影响。
排查方式也比较土但很有效:在出现样式错乱的元素上打开 DevTools,看 Computed 面板,逐条检查哪些样式来自非本区块的样式表。找到之后,把全局覆盖改成带作用域的写法。
从长远来看,我建议在团队规范里明确规定:任何区块不得在无前缀情况下直接写元素选择器。如果你发现有人写了 p { margin: 0 },不指名道姓批评的话,后面这类的坑会反复出现。
7.3 跨区块状态不同步:事件时序比数据来源更值得查
跨区块状态不同步的典型案例是:顶部选择“华东”,中间订单趋势图的数据还是“全国”的。很多人的第一反应是去查 API 是不是传错了参数,但真正的问题往往是事件触发的时序不对。
我遇到过一次,区块 A 的 mount 方法执行时,区块 B 还没加载完成,所以 A 发的事件 B 根本来不及处理。解决方式是在 Runtime 里增加一个“就绪等待”机制:区块在 mount 完成后向 Runtime 上报 ready 状态,依赖方在收到对方 ready 之后才允许发联动事件。
这个机制说白了不复杂,但确实能解决真实场景里的时序问题,值得做进 Runtime。
7.4 问题速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 区块加载白屏 | Worker 路由未匹配 / 版本号指向不存在资源 | 查看 Worker 日志 + curl 区块 URL |
| 样式错乱 | 有区块写全局样式 | DevTools 逐条查样式来源 |
| 状态不同步 | 事件时序问题 | 检查 Runtime 就绪状态机制 |
| 发版后旧资源依然被加载 | Edge Cache 未清 | 手动 Purge + 确认版本号更新 |
| 构建产物互相覆盖 | 文件名不含哈希 | CI 里加产物校验步骤 |
| 回滚后用户仍访问新版本 | Cookie 或 Header 缓存了版本号 | 清理 Cookie 或缩短版本号缓存时间 |
8. 目前这套架构的收益与遗留问题
跑了一段时间后,这个方案给我最大的体感是:跨团队协作的摩擦小了很多。以前动一个页面底部的小模块,要拉上整个前端小组评审、回归测试;现在只需要负责 footer 的团队自己改自己发,容器层只关心版本号变更。每个团队的发布窗口从“一周一次”变成了“随时可发”,看起来是个小转变,但对团队节奏的影响是显著的。
不过我也得坦白说几个遗留问题,仅供参考:
首先是调试体验。垂直微前端天然是分布式调试——页面上的每个区块都可能来自不同的项目,本地调试时想同时跑三个区块的本地服务,需要额外的代理工具配合。我目前是写了一个 docker-compose 方案,把三个区块的 dev server 和容器 Worker 都起在一个网络里,能解决大部分调试需求,但初始化体验依然比单体应用繁琐。
其次是新人的学习成本。垂直微前端要求所有参与者都理解“区块是独立的、可替换的”这套思维模型。一个习惯了写全局事件的新人,需要一定时间适应 Store 切片的约束。这个只能靠文档和 Code Review 慢慢来。
如果你也正在微前端方案的选型边缘徘徊,建议你先拿一个非核心页面做试点,凭实际体验再决定是否全量推广。我踩过的这些坑,多半是真实场景里绕不开的,提前避开总比后补要省力得多。
