前端依赖管理-npm依赖管理机制

前言

在众多前端工程化步骤中,依赖包的管理最直接影响着每一位开发者。安装、更新依赖包是每一个开发都会接触的事。但是,在排查bug的时候依赖包问题又往往是最容易被忽略的。因此,本文以node官方的包管理器npm为例,讲一下依赖包管理的问题。

npm 的安装机制

前端依赖管理-npm依赖管理机制

npm安装依赖的具体流程如上图所示,需要注意的是,当package.json与package-lock.json中依赖包声明版本不一致时会根据npm版本的不同进行不同安装策略。在npm v5.0.x中会根据package-lock.json下载;在npm v5.1.0 – v5.4.2中会根据package.json版本安装并更新package-lock.json文件;在npm v5.4.2及以上版本中当package.json声明的依赖版本规范与package-lock.json声明版本兼容则根据package-lock.json安装,如果不兼容,则按照package.json安装并更新package-lock.json。

依赖树扁平化

npm在确定首层依赖后会递归构建依赖树,工程本身为依赖树根节点,每个首层依赖模块都为根节点下的子树。每个节点在递归工程中会确定节点模块信息如版本、下载地址、压缩包地址等。在npm v2版本时,安装依赖包仅采用简单的递归安装,在根据 dependencies 和 devDependencies 属性中指定的包确定首层依赖后便递归安装各个包到子依赖的node_modules中,直到子依赖不再依赖其他模块。形成的依赖树如下图所示:
前端依赖管理-npm依赖管理机制

这样的目录结构层次分明、增删简单,但重复依赖会不断增大项目体积,造成大量冗余。为解决此类问题,npm v3的 node_modules 目录改成了更为扁平状的层级结构,尽量把依赖以及依赖的依赖平铺在 node_modules 文件夹下共享使用。npm v3会遍历所有节点,当发现重复模块时直接丢弃,只有遇到依赖版本不兼容时继续采用npm v2的处理方式继续安装。形成的依赖树如下图所示:
前端依赖管理-npm依赖管理机制

但是npm v3会带来一个新的问题,如果package.json中依赖顺序变化会导致依赖树的变化,具体如下图所示:
前端依赖管理-npm依赖管理机制

由此可见,npm v3带来的扁平化管理并未完全解决冗余问题。这类问题最后在npm v5版本引入package-lock.json文件配合依赖树扁平化得以解决。

缓存机制

前端依赖管理-npm依赖管理机制

通过npm config get cache 指令可以查看本地缓存,通常在/.npm文件下的_cacache目录下有三个文件:content-v2index-v5tmpindex-v5中存放的是content-v2文件的索引,而content-v2中存放的是依赖包的二进制文件。在安装资源的时候会根据package-lock.json文件中integrityversionname三个属性生成唯一的key,通过key去匹配index-v5文件中的缓存记录。如果存在缓存记录,并根据记录中的hash值去寻找在content-v2中对应的tar包。最后,通过pacote将二进制文件解压至项目中的node_modules目录中,省去了资源下载的网络开销。其中,pacote是依赖npm-registry-fetch来下载包的,npm-registry-fetch 可以通过设置 cache 字段进行相关的缓存工作。值得注意的是,缓存策略是从npm v5开始的,在npm v5之前每个缓存模块都在~./npmrc文件中以模块名的格式直接存储,存储格式为 {cache}{name}{version}

npm ci与npm install

根据npm install的安装机制,package-lock.json文件有可能会因为手动操作而改变。但有时候我们并不想lockfile有任何变化需要确保依赖树的绝对一致。npm ci指令与npm install相比,就具备确保依赖树一致的功能。npm ci会完全按照lockfile去安装依赖,但值得注意的是,当lockfile中的声明版本不满足package.json中声明版本指定的semver规则会报错退出,并不会往下执行或更新lock文件。

package.json

无论使用什么包管理器,package.json文件都是依赖库管理的核心文件。

创建package.json

创建package.json主要分为两种方法:当使用脚手架生成项目时,脚手架会自动生成package.json;使用npm initnpm init -y手动创建package.json。生成的基础内容如下:
前端依赖管理-npm依赖管理机制

常见属性

package.json中有许多属性,用以进行控制版本依赖、发包、校验等工作。具体属性及简要作用如下图所示:
前端依赖管理-npm依赖管理机制

在众多属性中,着重讲一下重要属性name、version、xxxxDependencies、resolutions及脚本配置属性script:

name:表示项目名称,该字段决定了你发布的包在 npm 的名字。

name属性命名规则

  • 名称必须小于或等于 214 个字符。这包括范围包的范围。
  • 作用域包的名称可以以点或下划线开头。如果没有范围,这是不允许的。
  • 新包的名称中不得包含大写字母。
  • 该名称最终成为 URL、命令行参数和文件夹名称的一部分。因此,名称不能包含任何非 URL 安全字符。

version: 表示项目的版本号,version属性必须采用major.minor.patch格式。minor代表主版本号,不可向前兼容的更改,比如系统重构、API重构等。minor为次版本号,代表功能模块变更这类可兼容的更改,一般为API新增等操作。patch为补丁版本,一般用于bug fix或安全问题。如果计划发包,「name」和「version」字段是必须的,名称和版本号会形成唯一标识。


xxxxDependencies

  • dependencies生产环境依赖:线上生产环境的依赖包
  • devDependencies开发环境依赖:开发依赖,不会自动被下载,只在开发环境中使用
  • peerDependencies兼容依赖:用于声明宿主环境所需依赖兼容版本,属性中声明的包不会被自动检测并安装也不会被打包。如果项目中以来不满足peerDependencies条件会打印警告。
  • bundledDependencies捆绑依赖:npm pack打包时将该属性所写依赖项打包到发布包中,方便用户安装时不需要手动安装这些依赖项。
  • optionalDependencies可选依赖:表示安装对应依赖失败也不会影响安装过程,optionalDependencies会覆盖dependencies中的同名依赖包,不建议使用会造成项目的不确定性和复杂性。

resolutions: 用于解决依赖项冲突的 npm 特殊字段,如果项目依赖于 package-a 和 package-b,而这两个包都依赖于 package-c,且两者依赖的 package-c 版本不同,可以用resolutions 字段来指定应该使用哪个版本。


script: 定义可执行脚本命令,供npm直接调用。
前端依赖管理-npm依赖管理机制

在终端中执行npm run start 相当于执行nodemon index.ts,而npm run是npm run-script的缩写。每当执行npm run,系统会自动新建一个Shell(一般是Bash),并在这个Shell中执行命令,所以只要Shell可执行的命令都可以写在脚本中。与此同时,系统当前目录的node_modules/.bin子目录加入PATH变量,执行结束后,再将PATH变量恢复原样。所以node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。
前端依赖管理-npm依赖管理机制

脚本中可以使用Shell通配符和传参:
前端依赖管理-npm依赖管理机制

其中,表示任意文件名, *表示任意一层子目录,用- -标明传入参数,也可以使用命令传入参数。
前端依赖管理-npm依赖管理机制

每个 npm script 有 pre 和 post 两个钩子, pre 钩子在脚本执行前将被触发, post 则是在脚本执行后触发。例如build脚本命令的钩子就是prebuildpostbuild
前端依赖管理-npm依赖管理机制

在执行npm run build的时候,会自动执行npm run prebuild && npm run build && npm run postbuild。因此可以在prebuildpostbuild中添加一些操作来优化流程。npm中默认提供的脚本命令如下:
前端依赖管理-npm依赖管理机制

除了默认脚本命令,自定义脚本命令也同样有这两个钩子,但是不支持双重pre或者双重post即preprebuild、postpostbuild。
除此以外,还可以通过环境变量process.env 对象与npm_package_前缀拿到package.json的字段值。
前端依赖管理-npm依赖管理机制

package.json中的版本锁定

在安装依赖时,无论是开发环境还是生产环境,package.json文件在版本号前可能会出现多种标志,

这些标志代表着package.json对于依赖包版本的不同管理控制策略。

  • ^ 表示更新【次版本】,例如package.json中版本号是^2.1.0,当发布包更新到2.2.0,哪怕没有重新安装,依赖库也可能自动更新到2.2.0。不过当发布包更新到3.0.0版本时表示为主版本更新,本地依赖库是不会更新到3.0.0版本的。
  • ~表示更新【补丁版本】,例如package.json中版本号是^2.1.0,当发布包更新到2.1.1,依赖库会自动更新到2.1.1,但当发布包更新到2.2.0时,依赖库不会更新至2.2.0版本。
  • *表示会安装最新版本的依赖包,比如*2.1.0,发布包更新到3.x.x的时候依赖库便会下载3.x.x。
  • >: 接受高于指定版本的任何版本。
  • >=: 接受等于或高于指定版本的任何版本。
  • =<: 接受等于或低于指定版本的任何版本。
  • <: 接受低于指定版本的任何版本。
  • 无符号: 仅接受指定的特定版本。
  • latest: 使用可用的最新版本。

因此,只要去掉package.json版本号前的标志就可以“绝对锁定”依赖版本号,去除依赖版本差异带来的bug。

package-lock.json

在npm install之后,项目希望总是生成完全相同的node_modules 树以确保项目稳定性。但npm3的安装机制会按照package.json里的顺序依次解析,依赖的顺序会影响node_modules 树的生成。此外,package.json文件也只能束缚项目的直接依赖,对于间接依赖没有管理锁定也会导致生成的node_modules 树不完全相同。

为解决上述问题,确保同一项目总是会生成相同的node_modules 树以确保稳定性。npm5中引入了package-lock.json文件来规范node_modules树的生成。
前端依赖管理-npm依赖管理机制

package-lock.json文件构成如上图所示,该文件将node_modules树的生成数据化,使依赖包的依赖关系一目了然。其中,requires与dependencies字段的功能常令人混淆。简单来说,requires表示所有需要安装的依赖,而dependencies 表示与根目录node_modules冲突的依赖,冲突的依赖会在dependencies属性中记录并安装在当前依赖下的node_modules文件中。

