package-manager
# 前言
这是一篇包管理器的基本知识的汇总笔记
# 为什么要有包管理器
# 一些基本概念
模块(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 命令名
即可调用
对比一下
# 安装一个包,不保存到package.json里
npm install axios
# 会把依赖包名称添加到 package.json文件dependencies键下
npm install axios --save
# 添加到 package.json文件 devDependencies键下
npm install axios --save-dev
# 安装package.json里的全部包
npm install
2
3
4
5
6
7
8
# 全局安装
使用命令npm install --global 包名
或 npm i -g 包名
就可以进行全局安装了
全局安装的包放置在一个特殊的全局目录,该目录可以通过命令npm config get prefix
查看
全局安装的包并非所有工程可用,它仅提供全局的 CLI 工具
大部分情况下,都不需要全局安装包,除非:
- 包的版本非常稳定,很少有大的更新
- 提供的 CLI 工具在各个工程中使用的非常频繁
- CLI 工具仅为开发环境提供支持,而非部署环境
# 包的配置文件
包的配置文件解决了下面几个问题
- 拷贝工程后如何还原?
- 如何区分开发依赖和生产依赖?
- 如果自身的项目也是一个包,如何描述包的信息
npm 将每个使用 npm 的工程本身都看作是一个包,包的信息需要通过一个名称固定的配置文件来描述,配置文件的固定名称为:package.json
你可以手动创建该文件,但是更多时候,我们使用下面的命令创建
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 --yes
或npm init -y
可以在生成配置文件时自动填充默认配置
但是实际上,package.json文件最重要的作用,是记录当前工程的依赖,下面两个属性记录了当前工程的依赖
- dependencies:生产环境的依赖包
- devDependencies:仅开发环境的依赖包
相关命令如下
## 安装依赖到生产环境(保存到dependencies)
npm i 包名
npm i --save 包名
npm i -S 包名
## 安装依赖到开发环境(保存到devDependencies)
npm i --save-dev 包名
npm i -D 包名
2
3
4
5
6
7
8
配置好依赖后,使用下面的命令即可安装依赖
## 本地安装所有依赖 dependencies + devDependencies
npm install
npm i
## 仅安装生产环境的依赖 dependencies
npm install --production
2
3
4
5
6
这样一来,在代码移植时,只需要移植源代码和package.json文件,然后重新安装,就可以在其他地方运行了(所以把代码发给别人时,不要把node_module也发过去了orz)
# 包的语义版本
有这样的场景:如果你编写了一个包A,依赖另外一个包B,你在编写代码时,包B的版本是3.4.1,你是希望使用你包的人一定要安装包B,并且是3.4.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.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
来演示
{
"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"
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这些脚本的运行方式是
npm run 脚本名称
另外,npm 还对某些常用的脚本名称进行了简化,下面的脚本名称是不需要使用run的:
- start
- stop
- test
所以你可以使用下面的命令,不用加run
npm start
最后,脚本中可以省略npx
比如有下面的脚本
{
"name": "webpacktest",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
}
}
2
3
4
5
6
7
8
9
如果不写在脚本里,需要在命令行里运行npx webpack
才能使用这个脚本
# 其他常用的npm命令
# 安装
安装指定版本的包
# 比如 npm install less@4.1.0
npm install 包名@版本号
2
# 查询
- 查询包安装路径
npm root [-g]
- 查看包信息
## 你可以使用下面的单词来代替view:v info show
npm view 包名 [子信息]
2
- 查询安装包
## 你可以使用下面的单词来代替list: ls la ll
npm list [-g] [--depth=依赖深度]
2
# 更新
- 检查有哪些包需要更新
npm outdated
- 更新包
## 你可以使用下面的单词来代替update :up、upgrade
npm update [-g] [包名]
2
# 卸载
## 你可以使用下面的单词来代替uninstall: remove, rm, r, un, unlink
npm uninstall [-g] 包名
2
# 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核心命令
# 初始化
yarn init [--yes/-y]
# 安装
添加新的包
yarn [global] add package-name [--dev/-D] [--exact/-E]
安装package.json中的所有依赖
yarn install [--production/--prod]
# 运行脚本和本地cli工具
使用下面的命令
# 运行脚本
yarn run 脚本名
# 运行本地安装的cli
yarn run cli名
2
3
4
cli名指的是node_module/.bin目录下的脚本名
# 查询
查看bin目录
yarn [global] bin
查询包信息
yarn info 包名 [子字段]
列举已安装的依赖
yarn [global] list [--depth=依赖深度]
yarn的list命令和npm的list不同,yarn输出的信息更加丰富,包括顶级目录结构、每个包的依赖版本号
# 更新
列举需要更新的包
yarn outdated
更新包
yarn [global] upgrade [包名]
# 卸载
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
这个命令在使用一些脚手架时特别方便
过去,我们都是使用如下的做法:
- 全局安装脚手架工具
- 使用全局命令搭建脚手架
由于大部分脚手架工具都是以create-xxx
的方式命名的,比如react的官方脚手架名称为create-react-app
因此,可以使用yarn create
命令来一步完成安装和搭建
例如:
yarn create react-app my-app
# 等同于下面的两条命令
yarn global add create-react-app
create-react-app my-app
2
3
4
# FAQ
- 01
- 009-Palindrome Number[回文数]03-10
- 02
- 008-String to Integer (atoi)[字符串转整数]03-10
- 03
- 004-Reverse-integer[整数反转]03-09