简述前端包管理工具机制和相关实践

标签: dev | 发表时间:2022-06-01 00:00 | 作者:
出处:http://itindex.net/relian

简述前端包管理工具机制和相关实践

npm 依赖管理机制

区别于 Python 的包管理工具 pip 的全局安装,npm 会安装依赖包到当前项目目录,使不同项目的依赖更成体系,这样做的好处是减轻了包作者的 API 兼容性压力;但是缺陷是如果两个项目依赖了一个相同的库,一般这个库会在这两个项目中各安装一次,即相同的依赖包会被多次安装。
我们先通过一张流程图(源自掘金)来了解下 npm install 的整体流程

可以看到执行 npm install 后依次会进行以下流程

  • 检查 package-lock.json
  • 通过和 package.json 对比确定是否远程获取包信息
  • 扁平化构建依赖树
  • 添加缓存
  • 下载包并解压到 node_modules
  • 生成新的 lock 文件 值得注意的是,早期 npm 版本(v5.0 - v5.4)发现 package.json 和 package-lock.json 不一致时,对依赖的安装方式是不一样的。 所以对于团队而言,最佳实践应该是保持 npm 版本的一致性!

缓存机制

我们可以从流程图中看到,npm install 的流程中会查找和使用缓存,以及下载包后会添加缓存的环节。由于依赖嵌套机制,项目中 node_moudles 占用的磁盘空间无疑是最大的,如果安装时每次都通过网络下载获取,那么时间成本是巨大的。常见的优化方式是“空间换时间”,npm 也通过缓存机制来解决这个问题。
简单了解下缓存的目录的和清除机制。
通过 npm config get cache命令可以查询到缓存目录:默认是用户主目录下的 .npm/_cacache 目录。
npm cache clean --force即可强制清除缓存。

yarn 带来了什么?

yarn 是于 2016 年诞生的,它的出现解决了历史上 npm 的很多问题,比如缺乏对于依赖的完整性和一致性保障(npm v3 版本还没有 package-lock.json),以及 npm 安装速度过慢的问题等。npm 目前已经迭代到 v8 版本,在很多方面已经借鉴了 yarn 的优点,但是我们不妨了解下 yarn 诞生时带来的理念。

  1. 确定性。通过 yarn.lock 等机制,保证了确定性,这里的确定性包括但不限于明确的依赖版本、明确的依赖安装结构等。即在任何机器和环境下,都可以以相同的方式被安装。
  2. 模块扁平化安装。将依赖包的不同版本,按照一定策略,归结为单个版本,以避免创建多个副本造成冗余。
  3. 更快的速度。yarn 采取并行安装的机制进行包的安装任务,提高了性能;yarn 引入的缓存机制使二次安装的速度更快。
  4. 更好的语义化。yarn 的命令更加简洁。 解决早期 npm 的依赖管理问题

文章的开始提到 npm 是将依赖放到项目的 node_modules 中,同时如果 node_modules 中的依赖 A 还依赖了其他依赖 B,那么 B 也会被安装到 A 的 node_modules 文件夹,依次递归最终形成非常复杂和庞大的依赖树。
这种依赖管理方式会随着项目的迭代,node_moudles 会变得越来越复杂,从而造成:

  • 非常深的项目依赖层级,难以排查问题
  • 依赖被重复安装,浪费磁盘,网络等资源,安装速度慢 那么 yarn 是如何解决这个问题的呢?那就是模块扁平化安装机制。假如我们有这样一个文件依赖结构。
  App   
 [email protected]
   [email protected]
 [email protected]
 [email protected]
   [email protected]

yarn 在安装依赖时会打平依赖,并对重复依赖进行提升,最终形成的依赖结构如下:

  App   
 [email protected]
 [email protected]
 [email protected]

但是需要注意的是: 模块的安装顺序可能影响 node_modules 内的文件结构。在 npm v3 版本中,假如 项目一开始依赖了 [email protected],此时 [email protected] 会被安装在顶层目录;随着迭代,又引入了模块 [email protected],而 [email protected] 又依赖了 [email protected],此时 [email protected] 会被安装在 [email protected] 下,因为顶层已经有一个 [email protected] 了。

