前言

使用npm install这个条命令对于我们前端开发者来说应该是形成“肌肉记忆”,之前一直对 npm 的相关知识停留在“会用”的阶段,但是内部的原理却不甚了解。直到项目中出现了一次由 npm 包引起的问题后,才开始下决心对 npm 包其中的工作流程做一个详细的梳理。

npm install 大概会经过以下图例中的几个阶段,下文将会梳理各个流程的实现细节,以及设计背景。

检查 Config

这是执行npm install之后的第一个阶段,这个阶段主要做的工作是根据当前项目中 NPM 的 Config 作为启动安装的配置,在终端中输入npm config ls -l后即可查看当前的 npm config 如下图,这其中包括了我们常用的 npm 包安装源、npm 包的命名空间、缓存以及缓存的行为等

Npm 支持在不同层级中使用配置项,并且具备不同的优先级。例如上图中显示的四个层级的配置项(按照优先级从高到低逐个解释):

0.cli configs(优先级最高): 指的是当前 npm 执行命令中加入的 一些配置(和一些默认值),例如在执行时设置了 registry 后,对应配置项便会在这里输出。

Package-lock.json

从文章开头的说明图里其实可以看出,是否存在有效的 package-lock 文件将会决定从npm服务端获取包信息构建依赖树这两步工作是否要执行

Package-lock 文件实际上是npm 5.x 版本新增文件,它的作用是锁定依赖结构,即只要你目录下有 package-lock.json 文件,那么你每次执行 npm install 后生成的 node_modules 目录结构一定是完全相同的。

例如当前需要安装的依赖包如下

{"name": "npm-demo","dependencies": {"buffer": "^6.0.3","ignore": "^5.1.9"}
}

其中ignore没有其他依赖项,而_buffer_还依赖_base64-js_和_ieee754_:

安装时的请求如下图,可以看到总共发起了 8 次请求。

当存在 package-lock.json 后,可以看到网络请求如下图,总共仅发送了 4 个安包的请求:

生成的 package-lock.json 如下

除此之外,我们还知道npm包 中的模块版本都需要遵循 SemVer规范, 因此我们安装依赖包的时候版本很多时候是不固定的,这就导致某些时候会由于版本的原因出现一些意料之外的报错。

semver规范 semver.org/

但是在使用 package-lock 后,这个问题得以解决。一旦执行npm install 后, 所有依赖包的完整性信息(版本、包的下载地址、sha512 文件摘要)都将会保存在这个文件中,后续如果需要重新安装依赖,则会直接从 package-lock 文件中读取,极大的提升了安装的效率。

构建依赖树

所谓依赖树就是 npm 包与其依赖包之间的关系,主要体现在node_modules内部的目录结构上。这里根据 NPM 在不同时期的表现上,可以分为两种·嵌套结构扁平结构

嵌套模式

在早期的 npm1npm2 中呈现出的是嵌套结构,就是说了每个依赖项自己的依赖都是存放自己的 node_modules 文件夹下。例如上面上面的例子中,执行 npm install 后,得到的 node_modules 中模块目录结构就是下面这样的:

这样的方式优点很明显, node_modules 的结构和 package.json 结构一一对应,层级结构明显,并且保证了每次安装目录结构都是相同的。

但是,试想一下如果 base64-js 当中又有依赖,那么又会继续嵌套下去,这样的设计存在一些问题:

0.依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下(文件路径最大长度为 260 个字符)。
1.大量重复的包被安装,文件体积超级大。比如上例中的 buffer 同级目录下的ignore如果也依赖相同版本的base64-js,那么 base64-js 会分别在两者的 node_modules 中被安装,也就是重复安装。

扁平模式

为了解决以上问题,NPM3.x 版本做了一次较大更新。其将早期的嵌套结构改为扁平结构:

  • 安装模块时,不管其是直接依赖还是子依赖的依赖,优先将其安装在 node_modules 根目录。

还是上面的依赖结构,我们在执行 npm install 后将得到下面的目录结构:

