构建基础 - 常见的包管理工具

509 阅读16分钟

代码共享方案

早期我们在使用第三方库或第三方框架的时候,我们需要手动对进行下载并手动进行引入

如果第三方库或框架产生了更新,我们需要手动去进行更新或删除操作

随着项目越来越大,所依赖的第三方库越来越多,并且所使用的第三方库可能还依赖了别的库

此时我们使用早期的使用第三方库或第三方框架的方式就变得非常麻烦,并且容易出错

  1. 必须知道第三方库的代码GitHub的地址,并且从GitHub上手动下载
  2. 需要在自己的项目中手动的引用,并且管理相关的依赖
  3. 不需要使用的时候,需要手动来删除相关的依赖
  4. 当遇到版本升级或者切换时,需要重复上面的操作

为此,我们急需一个工具 可以自动的帮助我们管理我们所依赖的第三方库

  1. 我们可以通过该工具将我们需要分享的代码分享到特定的仓库中
  2. 我们可以使用工具来安装、升级、删除我们的工具代码

这个工具就是包管理工具,目前有很多常用的包管理工具,主要有npm yarn pnpm

包管理工具下载的第三方库和框架会被自动添加到node_modules文件夹下

当我们使用模块化方式去引入对应库或框架的时候,根据模块化的查找规则

会自动去node_modules下找到对应库或框架 并进行引入

也就是说使用包管理工具后,我们只需要下载对应的库或框架后直接引入即可使用

并不需要去关心这个库或框架所在的仓库位置,以及如何去下载,管理,更新,维护或删除所使用的第三方库或框架

所有的这些包管理工具都会自动帮助我们进行完成

npm

npm(Node Package Manager)也就是Node包管理器, 最早被用于管理node环境下的第三方包依赖

但是目前已经不仅仅是Node包管理器了,在前端项目中我们也在使用它来管理依赖的包

当我们发布包的时候,我们使用npm工具可以将我们的代码提交到npm registry上

当我们下载对应包的时候,npm工具会自动去npm registry上下载或更新我们所需要使用的第三方包

同时,为了方便我们查找我们所需要使用的第三方包,npm提供了对应的官网

通过npm官网,我们可以查看和搜索所有被存放到npm registry上的库和框架

配置文件

事实上,我们每一个项目都会有一个对应的名为package.json的配置文件,无论是前端项目(Vue、React)还是后端项目(Node)

这个配置文件会记录着你项目的名称、版本号、项目描述等,也会记录着你项目所依赖的其他库的信息和依赖库的版本号

package.json本质就是一个拥有特定字段的JSON文件,通过这个json文件,可以帮助我们对项目进行配置和管理

一般情况下,我们的项目源代码会被放置到src目录下,而package.json文件会被放置到项目根目录下

生成方式

  1. 脚手架安装时候自动生成

  2. 手动添加

    # 项目初始化命名,执行该命令后,会在CLI中使用问答的方式生成基本的package.json
    # 如果我们在生成package.json的时候,所有的字段都使用默认配置,可以在执行init命令的时候 加上-y参数
    $ npm init -y
    

示例

