拓冰建站拓冰建站
首页 / 资讯中心 / 正文

共用代码处理指南:从复制粘贴到规范化公共模块发布

上一期做业务迭代时遇到一个很典型的困境同一个日期格式化逻辑在 A 项目里写了一遍B 项目要用直接复制过去C 项目管理后台要展示同类数据又复制了一份。三个月后数据格式规则调整三个项目各改各的结果线上行为不一致排查到最后才发现同一份逻辑在不同项目里已经被改出了三个版本。这个问题看起来小却是很多团队从“能做出来”走向“做得稳”的必经关卡。所以第一期我想认真聊聊共用代码处理那点事。这篇文章会从共用代码的概念讲起接着梳理共用代码的四个演进阶段再给出一个从业务代码中抽取公共模块并发布使用的完整实操流程最后补充版本管理、常见问题和工程建议。无论你是刚接触项目的新人还是正在苦恼多项目代码重复的后端、前端、全栈开发者这篇都值得收藏备用。1. 什么是共用代码它到底解决什么问题1.1 共用代码是什么共用代码字面理解就是“被多处使用的同一份代码”。但在实际工程里这个定义需要再扩一下多个项目复用的工具函数例如日期格式化、金额转换、字符串校验多个模块复用的 UI 组件例如弹窗、分页器、空状态提示多个服务复用的基础类例如统一返回体、异常结构、鉴权逻辑前后端共享的类型定义或接口协议例如 TypeScript 类型、OpenAPI 接口描述多个环境复用的配置片段例如 ESLint 规则、构建脚本、日志格式。也就是说共用代码不只是一个.java文件或一个.js函数凡是“一份逻辑要被多处引用”的内容都属于共用代码的范畴。1.2 共用代码处理的现实场景结合我自己的实际感受共用代码最常出现在下面几类场景。场景一多个前端项目共用一套后台管理系统基础能力。比如一个公司里同时有订单管理后台、用户运营后台、商品配置后台这三个项目都需要登录态校验、请求封装、统一的表格分页逻辑。如果每个项目都从零写一遍不仅工作量大而且后面接口调整时每个项目都要同步改一遍。场景二后端多个微服务共用基础依赖。一个微服务架构里可能有订单服务、用户服务、库存服务。它们之间也许没有直接的调用关系但都用到了统一错误码、统一日志切面、统一的安全校验注解。这些代码如果不抽出来每个服务的实现细节就会逐渐走样。场景三技术团队内部沉淀的“工具手册”。随着项目数量增加团队里会积累一些“这个工具函数我在上个项目里写过”“那个组件我记得有人封装过”的记忆。共用代码处理得好这些记忆就能沉淀成资产处理不好就只能靠口头传承和反复造轮子。1.3 为什么“共用代码”值得专门聊很多开发者一开始会觉得共用代码不就是“把重复代码抽出来”吗有什么好聊的但真正做起来问题比想象的复杂抽到什么粒度才合适抽太碎光维护依赖就累抽太粗业务代码反向耦合公共模块反而更难改。谁来维护公共代码没有负责人最后就会变成“没人管的代码”。怎么发布和升级改了公共代码业务项目什么时候跟是强制升级还是兼容并存出问题时怎么排查一个公共模块被十个项目引用线上出问题时的排查成本会成倍增长。这些问题不提前想清楚共用代码处理就很容易从“技术优化”变成“新的技术债来源”。这也是我写这一期内容的初衷把共用代码从“临时抽取”变成“体系化运作”。2. 共用代码处理的四个演进阶段共用代码处理不是一蹴而就的。一个团队往往要经历从“物理复制”到“统一基建”的演进过程。了解这个过程有助于你判断自己团队目前处于哪个阶段下一步该往哪走。2.1 第一阶段复制粘贴这是最原始、也最常见的阶段。项目 A 写了一个formatDate函数项目 B 要用顺手复制一份项目 C 要用再复制一份。特征没有统一的目录没有统一的包名代码出现多处副本修改时需要全局搜索逐个替换。这个阶段的优点只有一个快。当时当刻确实节省了开发成本。但隐患非常大改一处、忘三处是迟早的事。即使没有需求变更不同项目在复制后往往会进行局部改造这就导致同名函数在不同项目里的行为开始分叉。2.2 第二阶段公共工具类/公共函数当复制粘贴带来的维护成本逐渐暴露后团队会本能地开始“抽取”。常见做法是在项目内部或团队内部建立一个common、utils或utils.js文件夹把常用的工具函数集中放进去。特征开始有“沉淀”意识公共代码集中在某个目录但仍局限在单项目缺少版本概念基本是“复制整个文件”或“拉取最新代码”的方式在共享依赖关系不透明很难说清楚当前项目到底用了工具库的哪个版本。这个阶段比复制粘贴前进了一步但依然有局限。尤其是当一个公共工具目录被多个项目用“文件复制”的方式引入时本质上从“函数级别复制”变成了“文件级别复制”分叉问题并没有根除。2.3 第三阶段公共组件与公共库当团队意识到跨项目共享代码必须有清晰的边界和版本后就会走向第三个阶段把稳定的公共代码做成独立的包通过包管理工具分发。前端常见方案是发布 npm 包配合私有 npm 源如 Verdaccio、Nexus来管理后端常见方案是制作公共 jar 包或 Spring Boot Starter通过 Maven 私服如 Nexus、Artifactory分发。特征有独立的代码仓库有版本号支持语义化版本管理业务项目通过依赖声明引入可以设置发布权限由专门的同学或 CI 流程统一发布。这个阶段是当前中小团队中最推荐的形态。它解决了“复制粘贴导致分叉”的问题也让公共代码的变更可以被追溯、被回滚。2.4 第四阶段多包仓库与统一基建当公共代码数量变多团队开始追求更高效的协同效率时会进一步走向多包仓库Monorepo模式或者建设统一的前端工程化平台。在 Monorepo 里多个公共包和多个业务项目可以放在同一个仓库中通过 pnpm workspace、lerna、turborepo 等工具统一管理依赖和构建。公共包之间的版本关联更直观改动公共代码时可以同时看到下游使用方的代码。特征统一仓库、统一版本策略依赖关系可视化本地联调成本低对工程化能力和团队规范要求较高。这个阶段适合公共包数量多、业务项目之间关联紧密、团队对工程化有足够掌控力的情况。如果团队人数不多强行上 Monorepo 反而可能因为构建复杂度提升而拖慢效率。3. 动工之前什么样的代码适合抽成共用代码很多共用代码处理失败不是因为抽取技术不行而是抽取前没有做判断。下面是我认为比较实用的判断标准。3.1 适合抽取的几类代码适合抽取的代码通常具备“通用、稳定、边界清晰”三个特征。第一类是纯函数工具。比如日期格式化、金额转大写、身份证号校验、手机号脱敏。这类代码不依赖业务状态输入输出确定适合作为公共函数沉淀。第二类是协议与类型定义。比如前后端约定的接口返回结构{ code, message, data }、错误码枚举、TypeScript 的接口类型。同一份类型在两个端同时引用比各写各的更不容易出现契约漂移。第三类是横切逻辑。比如统一的登录态校验、统一的请求拦截器、统一的埋点上报、统一的异常处理。这类逻辑通常与具体业务无关但所有项目都需要。第四类是稳定的 UI 组件。比如基础按钮、弹窗、表格、分页器。如果团队内多个项目使用同一套设计规范这类组件非常适合沉淀为公共组件库。3.2 不适合抽取的几类代码与直觉相反并不是所有“被重复使用”的代码都适合抽取。第一类是仍在频繁变更的业务规则。比如某个营销活动的计算逻辑当前订单体系在用未来库存体系可能也要用。这种逻辑业务属性强且处于高频迭代期过早抽取会让公共包频繁发版维护成本极高。第二类是依赖大量业务上下文的代码。如果一段代码内部要调用当前项目的用户信息、权限状态、数据库访问对象那么把它抽出去反而会破坏封装性让公共模块反向依赖业务模块。第三类是暂时没有维护资源的代码。公共代码是需要持续维护的。如果一个模块抽出来之后没有同学愿意负责升级、修 bug、处理兼容性问题那它在公共包里的状态就是“僵尸模块”风险比留在业务项目里更大。3.3 抽取前先回答的三个问题在准备抽取一个共用模块之前建议先回答下面三个问题。第一这段代码是否至少会被两处以上使用只有一处使用时抽取它属于过度设计。第二这段代码的变更频率如何如果是业务热点逻辑可以延后抽取如果是基础工具、协议类代码则越早沉淀越好。第三谁来维护至少在团队里确认一个 owner负责人或者约定公共代码由核心成员共同评审维护。回答完这三个问题再决定是按“项目内公共目录”处理还是升级为“独立公共包”思路会清晰很多。4. 完整实战抽一个公共模块并发布使用理论讲完下面进入实操。这一节我会以一个前端项目为例演示如何把一个“多个管理后台都在用的请求封装与返回结构处理”抽取成独立 npm 包并发布到私有源。后端场景我会在章节末尾补充对应思路。4.1 业务背景与抽取目标假设团队里有三个管理后台项目订单后台、用户后台、商品后台。它们都有下面这些重复代码请求前统一加 token请求返回后统一处理{ code, message, data }结构当code ! 0时统一弹出错误提示提供一个公共的日期格式化函数。我们要把这部分逻辑抽取为一个 npm 包包名暂定为company/common-utils。这样三个管理后台只需要安装这个依赖即可。4.2 创建公共模块项目首先创建一个干净的目录并初始化 npm 项目。mkdir common-utils cd common-utils npm init -y初始化后的package.json需要我们手动调整关键字段。下面是一个参考配置{ name: company/common-utils, version: 0.1.0, description: 公司内部公共工具库包含请求封装、返回结构处理、日期格式化等, main: src/index.js, files: [ src ], scripts: { test: node test/index.test.js }, publishConfig: { registry: http://npm.internal.company.com }, dependencies: { axios: ^1.6.0 } }这里要解释几个关键字段。main字段指定包的入口文件业务项目引入这个包时加载的就是这个文件。files字段表示发布时只包含src目录避免把测试、本地配置等无关文件带出去。publishConfig.registry指定发布到公司私有 npm 源。注意这里我把 axios 放在了dependencies因为请求封装依赖 axios。如果你希望业务项目自己提供 axios 实例也可以把 axios 放到peerDependencies让使用方显式安装避免多个版本冲突。4.3 编写核心代码接下来在项目里创建src目录并编写几个模块文件。common-utils/ ├── src/ │ ├── index.js │ ├── request.js │ ├── format.js │ └── response.js ├── package.json └── README.md先看src/format.js这里提供一个简单的日期格式化工具函数。// 文件路径src/format.js // 简要说明日期格式化工具统一处理时间展示格式 function padZero(value) { return String(value).padStart(2, 0); } /** * 将 Date 对象格式化为 yyyy-MM-dd HH:mm:ss * param {Date|string|number} input 日期、时间戳或可被 Date 解析的字符串 * returns {string} 格式化后的字符串 */ function formatDateTime(input) { const date input instanceof Date ? input : new Date(input); if (Number.isNaN(date.getTime())) { throw new Error(formatDateTime: 无法解析的日期参数); } const year date.getFullYear(); const month padZero(date.getMonth() 1); const day padZero(date.getDate()); const hour padZero(date.getHours()); const minute padZero(date.getMinutes()); const second padZero(date.getSeconds()); return ${year}-${month}-${day} ${hour}:${minute}:${second}; } module.exports { formatDateTime, };接着看src/request.js这里基于 axios 封装一个带 token、自动处理错误提示的请求实例。// 文件路径src/request.js // 简要说明基于 axios 的通用请求封装 const axios require(axios); // 这里的 getToken 通常从 localStorage、cookie 或状态管理中读取 function getToken() { return localStorage.getItem(access_token) || ; } const request axios.create({ timeout: 10000, baseURL: import.meta.env?.VITE_API_BASE_URL || /api, }); // 请求拦截器自动携带 token request.interceptors.request.use( (config) { const token getToken(); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, (error) Promise.reject(error) ); // 响应拦截器统一处理后端返回结构 request.interceptors.response.use( (response) { const res response.data; // 约定code 为 0 时表示业务成功 if (res.code 0) { return res.data; } // 业务失败时根据项目需要弹出提示 const message res.message || 请求失败; // 这里通常调用项目内的全局提示组件例如 antd 的 message.error console.error([common-utils] request business error: ${message}); return Promise.reject(new Error(message)); }, (error) { // 网络错误、超时、HTTP 状态码错误等在这里处理 console.error([common-utils] request network error:, error.message); return Promise.reject(error); } ); module.exports request;再看src/response.js这里负责生成统一返回结构方便业务侧在测试或 mock 时使用。// 文件路径src/response.js // 简要说明统一返回结构生成器 /** * 生成成功返回体 * param {*} data 业务数据 * param {string} message 提示信息 * returns {{code: number, message: string, data: *}} */ function success(data, message ok) { return { code: 0, message, data, }; } /** * 生成失败返回体 * param {string} message 错误信息 * param {number} code 业务错误码 * returns {{code: number, message: string, data: null}} */ function fail(message error, code -1) { return { code, message, data: null, }; } module.exports { success, fail, };最后把三个模块统一在src/index.js中导出。// 文件路径src/index.js // 简要说明公共模块统一入口 const request require(./request); const { formatDateTime } require(./format); const { success, fail } require(./response); module.exports { request, formatDateTime, success, fail, };到这里一个最小可用的公共模块核心代码就完成了。实际项目中你还可以在这个包里继续沉淀金额处理、字符串脱敏、URL 参数解析等工具函数但要注意保持模块的单一职责不要什么代码都往里塞。4.4 本地联调npm link在正式发布前我们需要在业务项目里进行本地联调验证公共模块是否可用。这时可以使用npm link。第一步在common-utils目录下执行npm link这条命令会把当前包链接到全局 node_modules 中相当于注册了一个全局软链接。第二步进入某个业务项目目录比如订单后台order-admin执行npm link company/common-utils这条命令会在order-admin/node_modules下创建一个指向common-utils目录的软链接。之后你在order-admin里写代码修改common-utils的源码可以即时生效不需要反复npm install。第三步在业务代码中引入并验证。// 文件路径order-admin/src/api/index.js const { request, formatDateTime, success } require(company/common-utils); // 请求示例 async function fetchOrderList(params) { return request.get(/order/list, { params }); } // 格式化订单创建时间 const createTime formatDateTime(1720000000000); console.log(createTime); // 生成 mock 成功响应 console.log(success({ list: [] }, ok));验证无误后记得解除本地链接npm unlink company/common-utils这样做的原因是本地链接只在当前开发环境有效如果忘记解除后续npm install可能会出现诡异的“明明装了依赖却找不到模块”的问题。4.5 发布到私有源本地联调通过后就可以发布到私有 npm 源了。发布前需要确认公司是否有私有 npm 源服务例如 Verdaccio 或 Nexus是否已经配置好.npmrc中的 registry 地址当前账号是否有发布权限。确认后执行npm publish如果包名带了组织前缀company私有源通常要求配置组织归属。这一般在私有源管理后台或命令行按提示完成不同产品略有差异。关于私有源的选型这里简单说明一下。Verdaccio 是轻量级的私有 npm 方案部署简单适合中小团队Nexus 功能更全面可以同时管理 npm、Maven、Docker 等多种仓库适合已有 Java 技术栈、希望统一管理各种制品的团队。发布成功后业务项目从私有源正常安装即可npm install company/common-utils0.1.04.6 在业务项目中接入安装完成后业务项目里的引入方式与本地联调时完全一致。需要特别注意的是依赖锁定。如果你是第一次安装这个包package.json里会多出类似下面的依赖记录dependencies: { company/common-utils: ^0.1.0 }这里的^0.1.0表示允许安装0.x.y中比0.1.0新、但小于0.2.0的版本。公共库的语义化版本管理是否规范直接决定了这个范围写法是否安全。这一点我会在下一章详细展开。后端项目对应思路如果你的团队以 Java 为主可以把公共模块打成 jar 包发布到 Maven 私服然后通过 Maven 在多个服务中引入。Spring Boot 项目还可以制作公共 Starter把自动配置、自定义注解、统一异常处理等能力封装进去业务服务只依赖一个 starter 坐标即可。5. 共用代码的版本管理与迭代策略公共代码的版本管理是共用代码处理中最容易被忽略、又最容易出问题的环节。5.1 语义化版本规范我强烈建议公共包统一采用语义化版本Semantic Versioning格式为主版本号.次版本号.修订号。主版本号不兼容的 API 变更时递增例如修改了函数签名、删除了某个导出次版本号向后兼容的功能新增时递增例如新增了一个工具函数修订号向后兼容的缺陷修复时递增例如修复了某个函数的边界 bug。在升级公共包时业务项目只需要关注主版本号变化即可。如果公共库严格遵守这个规范业务项目在看到2.0.0时就能意识到“可能需要改代码”看到1.3.0或1.0.5时则可以更放心地升级。5.2 兼容性保障公共代码最大的风险是影响面不可控。一个工具函数被上千个调用点引用时有 bug修复带来的收益很大但引入的新问题也可能伤及很多调用方。因此给共用代码写测试不是可选项而是必须项。公共包的测试用例至少应覆盖正常输入下的输出是否符合预期边界输入例如 null、undefined、空字符串、超大数字异常输入时的报错行为是否合理。举个例子formatDateTime这个函数如果传入一个非法字符串我的实现是抛出异常。这个行为就需要测试用例明确声明后续维护者才不会误改。5.3 升级节奏与灰度发布公共包发布后不建议让所有业务项目在同一时间强制升级。比较稳妥的做法是先在内部验证公共包负责人用新版本跑一遍核心示例或内部项目选择一两个非核心业务项目作为试点升级观察日志与线上表现试点稳定后再逐步推进其余项目升级。同时变更内容一定要写清楚。我建议公共包的 CHANGELOG 至少包含变更类型新增、修复、破坏性变更变更内容描述涉及影响的模块或函数迁移指引如果是破坏性变更。这样其他项目的同学升级时不需要翻源码去猜“这个版本改了什么”。6. 常见问题与排查思路共用代码处理过程中总有一些高频问题。下面整理成表格方便快速查阅。问题现象常见原因解决思路业务项目更新公共依赖后行为没有变化lock 文件锁住了旧版本或构建缓存未清理检查 package-lock.json / pnpm-lock.yaml刷新依赖清理构建缓存本地 npm link 调试正常发布后业务项目引入报错main入口配置错误或files没有包含实际要发布的文件核对 package.json 的 main、files 字段确认发布包内容公共模块与业务项目的某些依赖版本冲突公共模块将依赖放在dependencies与业务项目版本不一致根据场景将依赖改为peerDependencies或统一私有源中的依赖版本多个业务项目使用不同版本的公共代码行为不一致版本策略松散缺少统一升级窗口明确语义化版本规范指定公共代码 owner按计划统一升级公共代码出现循环依赖公共模块反向依赖了业务模块或模块之间互相引用检查依赖方向拆细粒度或合并模块禁止公共模块反向调用业务代码公共包发布后业务项目无法从私有源拉取私有源权限未配置或包名未在私有源注册检查 .npmrc 配置、登录状态、私有源的项目权限设置下面再补充两个比较具体的排查场景。第一个是“更新依赖后功能没变化”。这个问题的根源往往是依赖锁定。比如业务项目package-lock.json中记录的版本是0.1.0你在公共包里发布了0.1.1但业务项目执行npm install时没有更新 lock 文件实际安装的还是旧版本。遇到这种情况可以先手动安装指定版本验证npm install company/common-utils0.1.1 --save-exact如果还不行再考虑清缓存npm cache clean --force rm -rf node_modules npm install第二个是公共模块出问题后的应急策略。公共代码的线上事故往往影响面大止血优先级高于定位原因。如果新版公共包引入严重问题最直接的手段是回退版本业务项目将依赖版本回退到上一个稳定版本重新部署即可。这就是语义化版本和 lock 文件的价值所在——每个版本都是可追溯的不会出现“代码回退不了”的情况。7. 最佳实践与工程建议这一节我想分享一些在真实项目中沉淀下来的工程经验希望能帮大家少走弯路。7.1 命名与边界公共包的命名要与业务解耦。比如company/common-utils就比company/order-utils合适前者表达的是“公司级通用工具”后者会让未来其他业务项目引入时产生心理门槛。模块边界要克制。一个公共包不要既做请求封装又做 UI 组件还做埋点统计。否则每次发布即使只改一个函数也会让使用方承担不必要的升级风险。宁可多维护两个小包也不要维护一个大杂烩包。7.2 代码审查与测试公共代码的审查标准应该高于业务代码。建议要求所有公共函数必须有 JSDoc 或清晰注释所有导出函数必须有测试用例覆盖破坏性变更必须在 CHANGELOG 中显著标注发布前至少有一次代码 review。如果团队里还没有测试基础哪怕先用 console 写简单断言也比完全没有测试好。后续再逐步引入 Jest、Vitest 或其他测试框架。7.3 文档与示例沉淀公共代码如果缺少文档使用方就会产生不确定性最终要么不敢用要么用错。我建议每个公共包都有一份简洁的 README包含包的定位和适用场景安装方式一个最小可运行示例所有导出函数的说明和参数表常见问题与故障排查。文档不用很长但一定要让新同学照着示例就能用起来。这也是一种降低沟通成本的方式。7.4 权限管理与安全边界公共代码的发布权限应当收敛不能所有人都能往私有源发布版本。比较稳妥的做法是只有公共代码的 owner 或 CI 流程具备发布权限发布前由 CI 自动执行测试测试通过才允许发布涉及 token、密钥逻辑的公共代码不要在仓库中提交任何真实凭据公共代码中处理敏感数据时遵守最小权限原则只在真正需要的地方读取和输出。尤其要注意公共包一旦被多个项目引用任何安全隐患都会被放大。发布前多检查一遍依赖来源、权限配置和敏感信息是值得的。7.5 避免过度复用最后一条也是我觉得最难的一条避免过度复用。有些开发者容易走向另一个极端——把所有看起来像的代码全部抽到公共包里结果公共包的 API 越做越复杂兼容逻辑越堆越多最终公共包本身变成了一个没有人能轻松维护的巨型模块。建议掌握这样一个原则公共代码要“重复出现两次再抽象出现三次再沉淀”。第一次使用先踏实写业务代码第二次使用可以考虑提取第三次使用才值得投入资源做成规范化的公共包。过早抽取和过晚抽取都是成本关键是在合适的时机动手。写在最后共用代码处理表面上是技术问题本质上是工程治理问题。复制一次是捷径复制两次是隐患复制三次就是实打实的技术债。很多团队在早期为了赶进度欠下的代码重复债往往会在业务的快速增长期集中爆发。如果你所在的项目正在经历“多处复制、不敢修改”的阶段我建议你从下面这一步开始先找一份在三个以上位置出现过的重复代码把它抽取成独立模块在本项目内统一引用再考虑是否升级为跨项目的公共包。不要一开始就追求 Monorepo、统一基建这类大架构先把“小循环”跑通再逐步扩大治理范围。这一期聊的是共用代码处理的基本思路和落地流程。后续我还会继续这个系列讲更深入的依赖管理与私有仓库搭建、Monorepo 实践、公共组件库建设等话题。如果你在实际处理共用代码时踩过什么坑也欢迎在评论区一起交流。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门