可以看出buffer的两个依赖包都被安装到了 node_modules 根目录下。这确实缓解了嵌套模式的两个问题,减少了包的冗余

此时我们若在模块中又依赖了 base64-js@1.0.1 版本:

{"name": "npm-demo","dependencies": {"base64-js": "1.0.1","buffer": "^6.0.3","ignore": "^5.1.9"}
}
  • 当安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的 node_modules 下安装该模块。

此时,我们在执行 npm install 后将得到下面的目录结构:

由于 buffer 依赖的 base64-JS 与当前项目依赖的 base64-JS 版本范围并不一致,因此前者被安装在了 buffer 文件夹下。

对应的,如果我们在项目代码中引用了一个模块,模块查找也进行了调整,流程如下:

  • 在当前模块路径下搜索
  • 在当前模块 node_modules 路径下搜素
  • 在上级模块的 node_modules 路径下搜索
  • 直到搜索到全局路径中的 node_modules

假设我们又依赖了一个包 buffer2@^6.0.3,而它也依赖了包 base64-js@^1.3.1,则此时的安装结构是下面这样的:

不难看出,由于项目存在base64-js@1.0.1不满足其他模块依赖的版本范围,因此仍然出现了冗余。所以 npm 3.x 版本并未完全解决老版本的模块冗余问题——甚至还会带来新的问题。

这里梳理了一下扁平化主要的问题:

0.依赖结构的不确定性
1.扁平化算法本身的复杂性很高,耗时较长。
2.项目中可以非法访问没有声明过依赖的包

后两个其实比较好理解,至于第一个依赖结构的不确定性这里可以举个例子解释下

假设两个不同的模块分别依赖了同一个模块,但是版本范围不一致

当项目用依赖了这两个模块,在构建依赖树时是这样的?

又或是这样?

答案是都有可能,这两种形式完全取决于buffer和 buffer2 在 package.json 中的声明顺序。

另外,为了让开发者在安全的前提下使用最新的依赖包,我们在 package.json 通常只会锁定大版本,这意味着在某些依赖包小版本更新后,同样可能造成依赖结构的改动,依赖结构的不确定性可能会给程序带来不可预知的问题。

这就是为什么会产生依赖结构的不确定问题,也是 package-lock 文件诞生的原因——为了保证 install 之后都产生确定的node_modules结构。

缓存

为了加快 npm 安装的效率,在执行 npm installnpm update命令下载依赖后,会存放到本地的缓存目录中,然后再将对应的的依赖复制到项目的node_modules 目录下。

通过 npm config get cache 命令可以查询到:在 LinuxMac 默认是用户主目录下的 .npm/_cacache 目录。

在这个目录下又存在两个目录:content-v2index-v5content-v2 目录用于存储 tar包的缓存,而index-v5目录用于存储tar包的 hash

npm(5.x) 在执行安装时,可以根据 package-lock.json 中存储的 integrity、version、name 生成一个唯一的 key 对应到 index-v5 目录下的缓存记录,从而找到 tar包的 hash,然后根据 hash 再去找缓存的 tar包直接使用。

我们可以找一个包在缓存目录下搜索测试一下,在 index-v5 搜索一下包路径:

grep https://npm.corp.kuaishou.com/base64-js/-/base64-js-1.0.1.tgz -r index-v5

命令执行后会返回对应的 hash 存放路径,这里是0d/dd目录下

打开文件,并格式化为 JSON 后,可以看到存在_shasum 字段,该字段表示这个库的缓存 hash,该值的前四位便表示在 content-v2/sha1 中的路径,例如这里是 6926,则表示content-v2/sha1/69/26目录下

基于缓存数据,npm 提供了离线安装模式,分别有以下几种:

  • --prefer-offline: 优先使用缓存数据,如果没有匹配的缓存数据,则从远程仓库下载(默认)。
  • --prefer-online: 优先使用网络数据,如果网络数据请求失败,再去请求缓存数据,这种模式可以及时获取最新的模块。
  • --offline: 不请求网络,直接使用缓存数据,一旦缓存数据不存在,则安装失败。

