1、前言
如果我看得更远,那是因为我站在巨人的肩膀上 -- 牛顿。
话说,牛顿这可不是在谦虚
法国科学家胡克认为自己发现的平方反比律
启发了牛顿发现万有引力定律
,但牛顿拒绝承认这一点(不承认平方反比律是胡克发现的,但还是承认是受平方反比律启发)。于是牛顿就在写给胡克的信件出现了上述这句话
而胡克是个驼背的矮个子,也就是说,牛顿的名言"如果我看得更远,那是因为我站在巨人的肩膀上"的本意是"我的成就与胡可你这个驼背的矮子无关",是牛顿阴阳怪气地骂胡克
胡克去世后,牛顿当上了英格兰皇家学会主席;立刻解散了胡克留下的实验室和图书馆,胡克的所有研究成果、学术资料和实验器材也都被牛顿下令销毁或者分散保存
所以说,牛顿是有道德瑕疵的,但牛逼也是真的。
站在巨人的肩膀上,如今已经延伸为通过借鉴前人的经验和智慧,更好实现自己的价值,这样无疑也是不错的鸡汤(干了这一碗,还有三碗~)
闲话少扯,回归正题
npm 的出现,无疑就是让我们可以站在巨人的肩膀上。npm 为你打开了连接整个 JavaScript 天才世界的一扇大门;它允许我们分享借鉴来自各大洲的开发者使用的包
2、核心概念
在理解 npm 之前,我们需要先弄清楚一些概念
- 模块 module
- 包 package
- 依赖 dependencies
- npm 注册表 registry
- npm CLI 命令
2.1、模块
模块的概念很好理解;什么,你说你不能理解?有的时候找找自己的原因,这么多年了技术能力涨没涨,有没有认真学习(手动狗头保命~)
咳咳,扯远了;事实上,Node 中的模块可以分为以下三部分
- 自己编写的模块
- Node 内置的模块
- 第三方模块
上一节中,我们学习了如何通过 require 和 module.exports 来实现模块,就是一个自己编写的模块;它允许你在当前项目复用
而 Node 本身也内置了许多模块,如 fs 文件操作模块、http 服务模块;更多内置模块可以前往 Node 官网
第三方模块:软件开发者将自己编写的模块,发布到 npm;我们从 npm 下载下来的这些模块,就是第三方模块;任何项目,只要你下载了,就可以使用这些模块
2.2、包
上述所说的第三方模块
被称为 包(package)
开发者将可复用的逻辑封装为模块发布到 npm 上,这些可以重复的框架代码被称作包或模块(第三方模块)
一个包可以是一个文件夹里放着几个文件,同时有一个叫做 package.json
的文件管理这个包的信息,最主要的就是依赖信息
2.3、依赖
包与包之间可以相互引用,即我们常说的项目依赖
它们由 package.json 文件中的 dependencies 或 devDependencies 字段定义;记录该项目所依赖的包版本
2.3.1、为什么需要依赖来记录?
当另一个项目需要复用我在当前项目封装的模块功能时;一般情况下,我们会将整个模块拷贝过去;当我们使用一个第三方模块时,我们也可以这么做,去拷贝第三方模块的 dist 内容(在非框架下编程,而模块又没有提供 CDN 版本,我们通常会这么做)
但当使用的模块数量增加时,这样无疑是非常繁琐的;依赖记录的目的就在于此,我们只需要执行 npm 的安装命令,就可以将这些记录在 package.json 中的依赖包,全部下载到本地(node_modules 文件下)
2.4、注册表
npm 注册表是一个巨大的数据库,保存了每个包(package)的信息;
所有的包管理工具、不同源访问的数据,都来自这个数据库。npm 逐渐可能会被更先进的包管理工具取代,但 register 的地位永远无法动摇
2.5、CLI
Command Line Interface 即命令行接口的缩写,开发者通过 CLI 与包管理工具打交道,如安装、更新依赖
不同的包管理工具的 CLI 也有所不同,如 npm 和 yarn
3、npm 与 Node.js
为什么想要聊聊 npm 与 Node.js 的关系呢?因为整个过程堪比爽文小说,怎能按耐住一颗八卦的心~
NPM 的全称是 Node Package Manager,是随同 NodeJS 一起安装的包管理和分发工具
之前就说过,好像不会使用 Node.js 根本不影响写前端页面;但是,不会使用 npm,却极大的影响着我们写前端页面;
大多数场景下,当我们安装完 Node.js 之后,我们几乎不使用 Node.js,只使用其中内置的 npm,愉快的玩耍着各种 npm 包,两者之间好像并没有关系
事实上,Node.js 与 npm 确实没有直接关系
历史背景: npm 开发出来后,它的作者曾经联系过 jQuery、Bootstrap,希望他们提交自己的软件包给 npm 进行管理;jQuery、Bootstrap 表示:你什么档次?让我提交软件包,完全不搭理...
于是又联系到 Node.js 的作者,当时 Node.js 并不火,而且缺一个包管理器,二者一拍即合
从此,npm 成为 Node.js 的一个依赖(上一节 Node.js 源码 devs 有说过),两个好基友相互扶持,一起做大做强,曾经对 npm 爱理不理的 jQuery、Bootstrap 也加入到了 npm 中(妥妥的屌丝逆袭爽文情节~)
"三十年河东,三十年河西,莫欺少年穷~",不要因为得不到别人肯定而轻易沮丧;他朝若遂凌云志,再行煮酒论英雄(干了这碗鸡汤,还有两碗)
4、npm CLI 命令使用
Node.js 内置了 npm,也就是说我们默认就可以使用 npm xxx 命令来进行包管理操作;前面我们说过关于 Node.js 的版本控制,可想而知,默认的 npm 跟随不同的 Node.js 版本不同肯定也会有所区别
如果你想更新的的 npm 版本,如更新到 9.9.0,可以在终端运行 npm install npm@9 -g
更新版本

