阅读 731

前端包管理工具原理(npm && yarn)

npm

install 的时候都做了些什么 ?

在我们敲下 npm install 的时候 , npm 会做以下几件事情 .

  1. 检查 config

npm 执行会先读取 npm config 和 npmrc .

npmrc 是有权重的 ,

项目级 npmrc > 用户级 npmrc > 全局 npmrc > npm 内置 npmrc

  1. 检查是否存在 package-lock.json
  • 如果有

    • 跟当前 package-lock 里声明的版本是否一致

      • 一致

      • 检查缓存

    • 不一致

      • 如果版本不一致 , 对应处理方法跟 npm 版本有关 . 在最新版本的 npm 中, 会检查依赖包兼容版本, 如果版本能兼容 , 则按照 package-lock 版本安装 , 反之按照 package.json 版本安装

      • (因为 npm 版本不同会导致处理方式不同 , 所以这里有一个最佳实践 , 团队的 npm 版本要相同)

  • 如果没有

    • 获取依赖包的信息

    • 构建依赖树

    • 扁平化

    • 检查缓存

  1. 检查缓存

    • 如果有缓存
      • 将对应缓存解压到 node_modules 下

      • 生成 package-lock

    • 如果没有缓存
      • 下载资源包

      • 检查资源包完整性

      • 添加到缓存中

      • 解压到 node_modules

      • 生成 package-lock

以上就是我们执行 npm install 的过程 .

可以看得出来 , 缓存功能在 npm 整体架构中起到非常关键的作用 . 每次安装依赖时都会对对应包创建缓存 . 那我们如何查看缓存呢 ?

npm 缓存

我们可以通过

npm config get cache
复制代码

这个命令获取 npm 缓存在本地的路径 . 我们进入该目录 , 进入 _cacache 目录 , npm 的缓存就放在该目录下

~/.npm/_cacache » ls 

content-v2 index-v5 tmp
复制代码

其中 content-v2 是 缓存2进制文件 , index-v5是缓存对应索引.

如何生成缓存

npm install 运行过程中 , 通过 pacote 先将依赖包下载到缓存中 , 把相应的包解压在 node_modules 下 .

pacote 依赖 npm-registry-fetch 来下载包, 在给指定路径下根据 IETF RFC 7234 生成缓存数据 .

如何利用缓存

在每次安装资源时 , 根据 package-lock.json 中存储的 integrity , version , name 信息生成一个唯一的 key . 这个 key 能对应到 index-v5 目录下的缓存记录 . 如果发现有缓存资源 , 就会找到 tar 包的 hash, 再次通过 pacote 把对应 2 进制文件 解压到对应 node_modules 下.

这样缓存的工作流程就结束啦 ~

npm link

想象一下 , 我们现在面临以下场景 .

我们开发了个依赖包 , 我们想要测试以下我们的依赖包 , 但是我们无法发布未经过测试的依赖包. 这下就成死循环了 ...

我们想到了一个最原始的方法 : 手动将依赖放到 node_modules 下 . 这样我们就可以开始测试了 . 这样虽然满足了当前的需求 , 但是一直手动 cv , 总是不优雅的 . 那么有没有一种能方法能解决这个需求呢 ?

我们的主角 npm link 登场

npm link的本质是创建一个软连接

工作原理是 : 将其链接到全局 node 模块安装路径中 . 为目标 npm 模块的可执行 bin 文件创建软连接 将其连接到全局 node 命令安装路径中 .

npx

npx 可以直接执行 node_modules/.bin 下的文件 , 可以自动去 node_modules/.bin 路径和环境变量 $path 中检查命令是否存在 .

npx 会在执行模块时 优先安装依赖 , 但是在安装结束后便删除该依赖 , 优点是 : 避免全局安装模块

(所以我们 npx create-react-app xxx 会默认安装 cra , 完成之后删除)

npm 中的源

npm 的本质就是个查询服务 (默认是 npmjs.org : registry.npmjs.org/)

我们可以使用 npm config set registry xxx来切换我们的源.

如果我们遇到了 , 不同的包的源不同 , 我们可以使用 npm 钩子 preinstall 执行 node 脚本 , 实现源切换 .