文件完整性

上面我们多次提到了文件完整性,那么什么是文件完整性校验呢?

在下载依赖包之前,我们一般就能拿到 npm 对该依赖包计算的 hash 值,例如我们执行 npm info 命令,紧跟 tarball(下载链接) 的就是 shasum(hash) :

用户下载依赖包到本地后,需要确定在下载过程中没有出现错误,所以在下载完成之后需要在本地在计算一次文件的 hash 值,如果两个 hash 值是相同的,则确保下载的依赖是完整的,如果不同,则进行重新下载。

整体流程梳理

  • 检查 config* 检查项目中有无 lock 文件。* 无 lock 文件:* 从 npm 远程仓库获取包信息* 根据 package.json 构建依赖树,构建过程:* 构建依赖树时,不管其是直接依赖还是子依赖的依赖,优先将其放置在 node_modules 根目录。* 当遇到相同模块时,判断已放置在依赖树的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的 node_modules 下放置该模块。* 注意这一步只是确定逻辑上的依赖树,并非真正的安装,后面会根据这个依赖结构去下载或拿到缓存中的依赖包* 在缓存中依次查找依赖树中的每个包* 不存在缓存:* 从 npm 远程仓库下载包* 校验包的完整性* 校验不通过:* 重新下载* 校验通过:* 将下载的包复制到 npm 缓存目录* 将下载的包按照依赖结构解压到 node_modules* 存在缓存:将缓存按照依赖结构解压到 node_modules* 将包解压到 node_modules* 生成 lock 文件看见未来

除了 npm 之外,还有还有第三方包管理工具值得推荐,这里介绍两款分别是yarnpnpm

YARN

yarn 是在 2016 年发布的,那时 npm 还处于 V3 时期,那时候还没有 package-lock.json 文件,就像上面我们提到的:不稳定性、安装速度慢等缺点经常会受到广大开发者吐槽。此时,yarn 诞生:

上面是官网提到的 yarn 的优点,在那个时候还是非常吸引人的。当然,后来 npm 也意识到了自己的问题,进行了很多次优化,在后面的优化(lock文件、缓存、默认-s…)中,我们多多少少能看到 yarn 的影子,可见 yarn 的设计还是非常优秀的。

yarn 也是采用的是 npm v3 的扁平结构来管理依赖,执行yarn安装依赖后默认会生成一个 yarn.lock 文件,还是上面的依赖关系,我们看看 yarn.lock 的结构:

可见其和 package-lock.json 文件还是比较类似的。

yarn 的缓存结构和 npm v5 之前的比较像,每个缓存的模块被存放在独立的文件夹,文件夹名称包含了模块名称、版本号以及 hash 等信息。使用命令 yarn cache dir 可以查看缓存数据的目录:

在缓存策略上和 npm 相反,yarn 默认使用 prefer-online 模式,即优先使用网络数据,如果网络数据请求失败,再去请求缓存数据。

除此之外,Yarn 还具有 pnp 的一个特性 「classic.yarnpkg.com/en/docs/pnp…」

按照普通的按照流程, npm/yarn 会生成一个 node_modules 目录, 然后 Node 按照它的模块查找规则在 node_modules 目录中查找. 但实际上 Node 并不知道这个模块是什么, 它在 node_modules 查找, 没找到就在父目录的 node_modules 查找, 以此类推. 这个效率是非常低下的.

但是 Yarn 作为一个包管理器, 它知道你的项目的依赖树. 那能不能让 Yarn 告诉 Node? 让它直接到某个目录去加载模块. 这样即可以提高 Node 模块的查找效率, 也可以减少 node_modules 文件的拷贝. 这就是Plug'n'Play的基本原理.

简单来说在 pnp 模式下, Yarn 不会创建 node_modules 目录, 取而代之的是一个.pnp.js文件, 这是一个 node 程序, 这个文件包含了项目的依赖树信息, 模块查找算法等等。