一般对 Node.js 版本有要求,过低版本的 Node 并不支持安装最新的 npm;使用 nvm 安装最新稳定版本 Node 后再运行上述命令即可完成更新(em...有点慢)
4.1、由一条 npm 更新命令引发的巴拉巴拉
npm install npm@9 -g
光这一条命令,能引发的讨论就非常多
4.1.1、安装过慢
从前车马很慢...现在不行了,外卖晚到 3 分钟可能就要被骂个狗血淋头。 安装过慢,作为头等大事,肯定是要优先解决的
为什么慢?很好理解,因为 npm 是外国网站,从外网下载东西,能不慢嘛
何解?翻X(捂嘴...这可不兴说,要坐牢的),那当然使用国内自己部署的服务器
国内部署和官网完全一样的 npm 服务器,只不过数据都是从 npm 拿过来的,除此之外,使用方式与方法完全一样
why?原理很简单,就是类似 taobao 等这一类的源,将所有公共 npm 包克隆到私有服务器上,然后每隔一段时间进行一次同步,发布到 CDN 上;财大气粗,怎一豪字了得~
注意事项:
- 国内源只会克隆公共 npm 包,这也就意味着,如果你使用私有 npm 包,使用 taobao 等国内源可能会出现问题
- 国内源会每隔一段时间进行一次同步,因此在使用最新依赖时可能会出现问题

- npm:官方提供的默认源,国外服务器,下载较慢
- yarn: 由 Facebook 提供,国外服务器,下载较慢
- tencent: 由腾讯提供,国内服务器,下载快
- taobao、npmMirror、cnpm: 均由淘宝提供,分别部署在北京、杭州、广州;不同 CDN 加快访问
切换为国内 taobao 源进行下载,下载慢的问题就解决辣~
arduino
npm config set registry https://registry.npm.taobao.org/
话说,要记住这样一堆地址真的好难哦~莫慌,nrm 来救场。
4.1.2、nrm
nrm 是什么?npm registry manager 的缩写,翻译成人话就是 npm 镜像源管理工具,目的就为了方便在各个不同 npm 源之间来回切换
1、安装
终端执行 npm install nrm -g
命令进行安装,安装完成后通过 nrm -V
如果能正常输出版本号就代表安装成功(注意,V 是大写 -.-!)
2、常用命令
nrm ls
查看所有镜像源,就是上面那张图;同时对当前使用源标 *nrm current
查看当前使用源nrm use <registry>
:切换源,如要切换到 taobao 源,使用命令nrm use taobao
即可
日常使用,上述 3 个命令就够用了;
对于选择困难症患者,国内提供了好些个源,选哪个真的好困难的说,用哪个最好呢?测试一下,哪个最快就选哪个!
nrm test [registry]
:测试源的访问速度; 不加 registry 时,默认测试所有的源速度;

