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会准确的存储每个依赖的具体版本信息,以保证在不同机器安装可以得到相同的结果。
下面以@babel/code-frame为例,看看yarn.lock 中会记录哪些信息
- 第一行
"@babel/code-frame@7.12.11包的name和语义化版本号,这些都来自package.json中的定义 version字段,记录的是一个确切的版本resolved字段记录的是包的URL地址。其中hash值,即dist.shasumdependencies字段记录的是当前包的依赖,即当前包在package.json的dependencies字段中的所有依赖.
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做了一些类似的改进
- 默认新增了类似yarn.lock的package-lock.json
- git依赖支持优化:这个特性在需要安装大量内部项目,或需要使用某些依赖的未发布版本时很有用。
- 文件依赖优化:在之前版本,如果将本地目录作为依赖来安装,将会把文件目录作为副本拷贝到 node_modules中。在最新的npm版本中,将改为使用创建symlinks的方式来实现,不再执行文件拷贝。这将会提升安装速度。目前yarn还不支持
- 速度和yarn构建时间不再有显著的差异
yarn和npm命令的区别
| npm | yarn | 说明 | |
|---|---|---|---|
| npm init | yarn init | 初始化项目,生成package.json文件| | |
| npm install 模块名 --save | yarn add 模块名 | 在目录下添加项目的依赖包,并在package.json下写入配置,最终安装的包在dependencies中 | |
| npm install 模块名 --save-dev | yarn add 模块名 --dev | 在目录下添加某个开发时依赖包,最终安装到devDependencies中 | |
| npm uninstall 模块名 | yarn remove 模块名 | 移除目录下指定的项目依赖包 | |
| npm update 模块名 --save | yarn upgrade 模块名 | 更新目录下指定的项目依赖包 | |
| npm install 模块名 -g | yarn global add 模块名 | 在全局下添加依赖包 | |
| npm config get key | yarn config get key | 查看配置key的值 | |
| npm config set registry registry.npm.taobao.org | yarn config set key value [-g | --global] | 设置配置项key的值为value |
| npm config delete key | yarn config delete key | 从配置中删除配置key | |
| npm list | yarn list | 查询当前工作文件夹所有的依赖 | |
| npm info package | yarn info package | 看包信息 |
相关概念
什么是registry
registry是模块仓库提供的一个查询服务,即我们常说的源。以yarn官方镜像源为例,它的查询服务网址是https://registry.yarnpkg.com,这个网址后面跟上模块名,就会得到一个JSON对象,里面是该模块所有版本的信息。比如,访问https://registry.npmjs.org/vue,就会看到vue模块所有版本的信息。
registry网址的模块名后面,还可以跟上版本号或者标签,用来查询某个具体版本的信息,registry.yarnpkg.com/vue/2.6.10
上面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
- 定义了模块的名称,其命名时需要遵循官方的一些规范和建议
- 模块名会是模块
url、命令行中的一个参数或者一个文件夹名称,所有非url安全的字符在模块名中都不能使用,一般使用validate-npm-package-name包来检测模块名是否合法 - 语义化模块名,可帮助开发者快速找到需要的模块,避免意外获取错误的模块
- 若模块名称中存在一些符号,将符号去除后不得与现有的模块名重复
name不能与其他模块名重复,可以通过下面命令查看模块名是否已经被使用
npm view <packageName>
模块存在,可以查看该模块的一些基本信息,如果该模块名从未被使用过,则会抛出404错误;或者去npm上输入模块名,若搜不到,则可使用该模块名
version
npm或yarn中模块版本都需遵循SemVer规范,该规范的标准版本号采用X.Y.Z的格式,其中X、Y 和 Z均为非负的整数,且禁止在数字前方补零。
X.Y.Z(主版本号.次版本号.修订号):
X.主版本号:进行不向下兼容的修改时,递增主版本号
Y.次版本号: 做了向下兼容的新增功能或修改
Z.修订号:做了向下兼容的问题修复
- 当某个版本改动比较大、并非稳定而且可能无法满足预期的兼容性需求时,要先发布一个先行版本
- 先行版本号可以加到
主版本号.次版本号.修订号的后面,通过-号连接一连串以句点分隔的标识符和版本编译信息
- 内部版本(alpha)
- 公测版本(beta)
- 正式版本的候选版本rc(即Release candiate)
常用的查看模块的版本
npm view <packageName> version # 查看某个模块的最新版本
npm view <packageName> versions # 查看某个模块的所有历史版本
描述信息(description&keywords)
description表示模块的描述信息,方便用户了解模块keywords给模块添加关键字- 当使用
npm检索模块时,会对模块中的description和keywords匹配,写了package.json中的description和keywords有利于增加模块的曝光率
项目依赖(dependencies & devDependencies)
-
dependencies: 指定了项目运行所依赖的模块(生产环境使用),如
antd、react等插件库;是生产环境所需要的依赖项,把项目作为一个npm包的时候,用户安装npm包时只会安装dependencies里面的依赖 -
devDependencies:指定项目开发所需要的模块(开发环境使用),如
webpack、babel等;在代码打包提交线上时,并不需要这些工具,所以将它放入devDependencies中
script
scripts是 package.json中的一种元数据功能,接受一个对象,对象的属性是通过npm run或yarn运行的脚本,值为实际运行的命令(通常是终端命令),将终端命令放入scripts字段,可以记录同时方便复用, 如下形式
"scripts": {
"start": "node index.js"
},
项目入口main
package.json中的另一种元数据功能,可以用来指定加载的入口文件。假如我们的项目是一个npm包,当用户安装包后,require('module')返回的是main中所列出文件的module.exports属性。- 不指定
main时,默认值是模块根目录下面的index.js文件
files发布文件配置
-
files用于描述使用npm publish后推送到npm服务器的文件列表,若指定文件夹,则文件夹内的所有内容都会包含进来 -
我们看下下载的
antd的package.json的files字段,如下所示:
"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字段的包时,- 如果是全局安装,
npm将会使用符号链接把这些文件链接到/usr/local/node_modules/.bin/; - 如果是本地安装,会链接到
./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"}
到此基本就结束了,如果还想了解更多,欢迎看下面这位大佬的这篇文章,非常详细,如下是思维导图和链接,有兴趣的同学可以点进去看看