前端包管理发展之路

包管理是什么?

        早期前端开发者通常会直接在 HTML 中通过 <script> 标签引入 JavaScript 文件,或者下载 ZIP 文件并手动将库文件放入项目中。这种方式在小型项目中可能有效,但在大型项目中,管理多个库的依赖关系和版本变得非常复杂和容易出错,为此迫切需要一个包管理工具实现依赖的管理。

        包管理是一种软件工程的实践,它涉及自动处理软件包的安装、升级、配置和移除。软件包是预先编写好的、可重用的代码,通常用于解决特定的问题或提供特定的功能。

        包管理的核心目的是为了简化开发者的工作流程,确保项目依赖的稳定性和一致性,并促进代码的重用。

        常见的包管理工具包括:

  • NPM:Node.js 的包管理工具,也广泛用于前端开发。
  • Yarn:Facebook 开发的包管理工具,提供了更快、更可靠的依赖关系管理。
  • PNPM:高性能的包管理工具,旨在改进 npm 和 Yarn 在某些方面的性能和依赖关系处理。

NPM

        NPM(Node Package Manager)是一个随 Node.js 一起发布的包管理工具,用来管理项目中的第三方包。

安装

        NPM 是 Node.js 默认的软件包管理工具,安装好 Node 之后,会默认安装好 NPM。NPM 本身也是基于 Node.js 开发。

// 设置 npm 镜像

npm config set registry https://mirrors.tencent.com/npm/ --global
// 查看镜像配置

npm config get registry

基本使用

初始化项目

        在项目文件夹中,打开命令行终端,并运行以下命令来初始化一个新的 Node.js 项目:

npm init -y

        这个命令会创建一个 package.json 文件,您可以在这个文件中定义项目的信息和依赖。

安装包

        使用 npm install 命令来安装包。您可以安装特定的包版本,或者在 package.json 中定义的依赖。例如:

npm install <package-name> // 安装最新版本的模块
npm install <package-name>@version_id // 安装指定版本的模块

        如果您想安装一个包并将其作为开发依赖(例如测试框架或构建工具),可以使用以下命令:

npm install <package-name> --save-dev
npm install <package-name>@version_id --save-dev

        这里我们说明一下运行时依赖和开发依赖的区别,两者存在很大差异:

        运行时依赖:依赖包安装到项目的 node_modules 目录中,并且会将包的版本信息添加到 package.json 文件的 dependencies 部分。

        开发时依赖:依赖包安装到项目的 node_modules 目录中,但是会将包的版本信息添加到 package.json 文件的 devDependencies 部分。devDependencies 中的包不会被打包进入生产环境的最终构建中。这些依赖项主要是用于开发和测试阶段的工具,比如测试框架、构建工具、代码分析器等。在生产环境中,你通常不需要这些工具。

安装所有包

npm install

更新包

        使用 npm update 命令来更新包到最新版本:

npm update <package-name>

卸载包

        如果您想卸载一个包,可以使用 npm uninstall 命令:

npm uninstall <package-name>

运行脚本

        在 package.json 中定义的脚本可以通过 NPM 运行。例如,package.json 中的配置如下:

{
  "name": "npm-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

        这里我们简单介绍一下如何运行脚本以及脚本运行的原理。

        首先,我们在 scripts 标签中配置了 start 命令,那么要运行这个命令只需要通过 npm run start 即可:

npm run start

image.png

        NPM 命令行工具会执行以下步骤来解析和执行 npm run start 命令:

  1. 读取 package.json 文件。
  2. 解析 package.json 中的 scripts 对象。
  3. 检查是否存在 start 键。
  4. 如果存在,执行与 start 键关联的命令。

        运行依赖包对应的可执行文件,这里我们以 rollup 为例:

npm install rollup --save-dev
{
  ...
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js",
    "build": "rollup"
  },
  ...
}

        我们可以通过 npm run build 来运行 rollup,如下:

image.png

        还有一种无需配置 scripts 即可运行第三方包的方式,通过 npx

npx rollup