上述 taobao 源访问速度最快,只要 258ms,那还等什么,赶紧切换!
细心的朋友可能已经发现,nrm test
和 nrm ls
命令执行的结果都有一个奇怪的东西:aliyun
这是一个添加的自定义源,因为公司代码部署在云效
上面,需要切换云效源进行一些调试,如何添加管理一个私有自定义源呢?以这个 aliyun 为例:
nrm add aliyun <url> [home]
:添加一个源; (比如:公司自己的私有源,上述云效源);nrm set-auth aliyun <value> [always]
:设置自定义源的授权信息;nrm set-email aliyun <value>
:给自定义源设置路径;nrm set-hosted-repo aliyun <value>
:设置发布到自定义源的 npm 托管仓储nrm del aliyun
: delete 自定义源
也就是说,只要有这个条件,你甚至可以搭建一套自己的服务器,用来进行依赖源的 CDN 加速
3、npm 私服搭建
有些模块,我们希望在公司内部可以复用,但不想开源或开放访问,这时在 npm 上发布依赖就不太合适;私服搭建应运而生
npm 私服是如何搭建呢?很显然,你不可能像 taobao 这样的大佬财大气粗的将 npm 所有公共依赖包克隆到私有服务器。
私服搭建首推 verdaccio,因为它使用简单,而且免费。它的原理也非常简单:
- 首先,在私服上上传发布私有的依赖包;
- 当用户访问私服下载依赖时,如果私服上存在,则直接下载;如果不存在,则重定向到 npm 的源头下载
4.2、指定依赖版本
上述命令通过 @9 来指定了 npm 包的依赖版本
npm 的规范的标准版本号采用 major.minor.patch
;即 主版本号.次版本号.补丁版本号
的格式, 取值为非负的整数,且禁止在数字前方补零。
每个元素必须以数值来递增
- 当模块进行 bug 修复发布时,增加补丁版本号的值
- 当模块发布新功能时,增加次版本号的值
- 当模块进行大升级时,增加主版本号的值
当前 dayjs 的最新版本为 1.11.10(测试时间:2023-10-14)
那么如命令 npm install dayjs@1
为什么会安装 1.11.10 版本的依赖呢?@ 后面对应的版本限定,主要有以下几种常用形式:
- latest: 安装最新版本
- ~ + 指定版本: 比如 ~1.11.10,表示安装 1.11.x 的最新版本(不低于 1.11.10),但是不安装 1.12.x
- ^ + 指定版本: 比如ˆ1.11.10,表示安装 1.11.x 的最新版本(不低于 1.11.10),但是不安装 2.x.x
- 指定版本: 比如 @1、@1.11、@1.11.10
- 指定主版本号时,会默认安装最新次、补丁版本的依赖;如
npm install dayjs@1
会安装 1.11.10 版本依赖,并在 package.json 记录 ^1.11.10 - 如果指定次版本号、补丁版本号,则会根据指定的版本安装并记录(行为不一致~);如
npm install [email protected]
会安装 1.1 版本依赖,并在 package.json 记录 1.1
- 指定主版本号时,会默认安装最新次、补丁版本的依赖;如
- 不添加任何限定:默认安装最新版本,并锁定主版本号(相当于 ^1.11.10)
注意: 如果大版本号为 0,则 ^ 的行为与 ~ 相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
除上述形式,还有许多诸如 @">=0.1.0 <0.2.0
限定版本区间的形式,具体可参考 npm help install
等文档自行查阅
上述是关于命令形式的版本指定;通常,我们都会在 package.json 进行
4.3、常用 npm 命令参数
npm 根据命令不同,提供了不同的命令参数;上述 npm 包更新命令的 -g 就是一个典型的参数
4.3.1、关于命令参数位置
以前在网上查找资料时,经常会被各种千奇百怪的命令参数位置搞得摸不着头脑;事实上,以下几种命令的执行结果是一样的
npm install npm@9 -g
npm -g install npm@9
npm install -g npm@9

但作为最佳实践,我们应该统一使用 npm install npm@9 -g
这种形式
4.3.2、全局安装(-g)
添加 -global 或缩写形式 -g 命令参数,可以控制安装全局使用的 npm 包
举例:npm install npm@9 -g
或 npm install npm@9 -global
那么我全局安装的包都跑到哪里去了呢?有时候,我想要删除全局的 node_modules,我到哪去找他?可以通过终端执行 npm config get prefix
查看
默认情况下,全局安装的包
- win 系统下路径是:/User/${username}/APPDATA/Roaming/npm/node_modules/
- mac 系统下路径是:/usr/local/lib/node_modules/

