代码共享方案
早期我们在使用第三方库或第三方框架的时候,我们需要手动对进行下载并手动进行引入
如果第三方库或框架产生了更新,我们需要手动去进行更新或删除操作
随着项目越来越大,所依赖的第三方库越来越多,并且所使用的第三方库可能还依赖了别的库
此时我们使用早期的使用第三方库或第三方框架的方式就变得非常麻烦,并且容易出错
- 必须知道第三方库的代码GitHub的地址,并且从GitHub上手动下载
- 需要在自己的项目中手动的引用,并且管理相关的依赖
- 不需要使用的时候,需要手动来删除相关的依赖
- 当遇到版本升级或者切换时,需要重复上面的操作
为此,我们急需一个工具 可以自动的帮助我们管理我们所依赖的第三方库
- 我们可以通过该工具将我们需要分享的代码分享到特定的仓库中
- 我们可以使用工具来安装、升级、删除我们的工具代码
这个工具就是包管理工具,目前有很多常用的包管理工具,主要有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文件会被放置到项目根目录下
生成方式
-
脚手架安装时候自动生成
-
手动添加
# 项目初始化命名,执行该命令后,会在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 也可以手动进行指定 |
| scripts | scripts属性用于配置一些脚本命令,以键值对的形式存在 配置后我们可以通过 npm run 命令的key来执行这个命令 对于常用的 start、 test、stop、restart可以省略掉run直接通过 npm start等方式运行 |
| dependencies | dependencies属性是指定无论开发环境还是生成环境都需要依赖的包 |
| devDependencies | 一些包在生成环境是不需要的,仅仅只在开发环境下需要使用 ,比如webpack、babel等 这个时候我们会通过 npm install webpack --save-dev,将它安装到devDependencies属性中 |
| peerDependencies | 项目依赖关系是对等依赖,也就是你依赖的一个包,它必须是以另外一个宿主包为前提的比如element-plus是依赖于vue3的,ant design是依赖于react、react-dom |
| homepage | 项目的主页地址 |
| repository | 项目的仓库地址 |
| engines | engines属性用于指定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 | 项目的版本 |
| lockfileVersion | lock文件的版本 |
| requires | 允许使用requires来跟踪模块的依赖关系 |
| dependencies | 某一个包所依赖的其它包 |
dependencies有具有如下属性:
| 字段名 | 说明 |
|---|---|
| version | 实际安装版本号 |
| resolved | 记录下载的地址,registry仓库中的位置 |
| requires/dependencies | 记录当前模块的依赖 |
| integrity | 从缓存中获取索引,再通过索引去获取压缩包文件 |
npm install原理
yarn
yarn是一个和npm功能相同的工具
yarn 是为了弥补 早期npm 的一些缺陷而出现的
早期的npm存在很多的缺陷,比如安装依赖速度很慢、版本依赖混乱等等一系列的问题
虽然从npm5版本开始,进行了很多的升级和改进,但是依然很多人喜欢使用yarn
安装
npm i yarn -g
命令对比
# 在存在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,依赖包将被 存放在一个统一的位置,因此
- 如果你对同一依赖包使用相同的版本,那么磁盘上只有这个依赖包的一份文件
- 如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来
- 所有文件都保存在硬盘上的统一的位置:
- 当安装软件包时, 其包含的所有文件都会硬链接到此位置,而不会占用 额外的硬盘空间
- 这让你可以在项目之间方便地共享相同版本的 依赖包
硬链接和软链接
| 分类 | 说明 |
|---|---|
| 硬链接(hard link) | 硬链接(英语:hard link)是电脑文件系统中的多个文件平等地共享同一个文件存储单元 简单来说 一个硬链接指向的是 真实数据在磁盘上的引用地址 删除一个文件名字后,还可以用其它名字继续访问该文件 |
| 符号链接(软链接soft link、Symbolic link) | 符号链接(软链接、Symbolic link)是一类特殊的文件 其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用 符号链接的功能类似于快捷方式 符号链接一般指向的是一个硬链接 |
示例
- 文件拷贝
# window
copy <源文件> <目标文件>
# mac
cp <源文件> <目标文件>
硬链接
# window
mklink /H <目标文件> <源文件>
# mac
ln <源文件> <目标文件>
- 软连接
# window
mklink <目标文件> <源文件>
# mac
ln -s <源文件> <目标文件>
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')的方式去引入第三方包对应的依赖包
这是十分危险的
anymatch并不存在于项目的package.json中anymatch只是bar对应的依赖包- 如果bar在版本升级的时候移除了anymatch的依赖
- 或者我们在项目中不在使用
bar这个第三方库的时候 - 就会导致
anymatch这个第三方库也一并被移除,直接导致代码无法正常执行
为此pnpm使用了非扁平化的文件方式,并通过软连接 + 硬链接的方式来解决我们可以访问 本不属于当前项目所设定的依赖包的问题
从pnpm官网的这张图这张图中可以看出
-
项目依赖的包bar会被存放到
node_modules的根目录下只不过这个链接只是个软连接 -
对应的硬链接被放到的
node_modules/.pnpm中 -
而bar文件加下存在对应的目录
node_modules其下存在如下同级文件夹
- bar对应的源文件
- bar所依赖的第三方包
- 这些依赖的第三方包依旧是软连接
- 真实的硬链接依旧是存放在
node_modules/.pnpm中
这样做的好处是,在node_modules的根目录下,只存在那些在package.json中定义过的依赖
这样我们就不会随便去访问那些本不属于当前项目所设定的依赖包
基本使用
# 安装
npm install pnpm -g
# 其它命令
# 获取当前活跃的store目录 也就是 .pnpm_store 目录
pnpm store path
# 从store中删除当前未被引用的包来释放store的空间
# pnpm在安装包的时候,会现在pnpm_store中创建对应的硬链接
# 对应项目在使用某个第三方包的时候,会先去pnpm_store中进行查找
# 如果存在对应的包的硬链接,直接在当前项目node_modules中对应位置创建对应的硬链接即可
# 因为其仅仅只是硬链接,并不是压缩文件,所以不存在解压等流程,所以其即起到了缓存的功能,又比缓存的速度更快
# 但是随着pnpm的使用,pnpm-store的体积会越来越大
# 而有的时候项目所使用的依赖已经移除,但是因为pnpm-sotre中存储的是对对应包的硬链接
# 因此其并不会因为项目中依赖包的移除而移除对于的硬链接,如果我们想要移除那些无用的硬链接的时候,可以执行如下指令
pnpm store prune
npm包发布流程
-
去npm官网上注册账号
-
完善要发布包的
package.json -
在命令行中进行远程登录
npm login
- 发布npm包
npm publish
- 更新包
- 修改版本号(最好符合semver规范)
- 重新发布
- 其它操作
# 删除已发布包
npm unpublish
# 让发布包过期
npm deprecate