package-lock.json什么时候会变

  • package-lock.json文件在npm install的时候会自动生成
  • 修改依赖位置,将部分依赖从开发依赖变成生产依赖,会影响package-lock.json中依赖的 dev 字段
  • 切换镜像时,执行 npm install 时也会修改 package-lock.json中的resolved字段
  • 使用npm install添加或npm uninstall移除包的时候,也会修改 package-lock.json
  • 更新某个包的版本的时候,也会修改 package-lock.json

package-lock.json需要递交到仓库嘛?

npm 官网建议:把 package-lock.json 一起提交到代码库中,不要 ignore,以此确保团队所有开发者及CI环节执行生成的依赖树一致。但是在执行 npm publish进行发包的的时候,应该将其忽略。因为发布的npm包需要被其他仓库所依赖,如果锁定了依赖包的版本,会导致发布的包与项目其他依赖包无法共享依赖造成不必要的冗余。npm默认不会把package-lock.json文件发不出去。

最佳实践建议

借用字节的一个小问题npm 和 yarn不一样吗?(续篇)文章作者的建议,个人认为很合理,仅供参考:

  • 优先去使用 npm 官方已经稳定的支持的版本, 以保证 npm 的最基本先进性和稳定性
  • 当我们的项目第一次去搭建的时候, 使用 npm install 安装依赖包, 并去提交 package.json、package-lock.json, 至于node_moduled目录是不用提交的。
  • 当我们作为项目的新成员的时候, checkout/clone项目的时候, 执行一次 npm install 去安装依赖包。
  • 当我们出现了需要升级依赖的需求的时候:
    • 升级小版本的时候, 依靠 npm update
    • 升级大版本的时候, 依靠 **npm install@ **
    • 当然我们也有一种方法, 直接去修改 package.json 中的版本号, 并去执行 npm install 去升级版本
    • 当我们本地升级新版本后确认没有问题之后, 去提交新的 package.json 和 **package-lock.json **文件。
  • 对于降级的依赖包的需求: 我们去执行npm install @ 命令后,验证没有问题之后, 是需要提交新的 package.jsonpackage-lock.json 文件。
  • 删除某些依赖的时候:
    • 当我们执行 npm uninstall 命令后, 需要去验证,提交新的 package.json 和 package-lock.json 文件。
    • 或者是更加暴力一点, 直接操作 package.json, 删除对应的依赖, 执行 npm install 命令, 需要去验证,提交新的package.jsonpackage-lock.json 文件。
  • 当你把更新后的package.jsonpackage-lock.json提交到代码仓库的时候, 需要通知你的团队成员, 保证其他的团队成员拉取代码之后, 更新依赖可以有一个更友好的开发环境保障持续性的开发工作。
  • 任何时候我们都不要去修改 package-lock.json,这是交过智商税的。
  • 如果你的 package-lock.json 出现冲突或问题, 我的建议是将本地的 package-lock.json文件删掉, 然后去找远端没有冲突的 package.jsonpackage-lock.json, 再去执行 npm install 命令。

总结

以前端基础建设与架构30讲中的五个问题作为总结。

  1. 删除node_modules和lockfiles文件再重新install的操作是否存在风险?答:存在风险,轻易删除lockfiles文件会导致项目安装的依赖在package.json版本控制范围内变动。
  2. 把所有依赖都安装到dependencies中,不区分devDependencies会有问题嘛?答:具体根据项目性质而定,前端spa应用项目或者ssg项目可以,后端、ssr或公开库不建议那么做。
  3. 应用依赖了公共库A和公共库B,同时公共库A也依赖了公共库B,那么公共库B会被多次安装或者重复打包嘛?答:应用依赖的公共库B与公共库A依赖的公共库B如果不存在冲突则会共用一个不会重复安装、打包。如果存在版本冲突,会在公共库B的依赖目录下再添加一个公共库B的依赖。
  4. 一个项目中,可以即有人用npm又有人用yarn嘛?答:lock文件不同,可能会存在冲突导致最终安装版本不一致。
  5. 是否应该递交lockfiles文件到项目仓库?答:通常应该把 package-lock.json 一起提交到代码库中,不要 ignore,以此确保团队所有开发者及CI环节执行生成的依赖树一致。但是在执行 npm publish进行发包的的时候,应该将其忽略。因为发布的npm包需要被其他仓库所依赖,如果锁定了依赖包的版本,会导致发布的包与项目其他依赖包无法共享依赖造成不必要的冗余。npm默认不会把package-lock.json文件发不出去。

参考资料

阮一峰老师的npm script的使用www.ruanyifeng.com/blog/2016/1…

工程的 package.json 中的 ^~ 该保留吗?juejin.cn/post/724481…

npm 依赖管理中容易被忽略细节blog.csdn.net/weixin_3984…

前端基础建设与架构30讲

字节的一个小问题npm 和 yarn不一样吗?(续篇)juejin.cn/post/707165…

原文链接:https://juejin.cn/post/7337517508150706211 作者:简昊

(0)
上一篇 2024年2月21日 上午10:42
下一篇 2024年2月21日 上午10:52

相关推荐

发表回复

登录后才能评论