前言

这是一篇包管理器的基本知识的汇总笔记

为什么要有包管理器

一些基本概念

  • 模块(module)

    通常以单个文件形式存在的功能片段,入口文件通常称之为入口模块主模块

  • 库(library,简称lib)
    以一个或多个模块组成的完整功能块,为开发中某一方面的问题提供完整的解决方案

  • 包(package)
    包含元数据的库,这些元数据包括:名称、描述、git主页、许可证协议、作者、依赖等等

包管理器的背景

随着CommonJS 的出现,node环境下的JS代码可以用模块更加细粒度的划分。一个类、一个函数、一个对象、一个配置等等均可以作为模块,这种细粒度的划分,是开发大型应用的基石。

为了解决在开发过程中遇到的常见问题,比如加密、提供常见的工具方法、模拟数据等等,一时间,在前端社区涌现了大量的第三方库。这些库使用 CommonJS 标准书写而成,非常容易使用。

然而,在下载使用这些第三方库的时候,遇到了非常多的问题

  • 下载过程繁琐
    • 进入官网或 github 主页
    • 找到并下载相应的版本
    • 拷贝到工程的目录中
    • 如果遇到有同名的库,需要更改名称
  • 如果该库需要依赖其他库,还需要按照要求先下载其他库
  • 开发环境中安装的大量的库如何在生产环境中还原,又如何区分
  • 更新一个库的过程也非常繁琐
  • 自己开发的库,要是想在下个项目中使用,也比较繁琐

包管理器的出现就是为了解决这个问题

包管理器

npm官网:https://www.npmjs.com/

最常见的包管理器有下面几个

  • npm
  • yarn
  • cnpm

但是,几乎前端所有的包管理器都是基于 npm 的,目前,npm 不仅是一个包管理器,也是其他包管理的基石

npm 全称为 node package manager,即 node 包管理器,它运行在 node 环境中,让开发者可以用简单的方式完成包的查找、安装、更新、卸载、上传等操作

npm 的出现,很快弥补了 node 没有包管理器的缺陷。于是不久后,node 在安装文件中内置了 npm,当开发者安装好 node 之后,就自动安装了 npm,不仅如此,node 环境还专门为 npm 提供了良好的支持,使用 npm 下载的包更加方便了。

npm

包的安装

本地安装

使用命令npm install 包名npm i 包名即可完成本地安装,本地安装的包出现在当前目录下的node_modules目录中

本地安装适用于绝大部分的包,它会在当前目录及其子目录中发挥作用

另外,安装一个包的时候,npm 会自动管理依赖,它会下载该包的依赖包到node_modules目录中
如果本地安装的包带有 CLI,npm 会将它的 CLI 脚本文件放置到node_modules/.bin下,使用命令npx 命令名即可调用

对比一下

1
2
3
4
5
6
7
8
# 安装一个包,不保存到package.json里
npm install axios
# 会把依赖包名称添加到 package.json文件dependencies键下
npm install axios --save
# 添加到 package.json文件 devDependencies键下
npm install axios --save-dev
# 安装package.json里的全部包
npm install

全局安装

使用命令npm install --global 包名npm i -g 包名就可以进行全局安装了

全局安装的包放置在一个特殊的全局目录,该目录可以通过命令npm config get prefix查看

全局安装的包并非所有工程可用,它仅提供全局的 CLI 工具

大部分情况下,都不需要全局安装包,除非:

  1. 包的版本非常稳定,很少有大的更新
  2. 提供的 CLI 工具在各个工程中使用的非常频繁
  3. CLI 工具仅为开发环境提供支持,而非部署环境

包的配置文件

包的配置文件解决了下面几个问题

  1. 拷贝工程后如何还原?
  2. 如何区分开发依赖和生产依赖?
  3. 如果自身的项目也是一个包,如何描述包的信息

npm 将每个使用 npm 的工程本身都看作是一个包,包的信息需要通过一个名称固定的配置文件来描述,配置文件的固定名称为:package.json

你可以手动创建该文件,但是更多时候,我们使用下面的命令创建

1
npm init

配置文件中可以描述大量的信息,包括:

  • name:包的名称,该名称必须是英文单词字符,支持连接符
  • version:版本
    • 版本规范:主版本号.次版本号.补丁版本号(例如12.4.6)
    • 主版本号:仅当程序发生了重大变化时才会增长,如新增了重要功能、新增了大量的API、技术架构发生了重大变化
    • 次版本号:仅当程序发生了一些小变化时才会增长,如新增了一些小功能、新增了一些辅助型的API
    • 补丁版本号:仅当解决了一些 bug 或 进行了一些局部优化时更新,如修复了某个函数的 bug、提升了某个函数的运行效率
  • description:包的描述
  • homepage:官网地址
  • author:包的作者,必须是有效的 npm 账户名,书写规范是 account <mail>,例如:zhangsan <zhangsan@gmail.com>,不正确的账号和邮箱可能导致发布包时失败
  • repository:包的仓储地址,通常指 git 或 svn 的地址,它是一个对象
    • type:仓储类型,git 或 svn
    • url:地址
  • main:包的入口文件,使用包的人默认从该入口文件导入包的内容
  • keywords: 搜索关键字,发布包后,可以通过该数组中的关键字搜索到包

