日常团队协作过程中,由于现状不得不使用相对滞后版本的依赖包,但又期望使用新版本的某些功能…
一、背景
通常情况下,遇到需要支持的底层功能,开发人员直接升级依赖包版本即可,但总有意外。比如:
- 日常团队协作开发不同于个人独立开发,因为历史原因,不得不使用某个依赖包相对滞后的某个版本,但又需要新版本依赖包的某些功能。
- 新版本包存在未知的bug,但你的团队期望使用新版本中的某些已验证功能。
- 依赖包已经更新到最新版本,但存在官方未修复的bug。
构建补丁(patch) 通常用于修复特定包中的问题,而不需要等待上游包发布新的版本。
二、patch-package打补丁
以下是一个典型的步骤:
1.生成补丁
I. 安装目标package & patch-package
# 安装目标package
$ pnpm add <package-name> # pnpm 安装需修补的包
$ pnpm list <package-name> # pnpm 查看包的安装位置
$ npm install <package-name> # npm 安装需修补的包
# 安装 patch-package 工具,以便管理和应用补丁
$ npm i patch-package --save-dev
II. 修改npm包
以本次需求为例,需要打补丁的包是@vant/cli 5.1.0
,无论是新老版本都没有支持我需要的功能。所以,
- 打开目标项目工程(A)/node_modules文件夹,确认版本是5.1.0
- 稳妥起见,打开一个废弃的前端工程(B),安装
@vant/cli 5.1.0
(我这里偷懒使用的项目工程),打开node_modules/@vant/cli
目录- 场景1: 将你期望的新版本对应功能,复制粘贴到当前工程(B)对应
node_modules/xxx
目录文件内 - 场景2: 自行修复bug或diy你期望的功能,到当前工程(B)
node_modules/xxx
对应目录文件内
- 场景1: 将你期望的新版本对应功能,复制粘贴到当前工程(B)对应
npm run dev
启动工程(B),测试功能是否满足期望。
III. 生成补丁文件
功能符合预期,此时cd到工程(B)根木录下,执行如下命令生成补丁文件:
$ npx patch-package <package-name> # 示例: npx patch-package @vant/cli
终端可看到执行记录如下:
$ npx patch-package @vant/cli
patch-package 6.4.7
• Creating temporary folder
• Installing @vant/cli@5.1.0 with npm
• Diffing your files with clean files
✔ Created file patches/@vant+cli+5.1.0.patch
💡 @vant/cli is on GitHub! To draft an issue based on your patch run
npx patch-package @vant/cli --create-issue
最终在工程(B)根目录下,生成工程(B)/patches/@vant+cli+5.1.0.patch
补丁文件,将它提交到git中。
补丁原理也不复杂,其实就是一些git diff
记录描述:patch-package
会将当前node_modules
下的源码与原始源码进行git diff
,并在项目根目录下生成一个patch
补丁文件。以我当前的为例:
diff --git a/node_modules/@vant/cli/site/index.html b/node_modules/@vant/cli/site/index.html
index 5764eed..e32fb9e 100644
--- a/node_modules/@vant/cli/site/index.html
+++ b/node_modules/@vant/cli/site/index.html
@@ -33,5 +33,8 @@
<body>
<div id="app"></div>
<script type="module" src="/desktop/main.js"></script>
+ <script>
+ window.localStorage.setItem("vantTheme", "light")
+ </script>
</body>
</html>
diff --git a/node_modules/@vant/cli/site/mobile.html b/node_modules/@vant/cli/site/mobile.html
index e5ff77b..084a028 100644
--- a/node_modules/@vant/cli/site/mobile.html
+++ b/node_modules/@vant/cli/site/mobile.html
@@ -42,5 +42,8 @@
<body>
<div id="app"></div>
<script type="module" src="/mobile/main.js"></script>
+ <script>
+ window.localStorage.setItem("vantTheme", "light")
+ </script>
</body>
</html>
2.执行补丁
基于以上操作,切回到目标项目工程(A),在package.json中添加命令如下:
- 安装patch-package工具
# 安装patch-package工具,以便管理和应用补丁 $ npm install --save-dev patch-package
- 创建补丁文件夹:在项目根目录下新建一个名为
patches
的文件夹,用于存放补丁文件 - 切回到目标项目工程(A),在
package.json
中添加命令{ "scripts": { "postinstall": "patch-package" // 会在包安装完成后执行 } }
- 执行
npm install
命令:postinstall
脚本确保每次执行pnpm install
或npm install
安装依赖后,自动应用存放在patches
文件夹中的补丁$ npm i install 依赖包 ... > @zto/mzui@3.1.1-beta.1 patch > patch-package patch-package 6.4.7 Applying patches... @vant/cli@5.1.0 ✔ ... patch-package finished.
- 验证补丁效果:重新运行项目,检查补丁是否成功应用,并确保业务运行正常
三、其他方式:修复特定问题
想要单纯修改npm
包还有其他方式
1.对于单文件
- 拷贝覆盖法
- 修改引用法
利用postinstall
勾子,执行cp 修改过的文件 ./node_modules/包名/原始文件
,最终node_modules
下的文件被覆盖成修改后的文件:
{
"scripts": {
// 即每次install包后用修改后文件覆盖原始文件
"postinstall": "cp ./patches/vant-cli/* ./node_modules/vant/cli/site"
}
}
配置一个webpack alias
别名,如**‘原始文件的引用路径’: ‘修改后文件的引用路径’**,使得最终修改后的文件被引用,如:
// vite 原理类似
resolve: {
alias: {
'antd/upload': path.resolve(__dirname, './patched/upload/*'),
}
}
2.多个文件/整体项目
- 直接使用完成的源码,不再通过npm包方式引用
- 修改后的源码,发布到私有的npm仓库上,供项目使用
四、补丁管理问题
patch-package
优势: 使用git diff来记录补丁,比重写一份源码的方法更节省空间、安全和便捷patch-package
可在日常开发中优雅的解决鱼和熊掌不可兼得的难题。但问题出现时,最好还是从官方渠道寻求解决方案,如提issue并关注版本更新和bug修复情况,以便及时更新或者移除补丁
1.补丁包的日常管理和自动化管理
补丁包的日常管理: 一定要遵循 及时移除补丁 的原则,一旦官方package
发布了修复版本,请立即移除旧的补丁,避免可能的未知影响。
如果你想实现自动化管理,请参考 cloud_notes 补丁的管理
2.首次构建失败
新增补丁在 “构建平台"首次构建时 可能会面临构建失败,可清除工作目录后重新尝试构建
3.某个package
,存在多个版本的补丁包文件时,如何patch?
实际场景,描述如下:
- 如果存在
@lianpf/umi
的多个版本补丁包:如@lianpf+umi+1.6.0.patch
、@lianpf+umi+1.5.3.patch
、@lianpf+umi+1.5.0.patch
等 - 且以上补丁包都位于项目的
patches
目录下
那么在执行 "postinstall": "patch-package"
时,patch-package
将如何处理这些补丁包?
以上例子中,假设项目中安装 @lianpf/umi
的某个版本(例如 1.5.3
),则 结论如下:
patch-package
将只尝试应用@lianpf+umi+1.5.3.patch
。即只会针对已安装版本,尝试对应版本的补丁。- 如果不存在
1.5.3
版本的补丁,则不会应用任何补丁
所以,要 确保补丁文件与已安装的包版本相匹配 ,以避免潜在的错误和冲突。
补丁包匹配的原理和详细步骤如下(若不关注,则可直接忽略):
1.补丁扫描:patch-package
会扫描patches
目录下的所有.patch
文件,并识别出每个补丁文件的目标包名和版本号
2.对每个补丁文件,进行版本匹配:patch-package
会检查node_modules
中是否存在与【补丁文件中指定的版本号】相匹配的【包】
3.补丁应用:
- 找到与补丁文件中指定的版本号相匹配的包:
patch-package
会尝试将该补丁应用到该包上 - 没有找到完全匹配的版本号,但找到了一个可以“向下兼容”的较低版本:(例如,安装了
@lianpf/umi@1.6.0
但存在@lianpf+umi+1.5.3.patch
),patch-package
通常不会自动应用该补丁(因为补丁是为特定版本设计的,并且可能包含仅适用于该版本的更改)。但是,如果项目配置或patch-package
的某个特定版本允许这种“宽松”的匹配(这在标准行为中是不常见的),则可能会尝试应用补丁,但结果可能是不确定的。
4.处理多个补丁:
- 如果存在某个包的多个版本补丁文件,并且项目中安装了与这些版本之一相匹配的包,则
patch-package
将只尝试应用与已安装版本相匹配的补丁。
5.错误和警告:
- 指出补丁未被应用: 尝试应用一个与任何已安装包版本都不匹配的补丁,
patch-package
可能会记录一个错误或警告,指出补丁未被应用。 - 错误记录: 尝试应用补丁时遇到任何问题(如文件冲突、语法错误等),
patch-package
会记录相应的错误或警告。
五、常见问题
1.patch-package 在 pnpm 环境下无法正常工作
我来详细解释 “patch-package 在 pnpm 环境下无法正常工作” 的原因:
核心原因分析:
- pnpm 的依赖管理机制
- patch-package 的工作原理
i. pnpm 的依赖管理机制:pnpm 使用符号链接(symlink)和硬链接:
- pnpm 将所有依赖存储在全局的
.pnpm
目录中 node_modules
中的包实际上是指向.pnpm
目录的符号链接- 这与 npm/yarn 的平铺式
node_modules
结构完全不同
# npm/yarn 结构
node_modules/
├── @vant/cli/ # 真实目录
│ ├── lib/
│ └── package.json
# pnpm 结构
node_modules/
├── @vant/cli/ # 符号链接 -> .pnpm/@vant+cli@5.1.0/node_modules/@vant/cli
└── .pnpm/
└── @vant+cli@5.1.0/
└── node_modules/
└── @vant/cli/ # 真实目录
- ii. patch-package 的工作原理
patch-package 期望的工作流程:
- i.读取
node_modules
中的包 - ii.计算文件差异
- iii.生成补丁文件
- iv.应用补丁时直接修改
node_modules
中的文件
冲突点:
问题1:路径解析错误
// patch-package 期望的路径
node_modules/@vant/cli/lib/compiler/gen-package-entry.js
// pnpm 实际路径
.pnpm/@vant+cli@5.1.0/node_modules/@vant/cli/lib/compiler/gen-package-entry.js
问题2:依赖关系解析失败 从错误信息可以看出:
TypeError: Cannot read properties of undefined (reading 'dependencies')
at Object.getPackageResolution
这是因为 patch-package 尝试读取 package.json
中的 dependencies
字段,但在 pnpm 的符号链接结构中,依赖关系的解析逻辑不同。
问题3:文件锁定和权限
- pnpm 的
.pnpm
目录通常是只读的 - 符号链接可能导致文件修改权限问题
具体的错误场景
实际的错误:
mobile-ui $ ls -la node_modules/@vant/cli
lrwxr-xr-x@ 1 lianpf staff 137 7 15 14:07 node_modules/@vant/cli -> ../.pnpm/@vant+cli@5.1.0_@algolia+client-search@5.32.0_
@types+markdown-it@14.1.2_search-insights@2.17.3_vue@3.2.27/node_modules/@vant/cli
lianpf@lianpfdeMac-Pro mzui %
以上的输出就是问题所在。node_modules/@vant/cli
是一个符号链接,指向 .pnpm
目录中的真实文件。
mobile-ui $ ls -la node_modules/.pnpm/@vant+cli@5.1.0*/node_modules/@vant/cli/lib/compiler/gen-package-entry.js
-rw-r--r--@ 1 lianpf staff 2202 3 15 2023 node_modules/.pnpm/@vant+cli@5.1.0_@algolia+client-search@5.32.0_@types+markdown-it@14.1.2_search-insights@2.17.3_vue@3.2.27/node_modules/@vant/cli/lib/compiler/gen-package-entry.js
根本原因:
- pnpm 的符号链接结构与 patch-package 的预期不符
- 依赖解析机制的差异导致路径和权限问题
解决方案:
- 方案A:使用 .npmrc 配置
# .npmrc shamefully-hoist=true # 让 pnpm 行为更像 npm。但失去了 pnpm 的优势,可能导致其他问题
- 方案B:使用 pnpm patch —— 适用于
pnpm
新项目# pnpm 原生支持。但是需要 pnpm 6.24.0+,语法与 patch-package 不兼容 $ pnpm patch @vant/cli
- 方案C:自定义方案 + pnpm patch 【最终】 —— 适用于
npm迁移到pnpm
的项目// 注意 📢:该方案兼容性好,可控性强。但需要维护自定义脚本 // 1.现有patch包:使用系统 patch(自定义脚本) patch -p1 -i patches/@vant+cli+5.1.0.patch // 2.新的修改:手动修改特定文件,构建pnpm patch包
【方案C】完整内容,请参考:《npm 到 pnpm:补丁包(patch)迁移》
参考
最后, 希望大家早日实现:成为编程高手的伟大梦想!
欢迎交流~

本文版权归原作者曜灵所有!未经允许,严禁转载!对非法转载者, 原作者保留采用法律手段追究的权利!
若需转载,请联系微信公众号:连先生有猫病,可获取作者联系方式!