你有没有遇到过这样的“克隆地狱”?每接一个新客户,就把整个管理后台仓库 fork 一份,用户、角色、权限、内容管理、多语言这些核心模块原地抄走,再往上贴几个定制功能。前三个客户还行。到第十个时,权限系统发现一个 bug,你得手动把补丁搬到十个已经悄悄分叉的代码库里。大部分时间都花在搬运,而不是创造新功能。
这就是我们团队过去几年的日常。我们给客户交付的是白标管理面板,同一个内核撑起不同客户的后台。客户 A 需要房源模块,客户 B 要货物追踪,其他一切看起来都一样。最初我们用 git clone 解决“差异化”,然而当修复成本超过构建成本时,我们决定把所有的 fork 收进一个 pnpm monorepo:一个共享核心包,每个客户一个薄薄的壳。
![]()
我们的约束很明确:每个客户有自己的后端、数据库和域名,因此一个应用对应一个后端,一套 VITEAPIURL 环境变量就够。品牌、面板语言和权限在运行时由后端(settings 端点、i18n 端点、JWT 声明)提供,前端壳非常薄。而定制的功能不是配置项,是真正的代码——房源模块包含自己的类型、服务、hooks 和页面,绝不允许出现在其他客户的打包产物里。这个限制直接否定了“单租户部署加特性开关”的路线,因为特性开关能隐藏界面,却拦不住定制代码进入别人的 JavaScript 包。我们保留了按客户构建的模式,但必须消灭重复。
仓库的最终结构是一个核心包 @bo/core 和各个客户应用。核心包里没有构建步骤,package.json 直接导出原始 .tsx 源码。这样每一个客户应用都能从同一份核心代码出发,仅携带自己的定制模块和独有路由。两个机制很快就跑通了,但第三件事卡了我们整整一周:让 TanStack Router 在 monorepo 里正常工作。官方 GitHub issue 讨论区里不少人说这种结构不支持,但我们发现它其实可以,只是必须遵守标题里提到的那条规则。
规则就是:除了路由树,其他一切都可以共享。听起来反直觉,却恰好解开了死结。我们不再把路由定义放在核心包里让所有应用共用,而是允许每个客户维护自己的路由树,核心包只输出布局、守卫、通用页面和配置。定制路由像插件一样挂在客户的应用上,绝不会泄露给其他客户。这样一来,TanStack Router 的代码拆分和类型安全都能保留,而每个客户的构建物里只包含自己真正需要的代码。
踩过一周的坑后,这个策略变得意外干净。现在新建一个客户只需跑一条脚本,通过 _template 脚手架空壳起步,填上环境变量和定制路由,就能复用全部核心能力。再也没有十个仓库等着 port 补丁,我们终于重新回到“构建”而不是“搬运”的节奏上。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.