image.png

        npx 的原理是判断 node_modules 下的 .bin 文件夹下是否有对应的可执行文件,有的话 npx 会运行该文件。如果没有的话,则去读取依赖包的 package.json 中的 main 选项,运行对应的入口文件。

截屏2024-05-29 23.18.08.png

管理全局包

        使用 -g 标志来全局安装包,这样可以在任何地方使用这个包。例如,安装一个全局命令行工具:

npm install -g <package-name>

package.json

        当我们执行npm init命令后,会在当前目录下生成 package.json 文件,该文件是 Node.js 项目中非常重要的一个文件,用于定义项目的元数据、依赖关系、脚本和其他配置文件。

        以下是 package.json 文件中的一些关键字段和它们的作用:

name

  • 项目的名称,通常是小写的。这个名字在 npm 上是唯一的,用于区分不同的项目。

version

  • 项目的版本号,遵循语义版本控制(SemVer)规范。版本号由三个数字组成:主版本号(major)、次版本号(minor)和补丁版本号(patch)。

description

  • 项目的简短描述,用于帮助其他开发者了解项目的用途。

main

  • 指定项目的主入口文件。这个文件通常位于项目的根目录下,Node.js 会在运行时自动导入这个文件。这个入口点是当用户通过 node 命令运行项目时,Node.js 应该加载的文件。

        main 字段的值可以是相对路径或绝对路径,指向一个 JavaScript 文件。例如,如果你的项目结构如下:

my-project/
|-- node_modules/
|-- package.json
|-- index.js
|-- other-files/

        在 package.json 文件中,你可以这样指定 main 字段:

{
  "name": "my-project",
  "version": "1.0.0",
  "main": "index.js"
}

        当用户在命令行中运行 node my-project 时,Node.js 会自动加载 my-project/index.js 文件,并执行其中的代码。

script

  • 一个对象,用于定义项目生命周期中的脚本命令。例如,"start": "node index.js" 命令可以用于启动项目。

dependencies

  • 一个对象,用于定义项目的依赖关系。这些依赖关系会被包含在 node_modules 目录中,并在 package.json 文件中列出。

devDependencies

  • 一个对象,用于定义项目开发过程中所需的依赖关系。这些依赖不会被包含在生产环境的构建中。

peerDependencies

  • 一个对象,用于定义项目的依赖关系,这些依赖需要在安装该包之前已经安装了指定的版本。
  • 这些依赖项不会被添加到 node_modules 目录中,也不会在运行项目时被加载。
  • peerDependencies 字段中的包版本是建议的,而不是强制的。它只是提供一个建议,告诉用户在安装该包之前应该安装哪些其他包的版本。

        例如,如果你有一个包 my-package,并且它依赖于另一个包 some-other-package 的特定版本,你可以这样定义 peerDependencies

{
  "name": "my-package",
  "version": "1.0.0",
  "peerDependencies": {
    "some-other-package": "^1.2.3"
  }
}

        在这个例子中,some-other-package 必须安装版本 1.2.3 或更高版本,才能与 my-package 一起工作。如果用户已经安装了一个与 peerDependencies 指定的版本不兼容的 some-other-package 版本,npm 不会强制用户安装一个新版本。

browserslist

  • 用于指定项目支持的浏览器版本。这有助于确定项目中的 JavaScript 代码应该被编译成哪个版本。Babel、Parcel、Webpack 等,这些工具可以读取 browserslist 配置来确定应该使用哪个版本的 JavaScript 引擎。
{
  "name": "my-project",
  "version": "1.0.0",
  "browserslist": [
    "last 2 versions",
    "Firefox ESR",
    "not dead"
  ]
}

        在这个例子中,last 2 versions 意味着支持过去两个版本的主流浏览器,Firefox ESR 表示支持最新版本的 Firefox 扩展支持版,not dead 表示排除那些已经不再更新的浏览器。

engines

        engines 字段用于指定项目运行所需的 Node.js 版本。这个字段可以帮助确保项目在特定的 Node.js 版本上运行,从而提高项目的稳定性和可预测性。

        例如,如果你有一个包 my-package,并且它需要运行在 Node.js 10.x 版本上,你可以这样定义 engines

