npm 是 Node.js 默认的、以 JavaScript 编写的包管理工具,如今,它已经成为世界上最大的包管理工具,是每个前端开发者必备的工具。不知你是否遇到过下面问题:

哎?我本地明明是好的,线上的依赖怎么就报错不行了呢?
一言不合就删除整个node_modules目录然后重新npm install

今天我们聊聊npm模块相关的东西。

semver

npm 依赖管理的一个重要特性是采用了语义化版本 (semver) 规范,作为依赖版本管理方案。

semver规定的模块版本号格式为:MAJOR.MINOR.PATCH,即主版本号.次版本号.修订号。版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,例如新增了breaking change。
  2. 次版本号:当你做了向下兼容的功能性新增,例如新增feature。
  3. 修订号:当你做了向下兼容的问题,例如修复bug。

对于npm包的引用者来说,经常会在package.json文件里面看到使用semver约定的semver range来指定所需的依赖包版本号和版本范围。常用的规则如下表:

此外,任意两条规则,用空格连接起来,表示“与”逻辑,即两条规则的交集: 如 >=2.3.1 <=2.8.0 可以解读为: >=2.3.1<=2.8.0

任意两条规则,通过 || 连接起来,表示“或”逻辑,即两条规则的并集: 如 ^2 >=2.3.1 || ^3 >3.2

在修订版本号的后面可以加上其他信息,用-连接,比如:

  • X.Y.Z-Alpha: 内测版
  • X.Y.Z-Beta: 公测版
  • X.Y.Z-Stable: 稳定版

从npm install说起

npm install命令用来安装模块到node_modules目录。npm install的具体原理是什么呢?

  1. 执行工程自身 preinstall
  2. 确定首层依赖模块:首层依赖是package.jsondependenciesdevDependencies字段直接指定的模块。每一个首层依赖模块都是模块依赖树根节点下面的一颗子树。
  3. 获取模块

获取模块是一个递归的过程,分为以下几步:

  • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中的模块版本往往是 semantic version。此时根据package.json和版本描述文件(npm-shrinkwrap.jsonpackage-lock.json),(不同npm版本的策略不同,后续我们会详细介绍)。如 package.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合1.x.x形式的最新版本。
  • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
  • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。

4. 模块扁平化(npm3后支持)

上一步获取到的是一颗完整的依赖树,下面会根据依赖树安装模块。模块安装机制有两种:嵌套式安装机制扁平式安装机制

例如某工程下直接依赖了A和B两个包,且他们同时依赖了C包。

  • 嵌套式

npm3之前使用的是嵌套式安装机制,严格按照依赖树的结构进行安装,这可能会造成相同模块大量冗余的问题。

  • 扁平式

npm3之后使用的扁平式安装机制,但是需要考虑一个问题:

工程同时依赖一个模块不同版本该如何解决?

npm3引入了dedupe过程来解决这个问题。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

重复模块:semver兼容的相同模块。例如lodash ^1.2.0lodash ^1.4.0。如果工程的两个模块版本范围存在交集,就可以得到一个 兼容版本,不必版本号完全一致,这可以使得更多冗余模块在dedupe过程中被去掉。

上例中如果A包依赖C@1.0.0,B包依赖C@2.0.0,此时两个版本并不兼容,则后面的版本仍会保留在依赖书中。如下图所示:

实际上,npm3仍然可能出现模块冗余的情况,如下图,因为一级目录下已经有C@1.0.0所以所有的C@2.0.0只能作为二级依赖模块被安装

npm提供了npm dedupe指令来优化依赖树结构。这个命令会去搜索本地node_modules中的包,并且通过移动相同的依赖包到外层目录去尽量简化这种依赖树的结构,让公用包更加有效被引用。

5. 安装模块

