31.JS高级-包管理工具详解

该系列文章连载于公众号coderwhy和掘金XiaoYu2002中

  • 对该系列知识感兴趣和想要一起交流的可以添加wx:coderwhy666,拉你进群参与共学计划,一起成长进步
  • 后续JavaScript高级知识技术会持续更新,如果喜欢我们的文章,欢迎关注、点赞、转发、评论,大家的支持是我们最大的动力

脉络探索

  • 作为超出JS语言本身知识范围的包管理工具,本不该被规划到这次JS高级系列中,而应该归属于Node生态链中的内容,但由于在实际项目中占比越来越高了,高到不用不行的程度,因此在JS阶段,对Node生态有一定基础的理解,是有必要的,也是我们学习的原因

    • 在本章节中,我们会先安装NodeJS引出话题,进而了解npm官网的作用以及掌握package.json配置文件的各种信息
    • npm的install命令系列进行学习并了解其内在原理,学习除了npm之外的其余包管理器,包括yarn、cnpm、pnpm
    • 对于pnpm是如何成为当下最流行的包管理工具也会深入学习,了解操作系统中软链接和硬链接的概念,看pnpm是如何解决yarn、npm都无法解决的node_modules文件夹庞大问题,以及由各类包管理工具所延伸出来的各种常见命令,都会在本章节进行讲解
  • 当掌握了如何使用包管理器后,我们会来尝试自己发布一个包,让全世界的程序员都能看到和使用

一、npm包管理工具的作用与下载

1.1 代码共享方案

  • 我们已经学习了在JavaScript中可以通过模块化的方式将代码划分成一个个小的结构:
    • 在以后的开发中我们就可以通过模块化的方式来封装自己的代码,并且封装成一个工具
    • 这个工具我们可以让同事通过导入的方式来使用,甚至我们可以分享给世界各地的程序员来使用,这种开源精神是促使计算机领域蓬勃发展的主要原因之一
    • 我们未来使用各种框架,各种第三方库,绝大多数都是开源的,由社区一起推进维护
  • 那如果我们也写了一个工具,想分享给世界上所有的程序员使用,让所有人都能看见我们的杰作,有哪些方式呢?
  • 方式一:上传到GitHub上、其他程序员通过GitHub下载我们的代码手动的引用
    • 缺点是大家必须知道你的代码GitHub的地址,并且从GitHub上手动下载
    • 需要在自己的项目中手动的引用,并且管理相关的依赖
    • 不需要使用的时候,需要手动来删除相关的依赖
    • 当遇到版本升级或者切换时,需要重复上面的操作(把版本1从项目中删除,从GitHub中下载版本2,将版本2导入项目,手动管理版本2),这真的太麻烦了,步骤很多,虽然这个方式是有效的,但是这种传统的方式非常麻烦,并且容易出错
    • PS:我们的工具、库或者框架,对于别人来说就是一个依赖,之所以叫做依赖,是因为大家使用了我们的工具来帮助他们完成某项任务,这个依靠过程就叫做依赖,一部分的成功运作依赖于另一部分的存在和正确运作。这种依赖关系是双向的,依赖项的变化可能会影响到依赖它的软件,同样,软件的需求也可能推动依赖项的发展和更新
  • 因此,社区迫切需要一个工具的出现,来改变现有的效率困境,因为目前的做法极大程度影响了程序员开源程序所带来影响力以及用户使用的体验
    • 如果真的有这样一个工具出现,我希望它是怎么样的呢?至少不需要我们每次都得手动打开GitHub手动下载,那我们就需要舍弃可视化的做法,转向于命令行工具,因为命令行效率是绝对高于可视化界面的,终端的大黑框是程序员的浪漫
    • 如果使用命令行工具,我希望不需要我手动输入网址,毕竟一个网址很长,哪怕我知道URL的组成部分,我也得知道这个库来自GitHub的哪个仓库,这还需要我知道持有该仓库的用户名,那么多库的用户名我怎么能记得住呢?因此最好是只需要知道库的名称就好了,我记不住那么多的用户名和开源位置,万一他在gitee开源咋办对吧
    • 因此我在下载时,应该从开源平台+用户名+仓库简化到只有仓库,因为我们要使用,应该聚焦最主要的事情上。在这个基础上,我还想要卸载也快点、版本升级也快点、最好能帮我把这么多库一起统一管理了,这样省心
    • 对于库的开发者来说,他们也有自己的需求,他们也希望自己能够输入一行命令后,按下回车键后,新版本的库就上传上去了,这样他们就能够专注于库开发本身
  • 看上去是有点太贪心了,但也许真能实现呢?确实是实现了,现在可以使用一个专业的工具来管理我们的代码
    • 我们通过工具将代码发布到特定的位置
    • 其他程序员直接通过工具来安装、升级、删除我们的工具代码
    • 显然,通过这第二种方式我们可以更好的管理自己的工具包,其他人也可以更好的使用我们的工具包
    • 这个专业的工具叫做包管理工具 ,准确的说是叫做包管理的工具,什么是包?我们的工具包或者组件包、框架包都是包,这个专业工具就是为了管理所有的包,因此叫做包管理
  • 有很多的包管理工具,我们会一一学习

1.2 包管理工具npm

npm(node pm)Node.js 的包管理工具,也是目前 JavaScript 生态系统中最广泛使用的包管理工具之一。它用于管理 JavaScript 项目的依赖项,让开发人员可以方便地安装、更新、卸载各种开源库和工具

  • 这是我们最先接触的包管理工具,在最早的时候,这是为Node所使用,但目前已经不局限于Node,而是在所有的前端项目中都可以使用它来进行管理依赖,比如vue、vue-router、pinia、express、koa、react、react-dom、axios、babel、webpack等等,都会使用npm工具
  • 作为Node的默认包管理工具,在下载Node的时候,npm就已经跟随下载下来了,在之前所有的终端案例操作,我们都是通过Node进行的,我相信大部分同学下载过Node,但在这里也依旧为大家提供一份下载安装步骤
  • 需要注意的是,与普遍的看法相反,npm 实际上并不是 "Node Package Manager" 的缩写;它是一个递归的反向首字母缩略词(bacronymic abbreviation),表示 "npm is not an acronym"(npm 不是一个缩写词)。如果该项目被命名为 "ninaa",那么它才会是一个缩写词。npm 的前身实际上是一个名为 "pm" 的 bash 工具,它是 "pkgmakeinst" 的简写,这是一个用于在各种平台上安装不同内容的 bash 函数。如果 npm 曾经被认为是一个缩写词的话,那么它的含义可能是 "node pm",或者可能是 "new pm"
    • 该解释来自npm的作者,具备最高级别的权威性,可以从npm - npm (npmjs.com)的底部查看

1.3 安装NodeJS

表31-1 NodeJS安装版本区别

LTS Current
长期支持维护版 尝鲜版(也叫实验版、小白鼠版)

图31-1 Node官网界面

  • 通常点击这里之后,Node官网会直接根据我们的电脑环境,自动选择最适合的版本,当然也可以点击左上角的Download选项去下载想要的其他版本
    • 注意下载下来的安装包格式:
      1. 后缀.msi是都配置好的
      2. 后缀.zip是一个压缩包,还要自己解压后进行配置

图31-2 Node版本下载界面

  • 顺着步骤一步步安装下去就行了

1.3.1 检查是否安装成功

  • 通过快捷键win+R呼出Windows的运行小窗口,在里面输入cmd(也就是终端)

图31-3 Ctrl+R呼出的运行小窗口

  • 输入一下命令检测是否安装成功,这里的v是指英文version(版本),而node、npm、npx都是安装node之后自带的
    1. npm -v
    2. node -v
    3. npx -v
  • 如果有弹出具体的版本号,即为安装成功

图31-4 检查Node是否安装成功

1.3.2 npm官网

图31-5 npm官网界面

  • 这个官网能做什么事情?
    • 通过搜索包的名称、关键词、描述等信息找到需要的包,然后浏览包的信息
    • 每个包在官网都有一个详细页面,包括包的版本信息、安装方法、依赖关系、下载次数等,且大多数包的页面都会附有详细的使用文档、安装指南、配置示例
    • npm 官网还会将包进行分类或者推荐一些流行的包,帮助我们了解当前社区中常用和热门的工具