4.3.3、生产依赖(--save)
生产依赖是什么?即构建必须的依赖,少了它,你的项目就无法运行;如 axios、vue、pinia、element-plus 等为程序提供核心功能的依赖
添加 --save 或缩写形式 -S 命令参数,可以控制生产阶段的依赖,即记录在 package.json 的 dependencies 中;该参数可以省略
举例: npm install dayjs
或 npm install dayjs --save
或 npm install dayjs -S
package.json 中将在 dependencies 记录依赖
json
"dependencies": {
"dayjs": "^1.11.10",
}
4.3.4、开发依赖(-D)
开发依赖是什么?我们用来开发或调试时安装的一些依赖包;如用来调试 node.js 的 nodemon、控制代码格式的 eslint、husky、prettier、stylelint 等等等等
添加 --save-dev 或缩写形式 -D 命令参数,可以控制开发阶段的依赖,即记录在 package.json 的 devDependencies 中
举例: npm install nodemon -D
或 npm install nodemon --save-dev
package.json 中将在 devDependencies 记录依赖
json
"devDependencies": {
"nodemon": "^3.0.1",
}
4.3.5、可选依赖(-O)
可选依赖是什么?一般是一些增强插件的依赖,即使依赖找不到或安装报错也不影响项目运行;如增强 console.log 色彩的插件 colors
添加 --save-optional 或缩写形式 -O 命令参数,可以控制可选依赖,即记录在 package.json 的 optionalDependencies 中
举例: npm install dayjs -O
或 npm install dayjs --save-optional
package.json 中将在 optionalDependencies 记录依赖
json
"optionalDependencies": {
"colors": "^1.4.0",
}
4.3.6、精确版本(-E)
添加 --save-exact 或缩写形式 -E 命令参数,可以控制精确版本号
举例: npm install dayjs -E
或 npm install dayjs --save-exact
package.json 中 dependencies 依赖的 ^ 将取消;执行 npm i dayjs
安装依赖时,将不再会更新这个包的新功能、补丁修复
json
"optionalDependencies": {
"dayjs": "1.11.10",
}
4.4、常用 npm 命令
练习 npm 之前,我们需要选择一个包,作为测试。那么问题来了,如何知道有哪些包可以使用呢?
- 明确目的,即确定你需要什么功能的包;比如我想要找一个
时间格式处理
的包 - 搜索有哪些相关的包;直接 GPT(或百度)搜索,
时间格式处理的包哪个比较好用
- 选择一个具体的包,进入 npm 官网搜索具名包,如 dayjs
,点击即可进入详情查看使用文档
这样我们就找到了我们想要包;当然也可以跳过第二步,直接在官网搜索,只是结果会不够精确
4.4.1、npm help
命令格式: npm help <command>
我们不可能记住每个命令的全部用法,当遇到问题或遗忘时,我们就需要查看帮助文档,以npm help install
为例,将打开 npm install 命令的使用文档
这里打开的是你本地 node_modules 下的静态文件,完全不需要考虑官网访问慢的问题
4.4.2、npm init
前面说过,package.json 的作用就是记录项目依赖,其他人使用的时候根据 package.json 的配置从 npm 官网下载,可以手动新建,也可以通过 npm init 创建
npm init
自选配置创建 package.json
npm init -y | npm init -yes
采用默认配置创建,不需要一项一项的选择
npm init -f | npm init -force
注意,npm init -f
与 npm init -y
功能基本一致,都用来跳过交互配置生成 package.json。从语义角度,看起来好像会强制执行初始化操作并覆盖现有文件,实际并不会覆盖

上述截图中,执行 npm init -y
并没有覆盖我设置的信息和安装的依赖
4.4.3、npm install
语法格式: npm install <package>
进行依赖安装,如在项目下执行 npm install dayjs
,将会有以下变动
- node_modules 文件夹下生成 dayjs 依赖
- package.json 会记录 dayjs 最新版本的 dependencies
- package-lock.json 锁定 dayjs 的版本号
以下是 npm install 常用场景:
npm install
安装当前项目下的所有依赖,也是最常用的命令
npm install dayjs
安装某个包的最新依赖,并记录到 package.json 的 dependencies
npm install nodemon -D
安装某个包的最新依赖,并记录到 package.json 的 devDependencies
npm install nrm -g
安装全局依赖
npm install --legacy-peer-deps
用于解决 npm 版本过高导致的依赖冲突问题;会绕过 peerDependency 里依赖的自动安装;它告诉 npm 忽略项目中引入的各个依赖模块之间依赖相同但版本不同的问题
4.4.4、npm update
用以更新依赖,常用在项目升级时,如旧项目进行 webpack 的升级
npm update webpack
将依赖更新到最新
npm install webpack@5
将依赖更新到主版本为 5 的最新版本
4.4.5、npm uninstall
用来卸载依赖,与 install 对象,会移除 node_modules、package.json、package-lock.json 中对应的容易
如项目从 vuex 切换到 pinia 后,就可以 npm uninstall vuex
卸载掉这个依赖了
4.4.6、npm view
语法格式: npm view <package>
用来查看模块的信息,如版本、开源协议、依赖信息等;常用来检查模块依赖于哪些其他模块
npm view vuepress
有时候,我们只安装了一个包,但 node_modules 中却下载许多模块包,这是因为安装的模块又依赖了许多其他模块;想要知道安装模块依赖了哪些模块,此时就可以利用 view 命令

