NPM
NPM 安装机制
执行 npm install :
-
读取 npm 配置文件
.npmrc配置文件优先级: 项目级 > 用户级 > 全局 > npm 内置
-
检查是否存在
package-lock.json文件:- 若不存在, 则根据
package.json递归构建依赖树并从缓存或远程仓库下载相关资源, 并生成package-lock.json文件; - 若存在, 则对比
package-lock.json和package.json文件中声明的依赖规范是否一致: - 若一致, 直接使用
package-lock.json中的信息从缓存或远程仓库加载依赖文件; - 若不一致, NPM v5.4.2+ 会按照
package.json文件安装依赖并更新package-lock.json文件(NPM 旧版本的处理方式有所不同, 详见下文).
- 若不存在, 则根据
-
构建扁平化依赖树: 无论是直接依赖还是嵌套依赖, 都优先将依赖包放在
node_modules根目录.当遇到相同模块且版本冲突时, 将后者放在当前模块的
node_modules目录下.
NPM 不同版本对于 package-lock.json 文件处理方式的不同, 可能导致安装的依赖包版本不一致, 所以团队内应保持 NPM 版本一致.
NPM 缓存机制
获取 NPM 本地缓存路径: npm config get cache , 默认为 ~/.npm .
缓存路径下的 _cacache 目录下有三个子目录:
content-v2二进制文件, 将此目录扩展名改为.tgz并解压则可以得到依赖包资源index-v5同样可通过修改扩展名解压, 是content-v2资源的索引文件tmp临时文件
NPM 缓存策略: 执行 npm install 时, 若缓存中存在依赖包, 则通过 pacote 将依赖包解压到对应的 node_modules 目录下; 若缓存中不存在, 则先从远程仓库下载依赖包到缓存中, 再从缓存解压到项目 node_modules 中.
在安装资源时, 根据 package-lock.json 中依赖包的 integrity name 和 version 信息生成一个唯一的 key, 根据这个 key 从 index-v5 目录中寻找缓存资源的 hash , 如果找到了就可以从 content-v2 中找到对应的 tar 包, 通过 pacote 将二进制文件解压到项目的 node_modules 中.
NPM v5 之前的缓存文件是以
{cache}/{name}/{version}的形式直接存储在.npm目录下.
npm link 和 npx
npm link 为目标模块创建软连接, 将其链接到全局安装路径中, 可用于在模块发布之前在本地调试和使用.
npx 的两个作用:
- 可直接执行
node_modules/.bin和 环境变量PATH中的命令, 在之前只能通过在package.json中定义script或手动定位到命令所在目录来执行; - 执行一个需要安装依赖的命令, 在临时目录中安装依赖, 并在执行完成后删除相关依赖, 避免在全局安装模块. 可用于使用脚手架生成项目:
npx create-react-app my-app.
package-lock.json 中的依赖
package-lock.json 文件中的 dependency 主要有以下属性:
- Version:版本号
- Resolved:安装源(下载地址)
- Integrity:表明包完整性的 Hash 值
- Dev:该依赖是否为顶级模块的开发依赖或者是传递依赖
- requires:需要的所有依赖项, 对应该依赖包
package.json中dependencies中的依赖项 - dependencies:依赖包
node_modules中依赖的包, 当子依赖的依赖与当前根目录中的依赖冲突后才有此属性
lockfiles 与 NPM 版本问题总结
- 早期 NPM 使用
npm-shrinkwrap.json锁定版本, 与package-lock.json不同点在于:NPM 包发布时默认将npm-shrinkwrap.json同时发布; -
package-lock.json是 NPM v5.x 版本新增特性, 而 v5.6 以上才逐步稳定, 在 5.0 - 5.6 之间对package-lock.json的处理逻辑进行过几次更新:- 在 v5.0.x 中,
npm install时根据package-lock.json文件下载, 不考虑package.json内容; - v5.1.0 - v5.4.2,
npm install会无视package-lock.json文件而下载最新的依赖包并更新package-lock.json; - v5.4.2 后:
- 如果项目只有
package.json文件,npm install后会根据它生成一个package-lock.json文件; - 如果存在
package.json和package-lock.json文件,同时package.json的semver-range版本 和package-lock.json中版本兼容,npm install会根据package-lock.json下载; - 如果存在
package.json和package-lock.json且package.json的semver-range版本和package-lock.json中版本不兼容,npm install时package-lock.json将会更新到兼容package.json的版本; - 如果
package-lock.json和npm-shrinkwrap.json都存在于项目根目录,package-lock.json将会被忽略.
- 在 v5.0.x 中,
xxxDependencies 声明总结
NPM 中共有 5 中依赖声明:
dependencies项目依赖, NPM 包被下载时, 它的项目依赖会被一起下载.devDependencies开发依赖, 只在开发阶段或开发环境用到的依赖. 在实际业务中只是一个规范, 依赖是否被打包完全取决于项目中是否引入了该模块, 开发依赖在npm install时也会被下载.peerDependencies同版本依赖, 一般用于在基于某个框架或核心库做扩展库或中间件时, 来声明宿主环境, 如开发基于 React 的 UI 组件库时就可以声明"peerDependencies": { "react": "^17.0.0" }.bundledDependencies捆绑依赖, 在npm pack时会在压缩包中包含捆绑依赖中声明的安装包; 业务方使用npm install xx安装压缩包时也会安装捆绑依赖中声明的包. 需要注意的是, 此包必须在dependencies或devDependencies中声明过.optionalDependencies可选依赖, 表示安装失败也不影响整个过程, 一般不建议使用.
CI 环境中的 NPM 优化
使用 npm ci 代替 npm install
npm ci 是专用于 CI 环境的安装命令, 它与 npm install 的主要不同有:
- 项目中必须存在
package-lock.json和npm-shrinkwrap.json; npm ci完全根据package-lock.json安装依赖, 保证整个团队的依赖包完全一致, 同时安装过程也更迅速;npm ci执行安装时会删除现有的node_modules并重新安装;npm ci只能一次安装整个依赖包, 无法单独安装;- 若
package-lock.json和package.json冲突会直接报错; npm ci不会修改package.json和package-lock.json.
基于 npm ci 命令的特性, 可得出以下 CI 环境中的 NPM 优化方法:
- 提交
package-lock.json到仓库; - 缓存
node_modules文件;
适用于团队的 NPM 最佳实践
- 使用 NPM v5.4.2 以上的版本, 最好统一 Node.js 版本.
- 项目初次搭建时使用
npm install安装依赖, 并提交 package.json、package-lock.json, 不提交 node_modules 目录. - 其他成员首次 clone/checkout 项目后, 执行
npm install安装依赖包. -
升级依赖包:
- 使用
npm update升级到新的小版本; - 使用
npm install @升级大版本; - 也可以手动修改 package.json 中的版本号并执行
npm install升级版本; - 本地验证升级后无问题再提交新的 package.json、package-lock.json 文件.
- 使用
- 降级依赖包:执行
npm install @命令,验证没问题后提交新的 package.json、package-lock.json 文件. -
删除依赖包:
- 执行
npm uninstall xx命令, 验证没问题后提交新的 package.json、package-lock.json 文件; - 或手动更改 package.json 删除依赖并执行
npm install命令, 验证没问题后提交新的 package.json、package-lock.json 文件.
- 执行
- 任何成员更新 package.json、package-lock.json 后, 其他成员在拉取代码后执行
npm install更新依赖. - 禁止修改 package-lock.json.
- 若 package-lock.json 出现冲突或问题,建议删除本地 package-lock.json 文件, 引入远程的 package-lock.json 和 package.json 文件, 再执行
npm install.
Yarn
Yarn 是在 NPM 处于 v3 时为了解决 NPM 在依赖包的版本确定性(无 lock 文件)、扁平化安装、网络性能、缓存机制等方面的问题而出现的包管理器.
目前 NPM 也吸收了 Yarn 的很多优势特点, 改进了本身存在的诸多问题, 所以二者目前并没有明显的优劣之分.
yarn.lock 与 package-lock.json 相比, 除了文件格式不同, 另一个显著区别是其中的 子依赖 的版本号不是固定版本, 而是类似于 ^4.0.1 这样的版本规则, 这意味着 yarn.lock 必须和 package.json 文件相配合才能确定 node_modules 目录结构.
Yarn 和 NPM 的另一个显著区别是: Yarn 默认使用 prefer-online 模式, 即优先使用网络资源, 请求失败后才去读取缓存资源.
Yarn 安装机制
Yarn 的安装过程可分为 5 个步骤:
-
Checking
检查项目中是否存在 npm 相关文件如
package-lock.json等, 若有会提示用户可能导致冲突; 也会检查当前环境的操作系统、CPU 等信息. -
Resolving
解析依赖树:
- 获取首层依赖, 即
package.json中的dependenciesdevDependencies和optionalDependencies内容 - 遍历首层依赖, 逐个递归查找嵌套依赖: 对于每个包, 尝试从
yarn.lock中获取版本信息, 若获取不到, 则从远程仓库获取满足版本规则的最高版本的版本信息. - 通过步骤 b 最终得到所有依赖的具体版本信息和下载地址.
- 获取首层依赖, 即
-
Fetching
检查缓存中是否存在当前依赖包, 对于不存在的包会维护一个 fetch 队列, 将依赖包下载到缓存目录.
-
Linking
将依赖包从缓存目录 扁平化 复制到项目
node_modules下. -
Building
对存在二进制包的依赖包进行编译.
依赖嵌套问题
早期 NPM 的树形依赖
早期 NPM 的依赖包采用自然的树形依赖: 将项目的直接依赖放到项目 node_modules 根目录, 若直接依赖 A 还依赖模块 B, 则将模块 B 放在模块 A 的 node_modules 下. 在稍复杂的项目中形成“嵌套地狱”:
- 依赖树层级过深, 一方面难以调试, 另一方面可能出现文件路径过长导致的一些问题;
- 依赖树的不同分支可能存在重复的依赖包, 导致安装过慢、浪费空间.
扁平化依赖
对于没有版本冲突的依赖包, 扁平化地将依赖包放在 node_modules 根目录.
存在版本冲突时则嵌套安装, 如 模块 A 依赖 模块 B-v1.0 , 模块 C 依赖 模块 B-v2.0 , 则将 模块 B-v2.0 安装在 模块 C 的 node_modules 中.
冲突模块 B 不同版本的安装路径取决于模块 A 和 C 的安装顺序, 先安装的版本放在根目录.
此时, 依赖包的安装顺序对依赖树影响很大: 若 模块 A 依赖 模块 B-v1.0 , 而 模块 C、D、E 都依赖 模块 B-2.0 , 则会导致 C、D、E 模块下都存在重复性的 模块 B-v2.0 .
执行 npm dedupe 会尝试通过将依赖关系向上移动来尽可能删除重复依赖.
Yarn 则会在安装依赖时自动执行 dedupe 命令.