使用npm init --yesnpm init -y可以在生成配置文件时自动填充默认配置

但是实际上,package.json文件最重要的作用,是记录当前工程的依赖,下面两个属性记录了当前工程的依赖

  • dependencies:生产环境的依赖包
  • devDependencies:仅开发环境的依赖包

相关命令如下

1
2
3
4
5
6
7
8
## 安装依赖到生产环境(保存到dependencies)
npm i 包名
npm i --save 包名
npm i -S 包名

## 安装依赖到开发环境(保存到devDependencies)
npm i --save-dev 包名
npm i -D 包名

配置好依赖后,使用下面的命令即可安装依赖

1
2
3
4
5
6
## 本地安装所有依赖 dependencies + devDependencies
npm install
npm i

## 仅安装生产环境的依赖 dependencies
npm install --production

这样一来,在代码移植时,只需要移植源代码和package.json文件,然后重新安装,就可以在其他地方运行了(所以把代码发给别人时,不要把node_module也发过去了orz)

包的语义版本

有这样的场景:如果你编写了一个包A,依赖另外一个包B,你在编写代码时,包B的版本是3.4.1,你是希望使用你包的人一定要安装包B,并且是3.4.1版本,还是希望他可以安装更高的版本,如果你希望它安装更高的版本,高的什么程度呢?

  1. 有的时候,我们希望:安装我的依赖包的时候,次版本号和补丁版本号是可以有提升的,但是主版本号不能变化
  2. 有的时候,我们又希望:安装我的依赖包的时候,只有补丁版本号可以提升,其他都不能提升
  3. 有时我们甚至希望依赖包保持固定的版本,尽管这比较少见

这样一来,就需要在配置文件中描述清楚具体的依赖规则,而不是直接写上版本号那么简单。

我们可以用语义版本来描述包

符号 描述 示例 示例描述
> 大于某个版本 >1.2.1 大于1.2.1版本
>= 大于等于某个版本 >=1.2.1 大于等于1.2.1版本
< 小于某个版本 <1.2.1 小于1.2.1版本
<= 小于等于某个版本 <=1.2.1 小于等于1.2.1版本
- 介于两个版本之间 1.2.1 - 1.4.5 介于1.2.1和1.4.5之间
x 不固定的版本号 1.3.x 只要保证主版本号是1,次版本号是3即可
~ 补丁版本号可增 ~1.3.4 保证主版本号是1,次版本号是3,补丁版本号大于等于4
^ 次版本和补丁版本可增 ^1.3.4 保证主版本号是1,次版本号可以大于等于3,补丁版本号可以大于等于4
* 最新版本 * 始终安装最新版本

版本依赖控制始终是一个两难的问题

如果允许版本增加,可以让依赖包的bug得以修复,甚至可以带来一些性能提升,但同样可能带来不确定的风险(新的bug)

如果不允许版本增加,可以获得最好的稳定性,但失去了依赖包自我优化的能力

而有的时候情况更加复杂,如果依赖包升级后,依赖也发生了变化,会有更多不确定的情况出现

基于此,npm 在安装包的时候,会自动生成一个 package-lock.json 文件,该文件记录了安装包时的确切依赖关系

当移植工程时,如果移植了 package-lock.json 文件,恢复安装时,会按照 package-lock.json 文件中的确切依赖进行安装,最大限度的避免了差异

npm脚本

在开发的过程中,我们可能会反复使用很多的 CLI 命令

这些命令可能很长,难以记忆,所以npm设置了脚本,只需要在 package.json 中配置 scripts 字段,即可配置各种脚本名称

我们用react脚手架创建的项目的package.json来演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "react-ts-test",
"version": "0.1.0",
"private": true,
"dependencies": {
// ...
},
// 脚本
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
}

这些脚本的运行方式是

1
npm run 脚本名称

另外,npm 还对某些常用的脚本名称进行了简化,下面的脚本名称是不需要使用run的:

  • start
  • stop
  • test

所以你可以使用下面的命令,不用加run

1
npm start

最后,脚本中可以省略npx

比如有下面的脚本

1
2
3
4
5
6
7
8
9
{
"name": "webpacktest",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
}
}

如果不写在脚本里,需要在命令行里运行npx webpack才能使用这个脚本

其他常用的npm命令

安装

安装指定版本的包

1
2
# 比如 npm install less@4.1.0
npm install 包名@版本号

查询

  1. 查询包安装路径
1
npm root [-g]
  1. 查看包信息
1
2
## 你可以使用下面的单词来代替view:v info show
npm view 包名 [子信息]
  1. 查询安装包
1
2
## 你可以使用下面的单词来代替list: ls  la  ll
npm list [-g] [--depth=依赖深度]

更新

  1. 检查有哪些包需要更新
1
npm outdated
  1. 更新包
