相关参考,推荐阅读
https://classic.yarnpkg.com/en/docs/
https://www.sitepoint.com/yarn-vs-npm/
https://mp.weixin.qq.com/s/Yr2_Jr0wv3yFDEQuGVqdyw
https://docs.npmjs.com/cli/v7/commands/npm-install
https://classic.yarnpkg.com/en/docs/cli/install
https://classic.yarnpkg.com/blog/2016/11/24/lockfiles-for-all/
背景
做为一个Web前端,项目开发时经常会用到 npm
和 yarn
。平常只是用于安装项目依赖并没有过多研究内部更新机制及理念,本文在这里为大家介绍一下。
在开始之前我们可以先带着问题去阅读方便我们更深入理解,使用 npm
或 yarn
管理项目依赖时,常遇到的问题:
- 项目依赖出现问题怎么办?常见操作删除
node_modules
,重新install
,是否有风险? - 把所有依赖都安装到 dependencies 中,不区分 devDependencies 会有问题吗?
- 我们的应用依赖了 pkg-a 和 pkg-b,同时 pkg-a 也依赖了 pkg-b,那么 pkg-b 会被多次安装或重复打包吗?
- 项目开发时我使用 npm 同事使用 yarn,这会引发什么问题?
- 是否要提交 lockfile(package-lock.json/yarn.lock) 到项目仓库呢?
- lockfile 在 git 操作时,时常会出现大量的冲突,你是怎么解决的呢?
npm 内部机制和背后的思考
先来看下第一个问题, “删除 node_modules,重新 install” 这样解决依赖安装问题百试不爽,其中的原理是什么?这样做是否存在风险?下面我们一起探究一下。
npm 会优先将依赖包安装到项目目录。 这样做的好处是使不同项目的依赖各成体系,同时还减轻了包作者的 API 压力;缺点也比较明显,如果我们的 repo_a 和 repo_b 都有一个相同的依赖 pkg_c,那么这个公共依赖将在两个项目中各被安装一次。也就是说,同一个依赖可能在我们的电脑上多次安装。
npm install
上图是 npm 安装依赖大致的过程,其中这样几个步骤需要关注:
1、检查配置。包括项目级、用户级、全局级、内置的 .npmrc 文件。
2、确定依赖版本,构建依赖树。确定项目依赖版本有两个来源,一是 package.json 文件,一是 lockfile 文件,两个确认版本、构建依赖树的来源,互不可少、相辅相成。如果 package-lock.json 文件存在且符合 package.json 声明的的情况下,直接读取;否则重新确认依赖的版本。
3、下载包资源。下载前先确认本地是否存在匹配的缓存版本,如果有就直接使用缓存文件,如果没有就下载并添加到缓存,然后将包按依赖树解压到 node_modules 目录。
4、生成 lockfile 文件。
可以确认这样几个逻辑:
1、构建依赖树的过程中,版本确认需要结合 package.json 和 package-lock.json 两个文件。先确认 package-lock.json 安装版本,符合规则就以此为准,否则由 package.json 声明的版本范围重新确认。特别地,若是在开发中手动更改包信息,会导致lockfile 版本信息异常,也可能由 package.json 确认。确认好的依赖树会存到 package-lock.json 文件中,这里跟 yarn.lock 存在差异。
2、同一个依赖,更高版本的包会安装到顶层目录,即 node_modules 目录;否则会分散在某些依赖的 node_modules 目录,如:node_modules/expect-jsx/node_modules/react 目录。
3、如果依赖升级,造成版本不兼容,需要多版本共存,那么仍然是将高版本安装到顶层,低版本分散到各级目录。
4、lockfile 的存在,保证了项目依赖结构的确定性,保障了项目在多环境运行的稳定性。
yarn 安装理念以及破解依赖管理困境
yarn 作为区别于 npm 的依赖管理工具,诞生之初就是为了解决历史上 npm 的某些不足,比如 npm 缺乏对于依赖的完整性和一致性保障,以及 npm 安装速度过慢的问题等,尽管 npm 发展至今,已经在很多方面向 yarn 看齐,但 yarn 的安装理念仍然需要我们关注。yarn 提出的安装理念很好的解决了当时 npm 的依赖管理问题:
1、确定性。通过 yarn.lock 等机制,保证了确定性,这里的确定性包括但不限于明确的依赖版本、明确的依赖安装结构等。即在任何机器和环境下,都可以以相同的方式被安装。
2、模块扁平化安装。将依赖包的不同版本,按照一定策略,归结为单个版本,以避免创建多个副本造成冗余。(npm 也有相同的优化)
3、更好的网络性能。Yarn 采用了请求排队的理念,类似并发连接池,能够更好地利用网络资源;同时引入了更好的安装失败时的重试机制。(npm 较早的版本是顺序下载,当第一个包完全下载完成后,才会将下载控制权交给下一个包)
4、引入缓存机制,实现离线策略。(npm 也有类似的优化)
yarn.lock 文件结构
以 react 等依赖为例,先大致了解一下 yarn.lock 文件的结构以及确定依赖版本的方式:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
expect-jsx@^5.0.0:
version "5.0.0"
resolved "[http://registry.npmjs.org/expect-jsx/-/expect-jsx-5.0.0.tgz#61761b43365f285a80eb280c785e0783bbe362c7](http://registry.npmjs.org/expect-jsx/-/expect-jsx-5.0.0.tgz#61761b43365f285a80eb280c785e0783bbe362c7 "http://registry.npmjs.org/expect-jsx/-/expect-jsx-5.0.0.tgz#61761b43365f285a80eb280c785e0783bbe362c7")"
integrity sha1-YXYbQzZfKFqA6ygMeF4Hg7vjYsc=
dependencies:
collapse-white-space "^1.0.0"
react "^16.0.0"
react-element-to-jsx-string "^13.0.0"
react-rater@^6.0.0:
version "6.0.0"
resolved "[http://registry.npmjs.org/react-rater/-/react-rater-6.0.0.tgz#2e666b6e5e5c33b622541df6a7124f6c99606927](http://registry.npmjs.org/react-rater/-/react-rater-6.0.0.tgz#2e666b6e5e5c33b622541df6a7124f6c99606927 "http://registry.npmjs.org/react-rater/-/react-rater-6.0.0.tgz#2e666b6e5e5c33b622541df6a7124f6c99606927")"
integrity sha512-NP1+rEeL3LyJqA5xF7U2fSHpISMcVeMgbQ0u/P1WmayiHccI7Ixx5GohygmJY82g7SxdJnIun2OOB6z8WTExmg==
dependencies:
prop-types "^15.7.2"
react "^16.8.0"
react-dom "^16.8.0"
//一或多个具有相同版本范围的依赖声明,确定一个可用的版本。这就是 lockfile 的确定性。
react@^16.0.0, react@^16.8.0:
version "16.14.0"
resolved "[http://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d](http://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d "http://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d")"
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
//如果同一个依赖存在多个版本,那么最高版本安装在顶层目录,即 node_modules 目录。
react@^17.0.1:
version "17.0.2"
resolved "[http://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037](http://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037 "http://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037")"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
从上面依赖版本描述的信息中,可以确定以下几点:
1、所有依赖,不管是项目声明的依赖,还是依赖的依赖,都是扁平化管理。
2、依赖的版本是由所有依赖的版本声明范围确定的,具备相同版本声明范围的依赖归结为一类,确定3、一个该范围下的依赖版本。如果同一个依赖多个版本共存,那么会并列归类。
4、每个依赖确定的版本中,是由以下几项构成:
- 多个依赖的声明版本,且符合 semver 规范;
- 确定的版本号 version 字段;
- 版本的完整性验证字段
- 依赖列表
5、相比 npm,Yarn 一个显著区别是 yarn.lock 中子依赖的版本号不是固定版本。 也就是说单独一个 yarn.lock 确定不了 node_modules 目录结构,还需要和 package.json 文件进行配合。
yarn install
以下是在 yarn 安装依赖时的步骤:
1、检查(checking) 主要是检查项目中是否存在一些 npm 相关的配置文件,如 package-lock.json 等。如果存在,可能会警告提示,因为它们可能会存在冲突。在这一阶段,也会检查系统 OS、CPU 等信息。
2、解析包(resolving packages) 这一步主要是解析依赖树,确定版本信息等。首先获取项目 package.json 中声明的首层依赖,包括 dependencies, devDependencies, optionalDependencies 声明的依赖。接着采用遍历首层依赖的方式获取依赖包的版本信息,以及递归查找每个依赖下嵌套依赖的版本信息,并将解析过和正在解析的包用一个 Set 数据结构来存储,这样就能保证同一个版本范围内的包不会被重复解析。
- 对于没有解析过的包,首次尝试从 yarn.lock 中获取到版本信息,并标记为已解析;
- 如果在 yarn.lock 中没有找到包,则向 Registry 发起请求获取满足版本范围的已知最高版本的包信息,获取后将当前包标记为已解析。
总之,在经过复杂的解析算法后,我们就确定了所有依赖的具体版本信息以及下载地址。
3、获取包(fetching packages) 这一步主要是利用系统缓存,到缓存中找到具体的包资源。首先会尝试在缓存中查找依赖包,如果没有命中缓存,则将依赖包下载到缓存中。对于没有命中缓存的包,Yarn 会维护一个 fetch 队列,按照规则进行网络请求。这里也是 yarn 诞生之初解决 npm v3 安装缓慢问题的优化点,支持并行下载。
如何判断有没有命中缓存?
判断系统中存在符合 "cachefolder+slug+node_modules+pkg.name" 规则的路径,如果存在则判断为命中缓存,否则就会重新下载。值得注意的是,不同版本的包在缓存中是扁平化管理。以下是缓存中 webpack 的依赖缓存,可以通过 yarn cache dir 查看。
4、链接包(linking dependencies) 这一步主要是将缓存中的依赖,复制到项目目录下,同时遵循扁平化原则。前面说到,npm 优先将依赖安装到项目目录,因此需要将全局缓存中的依赖复制到项目。在复制依赖前,Yarn 会先解析 peerDependencies,如果找不到符合 peerDependencies 声明的依赖版本,则进行 warning 提示(这并不会影响命令执行),并最终拷贝依赖到项目中。
5、构建包(building fresh package) 如果依赖包中存在二进制包需要进行编译,会在这一步进行。
如果破解依赖管理困境
在 npm v2 时期,安装的依赖会存在于引用依赖的 node_modules 目录,如果依赖过多,会形成一颗巨大的依赖树。这种结构虽然简单明了,但是对于大型项目十分不友好。依赖层级深对开发排查不利,并且依赖的复用也是问题。在 npm v3 中引入扁平化的概念。看几个场景的例子