Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

npm依赖管理那些事 #7

Open
stormqx opened this issue Jun 30, 2019 · 0 comments
Open

npm依赖管理那些事 #7

stormqx opened this issue Jun 30, 2019 · 0 comments
Labels

Comments

@stormqx
Copy link
Owner

stormqx commented Jun 30, 2019

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来指定所需的依赖包版本号和版本范围。常用的规则如下表:

类型 符号 含义 Range
Caret Ranges ^ 指定的 MAJOR 版本号下, 所有更新的版本 ^1.2.3 := >=1.2.3 <2.0.0
Tilde Ranges ~ 指定 MAJOR.MINOR 版本号下,所有更新的版本 ~1.2.3 := >=1.2.3 <1.3.0
X-Ranges Xx* 指定任何的Xx*以后的版本号下,所有更新的版本 *:= >=0.0.0(任何版本满足)
1.x:= >=1.0.0 <2.0.0(匹配主要版本)
1.2.x:= >=1.2.0 <1.3.0(匹配主要版本和次要版本)
Hyphen Ranges - 制定版本范围之间的所有更新的版本 1.2.3 - 2.3.4 := >=1.2.3 <=2.3.4

此外,任意两条规则,用空格连接起来,表示“与”逻辑,即两条规则的交集: 如 >=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.json package-lock.json),(不同npm版本的策略不同,后续我们会详细介绍)。如 package.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合1.x.x形式的最新版本。
    • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
    • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
  4. 模块扁平化(npm3后支持)

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

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

    • 嵌套式

      +-------------------------------------------+
      |                   app/                    |
      +----------+------------------------+-------+
                 |                        |
                 |                        |
      +----------v------+       +---------v-------+
      |                 |       |                 |
      |    A@1.0.0      |       |      B@2.0.0    |
      |                 |       |                 |
      +--------+--------+       +--------+--------+
               |                         |
         +-----v-----+             +-----v-----+
         |  C@1.0.0  |             |  C@1.0.0  |
         +-----------+             +-----------+
      
      

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

    • 扁平式

               +-------------------------------------------+
               |                   app/                    |
               +-+---------------------------------------+-+
                 |                                       |
                 |                                       |
      +----------v------+    +-------------+   +---------v-------+
      |                 |    |             |   |                 |
      |  A@1.0.0.       |    |  C@1.0.0    |   |      B@2.0.0    |
      |                 |    |             |   |                 |
      +-----------------+    +-------------+   +-----------------+
      
      

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

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

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

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

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

               +-------------------------------------------+
               |                   app/                    |
               +-+---------------------------------------+-+
                 |                                       |
                 |                                       |
      +----------v------+    +-------------+   +---------v-------+
      |                 |    |             |   |                 |
      |  A@1.0.0        |    | C@1.0.0     |   |      B@2.0.0    |
      |                 |    |             |   |                 |
      +-----------------+    +-------------+   +--------+--------+
                                                        |
                                                        | 
                                               +--------v--------+
                                               |                 | 
                                               |      C@2.0.0    |
                                               |                 |
                                               +-----------------+
      

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

               +-----------------------------------------------------------------+
               |                   app/                                          |
               +-+---------------------------------------+---------------------+-+
                 |                                       |                     |
                 |                                       |                     |
      +----------v------+    +-------------+   +---------v-------+      +-------------+
      |                 |    |             |   |                 |      |             |
      |  A@1.0.0        |    | C@1.0.0     |   |      B@2.0.0    |      | D@1.0.0     |
      |                 |    |             |   |                 |      |             |
      +-----------------+    +-------------+   +--------+--------+      +------+------+
                                                        |                      |
                                                        |                      |
                                               +--------v--------+    +--------v--------+
                                               |                 |    |                 |
                                               |      C@2.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的能力。

实现方案

hawkeye

例子

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

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

相关链接

@stormqx stormqx changed the title npm依赖管理 npm依赖管理那些事 Jun 30, 2019
@stormqx stormqx added the npm label Jun 30, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant