熟悉Npm和Yarn

1,040 阅读14分钟

npm和yarn.png

Yarn是什么?

Yarn是由Facebook、Google、Exponent和Tilde联合推出了一个新的JS包管理工具 ,如官方文档中写的,"快速、可靠、安全的依赖管理工具。",Yarn是为了弥补npm的一些缺陷而出现的。

npm有哪些缺陷

  • npm install非常慢。尤其是新项目克隆下来要半天,删除node_modules,重新npm install时还是这样
  • 同一个项目,安装时无法保持一致性。由于package.json文件中版本号的特点,下面三个版本号在安装的时候代表不同的含义:
"1.0.2" # 表示安装指定的1.0.2版本
"~1.0.2" # 表示安装1.0.X中最新的版本,但是不包括1.1.0,项目不会出现大的问题
"^1.0.2" # 表示安装1.X.X中最新的版本,包括1.1.0,但是不包括2.0.0,^版本更新可能比较大,会造成项目代码错误

这种情况会经常出现在我们平时开发的项目中,有的同事安装是可以的,有的却有问题,这就是因为安装的包版本不一致导致的

  • 安装时,包会在同一时间下载和安装,中途一个包抛出一个错误,npm会继续下载和安装包。

正是因为npm有这样的一些问题,yarn产生了,下面我们看下yarn相对npm都有哪些优势:

yarn的优势

  • 速度快,主要有两个方面的原因,一是并行安装,无论npm还是yarn在执行包的安装时,都会执行一系列任务。npm是按照队列执行每个package,也就是说必须要等到当前package安装完成之后,才能继续后面的安装。而Yarn是同步执行所有任务,提高了性能;二是离线模式,如果之前已经安装过一个软件包,用yarn再次安装时之间从缓存中获取,不用像npm那样再从网络下载了.

  • 安装版本统一: yarn有一个锁定文件yarn.lock,记录了安装上的模块的版本号等信息,每次只要新增了一个模块,yarn就会创建(或更新)yarn.lock文件,这样就保证了每一次拉取同一个项目的所有依赖时,大家使用的都是一个版本。npm其实也可以,但是需要用户手动执行npm shrinkwrap命令,即执行该命令时,会生成一个锁定文件,通yarn.lock;yarn和npm不用的一点就是,yarn的锁定文件是默认自动生成的,而npm是需要手动执行命令才会生成。

  • 更简洁的输出:npm输出信息冗长。在执行npm install时,命令行会不断打印出所有被安装上的依赖。而yarn简洁很多,默认情况下,结合了emoji直观地打印出必要的信息。

  • 更好的语义化: 比如yarn add/remove,比npm原本的 install/uninstall更清晰

  • 多注册来源处理:所有的依赖包,不管被不同的库间接关联引用多少次,安装这个包时,只会从一个注册来源安装,防止出现混乱不一致

yarn的特点

  • yarn在下载包时,会缓存每个下载过的包,所以再次使用时无需重复下载
  • yarn利用了并行下载的特点(可同时下载多个包),以最大化资源利用率,因此安装速度更快

缓存

yarn会将安装过的包缓存下来,这样再次安装相同包的时候,就不需要再去下载,而是直接从缓存文件中直接copy进来,可以通过命令yarn cache dir查看yarn的全局缓存目录。yarn会将不同版本解压后的包存放在不同目录下,目录以:npm-[package name]-[version]-[shasum]来命名。shasum 即registry获取的dist.shasum。可以通过命令查看已经缓存过的包

yarn cache list    列出已缓存的每个包
yarn cache list --pattern <pattern>  列出匹配指定模式的已缓存的包

yarn.lock

yarn.lock会准确的存储每个依赖的具体版本信息,以保证在不同机器安装可以得到相同的结果。

image.png 下面以@babel/code-frame为例,看看yarn.lock 中会记录哪些信息

  1. 第一行"@babel/code-frame@7.12.11包的name和语义化版本号,这些都来自package.json中的定义
  2. version字段,记录的是一个确切的版本
  3. resolved字段记录的是包的URL地址。其中hash值,即dist.shasum
  4. dependencies字段记录的是当前包的依赖,即当前包在package.jsondependencies字段中的所有依赖.