图31-6 axios包对应详情页

  • 如果我们想要发布和管理包,也需要通过这个官网,发布自己的包其实是发布到registry上面的,当我们安装一个包时其实是从registry上面下载的包

图31-7 npm对应registry列表包

1.3.3 npm配置文件解析

  • 那么对于一个项目来说,我们如何使用npm管理这么多包呢?

    • 事实上,我们每一个项目都会有一个对应的配置文件,无论是前端项目(Vue、React)还是后端项目(Node)
    • 这个配置文件会记录着我们项目的名称、版本号、项目描述
    • 也会记录着你项目所依赖的其他库的信息依赖库的版本号
  • 这个配置文件就是package.json,当我们使用框架时,框架模板会默认配置,也可以手动生成

    • 方式一:手动从零创建项目,npm init --y,init是初始化的意思,-y是yes的缩写,代表愿意接受默认配置,可以极大幅度简化配置流程
    • 方式二:通过脚手架创建项目,脚手架会帮助我们生成package.json,并且里面有相关的配置
  • 之所以有该配置文件,是因为通过npm下载下来的包具备一定的体积,会放在自动生成的node_modules文件夹中,当管理的包数量一多,会很"重",占几百MB甚至达到GB级别都有可能

    • 此时如果我们还想要将该项目上传GitHub或者传给同事,这庞大的包文件将会是传输的主要拦路虎,为了传递不到50MB的项目,还需要拖着数十倍于自身重的包一起传输,感觉是有点划不来
    • package.json配置文件就是为了解决该问题而存在的,当我们需要上传项目到GitHub或者传给同事时,我们就将node_modules文件夹删除然后传递(通过git上传可以设置忽略该文件夹),用户或者同事想要运行项目时,再通过npm命令下载包,实现运行。package.json配置文件在这个过程就做到了记录需要下载哪些包,以确保能够所有用户都能根据该配置文件下载到正确的包
  • 在上述内容中,存在着很多的配置项,都是属于常见的部分,但一开始时并不需要配置这么多,我们可以先来看一个package.json中,必须填写的属性:name、version

表31-2 package.josn基础配置项

属性 意思
name(必填) 项目的名称
version(必填) 当前项目的版本号
description 描述信息,很多时候是作为项目的基本描述
author 作者相关信息(发布时用到)
license 开源协议(发布时用到)
  • private属性 :记录当前的项目是否是私有的,当值为true时,npm是不能发布它的,这是防止私有项目或模块发布出去的方式,因为不是所有项目或者模块都希望开源

  • main属性 :用于设置程序的入口,比如我们使用axios模块 const axios = require('axios'),如果有main属性,实际上是找到对应的main属性查找文件的(就不会默认找到index.js、json、node文件去了)

    • 引入的时候直接输入main的名字,而不需要输入完整路径了(使用和阅读体验上更加简洁)
js 复制代码
const axios1 = require('axios');
//没有配置package.json的导入方式
const axios2 = require('./node_modules/..../axios/index.js');

图31-8 axios目录结构以及对应的package.json配置

  • 实际上,通过脚手架创建的情况会更多,使用npm init则更多是看情况决定,前者已经封装好了,使用起来更方便
    • 脚手架作用是快速创建项目的基本结构,它不是框架,这点需要明确
    • 常见的脚手架有Create React App (CRA)Vue CLI等等,脚手架出现的原因以及背后的历史,这里不再深入,在学习项目时,这些都是可以学习到的

图31-9 npm init初始化所需要配置的信息

1.3.4 常见属性

  • scripts属性用于在package.json文件中配置一些脚本命令,以键值对的形式存在
    • 配置后我们可以通过 npm run 命令的key来执行这个命令,对应的value值是一个可执行的脚本
    • 使用该命令可以统一运行的方式,在执行命令时,也可以更加方便,拿到一个项目,大家通常安装好包之后,基本上可以无脑npm run dev或者npm run start先把项目跑起来,这就是形成一个统一规范的好处,可以最快程度上手
js 复制代码
"scripts": {               // 项目中常用的脚本命令集合,可以使用 npm run <script-name> 运行这些脚本
  "start": "node index.js",// 运行项目的启动脚本,通常用于启动应用
  "test": "mocha",         // 测试脚本,用于运行单元测试
  "build": "webpack",      // 构建脚本,通常用于打包应用程序代码
  "lint": "eslint .",      // 使用 ESLint 进行代码质量检查
  "dev": "nodemon index.js"// 开发环境下的启动命令,使用 nodemon 实现自动重启
},
//npm run start  =>  node index.js(本质上前者所运行的就是后者的命令)
//npm run test
//npm run build
//npm run lint
  • 那npm start和npm run start的区别是什么?
    • 没有任何区别,前者是后者的简写形式,在使用上会更方便
    • 需要注意,可以简写省略的只有几个特殊脚本,分别为:start、 test、stop、restart
  • npm run命令首先在当前项目的 node_modules/.bin 目录中查找 运行文件的路径 的可执行命令
    • 如果在项目的 node_modules/.bin 中没有找到,它会尝试在全局的 node_modules 目录中查找
    • 如果全局目录中也没有,npm 会继续在系统的环境变量中查找
    • 如果这些位置都没有找到,npm 会报错,指出无法找到命令
js 复制代码
"scripts": {
  "start":"运行文件的路径"
}

//运行快捷键 npm run start
  • dependencies属性是指定无论开发环境还是生成环境都需要依赖的包,通常是我们项目实际开发用到的一些库模块vue、pinia、vue-router、react、react-dom、axios等等

    • 位于dependencies属性的内容,在项目build打包时,也会跟随一起打包
  • 与之对应的是devDependencies属性,在开发过程中需要,但是在生产环境 中不会使用的包。这包括测试框架(如jestmocha)、构建工具(如webpackgulp)、文档生成器、linters(如eslinttslint)等。当在本地开发或运行测试时,这些依赖项很重要,但当应用部署到生产环境时,它们并不是必需的。默认情况下,当执行npm install时,dependenciesdevDependencies都会被安装,但是如果在生产环境下(例如运行npm install --only=prod或设置NODE_ENV=production环境变量时),devDependencies不会被安装

    • 这个时候我们会通过 npm install webpack --save-dev,将它们安装到devDependencies属性中
  • 两种环境对应两种依赖关系,对应了生产依赖(也称为运行时依赖)和开发依赖

    • 安装时默认会将包添加到生产依赖,这是因为放在生产依赖下一定不会出问题,最多打包的时候"重"一些,但开发依赖就不一样了,不小心将生产依赖放到开发依赖中,一旦打包构建基本上直接跑不起来
    • 但如果我们非常明确需要的就是开发依赖,在npm下载时可以设置,这是由开发者决定的,包本身是没办法决定它是被作为生产依赖 还是开发依赖
js 复制代码
//开发环境依赖设置
npm install xxx --save-dev//全写
npm install xxx --D//简写
  • 通过安装第三方库,例如axios时,我们在node_modules文件夹中,可以看到不止有axios这一个内容,还存在一堆叫不上名称的文件夹,这些又是什么?
    • 这些是axios包本身所依赖的内容,这些都是很能理解的事情
    • 虽然axios本身是一个库,但并不是所有内容都是开发者自己手搓出来的,如果社区里已经有一些非常优秀的代码和库,然后我们在编写axios库时,刚好可以用上,我们是选择手搓还是使用现成的?从效率和体验上来说没道理不选择后者
    • 因此在下载axios库时,axios内的package.json配置文件的dependencies属性件还有配置对应的依赖信息,所以会将axios依赖的库一起下载下来,从而形成一整条完整的依赖链
    • 缺少这些必要的库时,axios也就无法正常使用了