{
  "name": "my-package",
  "version": "1.0.0",
  "engines": {
    "node": ">=10.0.0"
  }
}

        在这个例子中,my-package 需要在 Node.js 10.x 或更高版本上运行。如果用户尝试在低于 10.0.0 的 Node.js 版本上安装 my-package,npm 会显示一个警告,但仍然允许安装。

type

        用于指定项目的类型,这个字段可以用于区分不同的项目类型,例如 CommonJS 模块、ES6 模块、UMD(Universal Module Definition)模块等。

        例如,如果你的项目是一个 ES6 模块,你可以这样定义 type

{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module"
}

        在这个例子中,my-project 是一个 ES6 模块。如果用户尝试在项目中使用 CommonJS 语法,例如使用 require()module.exports,可能会遇到解析错误。

package-lock.json

        当执行 npm install之后,会生成一个package-lock.json 的文件,它记录了项目依赖的包及其版本信息。这个文件是由 npm 自动生成的,它用于锁定项目的依赖版本,确保在不同机器上安装依赖时保持一致性

        为什么 package.json 中不能固定版本,通常我们在 package.json 中指定的依赖版本是一个范围,而不是一个确切的版本。比如:

{
  "dependencies": {
    "lodash": "^4.17.19"
  }
}

        这个声明表示 lodash 包应该安装其版本 4.17.19 或更高版本,但并不要求安装确切的 4.17.19 版本。除了 ~ 符号表示范围外,还有其他符号同样可以表示返回:

{
  "dependencies": {
    "lodash": "^4.17.19",
    "express": "*",
    "mocha": "1.x",
    "request": "^2.x.0"
  }
}

        ^ 表示最新主版本(major)和次版本(minor),但不包括补丁版本(patch)。例如,^1.2.3 表示 1.2.x,但不包括 1.2.4

        ~表示最新次版本和补丁版本,但不包括主版本。例如,~1.2.3 表示 1.2.x,但不包括 1.3.0

        * 表示任意版本,但不包括预发布版本(例如,1.2.3-alpha.1)。例如,1.2.3 表示 1.2.3,但不包括 1.2.3-alpha.1

         主版本(x)表示主版本,但不包括次版本和补丁版本。例如,1.x 表示 1.0.01.1.01.2.0 等,但不包括 2.0.0

         次版本(x)表示次版本,但不包括主版本和补丁版本。例如,x.2.3 表示 1.2.32.2.33.2.3 等,但不包括 1.2.4

         补丁版本(x)表示补丁版本,但不包括主版本和次版本。例如,x.x.3 表示 1.1.32.2.33.2.3 等,但不包括 1.2.4

        当使用 npm install 命令安装依赖时,npm 会创建一个依赖树,并确定每个包的确切版本。这个依赖树和版本信息会被记录在 package-lock.json 文件中。package-lock.json 文件中的依赖版本信息是精确的,这意味着 npm install 命令在后续的安装过程中会使用这些确切的版本,而不是重新解析依赖关系。

NPM 版本

         介绍了 NPM 的基本使用后,接下来了解一下 NPM 的各个版本以及分别解决了什么问题。

NPM 1.X

        当前版本提供了基本的命令安装、更新和卸载包。但是当前版本安装依赖的方式是将所有依赖包直接安装在 node_modules 的根目录下,而不考虑依赖之间的层级关系。这种方式可能导致依赖关系混乱,且在不同的项目中可能会产生依赖冲突。

        假设我们有一个 Node.js 项目,它依赖于两个包:package-apackage-bpackage-a 又依赖于 package-c

node_modules/
├── package-a/
├── package-b/
└── package-c/ (直接安装在根目录下,可能与package-a的依赖版本冲突)

