npm & yarn 包管理机制
npm 内部机制和背后的思考
npm install
- 检查配置。包括项目级、用户级、全局级、内置的 .npmrc 文件。
- 构建依赖。根据 package.json 和 lock file。如果 package-lock.json 文件存在且符合 package.json 声明的的情况下,直接读取;否则重新确认依赖的版本。
- 下载包资源。先检查本地缓存中是否存在匹配版本,有则直接解压到项目的 node_modules 中;如果没有则重新下载并添加到缓存,然后将包按依赖树解压到 node_modules 目录
- 生成 lock file
info
需要明确几点信息:
在构建依赖的时候需要结合 package.json 和 lock file 两个文件。先确认 package-lock.json 安装版本,符合规则就以此为准,否则由 package.json 声明的版本范围重新确认。特别地,若是在开发中手动更改包信息,会导致 lockfile 版本信息异常,也可能由 package.json 确认。确认好的依赖树会存到 package-lock.json 文件中。
如果一个包被多个依赖所依赖,那么第一个解析到的版本会被安装在顶层,其余的不同版本则会被安装到依赖自己的node_modules目录下
如果依赖升级,造成版本不兼容,需要多版本共存,那么仍然按照2
yarn
yarn解决了当时npm存在的问题
- 确定性。lock文件保证了依赖版本的确定性,保证在所有电脑上install都可以得到一致的结果。
- 依赖扁平化。(npm 也有相同的优化)
- 更好的网络性能。Yarn 采用了请求排队的理念,类似并发连接池,能够更好地利用网络资源;同时引入了更好的安装失败时的重试机制。(npm 较早的版本是顺序下载,当第一个包完全下载完成后,才会将下载控制权交给下一个包)
- 引入缓存机制,实现离线策略。(npm 也有类似的优化)
yarn.lock文件
以 React 为例
# 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"
info
注意一下几点:
- 所有依赖,不管是项目声明的依赖,还是依赖的依赖,都是扁平化管理。
- 依赖的版本是由所有依赖的版本声明范围确定的,具备相同版本声明范围的依赖归结为一类,确定一个该范围下的依赖版本。如果同一个依赖多个版本共存,那么会并列归类。
- 每个依赖确定的版本中,是由以下几项构成:
- 多个依赖的声明版本,且符合 semver 规范;
- 确定的版本号 version 字段;
- 版本的完整性验证字段
- 依赖列表
- 相比 npm,Yarn 一个显著区别是 yarn.lock 中子依赖的版本号不是固定版本。 也就是说单独一个 yarn.lock 确定不了 node_modules 目录结构,还需要和 package.json 文件进行配合。
yarn install
检查(checking)主要是检查项目中是否存在一些 npm 相关的配置文件,如 package-lock.json 等。如果存在,可能会警告提示,因为它们可能会存在冲突。
解析包(resolving packages)这一步主要是解析依赖树,确定版本信息等。首先获取项目 package.json 中声明的首层依赖,包括 dependencies, devDependencies, optionalDependencies 声明的依赖。接着采用遍历首层依赖的方式获取依赖包的版本信息,以及递归查找每个依赖下嵌套依赖的版本信息,并将解析过和正在解析的包用一个 Set 数据结构来存储,这样就能保证同一个版本范围内的包不会被重复解析。
- 对于没有解析过的包,首次尝试从 yarn.lock 中获取到版本信息,并标记为已解析;
- 如果在 yarn.lock 中没有找到包,则向 Registry 发起请求获取满足版本范围的已知最高版本的包信息,获取后将当前包标记为已解析。
获取包(fetching packages)这一步主要是利用系统缓存,到缓存中找到具体的包资源。首先会尝试在缓存中查找依赖包,如果没有命中缓存,则将依赖包下载到缓存中。对于没有命中缓存的包,Yarn 会维护一个 fetch 队列,按照规则进行网络请求。这里也是 yarn 诞生之初解决 npm v3 安装缓慢问题的优化点,支持并行下载。
链接包(linking dependencies)这一步主要是将缓存中的依赖,复制到项目目录下,同时遵循扁平化原则。前面说到,npm 优先将依赖安装到项目目录,因此需要将全局缓存中的依赖复制到项目。在复制依赖前,Yarn 会先解析 peerDependencies,如果找不到符合 peerDependencies 声明的依赖版本,则进行 warning 提示(这并不会影响命令执行),并最终拷贝依赖到项目中。
构建包(building fresh package)如果依赖包中存在二进制包需要进行编译,会在这一步进行。
如何破解依赖管理困境
在 npm v2 时期,安装的依赖会存在于引用依赖的 node_modules 目录,如果依赖过多,会形成一颗巨大的依赖树。这种结构虽然简单明了,但是对于大型项目十分不友好。依赖层级深对开发排查不利,并且依赖的复用也是问题。在 npm v3 中引入扁平化的概念。
- 场景一:不同 npm 版本安装依赖的结构
pkg-a@1.0.0 依赖 pkg-b@1.0.0,npm v3 是扁平化管理依赖。
- 场景二:不同 npm 版本处理依赖多版本共存问题
在场景一的基础上,安装 pkg-c@1.0.0,而它依赖另一个版本的 pgk-b@2.0.0。由于根目录下已存在 pkg-b@1.0.0 的依赖,npm v3 会把 pkg-b@2.0.0 安装到 pkg-c@1.0.0 依赖的 node_modules 目录。
为什么 pkg-b@1.0.0 在顶级,而 pkg-b@2.0.0 在子级呢?不是刚才说将高版本安装到顶层,低版本分散到各级目录。?
- 场景三:依赖的多版本的数量与依赖版本分布关系
在场景二的基础上,安装 pkg-d@1.0.0,而它也依赖 pkg-b@2.0.0。同样的,由于根目录下已存在 pkg-b@1.0.0 的依赖,npm v3 会把 pkg-b@2.0.0 安装到 pkg-d@1.0.0 依赖的 node_modules 目录。
你可能会疑问,此时存在2个 pkg-b@2.0.0 和1个 pkg-b@1.0.0,出现在顶级安装目录的不应该是 v2 版本而非 v1 版本嘛?
其实这是由依赖的安装顺序决定的,真就是依赖的某个版本如果出现在合适的时间,那么它就会被安装到顶级 node_modules 目录。不同版本的出场顺序导致依赖结构的差异,npm v3 注定不是稳定的包管理工具。
- 场景四:依赖版本存在重复和可用
在场景三的基础上,安装 pkg-e@1.0.0,它依赖 pkg-b@1.0.0。由于顶级目录已存在目标版本,因此 npm v3 会跳过该依赖的安装。
- 场景五:版本升级囧境在
场景三的基础上,如果更新了 pkg-a@2.0.0,同时它的依赖是 pkg-b@2.0.0。那么 npm v3 的执行顺序是,删除 pkg-a@1.0.0,安装 pkg-a@2.0.0,安装 pkg-b@2.0.0,留下了 pkg-b@1.0.0 在顶层目录,因此 pkg-b@2.0.0 会安装到 pkg-a@2.0.0 的 node_modules 目录。
- 场景六:依赖版本多目录存在且符合复用条件
在场景五的基础上,更新 pkg-e@2.0.0,它依赖了 pkg-b@2.0.0。那么 npm v3 的执行顺序是,删除 pkg-e@1.0.0,安装 pkg-e@2.0.0,删除 pkg-b@1.0.0,安装 pkg-b@2.0.0,于是出现以下结构。
此时你会发现,存在多个 pkg-b@2.0.0 分布在不同的 node_modules 目录,他们是不是只要在顶级目录存在一份即可?没错,我们删除 node_modules 目录重装,得到的就是你想的清晰的结构。
实际上,更优雅的方式是使用 npm dedupe 命令达到上述结构。而 yarn 在安装依赖时会自动执行 dedupe 命令。
正是由于上述一些 npm 历史的坑,所以更建议使用 yarn 作为项目协作的包管理工具。当然 npm 发展至今,很多问题已经优化掉,现在 yarn 和 npm 是两款互相看齐、互相获取灵感的依赖管理工具。
npm vs. yarn 💪
npm 和 yarn 作为两款相似的包管理工具,在一些功能实现上它们互相获取灵感。
相同点:
package.json 作为项目依赖描述文件。
lockfile 锁定版本依赖,在 yarn 中叫 yarn.lock,在 npm 中叫 package-lock.json,在 npm v7 也支持了 yarn.lock。它确保在不同机器或不同环境中,能够得到稳定的 node_modules 目录结构。
差异:
- 依赖管理策略。
- lockfile。package-lock.json 自带版本锁定+依赖结构,你想改动一些依赖,可能影响的范围要比表面看起来的复杂的多;而 yarn.lock 自带版本锁定,并没有确定的依赖结构,使用 yarn 管理项目依赖,需要 package.json + yarn.lock 共同确定依赖的结构。
注意 cnpm 不支持 package-lock
使用 cnpm install 时候,并不会生成 package-lock.json 文件。cnpm install 的时候,就算你项目中有 package-lock.json 文件,cnpm 也不会识别,仍会根据 package.json 来安装。所以这就是为什么之前你用 npm 安装产生了 package-lock.json,后面的人用 cnpm 来安装,可能会跟你安装的依赖包不一致。