yarn 默认并不会使用Pnp,需要在 package.json 中加入下面的配置后,重新安装依赖即可

 "installConfig": {"pnp": true}

PNPM

pnpm 是新一代的现代化包管理工具,它的**官方文档**是这样说的:

Fast, disk space efficient package manager

  • 包安装速度极快;
  • 磁盘空间利用高效。

安装:

npm i pnpm -g

速度快

这里放一张社区上的数据对比图,作为黄色部分的 pnpm,在绝多大数场景下,包安装的速度都是明显优于 npm/yarn/yarn PNP,绝大部分场景下速度甚至比他们快 2-3 倍。

依赖树(高效利用磁盘空间)

之前不论是 NPM 还是 YARN(非 PNP 模式),都没有彻底解决规避非法访问依赖和重复安装的风险,pnpm 的作者Zoltan Kochan发现 yarn 并没有打算去解决上述的这些问题,于是另起炉灶,写了全新的包管理器,开创了一套新的依赖管理机制。

还是以上面的依赖举例,执行安装

pnpm i

我们再去看看node_modules:

我们直接就看到那三个熟悉的依赖包,但值得注意的是这三个依赖后面都有小箭头,表示他们都只是软链接,指向的地址实际在.Pnpm 文件夹中

例如 buffer@6.0.3,存放的实际位置为

注意这里有三个文件,其中 base64-js/ieee754 是软链,被指向了.pnpm 目录下的对应包地址。

而项目根目录下 node_module 中指向的 buffer 即指向这里的 buffer 文件夹。

包本身依赖放在同一个node_module下面,与原生 Node 完全兼容,又能将 package 与相关的依赖很好地组织到一起,设计十分精妙。通过软链实现的嵌套模式,将之前最大的问题——模块安装冗余问题得到解决。

并且 pnpm 这种依赖管理的方式也很巧妙地规避了在 npm/yarn 中暂未得到解决的非法访问依赖的问题,也就是只要一个包未在 package.json 中声明依赖,那么在项目中是无法访问的。

支持 monorepo

monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的packages目录下,那么一个子项目就代表一个package。并通过 lerna 来进行管理。这里包括 elementUI-plus 以及我们的业务也都用到了这项技术概念。

我们在实际开发中可能都会遇到如果想要给所有 package 添加一个包,需要在项目根目录下执行 npm i 后还要执行一遍 lerna 的命令。而 pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,体现在各个子命令的功能上,比如在根目录下 pnpm add A -r, 那么所有的 package 中都会被添加 A 这个依赖,当然也支持 --filter字段来对 package 进行过滤。

总结

录下,那么一个子项目就代表一个package。并通过 lerna 来进行管理。这里包括 elementUI-plus 以及我们的业务也都用到了这项技术概念。

我们在实际开发中可能都会遇到如果想要给所有 package 添加一个包,需要在项目根目录下执行 npm i 后还要执行一遍 lerna 的命令。而 pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,体现在各个子命令的功能上,比如在根目录下 pnpm add A -r, 那么所有的 package 中都会被添加 A 这个依赖,当然也支持 --filter字段来对 package 进行过滤。

总结

目前来看 npm 在实际开发中还是有一定的缺陷,但是不得不承认它是成熟和稳定的一项包管理工具。并且它随 node 一起提供,目前能以足够好的方式处理包管理。作为开发者,我更希望 npm 能够受到社区这些有意思的包管理工具中受到启发(就像 yarn 一样),提供更好用的 npm。