Yarn在安装期间,会使用当前项目的yarn.lock文件。yarn.lock文件是在安装期间,由Yarn自动生成的,并且由yarn来管理,不应该手动去更改,更不应该删除yarn.lock文件,且要提交到版本控制系统中,以免因为不同机器安装的包版本不一致引发问题

yarn过程

首次执行yarn,会按照package.json中的语义化版本,向registry查询,并获取到符合版本规则的最新的依赖包进行下载,并构建构建依赖关系树。 比如在package.json中指定vue的版本为 ^2.0.3,就会获取符合2.x.x的最高版本的包。然后自动生成yarn.lock文件,并生成缓存

之后再执行yarn,会对比package.json中依赖版本范围和yarn.lock中版本号是否匹配

  • 版本号匹配,会根据yarn.lock中的resolved字段去查看缓存, 如果有缓存,直接copy,没有缓存则按照resolved字段的url去下载包
  • 版本号不匹配,根据package.json中的版本范围去registry查询,下载符合版本规则最新的包,并更新至yarn.lock

模块扁平化

在安装依赖时,会解析依赖构建出依赖关系树。 比如项目的依赖(即dependence和devDependences中的依赖,不包括依赖的依赖)中有A,B,C三个包,A和B包同时依赖了相同版本范围的D包。那么依赖关系树是这样的

├── A    				
│ └── D    
├── B    				
│ └── D  
├── C 

如果按照这样的依赖关系树直接安装的话,D模块会在A包和B包的 node_modules中都安装,这样会导致模块冗余

为了保证依赖关系树中没有大量重复模块,yarn在安装时会做去重操作,它会遍历所有节点,逐个将模块放在根节点下面,即项目的node-modules中。当发现有相同的模块时,会判断当前模块指定的 semver版本范围是否交集,如果有,则只保留兼容版本,如果没有则在当前的包的node-modules 下安装,所以上面的包关系就会变成下面这样:

├── A    				
├── B    				
├── C 
├── D

现如今的npm

有了yarn的压力后,npm做了一些类似的改进

  1. 默认新增了类似yarn.lock的package-lock.json
  2. git依赖支持优化:这个特性在需要安装大量内部项目,或需要使用某些依赖的未发布版本时很有用。
  3. 文件依赖优化:在之前版本,如果将本地目录作为依赖来安装,将会把文件目录作为副本拷贝到 node_modules中。在最新的npm版本中,将改为使用创建symlinks的方式来实现,不再执行文件拷贝。这将会提升安装速度。目前yarn还不支持
  4. 速度和yarn构建时间不再有显著的差异

yarn和npm命令的区别

npmyarn说明
npm inityarn init初始化项目,生成package.json文件|
npm install 模块名 --saveyarn add 模块名在目录下添加项目的依赖包,并在package.json下写入配置,最终安装的包在dependencies中
npm install 模块名 --save-devyarn add 模块名 --dev在目录下添加某个开发时依赖包,最终安装到devDependencies中
npm uninstall 模块名yarn remove 模块名移除目录下指定的项目依赖包
npm update 模块名 --saveyarn upgrade 模块名更新目录下指定的项目依赖包
npm install 模块名 -gyarn global add 模块名在全局下添加依赖包
npm config get keyyarn config get key查看配置key的值
npm config set registry registry.npm.taobao.orgyarn config set key value [-g--global]设置配置项key的值为value
npm config delete keyyarn config delete key从配置中删除配置key
npm listyarn list查询当前工作文件夹所有的依赖
npm info packageyarn info package看包信息

相关概念

什么是registry

registry是模块仓库提供的一个查询服务,即我们常说的源。以yarn官方镜像源为例,它的查询服务网址是https://registry.yarnpkg.com,这个网址后面跟上模块名,就会得到一个JSON对象,里面是该模块所有版本的信息。比如,访问https://registry.npmjs.org/vue,就会看到vue模块所有版本的信息。