将会更新工程中的node_modules,并执行模块中的生命周期函数(按照 preinstallinstallpostinstall 的顺序

6. 执行工程自身生命周期

当前 npm 工程如果定义了钩子此时会被执行(按照 installpostinstallprepublishprepare的顺序)。最后生成或者更新版本描述文件。

锁定npm依赖版本

你是否遇到过本地开发时一切正常,发布线上代码时因为安装依赖的错误导致服务不可用?如果是的话,你要一份版本描述文件。

简单的写死当前工程依赖模块的版本并不能真正锁定依赖版本,因为你无法控制间接依赖,如果间接依赖更新了有问题的模块,你的系统还是可能会有宕机的风险。

lock 文件是当前依赖关系树的快照,允许不同机器间的重复构建。其实npm5之前已经提供了lock文件 — npm-shrinkwrap.json。但是在npm5发布的时候创建了新的lock文件 — package-lock.json,其主要目的是希望能更好的传达一个消息,npm真正支持了locking机制。不过二者还是有一些区别点:

  1. 发布npm包时,package-lock.json不会被发布, 即使你将其显式添加到软件包的 files 属性中,它也不会是已发布软件包的一部分。npm-shrinkwrap.json可以被发布。
  2. npm-shrinkwrap.json向后兼容npm2、3、4版本,package-lock.json只有npm5以上支持。
  3. 可以通过npm shrinkwrap命令将package-lock.json转换成npm-shrinkwrap.json, 因为文件的格式是完全一样的。

曲折的package-lock.json

查阅资料得知,自npm 5.0版本发布以来,package-lock.json的规则发生了三次变化。

  1. npm 5.0.x版本,不管package.json怎么变,npm install都会根据lock文件下载。npm/npm#16866控诉了这个问题,我明明手动改了package.json,为啥不给我升包!然后就导致5.1.0的问题(是个bug)
  2. npm 5.1.0 - 5.4.1版本,npm insall会无视lock文件,去下载semver兼容的最新的包。导致lock文件并不能完全锁住依赖树。详情见npm/npm#17979
  3. npm 5.4.2版本之后,如果手动改了package.json,且package.json和lock文件不同,那么执行npm install时npm会根据package中的版本号和语义含义去下载最新的包,并更新至lock。

如果两者是同一状态,那么执行npm install都会根据lock下载,不会理会package实际包的版本是否更新。

好的依赖管理方案

  • 使用 npm: >=5.4.2 版本, 保持 package-lock.json 文件默认开启配置
  • 初始化:第一作者初始化项目时使用 npm install <package> 安装依赖包, 默认保存 ^X.Y.Z 依赖 range 到 package.json中; 提交 package.json, package-lock.json, 不要提交 node_modules 目录
  • 初始化:项目成员首次 checkout/clone 项目代码后,执行一次 npm install 安装依赖包
  • 升级依赖包:
  • 升级小版本: 本地执行 npm update 升级到新的小版本
  • 升级大版本: 本地执行 npm install <package-name>@<version> 升级到新的大版本
  • 也可手动修改 package.json 中版本号为要升级的版本(大于现有版本号)并指定所需的 semver, 然后执行 npm install
  • 本地验证升级后新版本无问题后,提交新的 package.json, package-lock.json 文件
  • 降级依赖包:
  • 正确: npm install <package-name>@<old-version> 验证无问题后,提交 package.json 和 package-lock.json 文件
  • 删除依赖包:
  • Plan A: npm uninstall <package> 并提交 package.jsonpackage-lock.json
  • Plan B: 把要卸载的包从 package.json 中 dependencies 字段删除, 然后执行 npm install 并提交 package.jsonpackage-lock.json
  • 任何时候有人提交了package.json, package-lock.json 更新后,团队其他成员应在 svn update/git pull 拉取更新后执行 npm install 脚本安装更新后的依赖包
  • 不要手动修改 package-lock.json
  • package-lock.json出现冲突时,这种是非常棘手的情况,最好不要手动解决冲突,如果有一处冲突解决不正确可能会导致线上事故。 建议的做法:将本地的package-lock.json文件删除,引入远程的package-lock.json文件,再执行npm install命令更新package-lock.json文件(这种做法能保证未修改的依赖不变,会存在一个风险:在执行npm install的时候,可能有些间接依赖包升级,根据semver兼容原则导致本次安装的和开发时的package-lock.json文件不同。这种情况就需要验证依赖包升级是否有影响)。
  • 部署安装依赖时,执行npm install命令。不要执行npm install <some-package-name>命令,因为这会导致package-lock.json文件同时被更新。

问题来了

上述最佳实践提到了当团队中有成员提交了package.json, package-lock.json 更新后,其他成员需要执行npm install来保证本地依赖的及时性,那么能否写一个插件将这个手动的环节自动化呢?答案是可以的,我们只需要在git post-merge钩子中检查git diff files是否包含了package.json文件,如果包含了该文件,则执行npm install命令。我们暂且给这个插件取名为hawkeye。当然,这个插件能干的事情不仅于此。

不知作为读者的你听到上述场景描述后,是否有种似曾相识的感觉?没错,lint-staged

lint-staged,从git staged files变化中匹配你想要的文件,再执行你配置的commands。
Hawkeye,从git diff files变化中匹配你想要的文件,再执行你配置的commands。
需要注意的是,他们都依赖于husky改造git hooks的能力。

实现方案

例子

假设有一个已经安装了hawkeyehusky的项目,package.json如下:

{"name": "My project","version": "0.1.0","scripts": {},"husky": {"hooks": {"post-merge": "hawkeye"}},"hawkeye": {"package.json": ["npm install"]}
}

相关链接

  • semver 语义化版本
  • semver(1) -- The semantic versioner for npm
  • 2018 年了,你还是只会 npm install 吗?
  • npm install algorithm
  • npm dedupe
  • npm install的实现原理
  • [译] 理解 NPM 5 中的 lock 文件
  • package-lock.json file not updated after package.json file is changed
  • why is package-lock being ignored?
  • lint-staged
  • hawkeye

删除时存在依赖_npm依赖管理那些事相关推荐

  1. yarn降版本_npm依赖版本锁定和管理

    前几天测试的时候遇到个问题,测试的时候出现依赖升级问题,由于测试同学是重新换了个机子,重装了环境,导致下载过程中依赖升级.npm带来便利的时候也带一些问题.如果您觉得比较啰嗦,直接看结果. 我们的vu ...

  2. (30)导入时如何定制spring-boot依赖项的版本【转载】【从零开始学Spring Boot】...

    此文章转载地址:http://www.tuicool.com/articles/RJJvMj3 请注重作者的版权. spring-boot通过maven的依赖管理为我们写好了很多依赖项及其版本,我们可 ...

  3. maven项目的依赖、继承管理

    目录 maven依赖 scope 依赖范围 依赖传递 依赖冲突 可选依赖 排除依赖 maven继承 继承 maven依赖 scope 依赖范围 其中依赖范围scope 用来控制依赖和编译,测试,运行的 ...

  4. 配置所需要的依赖_Maven依赖管理之依赖传递

    1 传递依赖 2.1 什么是传递依赖 当A 依赖B.B依赖C,在A中导入B后会自动导入C,C是A的传递依赖,如果C依赖D则D也可能是A的传递依赖. 演示: web中添加struts-spring的ja ...

  5. gradle 引入jar时自动引入父依赖_原创 | 看完此文,你对Gradle的理解又升级了

    前言 这一篇来介绍一些Gradle进阶的内容,当然进阶内容非常多,这篇文章就总结一些相对重要的.常用的一些知识点,比如Gradle的签名配置和依赖管理. 1.Android签名文件配置 在一般公司中, ...

  6. VC编译DLL时,如何不依赖VC运行库

    用VC编译的DLL如果依赖VC运行库,分发时如果目标系统不带对应版本的VC支行库,将导致DLl调用失败,在编译时应设置不依赖VC运行库: 在C/C++/Code Generation的Runtime ...

  7. Gradle 库依赖关系的管理

    查看库依赖关系 列出指定module的依赖关系 ./gradlew -q moduleName:dependencies > outfile.txt 列出多个指定module的依赖关系 ./gr ...

  8. java依赖_java 依赖、组合、聚合与继承

    java中类与类之间的关系 大部分的初学者只知道java中两个类之间可以是继承与被继承的关系,可是事实上,类之间的关系大体上存在五种-继承(实现).依赖.关联.聚合.组合. 接下来,简单的分析一下这些 ...

  9. 控制反转-依赖倒置-依赖注入

    控制反转:框架控制应用/组件 依赖倒置: 依赖注入:框架动态注入依赖关系到应用/组件 --------------------------------------------------------- ...

  10. Spark RDD 宽依赖窄依赖

    Spark RDD 宽依赖&窄依赖 1.窄依赖 2.宽依赖: 3.阶段的划分 4.宽依赖和窄依赖的作用: 1.窄依赖 每一个父RDD的Partition最多被子RDD的一个Partition使 ...

最新文章

  1. 如何轻松搞定 CRUD 的创建人、修改人、时间等字段的赋值
  2. 一个函数两个return
  3. 15家大数据公司被调查,数据行业面临大清洗?
  4. keyProperty=“id“ 和useGeneratedKeys=“true“作用
  5. 简单英文题 25 Sequence Search(python)
  6. Launch custom android application from android browser
  7. 卷积神经网络之AlexNet
  8. Java配置分离之Spring远程配置
  9. HTML基本标签和一些注释的问题
  10. session过期时间
  11. 自制超级精简版 360网盘6.5.2.1060(7文件,体积不到6M)
  12. 数字孪生应用案例及常用技术
  13. 微信公众号迁移,认证; 名称触发商标怎么办
  14. 使用nprobe+ntopng监控上百个路由器流量
  15. c语言上11e2是实型常量吗,在C语言中的实型变量分为两种类型.doc
  16. html 如何把文字和图片放到一行,DIV+CSS图片和文字如何显示同一行
  17. linux i2c 端口 usb,做了一个电容屏的IIC接口转USB
  18. 教你用html+js制作一个自己的点名系统,实例代码分享
  19. 2021Java笔试真题,满满干货指导
  20. 树的应用 —— 树、森林与二叉树的转换

热门文章

  1. 微信红包惊人秘密:谁最容易抢到大红包?
  2. jquery操作select、radio、checkbox表单元素
  3. Lady Gaga Feat. Colby O'Donis - Just Dance
  4. 使用WUCDCreator将SCSI、RAID、SATA、SAS驱动程序集成到光盘中
  5. cosmic中文翻译_cosmic是什么意思,cosmic翻译成中文,英译中-xyz翻译网
  6. java代码怎么动态修改xml配置文件内容_[MyBatis]-MyBatis框架-05-动态sql
  7. php如何输入错误返回,php – 从函数返回“错误”的最佳做法
  8. linux的mysql不允许连接_linux下允许mysql远程连接
  9. 在GridView中,设置字符超出单元格部分的内容用“...”表示
  10. Visual Studio 2008操作技巧