文档
dedupe
命令(npm/pnpm 通用)
作用:整理依赖树,合并重复依赖包,减少项目体积
用法:
bash
npm dedupe # npm 整理依赖 pnpm dedupe # pnpm 整理依赖(等价于 pnpm prune)
原理:分析
node_modules
中的重复包,通过符号链接或版本兼容策略合并,优化依赖结构
node版本工具:nvm
nvm ls
nvm use <20.x.x>
registry管理工具: nrm
nrm ls
nrm use <xxx>
workspace
总结对比
特性 | npm | pnpm |
---|---|---|
磁盘空间 | 包重复存储,占用更多空间 | 使用符号链接,减少重复存储 |
依赖安装速度 | 安装速度较慢,特别是大项目 | 更快,减少磁盘读取和文件复制 |
依赖管理 | 松散的依赖树结构,允许多版本 | 更严格的依赖管理,避免版本冲突 |
工作区支持 | 支持工作区,但性能可能较差 | 高效的工作区支持,性能优越 |
缓存机制 | 不共享缓存,重复下载依赖 | 使用全局缓存,避免重复下载 |
去重机制 | 去重较弱,可能有重复的包 | 更强的去重机制,避免重复包 |
CLI 命令 | 与 pnpm 类似,已熟悉 | 与 npm 类似,易于迁移 |
pnpm中硬链接和软链接
- 硬链接:主要用于 不同项目之间共享全局缓存中的依赖文件。避免多个项目重复存储相同版本的依赖包,节省磁盘空间。多个项目通过硬链接指向全局缓存中的同一个包文件,减少存储开销。
- 软链接:主要用于 在项目内部构建
node_modules
目录结构。它们帮助pnpm
实现依赖的正确解析,特别是处理嵌套依赖和实现扁平化结构,使得项目能够正确找到和使用依赖。
pnpm
依赖关系
<u>**a**</u>(`node_modules\.pnpm\bcryptjs@2.4.3\node_modules\bcryptjs\package.json`)
<u>**b**</u>(`node_modules\bcryptjs\package.json`)(符号链接)关系
项目package.json中所有包都在node_modules下类似 **b** 包,
包存储地址是通过软链接(符号链接)到 .pnpm下的 **a** 包,物理地址是 **a** 包硬链接的磁盘地址


一个包的不同版本是如何处理的:

包中的依赖是如何处理的



