Medal W-Key 前端基础设施现代化复盘
Medal W-Key Technology and Product
来源:https://medal.tv/blog/posts/w-key-in-frontend-synergizing-technology-and-product 作者
Zhang(Medal Frontend),2026-06-23 分类:开源项目分析/阅读/
文章核心 → Micro 现代化路径
Medal.tv(游戏录屏分享平台)Electron+Web 前端陷入恶性循环——老组件锁死依赖升级,新功能又让重构成本飙升。作者用”W-Key”(魔兽术语,只推一路不绕弯)比喻自己的策略:先重塑开发者体验,再让组件现代化水到渠成。
痛点与反模式
- 组件 prop 过多、行为不单一、默认值满天飞、新旧 API 混杂
- 修一个组件改动像 Deogen(Phasmophobia 怪物)一样令人恐惧
- 业务功能 1-2 工时估点,实际拖成数倍
宏(Macro)层 — 先动基础设施
| 步骤 | 解决问题 | 关键点 |
|---|---|---|
| PNPM monorepo | 组件库版本混乱、合并冲突 | 性能 + 工作区 |
| Vite + HMR | Electron 应用本地冷启动,改代码秒级反馈 | CEO Ken Colton 推动 |
| 规范化 import 路径 | AI 工具可靠识别死代码 | 包名 + 清晰路径 + 文件扩展名,禁用 barrel 桶 |
| Vite 统一生产构建 | 摆脱 Rollup,Glob Imports 简化拆包 | 处理 native module + externalize 依赖 |
| Electron + Web 代码合并 | 抽 client-agnostic utils/hooks/types 包 | 进一步 DRY |
微(Micro)层 — 组件现代化
- Bundle 优化(总减重 41.3 MB,从 ~44 MB 到 2.7 MB,减 94%):
- 删 component barrel 文件:-2.6 MB
- 外置 SoundAlertsData
.wav:-3.4 MB - Rollup 优先 ESM 改善 tree-shake:-4 MB
- 动态 import i18n 翻译文件:-13 MB(单项最大)
- 路由级 code split(顺手把 React Router v5 升到 v7):-5.4 MB
- UI 库迁移 + Styled Components → Tailwind + Shadcn (Radix UI) → Base UI
- 自定义 ESLint 规则 + 文档双重标记废弃组件,告诉团队”用什么替换什么”
- 测试;构建
关键数字
最终 renderer bundle:2.7 MB(从 ~44 MB 起算,减 94%)。整个过程”能站起来整个页面只用一天”。
讨论延伸 File 与 Tree-Shaking 实战
这部分不是文章原文,是阅读后与助手讨论沉淀的实践知识。
为什么 barrel file 默认禁用
components/index.ts 这种”桶文件”看起来很工程化(导入简洁、利于重构),但运行时 import 的是桶,不是真正的模块——export { Button } from './Button' 执行时会加载整个 Button.tsx 及其 import 链(hooks/utils/styles/副作用代码)。
打包器想 tree-shake 掉没用的导出,前提是能 100% 确定那个导出没有副作用。而业务组件天然有副作用:
- 组件 mount 时发请求
- i18n 库初始化读 localStorage
- 主题切换改 document.documentElement
- 状态管理建 store
- analytics/错误监控 import 时启动
Medal 给出真实数字:删一个 components 桶 → -2.6 MB;外置一个被桶路径意外拖入的 .wav → -3.4 MB。如果桶真的”按需加载”,这 10MB 不可能只改几行 import 路径就省出来。
推荐的写法
// ❌ 桶导入(业务代码默认禁用)import { Button, Modal } from '@/components';
// ✅ 显式深路径 + 扩展名import { Button } from '@/components/Button/Button';import { Modal } from '@/components/Modal/Modal';为什么 lodash-es 能保持 tree-shake
业务项目做不到的事,lodash-es 全做到了——这是”库 vs 业务”的本质边界,不是工具差异。
| 条件 | lodash-es | 业务代码桶 |
|---|---|---|
sideEffects: false 担保 | ✅ 主动承诺 | ❌ 不敢写(有真副作用) |
| 桶里 re-export 的粒度 | ✅ 几百个纯函数叶子 | ❌ Button→hooks→utils→styles,链长且脏 |
| 模块顶层无副作用 | ✅ 纯函数 | ❌ mount/init/subscribe |
| 作者 tree-shake CI | ✅ 严格测试 | ❌ 没有 |
| 纯 ESM 格式 | ✅ | 看项目 |
lodash-es 能 tree-shake 不是因为它有桶,是因为它有桶 + sideEffects: false + 叶子化导出 + 纯函数 + ESM + 作者严格维护。业务项目做不到这套组合拳,所以业务项目里桶就真的是炸弹。
库 vs 业务的分离策略
核心原则:库用桶,业务用显式路径。
| 场景 | 桶策略 |
|---|---|
| 公开 npm 包(antd、shadcn、radix-ui) | ✅ 必用桶(用户体验) + sideEffects: false |
| 公司内部跨业务组件库(字节 semi) | 用桶 + 严格 sideEffects 治理 |
| 单业务用的”通用组件” | ❌ 禁用桶,显式深路径 |
| 纯类型导出 | ✅ 安全(type-only import) |
| 纯叶子工具函数 | ✅ 影响小,可用 |
| 体积敏感的入口(路由、Electron renderer) | ❌ 必禁 |
单业务”通用组件库”的第三条路 模式
如果”通用组件库”只在公司内部一个项目复用,Shadcn 模式比 npm 包更省心——既保留”通用”,又规避”库 vs 业务”的桶陷阱。
- 不发布 npm 包,不维护版本
- 组件源码直接复制到业务项目里(
npx shadcn@latest add button) - 改组件不用发版本、不用等业务升级,直接改
- tree-shake 天然干净(路径都是显式的)
- Medal 用的就是 Tailwind + Shadcn(Radix UI)——他们选择 Shadcn 而不是自造 npm 组件库,本质就是规避了”库 vs 业务”的桶陷阱
自建 npm 组件库的硬性配置清单
如果坚持做对外/对内 npm 组件库,以下配置必须做到位:
{ "sideEffects": ["**/*.css", "**/*.scss"], "type": "module", "exports": { ".": { "import": "./dist/index.mjs", "types": "./dist/index.d.ts" } }}sideEffects白名单样式文件(CSS import 是真副作用,不能 tree-shake)- 每个组件必须是叶子,内部不要再 re-export 其他组件
- 加 tree-shake 测试到 CI(用 rollup-plugin-visualizer 或 webpack-bundle-analyzer 断言”引入 Button 不应包含 Modal 代码”)
- 类型导出用
export type显式标注,避免打包器误判 - monorepo 跨包引用时,调试必须跑生产构建验证——开发态通过不代表生产态通过(webpack 某些配置会把整个软链接包当 chunk 全部引入)
反直觉观点
- “删代码”比”写新代码”更难,因为没人鼓掌——需要先投资 DX(快反馈、可识别),再谈清理
- Macro 先于 Micro:先动 monorepo/包管理/构建系统这种”看不见的基础设施”,让组件级重构变成顺水推舟
- Bundle 优化最大单一来源不是 minify/gzip,而是动态 import 大文件(i18n 占 -13MB,占全部减量近三分之一)
一句话总结
Medal 的实践揭示了一个常被忽略的工程真理:前端现代化的最大杠杆点在基础设施层(Macro),不在组件层(Micro)。而实现 Macro 现代化的关键,是敢于在业务代码里禁用 barrel file、把 import 路径显式化——这是后续所有 tree-shake / 死代码删除 / AI 重构能成立的前提。
桶不是不能用,而是要分清”库 vs 业务”:库用桶 + sideEffects: false + 叶子化 + 纯函数 + 严格维护;业务用显式路径,或者直接用 Shadcn 模式从源头消除”库 vs 业务”的边界。
参考
- [[开源项目分析/阅读/Harness工程-Multi-Agent架构实践|Harness Engineering]] — 阿里数据研发 Multi-Agent 工程实践,基础设施先行 + Harness 模式与 Medal 的 Macro→Micro 同源
- [[前端/Typescript/Typescript|Typescript]] —
export type、verbatimModuleSyntax等语法层面的桶治理工具 - [[前端/Typescript/tsconfig|tsconfig]] —
isolatedModules+noUncheckedSideEffectImports等可在编译期辅助发现副作用的配置 - [[前端/Javascript/解决ESM Import 过多|解决ESM Import 过多]] — 与本文”显式深路径”思路同源,讨论 import 路径的工程化治理
- [[Architecture/Monorepo/Lerna|Lerna]] — Medal 用的 PNPM monorepo 是同思路的现代实现,可对照参考