pnpm: 最先进的包管理工具?

在各个场景下,pnpm 相比较于 npm(v8)和 yarn(v3)在性能上都有不错的提升。
pnpm 之所以有如此大的性能提升,简单来说 pnpm 是通过全局 store(目录 ${os.homedir}/.pnpm-store)来存储 node_modules 依赖的 hard-links,当在项目文件中引用依赖的时候则是通过 symlink 去找到对应虚拟磁盘目录下(.pnpm 目录)的依赖地址。相比于 npm 和 yarn 会在每个项目中都安装一份 node_moudles, pnpm 的全局 store 则实现了“安装一次,所有项目复用”,这样避免了二次安装带来的时间消耗。
除此之外,pnpm 本身的设计机制解决了 monorepo 的很多痛点,比如 ”幽灵依赖“”依赖重复安装“的问题。如图: 下面两小节内容源自:pnpm: 最先进的包管理工具 [1]

幽灵依赖

Phantom dependencies 被称之为幽灵依赖,解释起来很简单,即某个包没有被安装(package.json 中并没有,但是用户却能够引用到这个包)。
引发这个现象的原因一般是因为 node_modules 结构所导致的,例如使用 yarn 对项目安装依赖,依赖里面有个依赖叫做 foo,foo 这个依赖同时依赖了 bar,yarn 会对安装的 node_modules 做一个扁平化结构的处理(npm v3 之后也是这么做的),会把依赖在 node_modules 下打平,这样相当于 foo 和 bar 出现在同一层级下面。那么根据 nodejs 的寻径原理,用户能 require 到 foo,同样也能 require 到 bar。

  package.json -> foo(bar 为 foo 依赖)   
node_modules
  /foo
  /bar -> 依赖

那么这里这个 bar 就成了一个幽灵依赖,如果某天某个版本的 foo 依赖不再依赖 bar 或者 foo 的版本发生了变化,那么 require bar 的模块部分就会抛错。

依赖重复安装

这个问题其实也可以说是 hoist 导致的,这个问题可能会导致有大量的依赖的被重复安装,举个例子:
例如有个 package,下面依赖有 lib_a、lib_b、lib_c、lib_d,其中 a 和 b 依赖 [email protected],而 c 和 d 依赖 [email protected]
那么早期 npm 的依赖结构应该是这样的:

  - package   
  - package.json
  - node_modules
     - lib_a
       - node_modules <- [email protected]
     - lib_b
       - node_modules <- [email protected]
     _ lib_c
       - node_modules <- [email protected]
     - lib_d
       - node_modules <- [email protected]

这样必然会导致很多依赖被重复安装,于是就有了 hoist 和打平依赖的操作:

  - package   
  - package.json
  - node_modules
     - [email protected]
     - lib_a
     - lib_b
     _ lib_c
       - node_modules <- [email protected]
     - lib_d
       - node_modules <- [email protected]

但是这样也只能提升一个依赖,如果两个依赖都提升了会导致冲突,这样同样会导致一些不同版本的依赖被重复安装多次,这里就会导致使用 npm 和 yarn 的性能损失。
如果是 pnpm 的话,这里因为依赖始终都是存在 store 目录下的 hard links ,一份不同的依赖始终都只会被安装一次,因此这个是能够被彻彻底底的消除的。

项目中的相关场景实践和常见问题

npm link

适用场景:本地调试 npm 模块,将模块链接到对应的业务项目中运行 使用方法:假如我们需要把模块 pkg-a 链接到主项目 App 中,首先在 pkg-a 根目录中执行 npm link,然后在 App 根目录中执行 npm link pkg-a 即可。调试完可以使用 npm unlink 取消关联。原理:npm link 通过软连接将 pkg-a 链接到 node 模块的全局目录和可执行文件中,实现 npm 包命令的全局可执行。

npx

适用场景:在 npm 5.2.0 版本之后,npm 内置了 npx 的包。npx 是一个简单的 cli 工具,可以帮助我们快速的调试,还可以让我们在不通过 npm 安装包的前提下执行一些 npm 包。