NPM 2.X

        当前版本引入了本地依赖的概念,改变了上个版本一边扁平化的安装方式,依赖包会安装在他们所属的包的node_modules目录中。这样的好处是:

  1. 更好的依赖隔离:每个包都可以有自己的 node_modules 目录,这样可以更好地隔离依赖,减少依赖冲突的可能性。
  2. 更清晰的依赖结构:依赖树的结构更加清晰,每个包的依赖都直接位于其目录下,而不是全部堆积在项目的根 node_modules 目录中。
  3. 更灵活的版本控制:不同的包可以依赖不同版本的同一个包,因为每个包都有自己的依赖副本,这样就可以避免版本冲突。
  4. 更快的安装速度:虽然这种方式可能导致 node_modules 目录的嵌套加深,但是在某些情况下,它可以减少重复安装相同依赖的情况,从而提高安装速度。

        假设我们有一个 Node.js 项目,它依赖于两个包:package-apackage-bpackage-a 又依赖于 package-c

node_modules/
├── package-a/
│   └── node_modules/
│       └── package-c/ (安装在package-a的node_modules目录下)
└── package-b/

NPM 3.X

        NPM 3.x 引入了一个新的安装算法,它尝试将依赖树扁平化,即将同一版本的依赖包提升到顶层 node_modules 目录,而不是嵌套在各个包的 node_modules 目录中。这样做可以减少 node_modules 目录的深度,提高文件系统的查找效率。

        假设我们有一个 Node.js 项目,它依赖于两个包:package-apackage-bpackage-apackage-b都依赖于不同版本的 package-c,但是这些版本是兼容的。package-a 还依赖于 package-d

node_modules/
├── package-a/
├── package-b/
├── package-c/ (提升到顶层,因为package-a和package-b都依赖于它)
└── package-d/ (package-a的依赖,安装在顶层)

        如果 package-apackage-b 依赖于不同且不兼容的 package-c 版本,那么 package-c 的这两个版本不会被提升到顶层,而是分别安装在 package-apackage-bnode_modules 目录中。例如:

node_modules/
├── package-a/
│   └── node_modules/
│       └── package-c/ (特定版本,因为与package-b依赖的版本不兼容)
├── package-b/
│   └── node_modules/
│       └── package-c/ (另一个版本)
└── package-d/ (package-a的依赖,安装在顶层)

NPM 5.X

        该版本主要解决了在不同环境下安装依赖时可能产生不一致性的问题,引入了 package-lock.json 文件。

        NPM 后续还有多个版本,主要是做一些性能优化,这里不做过多介绍,感兴趣的同学可自行查阅。

YARN

        Yarn 为 Node.js 应用提供了一个新的依赖管理方式。Yarn 在 2016 年发布,旨在解决当时 NPM 在某些方面的问题,尤其是安装依赖的速度和一致性。

        Yarn 解决了 NPM 的以下问题:

  1. 安装速度:Yarn 引入了一个新的安装策略,它可以并行化操作,加快了包的安装过程。Yarn 还会缓存已安装的包,这样在多个项目中安装相同的依赖时,不需要重复下载。
  2. 一致性:Yarn 通过一个锁定文件(yarn.lock)确保了项目依赖的一致性。这个文件记录了每个依赖的精确版本,保证了在不同机器和不同时间安装依赖时都能得到相同的结果。
  3. 网络性能:Yarn 优化了网络请求,减少了安装过程中的请求次数,提高了网络利用率。
  4. 安全性:Yarn 在安装包之前会校验每个包的完整性,确保安装的依赖没有被篡改。
  5. 更好的错误处理:Yarn 提供了更详细的错误信息,帮助开发者更容易地诊断和解决问题。
  6. 多注册表支持:Yarn 允许开发者从不同的注册表源安装包,这样可以提高灵活性和可靠性。

安装

npm install --global yarn

基本使用

初始化项目

yarn init -y

安装包

yarn add [package]

yarn add [package] --dev

安装所有依赖

yarn install

更新包

yarn upgrade [package]

卸载包

yarn remove [package]

运行脚本

yarn run [script]

查看缓存信息

yarn cache list