npm view dayjs versions
有时候我们需要切换包的版本,那么我怎么知道有哪些版本的包可以安装呢,可以通过命令 npm view dayjs versions
形式的命令来查看所有版本

4.4.7、npm outdated
检查依赖是否有新版本可用;如果你安装依赖失败,可能就要检查一下是否是你的依赖中存在过时不兼容的;

4.4.8、npm adduser
用来登录 npm 账户,在发布包时需要用到;npm adduser
会打开 npm 的登录/注册页面,交互式完成即可
注意:如果你切换了源地址,那么打开的就是对应源的登录/注册
4.4.9、npm publish
将你的包发布到 npm 官网上,每一个前端,可能都能轻车熟路的在终端执行 npm install
来使用第三方模块包,但对包从哪里来的以及如何发布一个 npm 包却可能并不是很了解;以前的我,非常崇拜那些发布包的大神~
崇拜的一部分原因,来自于无知;直到我自己根据官网指南进行了尝试,才发现这实在是一个非常简单的过程 -.-!
5、npm 依赖发布指南
npm 官网推荐,一个 npm 包至少包含
- package.json 包基础信息
- README.md 说明文档
- index.js 入口文件
本节将从以下几个方面展开:
- 发布
- 迭代
- 使用
- 删除
5.1、第一步:项目初始化
创建项目
在你喜欢的目录下创建一个你喜欢的项目名。比如我,在桌面上创建了 love-npm 的项目文件夹(够喜欢了吧~)
初始化项目结构
即创建官网推荐的文件:package.json、README.md、index.js
通过 npm init -y
创建 package.json 文件,然后修改 name(例如:tiange-npm)等其他信息
项目根目录下新建一个 README.md,随便写点你喜欢的描述,例如:说好的鸡汤呢?还有一碗我没喝上呢...
项目根目录下新建一个 src 文件夹,然后在 src 下创建 index.js,同时修改 package.json 的 main 字段指向入口文件(src/index.js)