使用方法:
Before:一般情况下,如果我们想使用 es-lint, 会先通过 npm install es-lint, 然后在项目根目录执行 ./node_modules/.bin/es-lint your_file.js 或者 通过 package.json 的 npm scripts 调用 eslint。
After:npx es-lint your_file.js
原理:npx 在运行时会自动去 ./node_moudles/.bin 和 环境变量 寻找命令

是否提交 lock.json 到代码仓库

前面我们提到 yarn 带来了 .lock 文件的机制,使得在任何环境下执行 install,都能得到一致的 node_modules 安装结果。但是是否需要提交 lockfiles(package-lock.json/yarn.lock) 到代码仓库呢?
npm 官方文档 [2]是建议把 package-lock.json 文件提交到代码仓库的。在多人协作的项目中,这样做确实没有问题。但是如果开发的是库,在 npm publish 的时候最好忽略 lockfiles。因为库一般是被其他项目依赖的,在不使用 lockfiles 的情况下,由于新版 npm 和 yarn 的 hoist 机制,可以复用住项目已经加载过的包,减少依赖重复和体积。
但是存在这样一种现象:即使在一些发布时忽略 lockfiles 的库中,在主项目顶层存在相关依赖包的前提下,最终生成的 lockfile 仍然没复用主项目的包。这是为什么呢?原因是库的依赖包版本和主项目存在的依赖包版本不一致。具体看下图:主项目的 yarn.lock 中显示 browser 这个包依赖了 @babel/[email protected]

主项目 node_modules 顶层的 @babel/runtime 版本为 7.10.1

知道了原因,那么如何减少库项目的依赖项呢。到这里,解决方案也就呼之欲出了:

  1. 库项目尽量使用和主项目版本一致的依赖包
  2. 在库项目 package.json 的 “peerDevpendencies” 字段中声明主项目已有的依赖包

合入其他分支代码后编译报错

相信很多同学都遇到过和我一样的问题:当自己的 feat 分支代码合入 master 或者业务班车分支的代码时,重新 yarn 时,有时候会编译失败,报大量 "can't resolve module xxx"的错误。这种错误有很多情况是依赖版本不一致的问题,但是又极其难以定位,令人头痛。那么此时有另外一个思路,那就是从 master 拉一个最新的分支再进行合入。
但更好的解决方式是:建议在日常开发过程中,定时合入 master 代码,一方面可以合入最新的 feat,另一方面可以避免长时间不合入,最后在上线阶段合入代码,可能出现大量冲突,解决不当或遗漏而造成的编译问题。同时也可以考虑将工具升级为 pnpm,以解决潜在的“幽灵依赖”和“依赖嵌套”问题,同时带来性能上的提升。

参考资料

[1]

pnpm: 最先进的包管理工具: https://bytedance.feishu.cn/docs/doccngSUrvF0qPVmBE1rq1iPZQf

[2]

npm官方文档: https://docs.npmjs.com/cli/v7/configuring-npm/package-lock-json

相关 [前端 管理 工具机] 推荐:

简述前端包管理工具机制和相关实践

- - IT瘾-dev
简述前端包管理工具机制和相关实践. 区别于 Python 的包管理工具 pip 的全局安装,npm 会安装依赖包到当前项目目录,使不同项目的依赖更成体系,这样做的好处是减轻了包作者的 API 兼容性压力;但是缺陷是如果两个项目依赖了一个相同的库,一般这个库会在这两个项目中各安装一次,即相同的依赖包会被多次安装.

前端模块管理器简介

- - 阮一峰的网络日志
模块化结构已经成为网站开发的主流. 制作网站的主要工作,不再是自己编写各种功能,而是如何将各种不同的模块组合在一起. 浏览器本身并不提供模块管理的机制,为了调用各个模块,有时不得不在网页中,加入一大堆script标签. 这样就使得网页体积臃肿,难以维护,还产生大量的HTTP请求,拖慢显示速度,影响用户体验.

浅谈大型组织中前端管理架构