部署私有化 npm 的好处

  1. 确保高速 稳定的 npm 服务, 使发布私有模块更加安全

  2. 审核机制可保证模块质量和安全

yarn

install 的时候都做了些什么 ?

  1. 检测包

    • 检测是否有 npm 相关文件
    • 检查 os cpu 等信息
  2. 解析包

    • 获取 package.json 中依赖 遍历首层依赖获取依赖包版本信息(dependencies 和 devDependencies) , 递归获取嵌套依赖(首层依赖包中所需要的依赖)版本信息 , 将解析过 和 正在解析的包 用一个 set 存储 , 保证同一版本范围的包不会重复解析 .

    • 对于没解析过的包 , 会尝试获取版本 , 并标记成解析过 .

    • 如果在 yarn-lock 中没有找到包的声明 , 则向 registry 发起请求 , 获取满足版本范围的最高版本的包信息 获取后 标记成已解析 .

      -(这个流程结束后 得到了所有依赖的版本信息 和 下载地址)

  3. 获取包

    • 检查缓存中是否存在当前依赖包 , 不存在的包下载到缓存目录

    • yarn 根据 cacheFolder + slug + node_modules + pkg.name 生成一个 path 判断系统中是否存在该 path 证明是否有缓存

    • 对于没有命中缓存的包 yarn 会维护一个 fetch 队列 按照规则进行网络请求

  4. 链接包

    • 解析 peerDependencies (peerDependencies的作用是提示宿主环境去安装peerDependencies所指定依赖的包,在导入所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题.)

    • 如果发现冲突 给 warning 提示

    • 之后扁平化依赖树

    • 执行拷贝任务

    • 解压到 node_modules

  5. 构建包

    • 如果有 2进制包等需要构建的包 , 在这一步进行构建

为什么 npm 和 yarn 一致追求扁平化

如果让我们设计一个依赖系统 , 我们第一时间想到的应该就是 树 结构

生成树 表面很好 , 可以满足我们的需求 , 但是如果

  1. 项目依赖 A B , A B都依赖相同版本的依赖C , 重复安装会浪费资源

此时的依赖结构如下


app
|
-- A : V1.0 -> C : V1.0
-- B : V1.0 -> C : V1.0

复制代码

如果 C 又依赖于 D 呢? ... 感觉越来越不可控了 .

  1. 路径太长 windos 删除 node_modules 可能会出现问题.

这里就引出了一个词 依赖地狱

依赖地狱

通俗而言,“依赖地狱”指开发者安装某个依赖包时,依赖包中又依赖不同版本的其他依赖包。当我们依赖包数量增加时,依赖包越来越多,依赖关系也越来越深,这个时候可能面临版本控制被锁死的风险。

所以针对于依赖地狱的解决方案就是扁平化

// 扁平化前

app
|
-- A : V1.0 -> B : V1.0
-- C : V1.0 -> B : V2.0

// 扁平化后

app
|
-- A : V1.0
-- B : V1.0
-- C : V1.0 -> B : V2.0
-- D : V1.0 -> B : V2.0
复制代码

看到这可能有的同学要问了 , 扁平化好像没有解决依赖地狱的问题啊 ? 但是如果我们先安装 C 或者 D 模块 , 我们再来看下依赖关系

app
|
-- A : 1.0 -> B : V1.0
-- B : V2.0
-- C : V1.0
-- D : V1.0
复制代码

由此我们可以看出 , 重复模块哪个被放在外面 取决于安装顺序 .

我们回到第一种情况 , 如果 A 从 V1.0 升级到 V2.0 , 依赖 B : V2.0模块的话 , 依赖关系如下

app
|
-- A 2.0
-- B 2.0
-- C 1.0 -> B 2.0
-- D 1.0 -> B 2.0
复制代码

此时我们可以运行 npm dedupe(yarn 会自动执行该命令) 或者 删除 node_modules 重新安装依赖 , 优化后的依赖关系如下 .

app
|
-- A 2.0
-- B 2.0
-- C 1.0 
-- D 1.0 
复制代码

最后 : 欢迎大家在评论区跟我讨论 , 码字不易 , 麻烦多多三连支持~

文章分类
前端
文章标签