至此,你就完成了项目结构的初始化
5.2、第二步:发布 unscoped 包
发布之前,你需要切换你的源为 npm 官方源,同时进行登录 ;即 nrm use npm
切换到官方源,并 npm adduser
交互式登录
接下来,就是见证奇迹的时刻:执行 npm publish
此时你的包就发布成功了,可以前往 npm 官网上搜索(会有延迟,需要稍微等一会)
你也有可能没有上述过程那么美好的见证到奇迹,因为你遇到了错误 上述错误发生在我的 package.json 中 name 修改为 learn-npm 然后发布;
之前说过,公共包的 name 不能重复,也就是说能在 npm 官网上搜索到名字的包,都不能用;哎~好名字都被别人用了,取名字真的好难!
5.3、第三步:发布 scoped 包
作为一个贯彻语义化思维的前端,不能取一个见文生义的名字,让人难受不已,我就想发布包的名字叫 learn-npm
这很简单,你可以发布一个 scoped 包,即 name 以你的用户名为域空间,@ 符号后面的是你注册 npm 账户时的 username,如果不记得可以通过 npm whoami
查询。
perl
"name": "@louzb-bob/learn-npm",
再次执行 npm publist
,不出意外报错了~ 这是因为,npm publish 使用 scoped 发布是需要收费的,如果想要白嫖,可以选择面向公共发布,鼓励开源
以公共形式发布,npm publish --access public
,即完成发布
反骨仔:我要是不呢?偏偏我,就不想开源,怎么办?上面有说过,搭建自己的 npm 私服
5.4、第四步: 版本更新
我们刚发布的 @louzb-bob/learn-npm
里面,并没有什么实质性的内容,现在我想给他加一个处理 url 参数的公共方法,应该如何进行迭代呢?
首页,更新你的内容
src/index.js 添加方法
javascript
module.exports = {
/**
* 获取 url 参数
* @param {*} param 参数名,必填
* @param {*} url 获取地址,可选,默认使用当前页面的地址
*/
getUrlParams: (param, url) => {
let searchParams = url ? new URL(url).searchParams : new URLSearchParams(location.search);
return searchParams.get(param)
}
}
更新版本号并发布
修改 package.json 中的 version 字段,1.0.0 -> 1.0.1
也可以通过命令形式:npm version <major | minor | patch>
,如上述修改,也可以通过 npm version patch
命令实现
然后执行 npm publish --access public
5.5、第五步:使用包
小白:这个我熟~
1、项目里 npm install @louzb-bob/learn-npm
安装依赖后
2、创建 index.js,添加如下内容
javascript
let utils = require('@louzb-bob/learn-npm')
console.log(utils.getUrlParams('test', 'https://edu.51cto.com?id=1&test=123'))
3、终端执行 node index.js
;输出 123,嘿嘿嘿,这下我想在哪里用就在哪里用
5.6、第六步:删除包
为了保持 package 目录的干净,我们会删除一些无用的测试包,那么如何删除包?
- 前往官网,在 package 目录下找到删除
- 在包仓库,通过命令
npm unpublish @louzb-bob/learn-npm -f
强制删除
注意:npm 不鼓励任何形式的包删除,因为发布的包可能被其他人引用,如果我们删除了此包,其他人在重新安装含有我们包的依赖的工程时,出现找不到包问题
这也是为什么绝大多数的公司都会搭建自己的私有 npm 仓库,定期备份第三方模块,即使发布者删除了,依然不受影响
6、package.json 攻略
每个项目的根目录下面,一般都有一个 package.json 文件,定义项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。npm install 命令根据这个配置文件,自动下载所需的模块到本地 node_modules,从而让团队成员协同开发。
6.1、元信息
- name:项目名称;如果发布到 npm 上,其他人可以通过该名称安装;如 npm install dayjs
- version:版本号,遵循上述所说的
主版本号.次版本号.修订版本号
的规则;npm 版本号的机制非常重要,它允许用户根据自己的需要下载相应的版本(可以想象一下焦头烂额网上找 sdk 的场景!) - description:项目功能说明,用来辅助搜索
- keywords: 项目的技术关键词,用来辅助搜索
- author:项目作者,如果是团队开发的项目,记录为团队名
- license:开源许可证,默认为 ISC(与 BSD 许可功能相同),具体可参考开源许可证
- bugs:反馈 bug 的地址,一般对应项目源码仓库的 issue
- homepage: 文档首页地址
- contributors: 项目贡献者列表;每一个贡献者包含 name、email、url 信息
关于name、description、keywords
当我们在 npm 官网搜索一个包时,以搜索 dayjs
为例;会依次按 name -> keywords -> description 进行匹配;(类似 seo 中的 TDK)

必填项 name、version
name 和 version 是 npm 包管理机制的核心,一个用来区分不同的包(即不能发布一个同名的公共包);另一个则用来控制版本;属于必填字段
6.2、必知必会
6.2.1、scripts
脚本功能是 npm 最强大、最常用的功能之一;npm 允许在 package.json 中定义脚本命令,然后通过 npm run 执行
json
"scripts": {
"reinstall": "rimraf node_modules && npm install"
},
例如,我们在 package.json 中添加上述配置,此时 npm run test
几乎等价于 rimraf node_modules && npm install
为什么说几乎?因为通过脚本运行比直接运行更加强大,它可以拥有以下几个强大功能
6.2.1.1、执行多个脚本
npm 脚本可以一次执行多条,遵循以下规则
- & 并行执行,同时执行所有脚本
- && 串行执行,前一个脚本执行成功,才执行下一个脚本;失败则结束
- || 竞争执行,任意一个脚本执行成功,其他脚本都将不再执行
如上述 reinstall 脚本(需先全局安装 rimraf 工具 npm install rimraf -g
),将先删除 node_modules,删除成功后重新安装(一般用来依赖安装错误时重新安装)
6.2.1.2、参数
npm 脚本支持参数传递,最典型的应用就是不同环境传入不同的 mode,从而进行不同的逻辑处理
javascript
// package.json
"scripts": {
"reinstall": "rimraf node_modules && npm install",
"start": "node src/test/index.js",
"dev": "npm run start --mode=dev",
"test": "npm run start --mode=test"
},
// src/test/index.js
let utils = require('../index')
let test = utils.getUrlParams('test', 'https://edu.51cto.com?id=1&test=123');
let mode = process.env.npm_config_mode;
console.log(mode);
if (mode === 'test') {
// 处理测试环境逻辑
}
分别执行 npm run dev
和 npm run test
;控制台分别输出 dev、test

