上个月我接到一个有点特殊的任务:把elpis项目里的event-bus模块抽离成独立npm包,发布到公司内部的私有npm仓库。elpis是我们组维护的中台服务,模块体积不大,但已经被另外两个项目以复制代码的方式引用了好几轮。每次修一个bug,都要跨工程同步,漏一次就埋一个雷。这次抽离和发布的完整过程,就是这篇文章的内容。
拆包这件事在npm生态里算常规操作,但操作层面的细节远比表面看起来多。模块边界怎么划、依赖扔进哪一类、本地联调怎么处理、发布后如何保证主项目不回归,每一环都有讲究。这篇文章我会把所有关键点写清楚,包括最后我们踩过的几个环境坑,适合在单体项目或Monorepo里维护重复模块、并打算把它转成独立npm包的同学阅读。
1. 拆包动机:elpis里这个模块已经被三个项目盯上了
1.1 为什么是elpis里的event-bus
elpis是一个面向业务场景的事件处理服务,核心功能是接收上游事件流,做规则过滤和格式化后分发给下游。event-bus模块负责事件路由与分发的核心逻辑,它本身不依赖任何业务页面,接口相对稳定,但内部实现很厚重:包含通道管理、重试队列、死信处理、监控上报。
最初只有elpis自己用,相安无事。后来另外两个项目也需要类似能力,第一反应就是直接复制。复制了三轮之后问题开始显现:某个项目里的事件路由逻辑已经落后elpis主分支两个版本,重试算法修了一个边界条件,那边完全没有同步。等到线上出问题再对比代码,两边diff就是几百行,排查成本极高。这种“复制-粘贴-漂移”的模式,在多个项目并行迭代时几乎是必然走向失控的。
1.2 拆包能解决什么,不能解决什么
拆成独立npm包,最直接的好处有三个。
第一是版本化。抽离后event-bus有了自己的版本号,谁想升级谁自己决定,不再被elpis的发布节奏绑架。第二是标准化复用路径。其他项目要接入,一条npm install解决,不用再讨论要不要复制代码。第三是职责边界清晰。抽离后event-bus的测试用例、文档、维护责任都独立出来,评审代码时也更有针对性。
但拆包不是万能药。如果模块和主项目耦合极深、公共配置满天飞,抽离的代价会远超收益。我自己判断模块是否该拆有三个标准:有没有两个以上的外部消费者;模块边界是否清晰到能用一个入口函数概括;模块在可预见的未来是否还会持续迭代。三个条件都满足,才值得动手。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前先划边界:理清耦合点、依赖归属和API面
2.1 把elpis主项目里的耦合点全部找出来
抽离之前第一步不是复制代码,而是静下心来做一次耦合扫描。我对着elpis的代码库整体翻了一遍,把event-bus相关的import语句全部列出来,分三类处理。
第一类,它引入的elpis内部模块,例如配置读取、日志服务、监控上报。这些依赖必须解耦,不能直接带到独立包里。第二类,它依赖的elpis运行时环境信息,例如读取环境变量、获取某个全局常量、访问宿主服务的生命周期对象。第三类,它反向暴露出去的内部状态。
对第一类,合理的做法是给独立包定义接口,把底层实现留给消费者去注入。例如elpis的日志服务是对winston的封装,独立包不能直接import,我们就定义了logger参数,由外部传入。对第二类,统一调整成通过构造函数或options传入,彻底移除对全局环境的隐式依赖。对第三类,整理模块的公开入口,确定哪些函数、类型、常量是真正需要暴露的。
2.2 依赖分桶:dependencies、peerDependencies、devDependencies
依赖归类直接决定这个包好不好用。我的经验是三条规则。
包内部运行必须的依赖,放dependencies。event-bus需要lodash来处理对象合并,需要ioredis做队列持久化,这两个就放dependencies。宿主环境应该提供的依赖,放peerDependencies,并声明版本范围。比如elpis主项目统一使用某一套TS类型定义,如果event-bus也用到了,就