1
2
## 你可以使用下面的单词来代替update :up、upgrade
npm update [-g] [包名]

卸载

1
2
## 你可以使用下面的单词来代替uninstall: remove, rm, r, un, unlink
npm uninstall [-g] 包名

yarn

为什么要有yarn

yarn 是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具,它仍然使用 npm 的registry,不过提供了新的CLI 来对包进行管理

之所以会出现这种情况,是因为在过去,npm 存在下面的问题:

  • 依赖目录嵌套层次深:过去,npm 的依赖是嵌套的,这在 windows 系统上是一个极大的问题,因为windows 系统无法支持太深的目录(虽然npm后面也把依赖改成了扁平化的,但是当时没有改)
  • 下载速度慢
    • 因为npm对包的下载是串行的,即前一个包下载完后才会下载下一个包,导致带宽资源没有完全利用
    • 因为下载没有缓存,多个相同版本的包会被重复的下载
  • 无法定位错误位置:过去,npm 安装包的时候,每安装一个依赖,就会输出依赖的详细信息,导致一次安装有大量的信息输出到控制台。如果中途某个时候,一个包抛出了一个错误,但是npm会继续下载和安装包。因为npm会把所有的日志输出到终端,有关错误包的错误信息就会在一大堆npm打印的警告中丢失掉,并且你甚至永远不会注意到实际发生的错误。
  • 工程移植问题:由于 npm 的版本依赖可以是模糊的(因为npm使用的是语义版本),可能会导致工程移植后,依赖的确切版本不一致(npm5后才有package-lock.json)。

相对的,yarn就有下面的优势

  • node_module目录扁平化

  • 下载速度快

    • 并行下载
    • 本地缓存
  • 控制台仅输出关键信息

  • 使用了yanr-lock文件来记录依赖的确切版本

另外,yarn还优化了下面的内容

  • 增加了一些功能强大的命令
  • 让现有的命令更加语义化(比如把npm install react –save改为yarn add react)
  • 更加方便的yarn run 命令,它不仅仅会自动查看 package.json 中 scripts 下面的内容,还是查找 node_modules/.bin 下的可执行文件。也就是说,本地安装的CLI工具可以使用 yarn 直接启动

yarn 的出现给 npm 带来了巨大的压力,很快,npm 学习了 yarn 先进的理念,不断的对自身进行优化,到了目前的npm6版本,几乎完全解决了上面的问题:

在npm6版本,npm已经做了下面的优化

  • 目录扁平化
  • 并行下载
  • 本地缓存
  • 使用package-lock记录确切依赖
  • 增加了大量的命令别名
  • 内置了npx,可以启动本地的CLI工具
  • 极大的简化了控制台输出

npm6 之后,可以说npm已经和yarn非常接近,甚至没有差距了。

yarn核心命令

初始化

1
yarn init [--yes/-y]

安装

添加新的包

1
yarn [global] add package-name [--dev/-D] [--exact/-E]

安装package.json中的所有依赖

1
yarn install [--production/--prod]

运行脚本和本地cli工具

使用下面的命令

1
2
3
4
# 运行脚本
yarn run 脚本名
# 运行本地安装的cli
yarn run cli名

cli名指的是node_module/.bin目录下的脚本名

image-20210128202659228

查询

查看bin目录

1
yarn [global] bin

查询包信息

1
yarn info 包名 [子字段]

列举已安装的依赖

1
yarn [global] list [--depth=依赖深度]

yarn的list命令和npm的list不同,yarn输出的信息更加丰富,包括顶级目录结构、每个包的依赖版本号

更新

列举需要更新的包

1
yarn outdated

更新包

1
yarn [global] upgrade [包名]

卸载

1
yarn remove 包名

yarn的其他命令

在终端命令上,yarn不仅仅是对npm的命令做了一个改名,还增加了一些原本没有的命令,这些命令在某些时候使用起来非常方便

yarn check

使用yarn check命令,可以验证package.json文件的依赖记录和lock文件是否一致

这对于防止篡改非常有用

yarn audit

使用yarn audit命令,可以检查本地安装的包有哪些已知漏洞,以表格的形式列出,漏洞级别分为以下几种:

  • INFO:信息级别

  • LOW: 低级别

  • MODERATE:中级别

  • HIGH:高级别

  • CRITICAL:关键级别

yarn why

使用yarn why 包名命令,可以在控制台打印出为什么安装了这个包,哪些包会用到它

yarn create

这个命令在使用一些脚手架时特别方便

过去,我们都是使用如下的做法:

  1. 全局安装脚手架工具
  2. 使用全局命令搭建脚手架

由于大部分脚手架工具都是以create-xxx的方式命名的,比如react的官方脚手架名称为create-react-app

因此,可以使用yarn create命令来一步完成安装和搭建

例如:

1
2
3
4
yarn create react-app my-app
# 等同于下面的两条命令
yarn global add create-react-app
create-react-app my-app

FAQ

npm中的依赖包分类

npm 依赖管理中被忽略的那些细节

依赖共享和冲突