你是这样的npm install相关推荐

  1. npm install 提示权限不足 Error: EPERM: operation not permitted, unlink XXX

    问题描述 使用npm install出现 npm install 提示权限不足 Error: EPERM: operation not permitted, unlink XXX提示 原因 这里原因有 ...

  2. npm i和npm install的区别

    最近人用npm i来直接安装模块,但是有会报错,用npm install就不会报错,刚开始百思不得其解,它俩明明是同一个东西 后来查npm的帮助指令发现还是没区别,npm i仅仅是npm instal ...

  3. npm install 报错 npm ERR! code Z_BUF_ERROR 问题解决

    问题描述: 使用npm install命令安装依赖时,出现错误,报错信息如下: npm ERR! code Z_BUF_ERROR npm ERR! errno -5 npm ERR! zlib: u ...

  4. nodejs npm install -g 全局安装和非全局安装的区别

    1. npm install xxx -g 时, 模块将被下载安装到[全局目录]中. [全局目录]通过 npm config set prefix "目录路径" 来设置. 比如说, ...

  5. npm中package-lock.json的作用:npm install安装时使用

    简单理解: XYZ 的格式 对应为: 主版本号.次版本号.修订号,版本号递增规则如下: 主版本号:当你做了不兼容的 API 修改, 次版本号:当你做了向下兼容的功能性新增, 修订号:当你做了向下兼容的 ...

  6. npm install出现的错误

    在使用cnpm 安装node依赖包的时候,出现上述错误,网上搜索,说是有两个解决方案: 虽然提示不适合Windows,但是问题好像是sass loader出问题的.所以只要执行下面命令即可: 方案一: ...

  7. npm install 报权限错误,permission denied

    解决办法: 添加--unsafe-perm 参数,如 #npm install --registry=https://registry.npm.taobao.org  --unsafe-perm 说明 ...

  8. python中install语法错误_在“ npm install”之后,出现有关python中语法错误的错误吗?...

    我正在尝试为Exokit安装必要的依赖项,但是却收到与Python语法错误有关的错误. 这是我想尝试在浏览器中涉及VR的新内容.我已经从他们的github重新克隆了存储库,并直接从他们的网站下载了.我 ...

  9. react-antd项目中重新npm  install  导致自动升级antd版本,引发的样式问题

    ​ 解决方式: 1.指定安装固定ant版本: npm i antd@2.10.4 -save (在 node_modules同级目录下) 原因: 尽量不要直接用npm install(全局安装)命令, ...

  10. npm全局安装和本地安装和本地开发安装(npm install --g/--save/--save-dev)

    详细说明参考:http://www.cnblogs.com/PeunZhang/p/5629329.html 我个人理解: 1.全局安装(npm install -g)是为了用命令行,比如在windo ...

最新文章

  1. AI人才「用工荒」如何解决?看看这几家顶级公司的应对策略
  2. python之matloplib可视化
  3. 资讯|WebRTC M92 更新
  4. java高级断言_Java之断言
  5. mysql_常用命令
  6. 哇,居然可以用这种烙铁头拆元器件!!!
  7. .NET(C#)连接各类数据库
  8. 中文字符匹配java_java正则匹配HTML中a标签里的中文字符示例
  9. Java笔记-通过放射获取类中成员名及调用get方法及map构造JSON数据
  10. 利用fat jar插件生成可执行jar文件
  11. 单反相机很久没有更新产品问世了,真的已经被抛弃了吗?
  12. 瑞友杯虚拟化征文---瑞友天翼应用虚拟化之实战演示
  13. 领域驱动设计(DDD)入门概要
  14. java 支持 超大上G , 多附件上传
  15. 基于raspbian+motion的家庭监控网络
  16. 2021年WordPress博客装修美化(一)
  17. HTTP 错误 404.17 - Not Found 请求的内容似乎是脚本,因而将无法由静态文件处理程序来处理
  18. 转:衡量数据的离散程度
  19. JNDI注入学习(看不懂直接喷,别忍着!)
  20. 策略模式——多种发票上传实现案例

热门文章

  1. MFC多窗口切换—如TabControl
  2. win10安装autocad 2013出现command line option syntax error
  3. 【高通SIM卡】 单双卡NV配置
  4. 介绍一个法国的时间戳服务器
  5. 2021 IEEE编程语言排行榜:Python排名榜首
  6. 凡是过往,皆为序章!2020 总结与期待
  7. Z - Bitset
  8. 通过零代码ETLCloud实现马帮ERP数据自动化同步
  9. webpack打包发布(完结)
  10. 基于深度强化学习的长期推荐系统