image.png registry网址的模块名后面,还可以跟上版本号或者标签,用来查询某个具体版本的信息,registry.yarnpkg.com/vue/2.6.10

image.png

上面JSON对象中,有一个dist.tarball属性,是该版本压缩包的网址。dist.shasum属性相当于hash值,在lock和缓存时会使用到,后面会提到:

"dist": {
    "shasum": "a72b1a42a4d82a721ea438d1b6bf55e66195c637",
    "tarball": "<https://registry.npmjs.org/vue/-/vue-2.6.10.tgz>",
    "fileCount": 222,
    "unpackedSize": 2974862,
    ...
},

每次当我们执行yarn 时,会向registry查询得到上面的压缩包地址进行下载的。 在我们平常开发工作中,会有修改镜像源的场景,比如修改成淘宝源或者自己公司的私有源。 查看和设置源,可以通过yarn config命令来完成:

# 查看当前使用的镜像源
yarn config get registry

# 修改镜像源(比如修改成淘宝源)
yarn config set registry https://registry.npm.taobao.org/

重新认识package.json

我们知道每个项目根目录下都有一个package.json文件,里面定义了运行项目所需要的各种依赖和项目的配置信息等。很多人可能对它的了解紧紧停留在部分属性,并不是所有属性都知道,下面我们来重新认识下package.json。

下面我们看下常用的配置项:

必写属性

package.json中有非常多的配置项,其中必写的两个字段分别是name字段和version字段,它们是组成一个npm模块的唯一标识

name

  • 定义了模块的名称,其命名时需要遵循官方的一些规范和建议
  1. 模块名会是模块url、命令行中的一个参数或者一个文件夹名称,所有非url安全的字符在模块名中都不能使用,一般使用validate-npm-package-name包来检测模块名是否合法
  2. 语义化模块名,可帮助开发者快速找到需要的模块,避免意外获取错误的模块
  3. 若模块名称中存在一些符号,将符号去除后不得与现有的模块名重复
  4. name不能与其他模块名重复,可以通过下面命令查看模块名是否已经被使用
npm view <packageName>

模块存在,可以查看该模块的一些基本信息,如果该模块名从未被使用过,则会抛出404错误;或者去npm上输入模块名,若搜不到,则可使用该模块名

version

npmyarn中模块版本都需遵循SemVer规范,该规范的标准版本号采用X.Y.Z的格式,其中XY 和 Z均为非负的整数,且禁止在数字前方补零。

X.Y.Z(主版本号.次版本号.修订号):
X.主版本号:进行不向下兼容的修改时,递增主版本号
Y.次版本号: 做了向下兼容的新增功能或修改
Z.修订号:做了向下兼容的问题修复
  • 当某个版本改动比较大、并非稳定而且可能无法满足预期的兼容性需求时,要先发布一个先行版本
  • 先行版本号可以加到主版本号.次版本号.修订号的后面,通过-号连接一连串以句点分隔的标识符和版本编译信息
  1. 内部版本(alpha)
  2. 公测版本(beta)
  3. 正式版本的候选版本rc(即Release candiate)

常用的查看模块的版本

npm view <packageName> version # 查看某个模块的最新版本
npm view <packageName> versions # 查看某个模块的所有历史版本

描述信息(description&keywords)

  • description 表示模块的描述信息,方便用户了解模块
  • keywords给模块添加关键字
  • 当使用npm检索模块时,会对模块中的descriptionkeywords匹配,写了package.json中的descriptionkeywords 有利于增加模块的曝光率

项目依赖(dependencies & devDependencies)

  • dependencies: 指定了项目运行所依赖的模块(生产环境使用),如antd、 react等插件库;是生产环境所需要的依赖项,把项目作为一个npm包的时候,用户安装npm包时只会安装dependencies里面的依赖

  • devDependencies:指定项目开发所需要的模块(开发环境使用),如webpackbabel等;在代码打包提交线上时,并不需要这些工具,所以将它放入devDependencies