npm install & npm ci
npm install
和 npm ci
是 npm 中用于管理项目依赖的核心命令,二者在功能、使用场景等方面存在差异,具体如下:
1. npm install
(常用缩写 npm i
)
功能: 依据项目根目录的
package.json
安装依赖,若存在package-lock.json
或npm-shrinkwrap.json
,会尽量按锁文件记录的版本安装;若没有锁文件,会下载满足package.json
语义化版本范围(如^1.2.3
)的最新兼容版本 。 还可用于添加、更新、删除依赖(如npm install <包名>
新增依赖,npm install <包名>@<版本>
更新依赖 ),执行时可能更新package-lock.json
以同步实际安装的版本。使用场景: 开发过程中频繁使用,比如新项目初始化(首次拉取代码后安装所有依赖 )、添加新依赖(如引入
lodash
库 )、更新已有依赖版本(如把axios
从0.21.0
更到1.0.0
)等场景。示例:
shell# 安装 package.json 中所有依赖 npm install # 安装 lodash 包并添加到 dependencies npm install lodash # 安装指定版本的 axios 包 npm install axios@1.0.0
2. npm ci
功能: 专为自动化环境(如持续集成 CI、测试平台、部署流程 )设计,严格依据
package-lock.json
或npm-shrinkwrap.json
安装依赖。若package.json
里的依赖版本和锁文件不匹配,会直接报错退出,不会尝试更新锁文件;安装前会自动删除现有node_modules
目录,保证安装的依赖干净、一致;且无法用于添加单个依赖,安装过程“冻结”,不会修改package.json
和锁文件 。使用场景: 用于对依赖一致性要求极高的场景,像 CI/CD 流水线(保证每次构建使用相同版本依赖,避免因依赖差异导致构建/测试失败 )、生产环境部署(确保线上运行依赖与测试验证过的版本完全一致 )等。
示例:
shell# 基于锁文件安装所有依赖,安装前会删除现有 node_modules npm ci
3. 核心区别总结
对比维度 | npm install | npm ci |
---|---|---|
依赖安装依据 | 优先参考锁文件,无锁文件则按 package.json 语义化版本装 | 严格依据锁文件(package-lock.json /npm-shrinkwrap.json )安装 |
锁文件修改 | 可能更新锁文件(同步实际安装的依赖版本 ) | 永远不会修改锁文件和 package.json |
使用灵活性 | 可安装单个依赖、更新依赖等 | 只能安装整个项目依赖,无法添加/更新单个依赖 |
node_modules 处理 | 不会自动删除现有 node_modules | 安装前自动删除现有 node_modules |
版本冲突处理 | 尝试解决版本不匹配问题(可能更新锁文件 ) | 版本不匹配直接报错,不尝试解决 |
典型使用场景 | 开发时添加/更新依赖、新项目初始化等 | CI/CD 流程、生产环境部署等对依赖一致性要求高的场景 |
实际项目中,开发阶段常用 npm install
灵活管理依赖,而在自动化构建、部署等环节,为保障环境一致,会优先选用 npm ci
。
Git Hooks 触发机制详解:simple-git-hooks
如何工作
package.json文件配置:
"scripts": {
"lint": "eslint . --fix",
"prepare": "simple-git-hooks",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify",
"pre-commit": "pnpm typecheck && pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
},
当执行 git commit
时,simple-git-hooks
的触发过程是一个精心设计的自动化流程。下面我将详细解释这个机制:
完整触发流程
关键步骤详解
1. 安装阶段:prepare
脚本执行
当运行 pnpm install
或 npm install
时:
"scripts": {
"prepare": "simple-git-hooks"
}
simple-git-hooks
工具会:- 读取
package.json
中的"simple-git-hooks"
配置 - 在
.git/hooks
目录创建钩子文件:pre-commit
commit-msg
- 将配置的命令写入这些文件
- 读取
2. pre-commit 钩子文件内容(示例)
.git/hooks/pre-commit
文件内容:
#!/bin/sh
# Generated by simple-git-hooks
pnpm typecheck && pnpm lint-staged
3. commit-msg 钩子文件内容(示例)
.git/hooks/commit-msg
文件内容:
#!/bin/sh
# Generated by simple-git-hooks
pnpm sa git-commit-verify "$1"
4. 执行 git commit
时的流程
触发 pre-commit 阶段:
- Git 查找
.git/hooks/pre-commit
文件 - 执行文件中的命令:
pnpm typecheck && pnpm lint-staged
- 如果任一命令失败(返回非0状态码),提交中止
- Git 查找
执行 lint-staged:
json"lint-staged": { "*": "eslint --fix" }
- 对暂存区所有文件运行
eslint --fix
- 自动修复可修复的错误
- 如果有无法自动修复的错误,终止提交
- 对暂存区所有文件运行
触发 commit-msg 阶段:
- 当 pre-commit 通过后
- Git 查找
.git/hooks/commit-msg
文件 - 执行
pnpm sa git-commit-verify
并传递提交消息文件路径 - 验证提交消息格式(如 Conventional Commits)
- 如果验证失败,提交中止
技术要点总结
自动化安装:
prepare
脚本确保每次安装依赖时钩子配置都会更新
隔离执行环境:
- 钩子在独立的 shell 环境中执行
- 使用项目本地的 pnpm(通过
pnpm run
)
错误处理机制:
shell# pre-commit 实际执行逻辑 pnpm typecheck if [ $? -ne 0 ]; then echo "Type check failed!" exit 1 fi pnpm lint-staged if [ $? -ne 0 ]; then echo "Linting failed!" exit 1 fi
性能优化:
- 只对暂存文件(
git add
的文件)进行检查 - 避免全量检查提高速度
- 只对暂存文件(
实际开发中的行为
当开发者尝试提交:
$ git commit -m "feat: add new module"
控制台输出示例:
> Running pre-commit hook
$ pnpm typecheck
✔ No type errors found
$ pnpm lint-staged
✔ eslint --fix applied to 2 files
> Running commit-msg hook
$ pnpm sa git-commit-verify
✔ Commit message follows Conventional Commits
[main 3a8b7d2] feat: add new module
2 files changed, 45 insertions(+)
如果验证失败:
> Running pre-commit hook
$ pnpm typecheck
✖ Found 3 type errors
Pre-commit hook failed (add --no-verify to bypass)
lint-staged
配置
// 第一种
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"./packages/app/src/**/*.{js,vue}": [
"prettier --write"
]
}
// 第二种
"simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify",
"pre-commit": "pnpm typecheck && pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
},
pnpm lint-staged 等同于npx lint-staged
为什么可以直接在 Husky 配置中使用 "pre-commit": "lint-staged"
在 Husky 配置中直接使用 "pre-commit": "lint-staged"
而无需指定 pnpm
或 npx
前缀,这背后有精妙的设计原理。这是 Node.js 生态中模块解析机制和执行环境优化共同作用的结果:
核心机制解析
1. PATH 环境变量的魔法
当 Husky 执行钩子命令时,它会自动将项目的 node_modules/.bin
目录前置到系统 PATH 环境变量中:
# 实际执行环境类似:
PATH="./node_modules/.bin:$PATH" lint-staged
这样系统会优先在本地项目的 node_modules/.bin
中查找 lint-staged
可执行文件。
2. .bin 目录的特殊性
node_modules/.bin
目录包含所有安装依赖的软链接可执行文件:
# 项目目录示例
node_modules/
├── .bin/
│ ├── lint-staged → ../lint-staged/bin/lint-staged.js
│ ├── eslint → ../eslint/bin/eslint.js
│ └── ...
├── lint-staged/
└── ...
3. Husky 的执行上下文
Husky 在运行钩子命令时,会确保:
- 当前工作目录是项目根目录
- Node.js 可执行环境已配置
node_modules/.bin
在 PATH 最优先位置
完整执行流程
技术优势分析
直接使用 lint-staged
的好处
版本一致性
始终使用项目本地安装的特定版本(避免全局版本冲突)
零配置
无需在
package.json
的 scripts 中重复定义跨平台兼容
在 Windows/macOS/Linux 上行为一致
性能优化
避免额外进程创建开销(相比
pnpm lint-staged
少一个进程)
对比其他调用方式
调用方式 | 进程数 | 版本控制 | 启动速度 | 适用场景 |
---|---|---|---|---|
lint-staged | 1 | ✅ 本地 | ⚡️ 最快 | Git 钩子 |
npx lint-staged | 2 | ✅ 本地 | 中等 | 临时执行 |
pnpm exec lint-staged | 2 | ✅ 本地 | 中等 | 脚本中调用 |
pnpm lint-staged | 2 | ✅ 本地 | 较慢 | 需要 pnpm 环境 |
全局安装的 lint-staged | 1 | ❌ 全局 | 快 | 不推荐(版本冲突风险) |
为什么这样设计是安全的?
隔离性
每个项目有自己的
node_modules/.bin
,互不干扰确定性
package-lock.json
/pnpm-lock.yaml
确保依赖版本一致可追溯性
所有执行文件都来自
node_modules
目录安全边界
执行权限限制在项目目录内
实际应用场景
1. 推荐配置方式
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,ts,vue}": "eslint --fix"
}
}
2. 复杂场景扩展
"pre-commit": "lint-staged && cross-env NODE_ENV=test vitest run"
3. 调试技巧
# 查看实际执行路径
HUSKY_DEBUG=1 git commit
# 输出示例:
[husky] debug: PATH: /project/node_modules/.bin:/usr/bin:/bin
[husky] debug: Running command: lint-staged
总结
Husky 能直接执行 lint-staged
是因为它精心设计了执行环境,通过动态修改 PATH 环境变量,使系统能自动发现并优先使用项目本地安装的 node_modules/.bin/lint-staged
。这种设计体现了 Node.js 生态的模块化哲学:保持配置简洁,依赖本地化,同时确保执行环境的确定性。
这种机制不仅适用于 lint-staged
,也适用于所有安装在 devDependencies
中且提供 CLI 接口的工具(如 eslint
、prettier
、jest
等),是现代化前端工程的标准实践。