npm
install 的时候都做了些什么 ?
在我们敲下 npm install 的时候 , npm 会做以下几件事情 .
- 检查 config
npm 执行会先读取 npm config 和 npmrc .
npmrc 是有权重的
,
项目级 npmrc > 用户级 npmrc > 全局 npmrc > npm 内置 npmrc
- 检查是否存在 package-lock.json
-
如果有
-
跟当前 package-lock 里声明的版本是否一致
-
一致
-
检查缓存
-
-
不一致
-
如果版本不一致 , 对应处理方法跟 npm 版本有关 . 在最新版本的 npm 中, 会检查依赖包兼容版本, 如果版本能兼容 , 则按照 package-lock 版本安装 , 反之按照 package.json 版本安装
-
(因为 npm 版本不同会导致处理方式不同 , 所以这里有一个最佳实践 , 团队的 npm 版本要相同)
-
-
-
如果没有
-
获取依赖包的信息
-
构建依赖树
-
扁平化
-
检查缓存
-
-
检查缓存
- 如果有缓存
-
将对应缓存解压到 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 的好处
-
确保高速 稳定的 npm 服务, 使发布私有模块更加安全
-
审核机制可保证模块质量和安全
yarn
install 的时候都做了些什么 ?
-
检测包
- 检测是否有 npm 相关文件
- 检查 os cpu 等信息
-
解析包
-
获取 package.json 中依赖 遍历首层依赖获取依赖包版本信息(
dependencies
和devDependencies
) , 递归获取嵌套依赖(首层依赖包中所需要的依赖
)版本信息 , 将解析过 和 正在解析的包 用一个 set 存储 , 保证同一版本范围的包不会重复解析 . -
对于没解析过的包 , 会尝试获取版本 , 并标记成解析过 .
-
如果在 yarn-lock 中没有找到包的声明 , 则向 registry 发起请求 , 获取满足版本范围的最高版本的包信息 获取后 标记成已解析 .
-(这个流程结束后 得到了所有依赖的版本信息 和 下载地址)
-
-
获取包
-
检查缓存中是否存在当前依赖包 , 不存在的包下载到缓存目录
-
yarn 根据 cacheFolder + slug + node_modules + pkg.name 生成一个 path 判断系统中是否存在该 path 证明是否有缓存
-
对于没有命中缓存的包 yarn 会维护一个 fetch 队列 按照规则进行网络请求
-
-
链接包
-
解析 peerDependencies (
peerDependencies
的作用是提示宿主环境去安装peerDependencies所指定依赖的包,在导入所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题.) -
如果发现冲突 给 warning 提示
-
之后扁平化依赖树
-
执行拷贝任务
-
解压到 node_modules
-
-
构建包
-
如果有 2进制包等需要构建的包 , 在这一步进行构建
-
为什么 npm 和 yarn 一致追求扁平化
如果让我们设计一个依赖系统 , 我们第一时间想到的应该就是 树 结构
生成树 表面很好 , 可以满足我们的需求 , 但是如果
- 项目依赖 A B , A B都依赖相同版本的依赖C , 重复安装会浪费资源
此时的依赖结构如下
app
|
-- A : V1.0 -> C : V1.0
-- B : V1.0 -> C : V1.0
复制代码
如果 C 又依赖于 D 呢? ... 感觉越来越不可控了 .
- 路径太长 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
复制代码
最后 : 欢迎大家在评论区跟我讨论 , 码字不易 , 麻烦多多三连支持~