{
  "name": "foo", 
  "version": "1.0.0",
  "description": "", 
  "main": "index.js",
  "scripts": {  
    "test": "echo \"Error: no test specified\" && exit 1" 
  },
  "keywords": [], 
  "author": "", 
  "license": "ISC",
  "homepage": "https://github.com/coderwxf/example",
  "repository": {
    "type": "git",
    "url": "https://github.com/coderwxf/example"
  }
}
字段名描述
name项目名, 一般一个项目就会被看成是一个包 所以也可以被称之为包名
必填字段
对于一个包名,有着如下的命名规则:
1. 使用小写字母,数字组成 单词和单词之间使用中划线 连接
2. 字母和字母连接符可以使用点和下划线 但是不推荐
3. 项目名只能以数字和小写字母开头
version项目的版本号
必填字段
遵循semver规范
description项目的基本描述
author作者相关信息
license开源协议
keywords项目对应关键字
值类型是一个数组
private记录项目是否是私有属性
如果值为true,则无法使用npm等工具将其发布到公共仓库
一般用于防止误操作
main项目入口文件
如果不配置,默认为项目根目录下的index.js
也可以手动进行指定
scriptsscripts属性用于配置一些脚本命令,以键值对的形式存在
配置后我们可以通过 npm run 命令的key来执行这个命令
对于常用的 start、 test、stop、restart可以省略掉run直接通过 npm start等方式运行
dependenciesdependencies属性是指定无论开发环境还是生成环境都需要依赖的包
devDependencies一些包在生成环境是不需要的,仅仅只在开发环境下需要使用 ,比如webpack、babel等
这个时候我们会通过 npm install webpack --save-dev,将它安装到devDependencies属性中
peerDependencies项目依赖关系是对等依赖,也就是你依赖的一个包,它必须是以另外一个宿主包为前提的
比如element-plus是依赖于vue3的,ant design是依赖于react、react-dom
homepage项目的主页地址
repository项目的仓库地址
enginesengines属性用于指定Node和NPM的版本号
在安装的过程中,会先检查对应的引擎版本,如果不符合就会报错
其它字段有一些字段,是专门给某个第三方库所使用的,他们即可以配置到package.json中,也可以配置到一个独立的配置文件中(推荐)
如: browserslist, eslint等

版本管理

npm的包通常需要遵从semver版本规范, 也就是说主要格式为X.Y.Z

类型说明
X主版本号(major)当做了不兼容的 API 修改(可能不兼容之前的版本)
1. 可以是做了大版本的更新 2. 可能新的功能是不向前兼容的,即不兼容老版本代码
Y次版本号(minor)当做了向下兼容的功能性新增(新功能增加,但是兼容之前的版本)
Z修订号(patch)当做了向下兼容的问题修正(没有新功能,修复了之前版本的bug)
版本形式说明
x.y.z一个明确的版本号
^x.y.z表示x是保持不变的,y和z永远安装最新的版本
在安装对应包的时候,想要确保安装的包总是拥有最新的向前兼容特性并修复了对应bug时
可以使用这种版本模式
~x.y.z表示x和y保持不变的,z永远安装最新的版本
在安装对应包的时候,不需要对应的包拥有最新的功能特性,而是仅需要使用最新的已经修复了对应bug的版本时
可以使用这种版本模式

常见命令

安装npm包分两种情况:

安装方式指令说明
全局安装(global install)npm install <项目名> -g全局安装,会被安装到node所管理的一个全局文件夹下
一般是一些工具类的库会使用这种安装方式
如: yarn, pnpm, nodemon等
在使用全局安装的情况下,node会自动将对应的工具库添加到环境变量中
所以在全局安装某个库中,我们可以在任何路径的命令行中直接使用对应的库
项目(局部, 本地)安装(local install)npm install <项目名>局部安装某个库,对应的库会安装到安装命令执行所在的文件夹下的node_modules
一般是项目中使用的第三方依赖,会使用这种安装方式
# 根据package.json中的依赖配置安装对应的依赖包
npm install

# 默认可以省略-save或其简写-S 都表示安装到生产环境依赖中
npm install | i <包名> [<包名> <包名> ...]  [--save | -S]

# 安装成开发依赖
npm install | i <包名> [<包名> <包名> ...] -D | --save-dev 

# 卸载
npm uninstall <包名>

# 强制重新构建包
npm rebuild

# 清除对应缓存
npm cache clean

package-lock.json

package.json记录着的是依赖包的版本可以使用的范围,是一种比较宽松的依赖

但是实际使用中,我们有的时候,可能需要知道具体所下载的是那个版本的包,

因此我们需要一个地方来记录着我们所安装的依赖包的具体版本号

同时如果我们有多个项目,而这多个项目中依赖的某些包是相同的包,而且他们的版本号也是相同的

此时我们就没有必要重复去服务器下载对应的包,我们只需要开辟一块空间来缓存这些包对应的压缩包即可

下次在安装的时候,优先去缓存中进行查找,如果有且符合使用条件则使用,如果没有再去远程仓库下载

在这个过程中,我们必然需要一个文件来记录着这些缓存所在的位置