- - 掘金 架构
前端,现代前端分工变得越来越细致,页面制作、JavaScript框架设计、组件插件、交互设计、工程化脚手架等,项目中前端的占比也越来越高,继而出现了BFF (Back-end for Front-end 服务于前端的后端),这一切的助力离不开各大浏览器厂商的厮杀. 周末来跟大家分享大型组织中(前端工程师的人数开始超过15人)前端管理架构,主要涉及的是团队协作,如何让团队运作更加高效规范.

干掉前端!3分钟纯 Java 注解搭个管理系统,我直接好家伙

- - 掘金后端本月最热
最近接触到个新项目,发现它用了一个比较有意思的框架,可以说实现了我刚入行时候的梦想,所以这里马不停蹄的和大家分享下. 在我刚开始工作接触的项目都还没做前后端分离,经常需要后端来维护页面,有时候觉得自己好像天生不适合干前端,你要是让我研究研究后端的技术,看个中间件源码啊,分析分析什么框架底层原理啊,这都问题不大,偶尔搞一下 JS也可以.

前端技术

- - CSDN博客综合推荐文章
随着互联网产业的爆炸式增长,与之伴生的Web前端技术也在历经洗礼和蜕变. 尤其是近几年随着移动终端的发展,越来越多的人开始投身或转行至新领域,这更为当今的IT产业注入了新的活力. 尽管Web前端技术诞生至今时日并不长,但随着Web技术的逐渐深入,今后将会在以下几方面发力. JavaScript的兄弟们.

管理

- - 人月神话的BLOG
对于中小企业而言现在管理上欠缺的不是人治或者说儒家佛家等东方管理思想,而真正欠缺的是西方法治的科学管理方法. 现在很多中小企业花很多钱去听什么东方管理思想的培训是误入歧途,东西方管理思想需要融合,但是基础还是科学的管理方法和模式. 而在这个里面最重要的仍然是流程管理,知识管理,质量管理,项目管理这些内容,而不是简单的纯管理.

Web前端优化

- - JavaScript - Web前端 - ITeye博客
优点:直接使用浏览器内存的缓存数据,减少网站后台压力,用户体验(速度)好. 缺点:对于时时变化的动态页面,这种情况就不能容忍了,因为每次访问的都是第一次访问的内容,这样即使所请求的页面已经变化了,用户也不可能知道,所以此场景必须要消除这种缓存的影响. 延迟加载,将资源延迟到需要的时候的加载,例如detail页面,相关产品推荐,当用户浏览更多的信息往下拉动滚动时,才进行加载,异步加载可以大幅减少对后端资源的使用,在需要的时候加载,是资源合理使用常用的方式,但是也带来一个问题,当往下拉才去加载,如果性能不够好,用户的体验其实是不好的,“菊花”转动的时间会比较长,同时异步加载对前端性能的作用也是非常明显的,渲染的节点数量大幅减少.

Web 前端测试

- - Web前端 - ITeye博客
Web 网站测试流程和方法(转载). 进行正式测试之前,应先确定如何开展测试,不可盲目的测试. 一般网站的测试,应按以下流程来进行:. 1)使用HTML Link Validator将网站中的错误链接找出来;. 2)测试的顺序为:自顶向下、从左到右;. 3)查看页面title是否正确. (不只首页,所有页面都要查看);.

前端xss攻击

- - SegmentFault 最新的文章
实习的时候在项目中有接触过关于xss攻击的内容,并且使用了项目组中推荐的一些常用的防xss攻击的方法对项目进行了防攻击的完善. 但一直没有时间深入了解这东西,在此,做一个简单的梳理. xss跨站脚本攻击(Cross Site Scripting),是一种经常出现在web应用中的计算机安全漏洞,它指的是恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入的恶意html代码会被执行,从而达到恶意用户的特殊目的.

REDO管理

- - CSDN博客数据库推荐文章
一、什么是REDO LOG.  REDOLOG文件是十分重要的文件,它记录了Oracle的所有变化,是数据库实例恢复机制中最为关键的组成部分.     GROUP#    THREAD#  SEQUENCE#      BYTES  BLOCKSIZE    MEMBERS ARC STATUS           FIRST_CHANGE# FIRST_TIME     NEXT_CHANGE# NEXT_TIME.