神族九帝's blog 神族九帝's blog
首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)

神族九帝,永不言弃

首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)
  • 工作笔记

  • 前端基础建设与架构 30 讲

    • 开篇词 像架构师一样思考,突破技术成长瓶颈
    • npm 安装机制及企业级部署私服原理
    • Yarn 的安装理念及如何破解依赖管理困境
      • CI 环境上的 npm 优化及更多工程化问题解析
      • 横向对比主流构建工具,了解构建工具的设计考量
      • Vite 实现:从源码分析出发,构建 bundlele 开发工程
      • core-j 及垫片理念:设计一个“最完美”的 Polyfill 方案
      • 梳理混乱的 Babel,不再被编译报错困扰
      • 探索前端工具链生态,制定一个统一标准化 babel-preet
      • 从实战出发,从 0 到 1 构建一个符合标准的公共库
      • 代码拆分和按需加载:缩减 bundle ize,把性能做到极致
      • Tree Shaking:移除 JavaScript 上下文中的未引用代码
      • 如何理解 AST 实现和编译原理?
      • 工程化思维处理方案:如何实现应用主题切换功能?
      • 解析 Webpack 源码,实现自己的构建工具
      • 从编译到运行,跨端解析小程序多端方案
      • 原生跨平台技术:移动端跨平台到 Flutter 的技术变革
      • 学习 axio:封装一个结构清晰的 Fetch 库
      • 对比 Koa 和 Redux:分析前端中的中间件理念
      • 如何理解软件开发灵活性和高定制性?
      • 如何理解前端中面向对象的思想?
      • 如何利用 JavaScript 实现经典数据结构?
      • 剖析前端中的数据结构应用场景
      • npm cript:打造一体化的构建和部署流程
      • 自动化代码检查:剖析 Lint 工具和工程化接入&优化方案
      • 如何设计一个前端 + 移动端离线包方案?
      • 如何设计一个“万能”项目脚手架?
      • 同构渲染架构:实现一个 SSR 应用
      • 设计性能守卫系统:完善 CICD 流程
      • 实践打造网关:改造企业 BFF 方案
      • 实现高可用:使用 Puppeteer 生成性能最优的海报系统
      • 结束语 再谈项目的基建和架构,个人的价值和方向
    • vue2源码学习

    • 剖析vuejs内部运行机制

    • TypeScript 入门实战笔记

    • vue3源码学习

    • 2周刷完100道前端优质面试真题

    • 思维导图

    • npm发包

    • 重学node

    • 前端性能优化方法与实战

    • webpack原理与实战

    • webGl

    • 前端优化

    • Web3

    • React

    • 更多

    • 笔记
    • 前端基础建设与架构 30 讲
    wu529778790
    2024-04-07

    Yarn 的安装理念及如何破解依赖管理困境

    01 讲我们讲了 npm 的技巧和原理,但其实在前端工程化这个主题上除了 npm,还有不可忽视的 Yarn。

    Yarn 是一个由 Facebook、Google、Exponent 和 Tilde 构建的新的 JavaScript 包管理器。它的出现是为了解决历史上 npm 的某些不足(比如 npm 对于依赖的完整性和一致性保障,以及 npm 安装速度过慢的问题等),虽然 npm 目前经过版本迭代汲取了 Yarn 一些优势特点(比如一致性安装校验算法等),但我们依然有必要关注 Yarn 的思想和理念。

    Yarn 和 npm 的关系,有点像当年的 Io.js 和 Node.js,殊途同归,都是为了进一步解放和优化生产力。这里需要说明的是,不管是哪种工具,你应该做的就是全面了解其思想,优劣胸中有数,这样才能驾驭它,为自己的项目架构服务。

    当 npm 还处在 v3 时期时,一个叫作 Yarn 的包管理方案横空出世。2016 年,npm 还没有 package-lock.json 文件,安装速度很慢,稳定性也较差,而 Yarn 的理念很好地解决了以下问题。

    • 确定性:通过 yarn.lock 等机制,保证了确定性。即不管安装顺序如何,相同的依赖关系在任何机器和环境下,都可以以相同的方式被安装。(在 npm v5 之前,没有 package-lock.json 机制,只有默认并不会使用的npm-shrinkwrap.json。)

    • 采用模块扁平安装模式:将依赖包的不同版本,按照一定策略,归结为单个版本,以避免创建多个副本造成冗余(npm 目前也有相同的优化)。

    • 网络性能更好:Yarn 采用了请求排队的理念,类似并发连接池,能够更好地利用网络资源;同时引入了更好的安装失败时的重试机制。

    • 采用缓存机制,实现了离线模式(npm 目前也有类似实现)。

    我们先来看看 yarn.lock 结构:

    "@babel/cli@^7.1.6", "@babel/cli@^7.5.5":
      version "7.8.4"
      resolved "http://npm.in.zhihu.com/@babel%2fcli/-/cli-7.8.4.tgz#505fb053721a98777b2b175323ea4f090b7d3c1c"
      integrity sha1-UF+wU3IamHd7KxdTI+pPCQt9PBw=
      dependencies:
        commander "^4.0.1"
        convert-source-map "^1.1.0"
        fs-readdir-recursive "^1.1.0"
        glob "^7.0.0"
        lodash "^4.17.13"
        make-dir "^2.1.0"
        slash "^2.0.0"
        source-map "^0.5.0"
      optionalDependencies:
        chokidar "^2.1.8"
    

    该结构整体和 package-lock.json 结构类似,只不过 yarn.lock 并没有使用 JSON 格式,而是采用了一种自定义的标记格式,新的格式仍然保持了较高的可读性。

    相比 npm,Yarn 另外一个显著区别是 yarn.lock 中子依赖的版本号不是固定版本。这就说明单独一个 yarn.lock 确定不了 node_modules 目录结构,还需要和 package.json 文件进行配合。

    其实,不管是 npm 还是 Yarn,说到底它们都是一个包管理工具,在项目中如果想进行 npm/Yarn 切换,并不是一件麻烦的事情。甚至还有一个专门的 synp 工具,它可以将 yarn.lock 转换为 package-lock.json,反之亦然。

    关于 Yarn 缓存,我们可以通过这个命令查看缓存目录,并通过目录查看缓存内容:

    yarn cache dir
    

    Drawing 0.png

    值得一提的是,Yarn 默认使用 prefer-online 模式,即优先使用网络数据。如果网络数据请求失败,再去请求缓存数据。

    最后,我们来看一看一些区别于 npm,Yarn 所独有的命令:

    yarn import
    yarn licenses
    yarn pack
    yarn why
    yarn autoclean
    

    npm 独有的命令是:npm rebuild。

    现在,你已经对 Yarn 有了一个初步了解,接下来我们来分析一下 Yarn 的安装机制和思想。

    Yarn 安装机制和背后思想

    上一讲我们已经介绍过了 npm 安装机制,这里我们再来看一下 Yarn 的安装理念。简单来说,Yarn 的安装过程主要有以下 5 大步骤:

    检测(checking)→ 解析包(Resolving Packages) → 获取包(Fetching Packages)→ 链接包(Linking Packages)→ 构建包(Building Packages)

    图片14.png

    Yarn 安装流程图

    检测包(checking)

    这一步主要是检测项目中是否存在一些 npm 相关文件,比如 package-lock.json 等。如果有,会提示用户注意:这些文件的存在可能会导致冲突。在这一步骤中,也会检查系统 OS、CPU 等信息。

    解析包(Resolving Packages)

    这一步会解析依赖树中每一个包的版本信息。

    首先获取当前项目中 package.json 定义的 dependencies、devDependencies、optionalDependencies 的内容,这属于首层依赖。

    接着采用遍历首层依赖的方式获取依赖包的版本信息,以及递归查找每个依赖下嵌套依赖的版本信息,并将解析过和正在解析的包用一个 Set 数据结构来存储,这样就能保证同一个版本范围内的包不会被重复解析。

    • 对于没有解析过的包 A,首次尝试从 yarn.lock 中获取到版本信息,并标记为已解析;

    • 如果在 yarn.lock 中没有找到包 A,则向 Registry 发起请求获取满足版本范围的已知最高版本的包信息,获取后将当前包标记为已解析。

    总之,在经过解析包这一步之后,我们就确定了所有依赖的具体版本信息以及下载地址。

    Drawing 2.png

    解析包获取流程图

    获取包(Fetching Packages)

    这一步我们首先需要检查缓存中是否存在当前的依赖包,同时将缓存中不存在的依赖包下载到缓存目录。说起来简单,但是还是有些问题值得思考。

    比如:如何判断缓存中是否存在当前的依赖包?其实 Yarn 会根据 cacheFolder+slug+node_modules+pkg.name 生成一个 path,判断系统中是否存在该 path,如果存在证明已经有缓存,不用重新下载。这个 path 也就是依赖包缓存的具体路径。

    对于没有命中缓存的包,Yarn 会维护一个 fetch 队列,按照规则进行网络请求。如果下载包地址是一个 file 协议,或者是相对路径,就说明其指向一个本地目录,此时调用 Fetch From Local 从离线缓存中获取包;否则调用 Fetch From External 获取包。最终获取结果使用 fs.createWriteStream 写入到缓存目录下。

    Drawing 3.png

    获取包流程图

    链接包(Linking Packages)

    上一步是将依赖下载到缓存目录,这一步是将项目中的依赖复制到项目 node_modules 下,同时遵循扁平化原则。在复制依赖前,Yarn 会先解析 peerDependencies,如果找不到符合 peerDependencies 的包,则进行 warning 提示,并最终拷贝依赖到项目中。

    这里提到的扁平化原则是核心原则,我也会在后面内容进行详细的讲解。

    Drawing 4.png

    链接包解析流程图

    构建包(Building Packages)

    如果依赖包中存在二进制包需要进行编译,会在这一步进行。

    了解了 npm 和 Yarn 的安装原理还不是“终点”,因为一个应用项目的依赖错综复杂。接下来我将从“依赖地狱”说起,帮助你加深对依赖机制相关内容的理解,以便在开发生产中灵活运用。

    破解依赖管理困境

    早期 npm(npm v2)的设计非常简单,在安装依赖时将依赖放到项目的 node_modules 文件中;同时如果某个直接依赖 A 还依赖其他模块 B,作为间接依赖,模块 B 将会被下载到 A 的 node_modules 文件夹中,依此递归执行,最终形成了一颗巨大的依赖模块树。

    这样的 node_modules 结构,的确简单明了、符合预期,但对大型项目在某些方面却不友好,比如可能有很多重复的依赖包,而且会形成“嵌套地狱”。

    那么如何理解“嵌套地狱”呢?

    • 项目依赖树的层级非常深,不利于调试和排查问题;

    • 依赖树的不同分支里,可能存在同样版本的相同依赖。比如直接依赖 A 和 B,但 A 和 B 都依赖相同版本的模块 C,那么 C 会重复出现在 A 和 B 依赖的 node_modules 中。

    这种重复问题使得安装结果浪费了较大的空间资源,也使得安装过程过慢,甚至会因为目录层级太深导致文件路径太长,最终在 Windows 系统下删除 node_modules 文件夹出现失败情况。

    因此 npm v3 之后,node_modules 的结构改成了扁平结构,按照上面的例子(项目直接依赖模块 A,A 还依赖其他模块 B),我们得到下面的图示:

    图片10.png

    npm 不同版本的安装结构图 ①

    当项目新添加了 C 依赖,而它依赖另一个版本的 B v2.0。这时候版本要求不一致导致冲突,B v2.0 没办法放在项目平铺目录下的 node_moduls 文件当中,npm v3 会把 C 依赖的 B v2.0 安装在 C 的 node_modules 下:

    图片9.png

    npm 不同版本的安装结构图 ②

    接下来,在 npm v3 中,假如我们的 App 现在还需要依赖一个 D,而 D 也依赖 B v2.0 ,我们会得到如下结构:

    图片17.png

    npm 安装结构图 ①

    这里我想请你思考一个问题:为什么 B v1.0 出现在项目顶层 node_modules,而不是 B v2.0 出现在 node_modules 顶层呢?

    其实这取决于模块 A 和 C 的安装顺序。因为 A 先安装,所以 A 的依赖 B v1.0 率先被安装在顶层 node_modules 中,接着 C 和 D 依次被安装,C 和 D 的依赖 B v2.0 就不得不安装在 C 和 D 的 node_modules 当中了。因此,模块的安装顺序可能影响 node_modules 内的文件结构。

    我们继续依赖工程化之旅。假设这时候项目又添加了一个依赖 E ,E 依赖了 B v1.0 ,安装 E 之后,我们会得到这样一个结构:

    图片6.png

    npm 安装结构图 ②

    此时对应的 package.json 中,依赖包的顺序如下:

    {
        A: "1.0",
        C: "1.0",
        D: "1.0",
        E: "1.0"
    }
    

    如果我们想更新模块 A 为 v2.0,而模块 A v2.0 依赖了 B v2.0,npm v3 会怎么处理呢?

    整个过程是这样的:

    • 删除 A v1.0;

    • 安装 A v2.0;

    • 留下 B v1.0 ,因为 E v1.0 还在依赖;

    • 把 B v2.0 安装在 A v2.0 下,因为顶层已经有了一个 B v1.0。

    它的结构如下:

    图片5.png

    npm 安装结构图 ③

    这时模块 B v2.0 分别出现在了 A、C、D 模块下——重复存在了。

    通过这一系列操作我们可以看到:npm 包的安装顺序对于依赖树的影响很大。模块安装顺序可能影响 node_modules 内的文件数量。

    这里一个更理想的依赖结构理应是:

    图片4.png

    npm 安装结构图 ④

    过了一段时间,模块 E v2.0 发布了,并且 E v2.0 也依赖了模块 B v2.0 ,npm v3 更新 E 时会怎么做呢?

    • 删除 E v1.0;

    • 安装 E v2.0;

    • 删除 B v1.0;

    • 安装 B v2.0 在顶层 node_modules 中,因为现在顶层没有任何版本的 B 了。

    此时得到图:

    图片3.png

    npm 安装结构图 ⑤

    这时候,你可以明显看到出现了较多重复的依赖模块 B v2.0。我们可以删除 node_modules,重新安装,利用 npm 的依赖分析能力,得到一个更清爽的结构。

    实际上,更优雅的方式是使用 npm dedupe 命令,得到:

    图片2.png

    npm 安装结构图 ⑥

    实际上,Yarn 在安装依赖时会自动执行 dedupe 命令。整个优化的安装过程,就是上一讲提到的扁平化安装模式,也是需要你掌握的关键内容。

    结语

    这一讲我们解析了 Yarn 安装原理。

    前端基建 金句.png

    通过本讲内容,你可以发现包安装并不只是从远程下载文件那么简单,这其中涉及缓存、系统文件路径,更重要的是还涉及了安装依赖树的解析、安装结构算法等。

    最后,给大家布置一个思考题,npm v7 在 2020 年 10 月刚刚发布,请你总结一下它的新特性,并思考一下为什么要引入这些新的特性?这些新特性背后是如何实现的?欢迎在留言区分享你的观点。


    大前端引流.png

    对标阿里P7技术需求 + 每月大厂内推,6 个月助你斩获名企高薪 Offer。点此链接,快来领取!


    # 精选评论

    # *强:

    了解了 npm 的安装依赖的原理,还有 npm dedupe 命令,学到了!😎

    #     编辑回复:

        加油~

    # **的小叶酱:

    找了一下官方的文档,好像没有内部原理相关的内容。想请教一下大佬的学习方法🙋

    #     讲师回复:

        看源码其实是个很好的方式,没有文档,总有源码呀

    # **欣:

    其实 Yarn 会根据 cacheFolder+slug+node_modules+pkg.name 生成一个 path,判断系统中是否存在该 path,如果存在证明已经有缓存,不用重新下载 和 Yarn 默认使用 prefer-online 模式,即优先使用网络数据。如果网络数据请求失败,再去请求缓存数据。 这2段话是不是冲突了啊

    #     讲师回复:

        第一段表述其实有个前提: prefer-online 如果关闭了,那么在走缓存时,「`其实 Yarn 会根据 cacheFolder+slug+node_modules+pkg.name 生成一个 path,判断系统中是否存在该 path,如果存在证明已经有缓存,不用重新下载」

    # *舒:

    这一讲下来,我的感触是 是不是yarn是当时历史的产物,现在 npm 的功能已经比较完善了,不太需要 yarn 了

    #     讲师回复:

        目前来看,两者并驾齐驱,取决于团队技术选型

    # **彪:

    “相比 npm,Yarn 另外一个显著区别是 yarn.lock 中子依赖的版本号不是固定版本。这就说明单独一个 yarn.lock 确定不了 node_modules 目录结构,还需要和 package.json 文件进行配合。”npm的node_modules目录结构不也是通过lock和package.json一起决定的吗?希望予以解答,谢谢~

    #     讲师回复:

        这里的差别在于 yarn.lock 和 package-lock

    # **航:

    为什么npm不在安装或者更新依赖时自动执行npm dedupe

    #     讲师回复:

        npm dedupe 还是要留给开发者应用,毕竟它并不完全可靠,按你说的,npm 就很难有一致性可靠性了

    # **彪:

    npm v7更新:1. Workspaces: npm CLI的一组功能,可支持从单个顶级软件包中管理多个软件包2.">自动安装peerDependencies3. package-lock v2和对yarn.lock的支持

    # **宇:

    为什么b1.0和b2.0不能设计成平级,根据不同版本号区分

    #     讲师回复:

        历史的坑,最开始方案就不支持,如果只区分 major 那么 patch 和 fix 版本号还平级吗?平级的话,size 太大了,不平级,那区别也不多

    # *腾:

    想问一下侯大有调试yarn源码的技巧吗

    #     讲师回复:

        和调试其他源码没什么特别的地方,唯一需要注意的是,可以结合实践分析

    # 无:

    请教侯大阅读 yarn 源码的技巧,是生读源码还是配合调试工具进行的呢?研究 yarn install 部分的源码时,发觉基于 gulp 打包至 lib 的流程似乎难以实现源码逐行调试,有些疑惑🤔

    #     讲师回复:

        这个看自己喜好吧。我是遇见了问题,去翻源码。不会直接看源码

    # **鑫:

    这个示例项目同时依赖了两个不同版本的依赖 B v1.0 和 C 依赖的 B v2.0, 那么项目打包的时候是两个版本的 B 都被打包进项目了吗?

    #     讲师回复:

        是否都被打包,取决于业务上是否都有直接或间接应用

    # **飞:

    为啥不讲 npm v6 而是讲 v3

    #     讲师回复:

        v3 开始加入的某些新特性,v6 一直在沿用,对比新旧特性,就提到了 v3 这个本质不冲突啊,注意:v3 开始加入的某些新特性,v6 一直在沿用,沿用到现在,都一起讲了啊。那我为啥不讲新出的 v7...v7 也有些新特性,留在了思考题阶段

    # *帅:

    请问如果一个项目里,混用npm和yarn会产生什么影响呢

    #     讲师回复:

        锁版本机制不一样

    # **4299:

    受益匪浅,感谢老师,讲的太好了,深知自己的不足。

    编辑 (opens new window)
    上次更新: 2025/03/17, 12:21:00
    npm 安装机制及企业级部署私服原理
    CI 环境上的 npm 优化及更多工程化问题解析

    ← npm 安装机制及企业级部署私服原理 CI 环境上的 npm 优化及更多工程化问题解析→

    最近更新
    01
    Code Review
    10-14
    02
    ComfyUI
    10-11
    03
    vscode插件开发
    08-24
    更多文章>
    Power by vuepress | Copyright © 2015-2025 神族九帝
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×