而这个文件,就是package-lock.json文件

字段名说明
name项目的名称
version项目的版本
lockfileVersionlock文件的版本
requires允许使用requires来跟踪模块的依赖关系
dependencies某一个包所依赖的其它包

dependencies有具有如下属性:

字段名说明
version实际安装版本号
resolved记录下载的地址,registry仓库中的位置
requires/dependencies记录当前模块的依赖
integrity从缓存中获取索引,再通过索引去获取压缩包文件

npm install原理

image.png

yarn

yarn是一个和npm功能相同的工具

yarn 是为了弥补 早期npm 的一些缺陷而出现的

早期的npm存在很多的缺陷,比如安装依赖速度很慢、版本依赖混乱等等一系列的问题

虽然从npm5版本开始,进行了很多的升级和改进,但是依然很多人喜欢使用yarn

安装

npm i yarn -g

命令对比

image.png

# 在存在package.json文件的情况下,我们想要一次性统一安装package.json中配置的依赖时候

# 使用npm 
npm install

# 使用yarn
yarn install

# 此时对于yarn命令而言, install可以省略 (对于npm指令,install指令不可以省略)
yarn # 功能等价于 yarn install

npx

npx是npm5.2之后自带的一个命令, 我们经常使用它来调用项目中的某个模块的指令

默认情况下,当我们执行一个命名的时候,会先在当前目录中进行查找,如果没有找到,会根据环境变量去全局进行查找

如果找到直接执行,如果没有找到则报错

但是有的时候,我们希望执行的就是本地对应脚本,而非全局脚本

此时我们有如下执行方式

方式一

任何局部安装的可执行库,对应都会在node_modules/.bin下创建对应的可执行脚本链接

所以我们可以直接执行对应node_modules/.bin文件夹下的脚本

 ./node_modules/.bin/webpack --version

方式二

"scripts": {
  // 当执行package.json的scripts脚本的时候
  // 会优先在当前目录下的node_modules/.bin文件夹下查找对应的命令
  // 如果不存在,再去全局进行查找
  "version": "webpack --version"
}

方式三

# npx命令执行过程
# 1. 在当前目录下的node_modules/.bin文件夹中查找,找到就使用
# 2. 没有找打,去全局查找对应的可执行脚步,找到就使用
# 3. 如果依旧没有找到,会去npm registry中下载对应可执行脚本到本地临时文件夹中 
#    并执行对应脚本,执行完毕后会自动移除对应脚本
npx webpack --version

pnpm

pnpm 是 performant npm 缩写

当使用 npm 或 Yarn 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要保存 100 份该相同依赖包的副本

如果是使用 pnpm,依赖包将被 存放在一个统一的位置,因此

  • 如果你对同一依赖包使用相同的版本,那么磁盘上只有这个依赖包的一份文件
  • 如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来
  • 所有文件都保存在硬盘上的统一的位置:
    • 当安装软件包时, 其包含的所有文件都会硬链接到此位置,而不会占用 额外的硬盘空间
    • 这让你可以在项目之间方便地共享相同版本的 依赖包

image.png

硬链接和软链接

分类说明
硬链接(hard link)硬链接(英语:hard link)是电脑文件系统中的多个文件平等地共享同一个文件存储单元
简单来说 一个硬链接指向的是 真实数据在磁盘上的引用地址
删除一个文件名字后,还可以用其它名字继续访问该文件
符号链接(软链接soft link、Symbolic link)符号链接(软链接、Symbolic link)是一类特殊的文件
其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用
符号链接的功能类似于快捷方式
符号链接一般指向的是一个硬链接

image.png

示例

  1. 文件拷贝
# window
copy <源文件> <目标文件>

# mac
cp <源文件> <目标文件>

image.png

  1. 硬链接
# window
mklink /H <目标文件> <源文件>

# mac
ln <源文件> <目标文件>

image.png

  1. 软连接
# window
mklink <目标文件> <源文件>

# mac
ln -s <源文件> <目标文件>

image.png

pnpm创建非扁平的 node_modules 目录

最早的时候,npm下载对应的依赖包的时候,会创建类似于如下结构的node_modules