总结:
- 我们可以通过
npm run xx --name=value
的形式给脚本添加局部参数(等号两边不能有空格) - 局部参数需要通过
npm_config_xx
访问,如上述形式 js 中通过process.env.npm_config_name
的形式读取参数的 value - 不指定 value 时默认为 true;即
--name
js 读取到的 name 值时 true - 参数必须作用于脚本;如上述脚本,在将 start 脚本调整为
node src/test/index.js --mode=dev
并不能设置成功,因为 node src/test/index.js 不是一个 scripts 中的脚本 - 也可以通过 set 设置脚本全局参数,
set PORT=3003 && npm run start --mode=dev
,此时process.env.PORT
为 3003
6.2.1.3、编码时获取 package.json 中的配置
npm 脚本有一个非常强大的功能,就是可以使用 npm 的内部变量。

以上内容是使用 npm run start
打印的 process.env
,这些内容只在使用使用脚本启动时才有。如果你是通过 node src/test/index.js
命令启动,则不具有
这也就意味,通过脚本启动,可以在编码时使用 npm 对象的变量,如获取包名
arduino
console.log(process.env.npm_package_name)
6.2.2、config
管理脚本参数,除了以 --name 形式在脚本中添加参数,还可以使用 config 来添加
json
"config" : {
"port" : "8080"
},
package.json 配置上述 config 后,通过脚本启动(必须)后,js 中就可以通过process.env.npm_package_config_port
获取
6.2.3、dependencies 和 devDependencies
前面详细介绍过生产依赖 dependencies 和开发依赖 devDependencies
曾经的曾经,我一直严格遵循以下
- 调试(nodemon 等),打包(webpack 等),编译(TS 等),代码规范(eslint 等)放在开发依赖
- 程序编程使用的组件库,工具类(axios、vue、dayjs 等)放在生产依赖
因为我一直错误的认为,生产依赖会被打包,而开发依赖不会被打包进去,直到有一天,我不小心将一个工具包安装到 devDependencies 中,然后打包的产物仍然运行的飞起~~~
卧槽?难道这个开发依赖和生产依赖的位置并没有区别?只是推荐吗?
1、普通项目
作为普通项目使用,确实没有本质区别;无论是生产依赖 dependencies 还是开发依赖 devDependencies,都会被安装到 node_modules 文件下。
也就是说,对于项目而言,npm install 会自动下载 devDependencies 和 dependencies 下面的模块,无论你安装的开发依赖还是生产依赖,都最终会被打包进构建产物中,本质上没有分别。
但作为最佳实践。我们依然推荐,所有影响程序运行的依赖,安装到生产依赖 dependencies 中;所有不影响程序运行的辅助依赖,安装到开发依赖 devDependencies
2、本质区别
但在发布 npm 包的时候,dependencies 和 devDependencies 就会有非常大的不同
// tiange-npm 包
json
"dependencies": {
"dayjs": "^1.11.10"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
以之前发布的测试包为例,package.json 配置了不同的开发依赖和生产依赖
我们在 test 项目下重新安装 tiange-npm 这个包
- 删除 node_modules,清除本地缓存
- 清空 package.json 中 dependencies、devDependencies 内容
- 执行
npm install tiange-npm

查看 node_modules 内容,会发现,tiange-npm 这个包的 dependencies 的 dayjs 也被安装了,而作为 devDependencies 的 nodemon 则并没有安装
这就是 dependencies 和 devDependencies 的本质区别:在发布 npm 包的时候,本身 dependencies 下的模块会作为依赖,一起被下载;devDependencies 下面的模块就不会自动下载了。
这同时也就解释了为什么我只安装了一个包(如 nodemon),node_modules 下却增加了几十个模块,因为你还会下载 nodemon 包的所有 dependencies
6.2.4、模块类型
Node.js 默认遵循 CommonJS 规范,即通过 require、module.exports 导入导出模块
那我如果想要使用 ES6 模块,通过 import、export 导入导出模块,要怎么办呢?
- 通过文件名后缀,以
.mjs
告知 Node - 每个文件名后缀修改很麻烦的说?package.json 设置
"type": "module"
,然后就可以快乐的使用了
6.2.5、files 控制打包内容
在发布 npm 包时,我不希望一些测试的内容也被发布;则可以通过 files 来配置发布哪些内容,从而减小包的体积和安装时间
哪些文件应该被发布?npm 官网推荐
package.json
README
LICENSE
/LICENCE
mian
和bin
中使用的文件地址
npm 默认不会发布如 node_modules、.git 等文件,具体可参考 files 配置
一般情况下,我们只需要发布构建产物 dist,而不需要发布 src 等内容
json
"files": [ "dist/", "src/index.js", "*.md" ],
除了在 package.json 中配置 files,还可以在根目录下创建 .npmignore
文件来忽略不发布的内容(语法类似 .gitignore);且优先级比 files 配置更高(写在这个文件里边的文件即便被写在 files 属性里边也会被排除在外)
入口文件
javascript
// tiange-npm 包的 package.json 配置入口
"main": "src/index.js",
// test 项目使用
let utils = require('tiange-npm')
console.log(utils.getUrlParams('test', 'https://edu.51cto.com?id=1&test=123'))
我们直接通过 require('moduleName') 就获取到 src/index.js
暴露的模块;我们可以理解为 main 设置的就是模块的真实路径,也就是我们所说的入口
上述代码中,我们可以通过 let utils = require('tiange-npm/src/index')
同样可以拿到模块,也就是说,没有配置 main 入口的文件,我们也可以访问到模块内容
不同形式入口
有时候,我们封装的包,希望不局限以 CommonJS 模块使用,package.json 提供了以下不同的入口文件配置选项
- main: CommonJS 模块程序下入口文件,如
"main": "./index.js"
- module: ES 模块程序下入口文件,如
"module": "./index.mjs"
- browser:浏览器下入口文件,如
"module": "./index.mjs"
,这样 Webpack、Browserify 等打包工具,通过它就知道该打包哪个文件
多入口
exports 提供了 main 的现代替代方案(Node.js 版本 >10 时),允许定义多个入口点、环境之间的条件入口解析支持,以及 防止除 exports 中定义的入口点之外的任何其他入口点。
javascript
// package.json
"exports": {
".": {
"require": "./src/index.js",
"import": "./src/index.mjs"
},
"./log.js": "./src/log.js"
},
// src 下增加 log.js
module.exports = {
log: (str) => {
console.log(str)
}
}
// src 下增加 err.js
module.exports = {
err: (str) => {
console.log(str)
}
}
我们发布包以上述 exports 配置重新发布 tiange-npm,在使用项目 test 中重新安装 tiange-npm 包

不难发现,直接 require 达到 main 配置入口一致的效果(同理,import 则会解析 src/index.mjs);而且我们还可以快速访问到封装好的子模块 log
引入 exports 字段的现有包将阻止包的使用者使用任何未定义的入口点,如上述包中的 err.js 并没有在 exports 中暴露,此时 require('tiange-npm/err.js')
将引起报错

6.3、查漏补缺
6.4、package.lock.json
package-lock.json 是 npm 5 版本开始引入的一个文件,用于记录当前安装的每个包的确切版本号以及其依赖关系的锁定状态。
为什么需要 package-lock.json ?我们通过 package.json 来管理项目的依赖,默认情况下,都是锁定主版本,如以下形式
json
"dependencies": {
"tiange-npm": "^1.0.8"
}
前面说过,^1.. 会安装主版本号为 1 的最新依赖,如果长时间不执行 npm install,不同团队成员将使用不同版本的依赖,package-lock.json 就是为了解决此类问题
- 锁定依赖关系:确保每次安装相同的软件包时都会得到相同的版本。这是非常重要的,因为不同版本之间可能存在兼容性问题。
- 更快速的安装:当 package-lock.json 文件存在时,npm 会使用其中记录的下载地址直接从缓存中获取软件包,而无需重新下载。
- 精确控制:package-lock.json 文件中包含了软件包所需的所有子依赖项及其版本号,可以确保项目在复制或部署到其他环境时能够精确。
注意,package-lock.json 文件应该被纳入源代码管理工具(如Git)进行版本控制,从而确保团队成员、构建服务器或其他人在构建和部署项目时都使用相同的软件包版本。
7、结束语
历时将近一周,终于完成了这篇笔记的整理,整理笔记的过程也是再次强化大脑的记忆,当然,学习的初衷,永远不是记录;成长也不是许愿树,种下希望就会有收;此行虽难,亦不敢轻言放弃放弃,当砥砺奋进而已!
最后一碗鸡汤奉上,诸君尚能饮否~撒花