前言
在我们项目开发时,使用的第三方依赖难免会存在问题,这时候给包作者反馈问题或提 PR 不一定能及时被修复,而且我们有可能需要基于它做定制,就需要维护一个改动的版本。
第一种方式是 fork 依赖包的源码仓库,自己修改完新发布一个 npm 包,改用我们自己发的包,这种方式适合需要定制或大量改动时采用;
另一种方式直接修改 node_modules 下的依赖包代码,但 node_modules 下的改动会被 git 忽略,需要将改动保存起来,可以使用 patch-package 这个包,或者包管理器的 cli 命令 yarn patch ( yarn v2+ )、pnpm patch ,这种方式适合小改动;
本文将学习如何保存和应用 node_modules 下的代码改动,并通过调试源码了解 patch-package
原理。
patch-package 使用
命令格式:npx patch-package <package-name>
PS:
npx
命令可以让我们不用主动安装包就可以执行相应命令,它会先从本地项目依赖中判断是否已安装该包,未安装则会从远程拉取到 npm 缓存目录中,然后添加到系统环境变量 PATH 中。
例如对 node_modules 下的包 vue-countup-v3
做了修改,将 duration
prop 默认值改为 5,如下图:
- 在终端执行
npx patch-package vue-countup-v3
,会创建一个 patches 目录,里面是改动的包文件,文件名格式包名+版本号.patch
,文件内容就是改动的前后内容,是不是跟 git diff 结果一样,其实就是依赖 git diff 实现的。 - 当别人拉取项目需要应用改动时可以运行
npx patch-package
,或者可以配到 scripts 中的 postinstall,这样每次安装完依赖就会自动执行该命令(需要在项目中安装下该包)。
包管理器 patch
命令使用
如果使用 yarn(v2+) 或者 pnpm 作为包管理器,则可以直接使用 patch
命令来保存改动,而不用 patch-package
工具。
pnpm patch 使用示例(yarn patch 用法类似),假如我们要改动包 vue-countup-v3
:
- 在项目根目录终端运行
pnpm patch vue-countup-v3
,会创建一个临时目录,进到临时目录去修改包。duration
prop 改为 5 - 修改完之后,运行
pnpm patch-commit <path>
(path 就是临时路径),会创建一个 patches 目录,里面是改动的包文件,文件名格式包名+版本号.patch
(跟 patch-package 一样),并且还会在 package.json 中新增一个字段 "patchedDependencies"。 当别人拉取项目执行pnpm install
就会应用改动。当需要删除改动恢复原样,可以执行pnpm patch-remove vue-countup-v3@1.4.0
(需要指定版本号)。
patch-package 原理
调试源码准备
-
克隆仓库源码
git clone https://github.com/ds300/patch-package.git
,并安装依赖yarn install
; -
从 package.json 的
bin
字段可以看到patch-package
命令的入口是 index.js 文件,而 index.js 中引入了 dist/index.js 文件,因此我们需要先执行构建命令
yarn build
来生成 dist;从生成的产物中,可以看到每个文件是带有 sourcemap 的,以 base64 内联方式在文件末尾,是通过 tsconfig.json 的
inlineSourceMap
配置生成的。 -
添加调试配置;在 launch.json 中添加一个 type 为 node 的调试配置,
program
设置为 dist/index.js (即patch-package
命令的入口文件),通过args
传参(即填写改动过的包名),这里我随便找了一个包 ajv,往它里面新增了一个 add.js 文件;json// launch.json { "version": "0.2.0", "configurations": [ { "name": "调试 patch-package 生成 patch 文件", "program": "${workspaceFolder}/dist/index.js", "request": "launch", "skipFiles": ["<node_internals>/**"], "console": "integratedTerminal", "args": ["ajv"], "type": "node" } ] }
-
启动调试;源码的入口文件是 src/index.ts,我们可以通过执行命令时控制台打印的信息来找查打断点的位置。
要了解 patch-package 的实现原理可以从使用方面入手,当执行 npx patch-package <package-name>
用于保存改动生成 patch 文件;执行 npx patch-package
可以应用改动到 node_modules 中的相应包。
保存改动生成 patch 文件
执行 npx patch-package <package-name>
用于保存改动生成 patch 文件;
从控制台打印的信息可以看到大致步骤:
- 创建一个临时目录;
- 安装运行
patch-package
命令时指定的包; - git diff 包改动前后的内容;
- 创建相应的 patch 文件。
主要看 makePatch 函数,进到该函数中看具体实现;
我们看主要流程的每一个步骤,其他的一些高级功能和选项可以忽略;
-
创建一个临时目录;通过第三方包 tmp 的
dirSync
方法同步创建了一个临时目录,然后创建一个 package.json 文件; -
安装执行
patch-package
命令时指定的包;推断项目的包管理器(yarn 或 npm,不支持 pnpm),然后在临时目录中执行依赖安装命令; -
git diff 包前后改动的内容;先执行 git
init
、add
、commit
,生成一次 commit;然后把当前项目 node_modules 下对应包文件复制到临时目录的 node_modules 中,再执行 git add,然后执行 git diff 得到改动内容;
-
创建相应的 patches 目录及 patch 文件,将 git diff 的结果写入到 patch 文件中。
patch 文件应用改动到相应包
执行 npx patch-package
可以应用改动到 node_modules 中的相应包。
再添加一个调试配置用于调试应用改动
json
// launch.json
{
"name": "调试 patch-package 应用改动",
"program": "${workspaceFolder}/dist/index.js",
"request": "launch",
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal",
"type": "node"
}
从打印出的信息找到执行的函数是 applyPatchesForApp,进到该函数中看具体实现;
通过调试的 step into 可以看到函数的调用过程
发现在 readPatch 函数中会读取生成的 patch 文件,然后传递给 parsePatchFile
函数
parsePatchFile
中会对 patch 文件内容一行一行进行判断,根据 diff 内容判断是什么操作进行分类处理,生成一个包含 diff 信息的对象。
然后执行 executeEffects 函数,在其中处理 parse 得到的 diff 信息对象进行处理;
在 executeEffects 中,会根据不同的类型进行不同的处理,例如改动后删除了文件,那么 type 就是 "file deletion"
,就会通过 fs.unlinkSync 删除文件;这样就把 patches 文件里的改动内容应用到相应的包里面。
总结
当需要保存对安装的依赖包代码改动时,可以通过 patch-package 这个工具,执行 npx patch-package <package-name>
来保存改动生成 patch 文件,执行 npx patch-package
来应用改动;它的实现原理可以从使用角度通过调试源码来理解:
保存改动生成 patch 文件:首先会创建一个临时目录及 package.json 文件,执行依赖安装,再初始化一个 git 仓库,先提交一个 commit,然后把改动的包代码复制到临时目录,执行 git diff 得到改动内容,保存到 patch 文件中;
patch 文件应用改动到相应包:首先会去读取 patch 文件,一行行解析文件内容,根据不同的改动类型将解析结果分类,然后再根据不同类型做不同的文件操作。
除了使用 patch-package,也可以使用包管理器的 cli 命令 yarn patch
( yarn v2+ ) 和 pnpm patch
。