PNPM

        PNPM(Performant npm)是一个用于 JavaScript 的包管理器,旨在解决 npm 和 Yarn 在某些情况下的性能和安全性问题。

        PNPM 解决的一些主要问题包括:

        1. 重复的依赖项:传统的 npm 安装会将每个项目的所有依赖项安装在其 node_modules 文件夹中,这可能导致大量的重复文件和空间浪费。PNPM 使用一个内容寻址的存储来存储 node_modules 文件夹中的所有文件,因此,当不同的项目依赖于同一个包时,PNPM 只需要在磁盘上存储一次,不管它被安装了多少次。

        2. 提升的性能:PNPM 使用硬链接和符号链接来管理 node_modules,使得安装速度比 npm 快,尤其是在包含大量包的大型项目中。

        3. 更好的多重包管理:PNPM 支持多个版本的包共存,这意味着不同版本的包可以在同一项目中共存,而不会发生冲突。

        4. 更严格的依赖关系树:PNPM 创建一个非扁平化的 node_modules,确保了更严格的依赖关系树,这有助于减少意外引入不相关的包。

image.png

        5. 更安全的安装:PNPM 默认只允许使用来自 package.json 中声明的依赖项,这有助于防止安装恶意包。

        6. 节省磁盘空间:由于 PNPM 的去重机制,它能够节省大量的磁盘空间。

        7. 更好的网络性能:PNPM 可以并行下载多个包,提高了网络资源的利用效率。

软硬链接

        在文件系统中,链接是一种将文件或目录指向另一个文件或目录的机制。在 Unix 和类 Unix 操作系统(如 Linux)中,有两种主要的链接类型:硬链接(hard link)和软链接(又称符号链接,symbolic link 或 symlink)。

硬链接

        硬链接是文件系统中的一个目录条目,它引用的是文件的实际数据。简单来说,硬链接就是文件的另一个名字。一个文件可以有多个硬链接,所有这些链接都指向文件的同一个物理位置。以下是一些关于硬链接的特点:

  1. 相同的inode:硬链接与原始文件共享相同的inode(文件索引节点),这意味着它们实际上是指向磁盘上相同数据块的指针。

  2. 文件系统限制:硬链接不能跨越文件系统,也就是说,它们不能在不同的文件系统之间创建。

  3. 删除操作:只有当文件的最后一个硬链接被删除时,文件的数据才会被删除。只要至少有一个硬链接存在,文件的数据就仍然存在。

  4. 目录限制:不能对目录创建硬链接,这可能会导致循环链接和文件系统的一致性问题。

软链接

        软链接,也称为符号链接或symlink,是一个特殊类型的文件,它包含了一个指向另一个文件或目录的路径。与硬链接不同,软链接不包含目标文件的物理数据,而是指向目标文件的路径。以下是软链接的一些特点:

  1. 不同的inode:软链接有自己的inode,它与目标文件的inode不同。

  2. 跨文件系统:软链接可以跨越文件系统,这意味着它们可以指向不同文件系统中的文件或目录。

  3. 删除操作:删除软链接不会影响目标文件,但如果目标文件被删除,软链接将变成“悬空链接”(dangling link)。

  4. 目录链接:可以对目录创建软链接,这使得软链接在创建复杂的文件系统结构时非常有用。

image.png

image.png

安装

npm install -g pnpm

基本使用

pnpm init

初始化项目

安装包

pnpm add <package-name>

pnpm add -D <package-name>

安装所有依赖

pnpm install

更新包

pnpm update <package-name>

卸载包

pnpm remove <package-name>

查看缓存信息

pnpm store status

清理缓存

pnpm store prune

Monorepo

        PNPM 提供了对 Monorepo(单一代码库)的支持,允许你在一个单一的版本控制仓库中管理多个项目。Monorepo 的好处包括统一的工作流程、共享代码和依赖管理,以及更简单的跨项目变更。

        PNPM 实现 Monorepo 主要通过其内置的 workspace 功能,Vue3 项目就是一个通过 PNPM 实现的 Monorepo 的典型项目,这里不做过多展开,后续单独写一篇文章感受 Monorepo 的应用。

image.png

总结

        通过本篇文章,想必你对前端包管理有了深刻的认识,同时对于 NPM、Yarn以及 PNPM 的使用有了基本了解,对于包管理工具的选型,则通过各自的优缺点选出适合自己的工具。

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值