一个 18,525 行文件的拆分之道

AST 级耦合测绘与三阶段落地路径

可行性研究 2026-10-08 110 模块 · 158 条依赖边

摘要 本文评估一个单体插件按功能域拆分为多包组合的可行性。结论分三层:技术上可行(宿主的组合机制已在另一个 22 子包的工程上得到生产验证);拆分维度的第一刀应当落在「宿主进程半面 / 浏览器半面」;而真正的障碍不在打包机制,在于 lib/index.js 里一个 10,852 行、261 个方法的单类。

依赖图测绘显示 110 个模块中有 68 个零出边叶子(61.8%)。这批模块可以整组搬进子包而不触动任何逻辑——拆分是结构治理,不是减重。

这个插件目前是一个包。它的宿主半面与浏览器半面、语义引擎与白板、团队层与日历,全部住在同一个 package.json 之下。改前端要连带上 12.5 GB 的 Python 侧,装记忆检索就得连白板看板一起装。

拆分是用户提出的诉求。本文回答三个问题:能不能拆、该按什么维度拆、真正的障碍在哪。

一 先看结构

维度实测值
lib/ 文件数110 个 .js
lib/ 源码行数68,097 行
lib/index.js18,525 行 / 1,227 KB
lib/client.js(生成产物)18,857 行 / 2,133 KB
两者合计37,382 行 = lib/ 的 54.9%
发布包体积24.7 MB(243 文件)

两个文件占去整个目录超过一半的代码量。这个比例本身就是拆分难度的指标。

二 六十八个叶子

110 个模块中 68 个为零出边叶子模块(61.8%),依赖图呈星形拓扑;index.js 出边 78 条
图 1 依赖图形态。零出边的叶子模块可整组搬迁,不触及任何逻辑。
指标实测值
节点(模块)110
边(import 关系)158
零出边的叶子模块68 个(61.8%)
被依赖最多dsh-home.js (7)
出边最多index.js (78)

这是一个星形拓扑:中心是编排层,外围是叶子。已有若干成组的叶子模块共用同一批依赖,天然构成子包候选:

候选子包内容叶子证据
边界file-boundary / datadir / dsh-home / fs-retry被引 5 / 5 / 7 / 3,零出边
配置config-io / config-lock / shared-state-lock零出边
语义semantic-js / shadow-retrieval / python-sidecar-client零出边
白板wb-sidecar / wb-contract / board-mode零出边
团队team-identity / team-inject / team-transport* / team-outbox零出边
完整性degrade / evidence-store / intent-clean-safe零出边,被引 4

三 拦路的那一个类

把叶子搬走之后,剩下的还是那座山。

类行数方法数
ObserverRing1924
SessionRuntimeStore2328
MemoryEngine10,852261
ExternalMemory5,46713

MemoryEngine 一个类横跨一万余行。它的 261 个方法按语义分域之后,分布是这样的:写盘 17、runtime 13、白板 8、注入 7、配置 6、反思 5、检索 4、日历 3、技能 2、诊断 1——未归类 195 个。

最后一行的数字才是重点。近四分之三的方法无法归入任何功能域——它们共享状态、互相调用、边界模糊。按域拆分意味着要给这 261 个方法划出一条清晰的缝,而这条缝目前不存在。

因此可行性判断是:可拆,但这一刀不能先切这里。

四 对照组

「能不能拆」这个问题不必推演,有现成的答案。同一生态里有一个 22 子包的工程在生产运行。

维度对照组本仓
形态1 个聚合包 + 22 个子包1 个单包
聚合包 lib/index.js1,208 行(纯转发)18,525 行(全部逻辑)
exports 子路径24 个4 个
patch 行数24 行(每族一行)1 行
插件面板显示每族一行独立条目1 行

宿主的多包组合机制、独立显示与独立开关,在对照组上都已得到验证。本仓可以直接套用,不需要新的运行时能力。

五 最大的陷阱

拆分过程中最需要警惕的不是打包机制,而是一处语义细节:patch 文件的 insert 是「插入一行」,不是「替换」。

同一个包被安装两次,会得到两个 id 相同的行,从而触发插件重复注册。搬迁时必须保证旧行被移除,且这一对动作要配断言守住。

风险等级缓解
patch 行重复插入 ⇒ 双份注册高删旧与加新成对,配守卫断言
拆分单类引入跨包私有字段访问高前置解耦,本阶段不做
配置键迁移中保持路径解析口径不变即可零迁移
用户升级路径中保留原包作为全功能聚合包,向后兼容
回归套件的路径假设中拆包会大面积改路径,必须一次性改全

六 三阶段

按风险递增排序,路径是这样:

第一阶段,拆「宿主半面 / 浏览器半面」。零语义风险,收益最大:前端改版不再带上 Python 侧,首次安装更快。

第二阶段,把叶子模块整组包化。低风险,且前述六个候选子包都有明确的叶子证据。

第三阶段,动 MemoryEngine。高风险,须单独立项,且前置条件是完成解耦。

关于收益需要一句澄清:拆分是结构治理,不是减重。总代码量不会因此减少——它换来的是可单独开关、可单独发版、故障隔离,以及首次安装不必再拖着一个整体。

写在后面的话

「图难于其易,为大于其细。」

接到这个课题时,直觉是「先找个办法把那个大文件切开」。测绘做完才发现,问题不在刀快不快,而在缝在哪里——一个一万行的类里,近四分之三的方法无法归入任何功能域。强行切,切出来的不是模块,是碎片。

于是这份研究最终交出的不是「怎么拆那个类」,而是「先别拆它」。先搬走那六十八个叶子,先把两个半面分开,让结构自己长出缝来。

对照组还给了我们一个额外的好处:这条路已经有人走通过。可复用的不只是打包机制,还有它对「每一行可独立显示、可独立开关」这件事的坚持。

dsh-auto-memory 项目组 · 2026

相关报告