# 部分 只包含主要目录
node_modules # 项目根目录对应的node_modules
├── bar # 项目依赖包 bar
│   └── node_modules # bar 又依赖于下面两个包
│       ├── anymatch
│       └── form-data
└── foo # 项目依赖包
    └── node_modules # foo 又依赖于下面两个报
        ├── ajv
        └── form-data

使用上面结构具有很明显的问题

bar 和 foo 具有同样的依赖包 form-data , 且他们所依赖的form-data的版本可能是一致的

因为npm和yarn使用的是库的拷贝,所以就意味着会重复安装两次相同版本的form-data

于是 自npm5.x开始 和 yarn 一样采用扁平化node_modules结构

# 即所有软件包都将被提升到 node_modules 的 根目录下
# 这样在安装依赖前到node_modules下先查看之前对应的依赖包是否已经被安装
# 通过这种方式可以避免包的重复下载
node_modules
├── ajv
├── anymatch
├── bar
├── foo
└── form-data

但是我们知道,我们引入一个第三方包的时候,会默认去node_modules去查找对应的依赖包

于是 这样就导致了另一个问题 我们可以访问 本不属于当前项目所设定的依赖包

例如在本例中 我们项目的package.json依赖了两个第三方包foo 和 bar

但是因为foo 和 bar的依赖anymatch ajv formdata都被提升到了node_modules根目录下

于是我们可以在我们的代码中使用类似于const anymatch = require('anymatch')的方式去引入第三方包对应的依赖包

这是十分危险的

  1. anymatch并不存在于项目的package.json
  2. anymatch只是bar对应的依赖包
  3. 如果bar在版本升级的时候移除了anymatch的依赖
  4. 或者我们在项目中不在使用bar这个第三方库的时候
  5. 就会导致anymatch这个第三方库也一并被移除,直接导致代码无法正常执行

为此pnpm使用了非扁平化的文件方式,并通过软连接 + 硬链接的方式来解决我们可以访问 本不属于当前项目所设定的依赖包的问题

image.png

从pnpm官网的这张图这张图中可以看出

  1. 项目依赖的包bar会被存放到node_modules的根目录下只不过这个链接只是个软连接

  2. 对应的硬链接被放到的node_modules/.pnpm

  3. 而bar文件加下存在对应的目录node_modules

    其下存在如下同级文件夹

    • bar对应的源文件
    • bar所依赖的第三方包
      • 这些依赖的第三方包依旧是软连接
      • 真实的硬链接依旧是存放在node_modules/.pnpm

这样做的好处是,在node_modules的根目录下,只存在那些在package.json中定义过的依赖

这样我们就不会随便去访问那些本不属于当前项目所设定的依赖包

基本使用

# 安装
npm install pnpm -g

image.png

# 其它命令

# 获取当前活跃的store目录 也就是 .pnpm_store 目录
pnpm store path

# 从store中删除当前未被引用的包来释放store的空间
# pnpm在安装包的时候,会现在pnpm_store中创建对应的硬链接
# 对应项目在使用某个第三方包的时候,会先去pnpm_store中进行查找
# 如果存在对应的包的硬链接,直接在当前项目node_modules中对应位置创建对应的硬链接即可
# 因为其仅仅只是硬链接,并不是压缩文件,所以不存在解压等流程,所以其即起到了缓存的功能,又比缓存的速度更快
# 但是随着pnpm的使用,pnpm-store的体积会越来越大
# 而有的时候项目所使用的依赖已经移除,但是因为pnpm-sotre中存储的是对对应包的硬链接
# 因此其并不会因为项目中依赖包的移除而移除对于的硬链接,如果我们想要移除那些无用的硬链接的时候,可以执行如下指令
pnpm store prune

npm包发布流程

  1. npm官网上注册账号

  2. 完善要发布包的package.json

  3. 在命令行中进行远程登录

npm login
  1. 发布npm包
npm publish
  1. 更新包
  • 修改版本号(最好符合semver规范)
  • 重新发布
  1. 其它操作
# 删除已发布包
npm unpublish

# 让发布包过期
npm deprecate