script

scripts是 package.json中的一种元数据功能,接受一个对象,对象的属性是通过npm runyarn运行的脚本,值为实际运行的命令(通常是终端命令),将终端命令放入scripts字段,可以记录同时方便复用, 如下形式

"scripts": {
  "start": "node index.js"
},

项目入口main

  • package.json中的另一种元数据功能,可以用来指定加载的入口文件。假如我们的项目是一个 npm包,当用户安装包后,require('module')返回的是main中所列出文件的 module.exports属性。
  • 不指定main时,默认值是模块根目录下面的index.js文件

files发布文件配置

  • files用于描述使用npm publish后推送到npm服务器的文件列表,若指定文件夹,则文件夹内的所有内容都会包含进来

  • 我们看下下载的antdpackage.jsonfiles字段,如下所示:

"files": [
    "dist",
    "lib",
    "es"
],

private(定义私有模块)

一般公司的非开源项目,都会设置private属性值为true,因为npm拒绝发布私有模块,设置该字段可防止私有模块被无意发布出去

os(指定模块适用系统)

假如开发了一个模块,只能跑在darwin系统下,需要保证windows用户不会安装该模块,避免发生错误;使用os属性可帮助我们实现,该属性可以指定模块适用的系统,或指定不能安装的系统黑名单

"os" : [ "darwin", "linux" ] # 适用系统
"os" : [ "!win32" ] # 黑名单

cpu(指定模块适用cpu架构)

cpu更精准的限制用户安装环境

"cpu" : [ "x64", "ia32" ] # 适用cpu
"cpu" : [ "!arm", "!mips" ] # 黑名单

engines(指定项目node版本)

当我们新拉取一个项目时,由于和其他开发使用的node版本不同,会导致出现奇怪的问题,为了实现项目开箱即用,可以使用engines来指定项目node版本, 该属性也可指定适用的npm版本

"engines": {
    "node": ">=12.0.0"
 },

bin(自定义命令)

用过vue-cli脚手架的朋友,不知道大家有没有好奇过,为什么安装这些脚手架后,就可以使用vue create命令,其实这和package.json中的bin有关

  • bin用来指定各个内部命令对应的可执行文件的位置。当package.json提供了bin后,即相当于做了一个命令名和本地文件名的映射

  • 当安装带有bin字段的包时,

    1. 如果是全局安装,npm将会使用符号链接把这些文件链接到/usr/local/node_modules/.bin/
    2. 如果是本地安装,会链接到./node_modules/.bin/

我们看下下面的写法:

scripts: {  
  start: './node_modules/bin/cli.js build'
}

// 简写
scripts: {  
  start: 'cli build'
}

来看个例子:

如果要使用my-cli作为命令时,可以配置如下bin字段:

# `my-cli`命令对应的可执行文件为`bin`子目录下的`cli.js`,所以在安装了`my-cli`包的项目中,可以很方便地利用`npm`执行脚本
"bin": {
  "my-cli": "./bin/cli.js"
}

"scripts": {
  start: 'node node_modules/.bin/my-cli'
}

不过上面看起来好像和vue-create不太一样,如果需要一样的话,我们可以在cli.js文件中写入以下命令

# 这行命令的作用是告诉系统用`node`解析,这样上面的命令就可简写成`my-cli`了
#!/usr/bin/env node

总结

  • 所有node_modules/.bin/目录下的命令,都可以用npm run [命令] 或yarn [命令]格式运行
  • 在本地环境下,可行性文件放在node_modules下的.bin文件夹中,npm为scripts字段中的脚本路径自动添加了node_modules/.bin前缀,所以可以直接写
"scripts": {"start": "webpack"}
而不是
"scripts": {"start": "node_modules/.bin webpack"}

到此基本就结束了,如果还想了解更多,欢迎看下面这位大佬的这篇文章,非常详细,如下是思维导图和链接,有兴趣的同学可以点进去看看

image.png

juejin.cn/post/702353…