图31-10 包与包之间的依赖关系

  • 还有一种项目依赖关系是对等依赖 ,也就是我们依赖的一个包,它必须是以另外一个宿主包为前提 的,设置该项依赖来自peerDependencies属性
    • 比如element-plus是依赖于vue3的,ant design是依赖于react、react-dom
    • 对等依赖的作用在于确保项目中安装的主包和插件版本的兼容性。它帮助管理依赖之间的关系,以避免由于版本不匹配而导致功能的异常或不可用
    • 可以理解为,如果安装 element-plus ,而没有手动安装对应的 Vue ,在 npm 7 及以上版本中,npm 会自动尝试安装符合要求的 Vue 3,并且会尝试解决版本冲突
js 复制代码
//在element-plus中就能看到
"perrDependencies":{
    "vue":"版本"
}//这表示必须有Vue才能够使用
  • 这其实很有用,就像去餐馆打饭,会自动给饭配一个碗筷,购买米饭的前提是,有东西装米饭,而使用element-plus组件库时,得有对应Vue生态
  • 那为什么需要对等依赖呢?
    • 对于像 ReactVue 这样的框架,如果每个插件或者 UI 组件都把 ReactVue 作为自己的依赖项,那么在项目中会出现多个版本的框架副本,这不仅浪费空间,还可能导致版本冲突
    • 使用 peerDependencies 可以确保只有一个版本的宿主包(例如 VueReact)存在,被依赖的包就被称为宿主包

1.4 依赖的版本管理

  • 我们会发现安装的依赖版本出现:^2.0.3或~2.0.3,这是什么意思呢?
  • npm的包通常需要遵从semver版本规范
    • 这是是一种标准化的版本控制规范,使用 MAJOR.MINOR.PATCH 形式来表达软件版本的性质和变化
    • semver文档:semver.org/lang/zh-CN/
  • semver版本规范是X.Y.Z
    • X主版本号(major):做了不兼容的 API 修改(可能不兼容之前的版本,也称为破坏性更新)
    • Y次版本号(minor):做了向下兼容的功能性新增(新功能增加,但是兼容之前的版本)
    • Z修订号(patch):做了向下兼容的问题修正(没有新功能,修复了之前版本的bug)
  • 我们这里解释一下 ^和~的区别:
    • x.y.z :表示一个明确的版本号
    • ^x.y.z :表示x是保持不变 的,y和z永远安装最新的版本
    • ~x.y.z :表示x和y保持不变 的,z永远安装最新的版本
  • npm 和其他依赖管理工具中,版本控制还可以使用一些特殊的符号来指定对依赖的版本要求:

表31-3 npm中的符号表匹配总结

符号 示例 匹配范围 说明
精确匹配 1.4.2 1.4.2 只能使用版本 1.4.2
通配符 * 1.4.* 1.4.x 可以使用任何 1.4.x 的版本
1.* 1.x.x 可以使用任何 1.x.x 的版本
^ 符号 ^1.4.2 >=1.4.2 <2.0.0 可以使用任何主版本号为 1 的版本,不包括 2.0.0
~ 符号 ~1.4.2 >=1.4.2 <1.5.0 可以升级修订版本,但次版本号不变
  • 通常只需要关注主次两个版本号,与我们更为息息相关,且通常不会写死,而是配合特殊符号来增加弹性范围
  • 例如**^(Caret Operator)**是最常用的符号之一,用于表示允许向后兼容的次版本更新,但不允许有破坏性更新。这意味它会接受次版本和修订版本的更新,例如 ^1.4.2 可以升级到 1.5.0 甚至 1.9.9 ,但不会自动升级到 2.0.0 ,该升级作用于当我们重新安装包时自动生效(有时候不生效时因为有锁文件package-lock.json的存在)
    • ^的弹性空间比~更大,但后者更为稳定,从配置中存在的比例,可以预估一下对稳定性的需求有多高
    • 主版本号为 0 的特殊情况,表示该软件仍在初始开发阶段,API 可能频繁发生变化且不稳定,最好不使用

1.5 package中其余配置补充

  • 将之前讲过的主要配置去除,还有一些常见的配置值得了解一下,我们以JSON列表进行说明
    • engines属性用于指定Node和NPM的版本号,在安装的过程中,会先检查对应的引擎版本,如果不符合就会报错。事实上也可以指定所在的操作系统 "os" : [ "darwin", "linux" ],只是很少用到
    • browserslist属性用于配置打包后的JavaScript浏览器的兼容情况,否则我们需要手动的添加polyfills来让支持某些语法,也就是说它是为webpack等打包工具服务的一个属性(这里不是详细讲解webpack等工具的工作原理,所以不再给出详情)
js 复制代码
{
  // 项目存储库信息,通常用于在开源平台(如 GitHub)上找到该项目的代码
  "repository": {
    "type": "git",
    "url": "https://github.com/username/my-project.git"
  },

  // 项目的关键词,用于描述项目的特性,便于在 npm 搜索中发现
  "keywords": [
    "nodejs",
    "express",
    "sample"
  ],

  // 项目中需要用到的其他配置(用户自定义或框架依赖)
  "config": {
    // 自定义的配置,通常可以在 scripts 中使用
    "port": "3000"
  },

  // 项目中需要执行的依赖项的版本规则
  "engines": {
    // 指定项目需要的 Node.js 的版本
    "node": ">=14.0.0"
  },

  // npm 包发布的规则,指明哪些文件应该包含在发布包中
  "files": [
    "lib/",
    "bin/",
    "index.js"
  ],

  // 用于定义项目的依赖解析方式,支持 Yarn、pnpm 等
  "packageManager": "yarn@1.22.10",

  // 项目的浏览器兼容性设置
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

1.6 npm install命令

  • 安装npm包分两种情况:
    1. 全局安装(global install): npm install webpack -g
    2. 项目(局部)安装(local install): npm install webpack
  • 全局安装是直接将某个包安装到全局,那什么是全局呢?什么又是局部呢?
    • 所谓的"全局"是指该包被安装后,系统的 PATH 中加入了指向该包的可执行文件的路径,可以在任何目录下运行这个包提供的命令,前提是需要在安装Node之后配置一下环境变量
    • 局部安装 是指将某个包安装到当前项目目录 下的node_modules文件夹中,同时,依赖信息会被记录在该项目的 package.json 文件中的 dependenciesdevDependencies 字段中。这就说明了只有在该项目中才能运行该包的命名,其他目录则不行

表31-4 全局安装和局部安装的区别

特性 全局安装(Global Install) 局部安装(Local Install)
作用范围 整个系统(所有项目都可访问) 当前项目(只有该项目可以访问)
安装位置 系统的全局目录(如 /usr/local/lib/node_modules 项目的 node_modules 目录下
适用场景 命令行工具(如 WebpackESLint 项目依赖(如 ReactExpress
安装命令 npm install -g <package-name> npm install <package-name>
使用方式 直接在命令行中调用 在项目中通过 requireimport 调用,或通过 npm 脚本调用
脚本调用 可在任何地方使用命令 只能在项目脚本(如 npm run)中调用
  • 打开终端输入npm config list就可以看到prefix了,我们的全局命令都在这里

图31-11 查看全局命令列表

图31-12 全局命令存储文件夹

  • 绝大多数的包,都只需要局部安装即可,那我们要怎么判断哪些包需要全局安装,哪些包需要局部安装?
    1. 在大多数项目中高频使用的通用包,全局安装
    2. 只有特定项目使用的,局部安装
    3. 如果不清楚选择哪个,优先局部安装
  • 什么是通用包?即具备工具属性的包,也就是那些非import导入的包
    • 像axios这类包,不适合安装到全局,因为它是采用import导入使用,且都会在打包构建时一起打包进去
    • 如果全局安装了这些库(例如 Axios ),它们就不会被包含在项目的 node_modules 中。在团队协作或部署时,其他开发者或服务器可能没有安装这些全局依赖,导致项目在不同环境中的行为不一致,甚至无法正常运行
    • 因此需要区分清楚工具类包和依赖库,Node.js 默认不会在全局目录中查找依赖,因此全局安装的包无法直接通过 import 在项目中使用
  • 而局部安装分为开发时依赖和生产时依赖,这点在前面已经说明,而安装命令的install允许简写为i

图31-13 npm install分类

1.7 npm install原理

  • npm install <package>作为最基础的命令,在前面,我们已经学会了,这就是一个安装包的命令,但是我们是否思考过它的内部原理呢?它是怎么来安装包的?

    • 执行 npm install它背后帮助我们完成了什么操作?
    • 我们会发现安装包之后还会生成一个package-lock.json文件,它的作用是什么?
    • 从npm5开始,npm支持缓存策略(来自yarn的压力),缓存有什么作用呢?
  • 假设我们使用webpack开发项目,现有项目A与项目B,同时都需要局部使用,因此在两个项目中都npm i webpack

    • 它们是如何将包下载下来的?如果下载下来后,两个一样的vite会不会重复导致空间的浪费?
    • 如果电脑本地不止两个,而是十几个项目同时都在使用,存储空间的浪费会不断加剧,每次的重复下载都会占用网络带宽,下载速度也是一个问题
    • 所以早期的npm是很不好用的,安装速度很慢,版本安装管理混乱(不存在lock文件),且存在上述说明的问题。在那个时期,就出现了另外一个包管理工具yarn,目的就是为了解决早期npm存在的一系列问题
  • 早期的npm其实有很多人提了很多次修改意见,但npm没有吸取采纳解决,所以当时谷歌联合一系列大公司联合推出了yarn

    • 当时yarn慢慢有取代npm的趋势,npm因此产生对应的竞争压力,从npm5.x版本开始不断更新自己的特性,防止被淘汰
    • 到目前为止,npm已经快更新到11版本了,在npm官网中,可以看到npm对应的详细信息
    • 因此我们能够发现,npm作为一个包管理工具,它居然是一个包,这点性质很重要,证明了包管理工具从何而来,也启示了我们接下来想要安装新的包管理工具如何安装

图31-14 npm最新版本详情

  • 当npm更新之后,当我们使用npm从registry仓库中下载包时,其实是下载一个压缩包,这个压缩包有对应的配置文件来记录压缩包的对应信息
    • 而电脑本地的npm命令执行了npm i webpack -D时,会先去查找配置文件,看配置文件有没有对应下载记录,有则会根据配置文件进而找到已经下载过的压缩包,解压到我们对应的node_modules文件夹中,没有下载记录则去registry仓库进行下载(版本一致会直接从缓存中获取,版本不一致则下载新内容)
    • 通过该操作,会极大优化下载流程,缓解本地带宽、提高下载使用效率、降低registry仓库的高并发压力

图31-15 npm缓存流程

  • 以上是npm通过本地缓存实现优化的原理,但这并不是全部的优化,我们还漏掉了package-lock.json文件
    • lock其实是"锁"的意思,我们也可以称呼这个文件为配置锁文件,那它起到了什么作用?以及我们为什么会需要它?这就需要从假如没有这个文件说起了
    • 如果我们将项目传给同事,如何做到让他安装的版本和我们保持完全一致?因为我们是有类似^符号的弹性安装范围,在package.json配置文件中,对应要求会是大版本不变,小版本升级
    • 如果我们的axios版本为^1.7.7,到同事那边通过npm install安装,只通过package.json配置文件且axios更新了,为^1.8.8(新版本)。虽说小版本的升级大多数是有兼容的,但是万一不兼容呢?一旦出现问题,很难找出来,因为问题不可控,我们也不知道具体会出现怎么样的问题以及是如何出现的
  • package-lock.json文件,会在我们第一次安装库时,自动生成,该文件记录了对应所有的详细版本,关键记录来自构建依赖关系,例如下载axios时,该库也可能依赖其他库,其他库再继续依赖更多的库(间接依赖),形成一整条依赖链。这些内容在第一次下载时都是会先构建出完整的依赖关系后,再去registry仓库下载下来的,下载下来压缩包然后解压缩到node_modules文件夹
  • 当完成第一次下载安装时,会生成package-lock.json文件,信息来自构建依赖关系
  • 当存在锁文件,后续使用npm install进行安装时,都会通过锁文件去安装一致的版本(查找缓存),找到则直接从本地获取,没找到则取registry仓库下载,最终解压缩到node_modules文件夹
  • 如果下载版本不一致(例如我们明确下载更新版本),则重新构建依赖关系,去仓库下载压缩包解压缩到node_modules,然后重新构建lock锁文件

图31-16 npm install原理图

  • 在package-lock.json锁文件中的resolved属性中,能够找到对应的压缩文件,并且下载完全明确的版本1.7.7

图31-17 axios下载压缩包来源

  • 如果想要确定是否会从缓存中获取,可以通过命令npm get cache来查看缓存文件存放位置

图31-18 npm包缓存的存放位置命令

  • 从该路径中查找缓存文件存放,其中tmp为临时文件夹,不进行使用,主要为content-v2以及index-v5这两文件夹
    • index-v5是一个索引目录,记录content-v2的一个索引或者说是位置,也就是name + version + integrity(完整校验值)的一个哈希值。如果lock锁文件内的这三者和index-v5能够对上,就会去content-v2找到我们缓存的那个文件
    • 可以将项目中name + version + integrity组成的哈希值的看成一个钥匙就够了,而content-v2则是一个宝箱,index-v5则是一个钥匙孔。他们之前的关系就非常清晰了

图31-19 npm缓存包对应三文件夹

  • 打开index-v5文件夹,内部是一个随机生成的字符组合 ,这是一种哈希(或类似的编码)结构,用于存储项目依赖项或缓存文件,这样做的好处是可以避免不同版本或内容的依赖之间产生冲突,并确保每个缓存文件是唯一的且不会覆盖其他版本的文件

图31-20 index-v5索引文件夹

  • 索引文件内部包含大量信息,我们可以进行拆解,在内部可以看到关键信息,也就是缓存条目的键(data下的key)以及缓存的 URL 地址(metadata下的url)
    • key 是缓存系统中的唯一标识符 ,用于在缓存中快速查找和管理缓存条目
    • url 是用于描述资源的原始下载地址 ,提供了有关缓存条目的来源信息,但不会用于直接查找缓存

图31-21 index-v5索引文件详细信息

js 复制代码
{
  "hash": "1dcaa51952126e130a1522d67d846205cb679432", 
  // 唯一哈希值,用于标识当前缓存条目的唯一性。可能是用于快速查找或验证

  "data": {
    "key": "make-fetch-happen:request-cache:https://cdn.npmmirror.com/packages/%40apideck/better-ajv-errors/0.3.6/better-ajv-errors-0.3.6.tgz", 
    // 缓存条目的键,表明这是请求缓存,其中包含了请求的 URL

    "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", 
    // 包的完整性校验字段,使用 SHA512 算法。这是为了确保下载的包在传输中未被篡改,提供数据完整性验证

    "time": 1716788176939, 
    // 缓存的创建时间,表示为时间戳(毫秒)。这个值可用于判断缓存是否已过期

    "size": 13856, 
    // 缓存文件的大小,以字节为单位。这有助于管理缓存数据的存储大小

    "metadata": {
      "time": 1716788176803, 
      // 记录元数据的时间戳,与上面的时间戳类似,用于跟踪缓存元数据的创建时间

      "url": "https://cdn.npmmirror.com/packages/%40apideck/better-ajv-errors/0.3.6/better-ajv-errors-0.3.6.tgz", 
      // 这是缓存的 URL 地址,指向资源所在的具体位置

      "reqHeaders": {}, 
      // 请求头信息,当前为空对象,但通常可以包含如授权信息、内容类型等头部

      "resHeaders": {
        "cache-control": "max-age=86400", 
        // 响应头中的缓存控制策略,指示客户端或代理服务器可以缓存该资源的最大时间(秒)

        "content-type": "application/octet-stream", 
        // 响应头中的内容类型,表示这是二进制数据(压缩文件)

        "date": "Mon, 27 May 2024 01:30:50 GMT", 
        // 响应生成的日期时间,用于指示服务器返回响应的时间

        "etag": "\"172F83E452BFA924FF7B89FFDF898976\"", 
        // 实体标签,用于标识资源的版本。etag 可以帮助缓存系统确定资源是否发生了变化

        "last-modified": "Tue, 28 Jun 2022 11:27:22 GMT"
        // 资源的最后修改时间,表明该资源上次更新的时间,用于协商缓存以减少重复下载
      },

      "options": {
        "compress": true
        // 表明请求是否支持压缩。若为 true,表示该资源在传输过程中启用了压缩,以减少数据传输的体积
      }
    }
  }
}

1.8 npm其他命令

  • 除了npm install以及npm run两个系列的命令之外,npm还有很多其余命令,在这里我们总结一部分常见的
json 复制代码
# 初始化项目
npm init
# 初始化一个新的 Node.js 项目,并创建一个 package.json 文件。你也可以使用 `npm init -y` 来跳过向导,用默认值快速创建。

# 清除 npm 缓存
npm cache clean --force
# 清除 npm 的缓存,通常在遇到缓存损坏或需要重新下载依赖时使用。

# 查看全局安装的包
npm list -g --depth=0
# 列出所有全局安装的 npm 包,并使用 --depth=0 来只显示顶级包(不显示依赖的依赖)。

# 发布包到 npm registry
npm publish
# 将当前项目发布到 npm 的包仓库,通常用于发布开源的 npm 模块。后续会进行学习

# 删除 npm registry 上的包
npm unpublish <package-name> --force
# 从 npm 注册表中删除一个发布的包,通常需要使用 --force 来确认删除。

# 删除项目中的包
npm uninstall <package-name>
# 从项目中移除某个包,并自动更新 package.json 中的依赖。

# 更新项目中的包
npm update <package-name>
# 更新项目中的依赖包到最新的版本,并更新 package-lock.json。

# 检查项目中依赖的可用更新
npm outdated
# 列出当前项目中的所有过时的包,并显示当前版本、想要的版本和最新版本的对比信息。

# 检查项目中依赖的安全漏洞
npm audit
# 对当前项目的依赖进行安全审计,查找已知的安全漏洞。

# 修复项目中依赖的安全漏洞
npm audit fix
# 自动修复项目中的依赖安全漏洞,尝试升级相关的版本来修复已知问题。

# 版本管理
npm version <newversion> | major | minor | patch
# 更新当前项目的版本号,并自动修改 package.json 中的 version 字段。`major`、`minor`、`patch` 分别指主版本、次版本和补丁版本。

# 全局安装包
npm install -g <package-name>
# 将包安装到全局,通常用于安装命令行工具,使其在任何目录下可用。

# 锁定特定版本安装
npm install <package-name>@<version>
# 安装某个依赖包的特定版本,例如 `npm install express@4.17.1`。

# 获取 npm 配置信息
npm config get <key>
# 获取 npm 的配置信息,例如 `npm config get registry` 可以查看当前的 registry URL。

# 设置 npm 配置
npm config set <key> <value>
# 设置 npm 的配置信息,例如 `npm config set registry https://registry.npmmirror.com` 可以更改 npm 使用的镜像源。

# 查看帮助
npm help <command>
# 查看 npm 命令的帮助文档,例如 `npm help install` 会显示 `npm install` 的详细帮助信息。

# npm 链接全局模块
npm link <package-name>
# 将一个全局安装的包符号链接到当前项目中,常用于开发时将本地模块与项目联动使用。

# 显示所有脚本命令
npm run
# 显示 package.json 中定义的所有可用脚本命令。

# 更新 npm 自身
npm install -g npm
# 将 npm 自身更新到最新的版本。

二、包管理工具集合

2.1 yarn工具

  • 另一个node包管理工具是yarn,主要是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具(这种做法在国外常见,国内较少)
    • yarn 是为了弥补 npm早期 的一些缺陷(安装依赖速度很慢、版本依赖混乱等)而出现的
    • 到目前为止,yarn很多的精华都被npm所吸取,基本上没有差距,但是依然很多人喜欢使用yarn
    • 如果大家对yarn感兴趣,也可以从官网进行了解:Introduction | Yarn (yarnpkg.com)

表31-5 npm命令与yarn命令对照

功能 npm 命令 Yarn 命令
初始化项目 npm init yarn init
安装依赖(已有 package.json) npm install yarn install
安装指定包(作为生产依赖) npm install <package> yarn add <package>
安装指定包(作为开发依赖) npm install <package> --save-dev yarn add <package> --dev
全局安装包 npm install -g <package> yarn global add <package>
删除包 npm uninstall <package> yarn remove <package>
更新包 npm update <package> yarn upgrade <package>
升级包到指定版本 npm install <package>@<version> yarn upgrade <package>@<version>
查看过时的依赖 npm outdated yarn outdated
执行脚本命令 npm run <script> yarn <script>
发布包到 npm 仓库 npm publish yarn publish
查看全局安装的包 npm list -g --depth=0 yarn global list
清除缓存 npm cache clean --force yarn cache clean
锁定依赖文件 package-lock.json yarn.lock
添加依赖时不更新锁文件 npm install --no-save yarn add --no-lockfile
自动修复漏洞 npm audit fix yarn audit --fix
列出已安装的脚本命令 npm run yarn run
设置配置项 npm config set <key> <value> yarn config set <key> <value>
查看配置项 npm config get <key> yarn config get <key>
创建符号链接 npm link yarn link
删除符号链接 npm unlink yarn unlink

2.2 cnpm工具

  • 由于一些特殊的原因,某些情况下我们没办法很好的从 registry.npmjs.org (npm的registry仓库)下载下来一些需要的包
    • npm仓库位于国外,而我们位于国内,由于这类原因导致访问npm仓库需要一点运气,不是很可控
  • 但npm提供了一个镜像功能,对我们解决该问题的帮助很大,那什么是镜像功能?
    • npm 镜像是指 npm 官方仓库的一个复制版本(镜子映射的npm),通常由第三方或社区维护,用于加速包的下载
    • 镜像的内容包括所有官方 npm registry 的包,这些镜像定期与官方仓库同步,确保提供相同的包版本和数据
    • 镜像的目的是解决在某些地区下载官方仓库中的包较慢或者网络访问受限的问题,从而提高安装依赖时的速度
  • 那我们要如何使用镜像功能?镜像功能需要配合一个叫做镜像源的内容,这两者的概念有什么不同?
    • 镜像 是实际存储在服务器上的数据副本,包含了和官方服务器一致的内容,我们可以简单理解为在国内开一个服务器定时从国外npm仓库拷贝一份数据,这个国内的服务器就是镜像
    • 镜像源 则是用户访问这些镜像的入口 ,是一个 URL 地址,用户通过这个地址来获取需要的资源(也就是访问国内的服务器)。因此,镜像源是指向镜像的路径,是一种对外暴露的访问接口
    • 常见的镜像源有淘宝、华为云、腾讯云、中国科学技术大学等一系列大学镜像,选择其一进行使用即可
  • 因此我们的重点在于如何修改设置镜像源,通过npm命令:npm config set registry URL(镜像源)即可

图31-22 npm镜像原理

json 复制代码
# npm 官方镜像
https://registry.npmjs.org/
# 默认的 npm 官方镜像,所有 npm 包的原始来源地址。

# 淘宝 npm 镜像 (npmmirror)
https://registry.npmmirror.com/
# 阿里巴巴公司维护的 npm 镜像,又称为淘宝镜像。特别适合中国大陆地区的用户,加速 npm 包的下载。

# 淘宝 npm 镜像 (旧地址)
https://registry.npm.taobao.org/
# 这个地址是淘宝旧的 npm 镜像,仍然有效,重定向到新镜像 npmmirror。

# Yarn 官方镜像
https://registry.yarnpkg.com/
# Yarn 自己的 npm 镜像源,用户可以用这个地址来替代默认的 npm 官方源。

# 腾讯云 npm 镜像
https://mirrors.cloud.tencent.com/npm/
# 腾讯云维护的 npm 镜像,适合腾讯云用户以及想加速 npm 下载的用户。

# 中国科学技术大学 (USTC) npm 镜像
https://mirrors.ustc.edu.cn/npm/
# 中国科学技术大学提供的 npm 镜像,提供给国内用户,尤其是教育和科研用途。

# 华为云 npm 镜像
https://repo.huaweicloud.com/repository/npm/
# 华为云维护的 npm 镜像,用于加速 npm 包下载,适合中国地区的用户。

# npmjs 中国镜像 (cnpm)
https://r.cnpmjs.org/
# 一个由 cnpm 提供的镜像,类似于淘宝镜像的替代方案,也能用于加速下载。

# npm 镜像设置示例
npm config set registry https://registry.npmmirror.com/
# 将 npm 镜像设置为淘宝镜像,加快 npm 包的安装速度,特别适合中国大陆用户。
  • 但这里其实还有几个问题:
    1. 镜像源并不是时刻和npm仓库同步,这里存在一个间隔时间,比如淘宝是每隔10分钟同步一下
    2. 镜像未来有一天会有可能不再维护,而这件事情我们并不知道(不会特地通知我们),突然间不能继续使用,会对我们排查问题造成一定困扰,因此不能过多依赖于镜像,会存在一定风险
    3. npm官网是一个非盈利组织,相对于公司更多依靠于商业来说,稳定性会高很多(哪怕创始人不再维护,也会有一代代人接手)
  • 这时候,我们通常会使用cnpm,像这类工具就能够进行一个全局安装,以便日常使用,我们前面埋下的伏笔在这里也能体现出来(所有的包管理工具本身也是一个包),所以直接使用npm安装cnpm即可
    • cnpm的所有用法与npm保持一致,cnpm默认指向淘宝镜像,设置镜像源方式与npm一致
js 复制代码
npm install cnpm -g//安装
cnpm config set registry URL//设置镜像源
  • 此时,我们就具备两个包管理工具,同时指向于国内与国外两个仓库,当cnpm无法使用时,可以直接继续使用npm
    • 因为对于大多数人来说(比如我),不太希望随意修改npm原本从官方下来包的渠道
    • 因此可以使用cnpm配置切换镜像源的工具nrm,在国内多个镜像进行切换使用,让npm保持一定的存粹性

图31-23 cnpm与npm双重保险

2.3 npx命令

  • npx是npm5.2之后自带的一个命令,作用非常多,主要目的是为了更方便地执行 Node.js 包中的命令 ,而无需全局安装这些包。它解决了在项目开发过程中,频繁需要安装全局工具包所带来的困扰
    • 更方便的执行命令这句话怎么理解的?它是如何解决需要频繁安装全局工具包带来的困惑的?
    • 我们以webpack为例,全局安装的是webpack5.1.3,而项目安装的是webpack3.6.0
    • 那此时就有疑问了,在项目终端中使用webpack命令时,生效的是哪个版本的webpack?可以使用webpack --version检查一下,我们可能会以为是3.6.0版本,可惜并不是
  • 这就需要追述到在命令行中敲下一个命令时,会怎么做?
    • 首先会在当前目录查找当前命令是否存在,那我们的webpack不是才局部安装吗?应该是有才对,但其实是没有的
    • 因为webpack局部安装,是被安装到node_modules/.bin/webpack下的
js 复制代码
//局部安装低版本webpack
npm install webpack@3.6.0 -D
  • 在.bin文件夹下,存在众多可执行命令,其中有三种不同后缀的命令,不管是webpack还是vite或者其他工具都一样
    • .cmd 文件用于 Windows 的 CMD
    • .ps1 文件用于 Windows 的 PowerShell
    • 没有后缀的文件 是为 LinuxmacOSUnix 系统 设计的可执行脚本,通常通过 Shell(如 Bash)直接执行
  • 我们好像找到了答案,局部安装的命令原来在这里,那在项目的当前目前查找才找不到
    • 那么命令在当前目录找不到,就会去环境变量PATH(操作系统概念)中查找
    • 所以webpack执行命令时,是全局版本而非局部版本

图31-24 node_modules下的bin执行文件

  • 那如果我因为特殊原因就是需要使用低版本的webpack,防止高版本造成的干扰问题,有什么办法?
    • 难道将全局webpack卸载,然后重新安装低版本,那真的太麻烦了,其他地方要用高版本岂不是还得重新来一遍卸载安装的操作
    • 或者可以直接定位到.bin文件夹下去执行webpack命令,这比上一个卸载安装操作的做法更好,且正确执行到局部的webpack,但这依旧挺麻烦的,有没有更简单的做法?
js 复制代码
//手动执行.bin下的命令
./node_modules/.bin/webpack --version //3.6.0
  • 此时就能够利用npm run系列的效果,在package.json配置文件下的scripts属性中进行配置,利用脚本的威力来辅助我们
    • 通过scripts属性,可以将命令简化为npm run webpack,并且script属性执行时,会先从.bin文件夹下开始寻找
    • 但这样做,需要我们添加脚本的键值对,并且是固定的,意味着一个webpack命令就需要添加一个脚本,具体命令数量肯定不少
  • 因此我们需要更简单的做法,那就是npx命令
    • 使用npx替代npm,查找顺序会和scripts属性一样,先从当前node_modules文件夹下寻找
    • 这就是npx的原理,会到当前目录的node_modules/.bin目录下查找对应的命令
js 复制代码
{
  "name": "04_npx_demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "webpack": "webpack --version"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^3.6.0"
  }
}
  • 因此面对需要优先考虑当前项目的版本时,就可以采用npx来执行命令,在这个过程中我们也掌握了三种针对该问题的方式

表31-6 npm run系列命令查找工具包命令方式

方式 描述 示例命令或代码
方式一:明确查找 直接查找到 node_modules 目录下的 Webpack 可执行文件,手动运行 ./node_modules/.bin/webpack
方式二:定义脚本 package.json 中的 scripts 部分定义命令来执行 Webpack "scripts": { "build": "webpack" }
方式三:使用 npx 使用 npx 自动查找并执行项目的 Webpack,简化命令行调用,无需手动定义路径 npx webpack

2.4 pnpm工具(重要)

  • 目前最流行的包管理工具是pnpm,最快的是bun,我们主要讲pnpm
    • 因为bun功能非常多,远超JS高级系列的范畴,更多的可以去官网主动了解:bun.sh/
    • 通过流行框架或者构建工具所推荐的包管理器,能够知晓哪一个更加流行,Vite、Vue推荐的方式也是我们讲解的顺序,都是最主流的管理工具,而React推荐的是npx,都是讲解过的内容

图31-25 Vite推荐的包管理工具

图31-26 Vue推荐的包管理工具

2.4.1 pnpm出现前的痛点

  • 在学习pnpm包管理器之前,我们需要知道他出现的原因?每一个技术的出现,都是为了解决曾经的痛点问题,有哪些问题是npm不断更新下,仍旧还存在的问题呢?
  • 我们清楚,一个庞大的包很可能会依赖其他包,并且很可能不止一个包而是很多大大小小的包,而多个包Pinia、Vue、Vite、axios等等...会构建出一个庞大的下载链出来,这也是导致我们node_modules文件夹非常大的原因
    • 一个项目...多个项目下来,这是一个非常恐怖的累积量,项目太多,这些包甚至总体会占据几十GB接近上百GB,哪怕拥有缓存机制也是如此,因此下载包的版本不同的话,依旧会重新下载新版本的包,然后缓存中就会拥有越来越多版本的相同包,并且缓存机制是为了防止我们不停的从registry仓库下载。当版本相同时,不从registry仓库下载,而是从缓存中拷贝(缓存中复制一份解压缩到项目中),对本地来说,并没有做到减少包大小的作用
    • 因此这常常被前端程序员戏称为全宇宙最重的东西

图31-27 node_modules文件夹的"重量"

  • 面对这种情况,你会怎么做?在pnpm出来之前,大家的做法是删掉node_modules文件夹,等需要的时候再重新下载
    • 优化角度也就在不要删到公用部分的前提下,看能不能一键脚本删除...
    • 但我们知道这种做法只能是权宜之计,长期往来不仅麻烦,而且对npm官网的负荷也大
    • 不管是npm、yarn、cnpm都没有解决这个问题

图31-28 社群对node_modules的讨论

  • 因此当pnpm出现时,引起了社区很大的反响,到目前为止,已经是绝对的主流(经受时间考验),可以放心使用
    • 那么pnpm是怎么解决这个之前所有包都没解决的痛点问题的?让我们来看一下吧

2.4.2 什么是pnpm?

图31-29 pnpm官网自我介绍

  • 那么,有哪些公司在用呢?包括Vue在内的很多公司或者开源项目的包管理工具都切换到了pnpm

图31-30 使用pnpm的公司

2.4.3 硬链接和软链接的概念

  • pnpm解决遗留存储空间占据问题的关键在于硬链接软链接的概念,这两个概念源于操作系统的单词又是什么意思?
    • 硬链接 是一种指向文件物理数据块 的指针。对于硬链接来说,文件在磁盘中的物理数据会被多个文件名引用
    • 符号链接(软链接、Symbolic link)是一类特殊的文件 ,其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用,类似快捷方式(但实际并不是快捷方式)

图31-31 硬链接与软链接

  • 硬链接是直接指向于真实物理中存在的一个磁盘,文件就存在这个磁盘中,默认情况下,我们不能访问这个磁盘
    • 操作系统(Window、Linux)本身也是一个软件程序,介于应用程序与硬件的中间,在这层软件上进行了一个抽象
    • 当我们打开操作系统时,操作系统有一个专门的文件系统,我们平时看到的文件都来自该文件系统,例如abc.mp4等音频文件,当我们从操作系统的可视化界面点击该音频文件时,文件会通过一种寻址方式,找到我们位于真实磁盘内的真实数据,而这种查找方式就称为硬链接

图31-32 计算机架构分层

  • 而软链接是保存着某一个文件的路径,本身并没有内容,可以理解为这取出存放路径之后,就只是一个空壳,因此软链接的文件基本上不占内存,文件是0字节大小

图31-33 硬链接与软连接的指向情况

  • 如果我们有多个硬链接指向于同一个磁盘下的真实数据,也就是abc.mp4这个音频文件,我们不仅C盘下有,D、E、F盘下也有,并且都是C盘下的文件复制过来的
    • 在这种情况下,如果修改D、E、F盘下的文件信息,那C盘文件会也会同步修改,因为这些文件都指向于同一块真实数据
    • 对于操作系统来说,是可以区分这其中区别的,但我们通过应用层的播放器或者Node去读取这些音频文件的时候,它们是完全无法区分哪一个才是原始的硬链接,它们只知道通过该硬链接能够找到真实数据进行播放、读取
  • 接下来让我们来进入演练吧!

2.4.5 硬链接和软连接的演练

  • 文件的拷贝每个人都非常熟悉,会在硬盘中复制出来一份新的文件数据(也就是会造成磁盘的一部分空间)
    • 我们在原有的foo.js文件中复制出来一份文件(需要使用cmd终端)
js 复制代码
//cmd终端输入命令
window: copy foo.js foo_copy.js
macos : cp foo.js foo_copy.js

图31-34 终端输入命令

  • 但这种做法,本质上会连着真实硬盘内的数据也拷贝一份,在磁盘中指向的是不同的数据,因此改变数据foo.js文件内的数据,并不会改变foo_copy.js文件内的数据

图31-35 数据拷贝本质

  • 那我们需要怎么做,才能实现硬链接呢?
    • 在window中,mklink 命令用于创建链接,可以是硬链接或符号链接
    • /H 参数用于指定创建的是一个硬链接(Hard Link) ,默认情况下,如果不使用 /H 参数,mklink 会创建一个符号链接(软链接)
    • 需要注意,链接是后者链接前者(前者aaa.js是被创建出来的文件)
js 复制代码
window: mklink /H aaa.js bbb.js//创建硬链接的命令,aaa.js跟bbb.js建立起硬链接
macos : ln aaa.js bbb.js

图31-36 硬链接关系建立

  • 当我们创建成功后,对aaa.js的改变会同时在bbb.js中生效,其中同步间隔大概在0.5-1s左右,这是数据读取并生效的时间

图31-37 硬链接的同步改变

  • 如果使用软链接,则无需加/H,再来一次,这里需要需要注意,如果显示权限不够,则使用管理员权限开启终端再来一遍
    • 从文件的显示中可以看出看到左下角有一个斜箭头,这是软链接的标志
    • 打开的软链接文件,和预想中是一串地址并不相同,与正常硬链接的文件打开是一样的,这是因为在系统中打开软链接文件时,系统会自动解析这个软链接,找到它指向的目标文件,然后读取目标文件的内容。这个行为与硬链接的行为非常相似,结果是打开软链接和硬链接时看到的内容是相同的
    • 原因也来自操作系统对软链接的处理是透明的,即用户不需要关心这个文件是软链接还是原始文件,系统会自动完成所有解析和定位工作。因此,打开一个软链接文件和打开目标文件在效果上是完全一样的。可以编辑、保存该文件,而系统会将操作应用于它指向的目标文件

图31-38 软链接的内存指向

图31-39 软链接标志

  • 需要看清楚这其中的区别,我们将aaa.js文件放入上一层文件夹中,然后打开文件属性,可以看到目标清晰指向于bbb.js文件,一旦我们将bbb.js文件删除,那么aaa.js文件就打不开了也找不到了(无法读取)
    • 本质上,require进行引入模块时,如果通过符号链接,也是先去找到原文件再去找到磁盘中的原数据进行读取,通过硬链接的话,则就直接读取磁盘数据

图31-40 软链接指向硬链接证明

2.4.6 pnpm做了什么?

  • 当使用 npm 或 Yarn 时,如果有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 在硬盘上就需要保存 100 份该相同依赖包的副本
  • 而如果是使用 pnpm,依赖包将被 存放在一个统一的位置,因此:
    • 如果对同一依赖包使用相同的版本 ,那么磁盘上只有这个依赖包的一份文件
    • 如果对同一依赖包需要使用不同的版本 ,则仅有 版本之间不同的文件会被存储起来
    • 所有文件都保存在硬盘上的统一的位置:
      1. 当安装软件包时, 其包含的所有文件都会硬链接到此位置,而不会占用 额外的硬盘空间。需要记住存储的是所有的文件,而不是包的目录,因为硬链接无法操作目录,只能操作文件
      2. 这让我们可以在项目之间方便地共享相同版本的 依赖包,因为搭建硬链接并不会导致磁盘数据的拷贝

图31-41 pnpm依赖包硬链接方式

2.4.7 非扁平化的node_modules

  • 非扁平化不是pnpm的关键,但是一个很有意思的特色,当使用 npm 或 Yarn Classic 安装依赖包时,所有软件包都将被提升到 node_modules 的 根目录下,造成node_modules文件夹下会同时存在很多文件夹(直接或者间接安装的部分),我们很难区分哪些才是我们真正下载的,这时候通常需要去看package.json配置文件的信息
  • 这些本不属于当前项目所设定的依赖包,就是安装了webpack的时候,同时会下载下来一堆webpack所需要的包,这些不是主动下载的,但是也可以访问到,但是这个问题在pnpm中将不复存在,下载了webpack就只能看到webpack这个文件夹,其他的有真实地址,硬链接指向了磁盘空间,但是这些webpack需要的包没有创建软链接,我们没办法通过软链接找到硬链接再找到硬盘里的内容去调用,因此只能使用下载的那一个

图31-42 node_modules下的扁平化和软链接标志

之所以是软链接,是因为真实的地址是由pnpm保管的,这里的只是指向真实地址的软链接,然后真实地址硬链接到硬盘中,重复使用的时候就创建软链接就行了,就不会因为多个项目使用重复的包而重复下载包造成的大量空间的浪费了

  • pnpm 会将所有下载的包统一存储在全局内容存储(store)中,通常是目录 .pnpm-store ,当某个项目安装依赖时,pnpm 并不会将这些依赖的完整包复制到每个项目的 node_modules 中,而是通过一系列的硬链接和软链接 来管理依赖的文件结构
    • node_modules文件夹下的bar@1.0.0软链接到.pnpm内的硬链接所在处,而该硬链接所在处会链接到真实数据所在,也就是.pnpm store之后磁盘内的真实数据
    • 在文件结构深处也存在着一些软链接例如foo@1.0.0依赖者其他硬链接,该硬链接依旧指向于.pnpm store,最终指向磁盘内的真实数据,通过来回嵌套的软链接来管理多级依赖关系,但最终所有的依赖数据都是指向全局存储中的真实数据

图31-43 node_modules下的扁平化原理

表31-7 扁平化node_modules专业词汇解释

专业单词 意思
Hidden Folder(橙色虚线) 隐藏的文件夹
Folder 文件夹
Hard Link(红色虚线) 硬链接
Symbolic Link(绿色箭头) 软链接
Symbolic Link(Pay close attention to the direction of the arrows) 黄色箭头 软链接(密切注意箭头的方向)

2.4.8 pnpm的常见命令和store存储

js 复制代码
npm install -g pnpm
  • 以下是一个与 npm 等价命令的对照表,帮助大家快速入门,更多的命令则是建议大家去看官方文档进行对照,当做一个文档随查随用即可

表31-8 npm与pnpm命令对照

npm命令(pkg = package) pnpm等价命令
npm install pnpm install
npm install pnpm add
npm uninstall pnpm remove
npm run pnpm
  • 在pnpm7.0之前,统一的存储位置是 ~/.pnpm-store中的,这也是一种硬链接,是作为所有使用pnpm的总仓库,一处统一收集的位置,在该位置已经提前指向于磁盘内的数据,形成一个硬链接,因此当需要使用时,可以直接创建软链接指向于该硬链接,更加节省空间并且软链接不会导致依赖包被修改
    • 直接指向磁盘会形成硬链接,由需求所延伸出来的做法,所需的是软链接,因此中转站.pnpm-store是必要的,可以不叫这个名字,也可以不在~/.pnpm-store中进行存储,但一定需要有一个中转站性质的存放处,这是一种设计思想
  • 在pnpm7.0之后,统一的存储位置进行了更改:<pnpm home directory>/store

表31-8 不同操作系统下的pnpm全局存储路径

操作系统 pnpm 全局存储默认路径
Linux ~/.local/share/pnpm/store
Windows %LOCALAPPDATA%/pnpm/store
macOS ~/Library/pnpm/store
  • 我们可以通过一些终端命令获取当前活跃的store目录
    • 这是一个查看 pnpm 全局存储路径的命令
    • 返回当前项目所使用的 pnpm 全局存储(store) 的地址,也就是所有通过 pnpm 安装的包被存储的位置
js 复制代码
pnpm store path//终端输入返回pnpm所在store地址
//store地址就是用来放我们下载的那些包的地方,属于硬链接的那部分,只下载一次供软链接去连接
  • 另外一个非常重要的store命令是prune(修剪) :从store中删除当前未被引用的包来释放store的空间
    • 这样可以保持 pnpm 全局存储的整洁,避免存储过多不必要的数据
    • 假设在项目中更新了某个依赖包版本,而旧版本的包已经不再被任何项目引用。这时候就可以运行 pnpm store prune,删除这个旧版本的包
js 复制代码
pnpm store prune

三、发布npm包

3.1 注册npm账号

  • 如果我们希望自己也能够写一个工具包让全世界程序员一起来使用,那就可以编写后发布到npm的registry仓库中,让所有人都能够下载使用,但那要怎么做呢?
  • 首先我们需要注册npm账号www.npmjs.com/通过官网进去选择sig... up

图31-44 npm官网登录注册方案

  • 当通过Sign Up注册后,点击Sign In进行登录,登录界面如下

图31-45 npm官网登录界面

  • 注册结束后,回到我们的编辑器中,使用npm init创建一个package.json配置文件,此时最好不要-y一键跳过,因为作为上传npm官网的包,需要慎重填写一些内容,最终这些信息会呈现在npm官网让所有人都看得到

图31-1 初始化包填写内容

json 复制代码
{
  "name": "coderwhy_js", 
  // 项目名称:项目的唯一标识符,通常采用小写字母和连字符,不能包含空格。例如:my-awesome-project
  "version": "1.0.0", 
  // 项目版本:遵循语义化版本控制规范的版本号。通常从 "1.0.0" 开始,后续根据项目的变更进行更新。
  "description": "JS高级系列课程(共学计划),发布npm包教程", 
  // 项目描述:简要描述项目的功能或目标,有助于了解项目的作用。
  "entry point": "index.js", 
  // 入口文件:项目的主入口文件,即项目启动或执行的初始 JavaScript 文件。例如:index.js。
  "test command": "echo \"Error: no test specified\" && exit 1", 
  // 测试命令:项目的测试脚本,通常用来运行项目的单元测试。例如:可以填写 "jest" 来运行测试。
  "git repository": "https://github.com/coderwhy", 
  // Git 仓库地址:项目的 Git 仓库的远程地址,有助于版本控制和开源协作。
  "keywords": ["npm", "init", "example"], 
  // 关键词:项目的关键字,通常是一些与项目相关的描述性单词,有助于在 npm 中搜索。
  "author": "XiaoYu&coderwhy", 
  // 作者:项目的创建者或维护者的名字,可以包括邮箱或其他联系方式。
  "license": "ISC" 
  // 许可证:项目的许可证类型,用于声明项目的使用和分发权限,默认是 "ISC",也可以选择 "MIT"、"GPL-3.0" 等。
}
  • 在该基础上,还有一些信息是开发者常填写的,可以进行了解
js 复制代码
// 项目的关键词,用于描述项目的特性,便于在 npm 搜索中发现
"keywords": [
  "nodejs",
  "express",
  "sample"
],
// 项目中需要执行的依赖项的版本规则
"engines": {
  // 指定项目需要的 Node.js 的版本
  "node": ">=14.0.0"
},
// npm 包发布的规则,指明哪些文件应该包含在发布包中
"files": [
  "lib/",
  "bin/",
  "index.js"
],
// 用于定义项目的依赖解析方式,支持 Yarn、pnpm 等
"packageManager": "yarn@1.22.10",
// 项目的浏览器兼容性设置
"browserslist": [
  "> 1%",
  "last 2 versions",
  "not dead"
]

3.2 终端发布npm包

  • 准备好前置环节后,在编辑器终端登录npm账号,但需要注意的是,需要查看一下源是否是npm官网自带的,如果使用其他镜像源,例如淘宝的就不能够登录上去(会让我们去注册cnpm账号)
    • 使用npm login命令进行登录,然后直接npm publish即可将当前项目发布到npm上
js 复制代码
//发布的包内容:对传入两个数的增删乘方法
function add(num1, num2) {
  return num1 + num2
}

function sub(num1, num2) {
  return num1 - num2
}

function mul(num1, num2) {
  return num1 * num2
}

module.exports = {
  add,
  sub,
  mul
}
js 复制代码
//将镜像源设置回npm官方registry仓库
npm config set registry https://registry.npmjs.org/

3.3 修改包

  • 当我们对这个包在本地做出修改想要上传时,需要修改上传版本,确认我们本次提升是大版本更新还是功能更新,或者是打补丁
    • 如果不修改版本直接上传,会报403网络错误码,服务器会拒绝此次合并
    • 而当我们想要删除这个包或者让包过期也很简单,都只需要一行命令,但前提是npm账号已经登录才能操作
js 复制代码
//删除发布的包
npm unpublish
//让发布的包过期
npm deprecate

后续预告

  • 在下一章节中,我们会开始学习JSON,那什么是JSON呢?在本章节中的package.json配置文件的文件扩展名json就是我们要学习的内容,我们为什么会需要JSON,都运用在哪些方面,如何进行使用
    • 以及数据存储的方式localStorage和sessionStorage两个重要知识点,他们都有什么区别?
    • Storage常见的方法和属性,以及学习IndexedDB、认识客户端中的Cookie
  • 这些属于浏览器本身的知识点,会进行扩展一部分,但更多的还是聚焦于JavaScript高级本身,让我们下期见
相关推荐
前端百草阁17 分钟前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜17 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund40418 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish19 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple19 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five20 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序20 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫54120 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
酷酷的威朗普21 分钟前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
前端每日三省22 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript