npm 全称为 Node Package Manager,顾名思义是一个 NodeJS 包管理和分发工具,但是现在也广泛用于发布一些非 node 的包,比如 vue、react、axios 等。相信大家对 npm 哪怕不知其所以然,但是也没少用 npm install
或简写 npm i
安装过各种玩意,本文就稍稍深入点说说关于 npm 的那些事儿。
package.json
想要通过 npm 安装东西前必须得先有 package.json文件,用于记录项目中安装了哪些依赖,以及它们的版本等信息。生成 package.json 可以通过在命令行执行 npm init
然后一项项配置,或是 npm init -y
,一键生成,再或者通过一些诸如 vite 等工具,在创建项目时自动生成。
属性介绍
package.json 中有很多属性,其中有 2 个是必填的,name 和 version,记录项目的名称和版本。剩下的都是些选填的,不是每个项目里的 package.json 都有的属性,下面介绍几个常见的:
- private
记录当前项目是否是私有的,如果值为 true
,则不能通过 npm publish
发布。防止私有项目一不小心被发布到 npm 存储这些包的 registry 去,让其他人看到了小秘密。
- main
程序的入口文件,比如 axios 的 'main' 的值就是 'index.js' :
index.js 里就一句话,导出 ./lib/axios 向外暴露的对象,其实就是 axios 对象:
javascript
// index.js
module.exports = require('./lib/axios');
// ./lib/axios
// ...省略
module.exports = axios;
我们在项目中导入 axios 使用时,就能通过 main 去找到 index.js 然后最终得到 ./lib/axios 暴露出的 axios 对象了。
- script
配置一些脚本命令,都是以键值对的形式配置的,如下图:
然后可以通过 npm run 键名
来运行键值里的真正的运行任务命令。
我们可以自己写一个来举例,比如在 package.json 的同级目录 js 中有个 main.js,里面就是一句简单的打印:
javascript
// js\main.js
console.log('hello juejin!')
然后我们在 package.json 的 scripts 里添加键名为 juejin 的脚本命令,接着就是通过 node 去执行 main.js:
json
// package.json
{
"scripts": {
"juejin": "node js/main.js"
}
}
这样当我们在命令行执行 npm run juejin
时,就会执行打印了:
对于常用的命令,如 start、test、stop 等可以省略 run,即直接 npm start
,等效于 npm run start
。
- dependencies
记录项目在生产环境和开发环境都需要使用的包。比如我们在项目中通过 npm i axios
安装了 axios,那么就会在 dependencies 添加一条记录:
json
// package.json
{
"dependencies": {
"axios": "^0.27.2"
}
}
安装的 axios 会放在项目的 node_modules 目录中,虽然我们只是下载了个 axios,但当我们打开 node_modules 目录,会发现装了一堆东西:
这是因为 axios 本身,也有 2 个依赖(dependencies)的包:
其中 form-data 在 dependencies 中又注明了依赖其它 3 个包,依次类推,所以我们虽然只想安装个 axios,实际上却安装了 8 个包。
请注意,axios 或 form-data 等在开发环境依赖的包,也就是 devDependencies 里注明的包并不会一起被下载。
- devDependencies
记录项目在开发环境需要使用的包,比如 webpack 等工具,安装时添加后缀 --save-dev
或简写-D
:
shell
npm i webpack -D
在实际开发中,有时候并不会太严格地区分某个包到底是属于 dependencies 还是 devDependencies。比如你在安装某个仅仅是开发时的依赖时忘了加上 -D
,把它作为了生产环境下也需要依赖的包,在项目打包时也不会真的把它的代码打包进去,因为打包时 webpack 会生成一张依赖图,只有依赖图中包含的模块才会被打包。
- peerDependencies
如果你的项目中安装了 element-ui,那么我们在 node_modules\element-ui\package.json 里可以看到如下图所示属性:
因为 element-ui 是基于 vue 的,而在 element-ui 的 package.json 中,vue 是写在 devDependencies 中的,所以安装 element-ui 时不会自动安装 vue。在 peerDependencies 添加配置,就可以在发现用户项目中没有安装 vue 时提醒用户:
- engines
一般用于指定项目所需要的 node 和 npm 的最低版本号,比如下图就是截取自使用 vue-cli 创建的 vue2 项目:
- browserslist
用来配置项目打包后 js 浏览器或 node 版本的兼容情况的:
json
// package.json
{
"browserslist": [
"> 1%",
"last 2 versions",
"maintained node versions"
],
}
配置后类似于 Babel、Autoprefixer 等工具就会对此进行读取。实际上也可以不写在 package.json 中而是单独弄个 .browserslistrc 文件,Babel 等工具同样可以读取到,至于读取的优先级视各个工具而定:
xml
# .browserslistrc
> 1%
last 2 versions
maintained node versions # 所有还被 node 基金维护的 node 版本
包版本规范
前面我们也看到了,当安装完 axios 时,package.json 的 dependencies 里添加了一条键值对:"axios": "^0.27.2"
,键名就是包的名称,键值为包的版本号。npm 的包的版本号通常都遵从 semver 版本规范(semver 由 Semantic Versioning 两个词组成,也就是语义化版本控制),版本格式为:X.Y.Z
,版本号递增规则如下:
- X(主版本号,MAJOR):当你做了不兼容的 API 修改(可能不兼容之前的版本);
- Y(次版本号,MINOR ):当你做了向下兼容的功能性新增(有新功能,但是兼容之前的版本);
- Z(修订号,PATCH ):当你做了向下兼容的问题修正(没有新功能,只是对之前版本 bug 的修复)。
在 axios 的版本号前面还有个 ^
符号,意思是当我们直接 npm i
的时候,在没有 package-lock.json 文件的情况下,会去安装相同主版本号下,不小于指定次版本号和修订号的版本号,也就是 X 不变,Y 和 Z 不会小于 dependencies 里指定的。
还有个符号是 ~
,代表主版本号和次版本号不变,安装不小于指定修订号的版本,即 X、Y 不变,Z 不小于 dependencies 里的指定。
还有些不怎么常见的符号,比如>
、<
、>=
,<=
等,此处不再一一举例。 类似于 ^
符号的使用有时候会导致一些问题,比如我在项目中安装的 axios 是 0.27.2 的版本,但是过了阵子某个同事需要参与项目,他从仓库克隆项目到本地后执行 npm i
,安装的实际上是 0.28.0 的版本,就有可能会因为版本不同导致我这用得好好的功能在他那会报错。package-lock.json 文件的作用,就是避免出现这类问题。
npm install(npm i)
全局安装与局部安装
全局
当我们使用 npm i 包名
安装某个包时,如果在后面添加个 -g
即代表是全局安装,一般一些工具属性的包,比如用于切换 npm 源的 nrm,就可以全局安装:npm i nrm -g
,全局安装完后可以在任何路径下通过命令行工具执行相关命令,比如可以 nrm -V
查看版本。全局安装的位置可以通过下面的命令查看:
powershell
npm config get prefix
局部
不加 -g
则是局部安装,即仅仅在当前项目中安装,比如 npm i axios
。局部安装的都是在项目中需要使用的包,比如 webpack,虽然你可以把它看成是工具全局安装,但考虑到版本问题,一般我们都会在项目中安装一次 webpack。局部安装的文件放在项目中的 node_modules 目录下。
用 npx 执行局部命令
全局安装时,还会将包的可执行文件所在的路径添加到环境变量的 path 中,所以我们才可以在任何路径下执行包的相关命令。比如全局安装了版本为 5.33.2 的 webpack,就可以在任意位置执行 webpack -v
查看版本。而局部安装的包,比如我们在项目中也装了 webpack,不过版本为 3.5.6,我们进入项目目录,也执行 webpack -v
,此时得到的版本依旧是 5.33.2(但如果是新版本的 webpack,在项目目录下直接执行 webpack -v
得到的就是项目中安装的版本,我也不懂为何,还望路过的大佬指点迷津),因为执行命令的时候是去环境变量查找对应的可执行文件的路径的,找到的那个 webpack 的执行文件是全局的:
如果我们想查看项目中的 webpack 的版本,就需要来到项目中的 node_modules.bin 目录下,再执行 webpack -v
,执行的才会是项目中安装的 webpack 的可执行文件,才能得到 3.5.6 :
如果你觉得麻烦,那也可以在项目的 package.json 的 script 属性里添加脚本命令:
json
{
"scripts": {
"webpack": "webpack -v"
}
}
这样在项目中通过命令行运行 npm run webpack
,npm 会先去项目的 node_modules.bin 目录下找到 webpack 执行文件执行,得到的也会是 3.5.6。 如果你还是觉得麻烦,就可以在项目中直接使用 npm5.2 之后自带的 npx 命令,执行:
powershell
npx webpack -v
npx 就会直接调用项目中 node_modules.bin 目录下的 webpack 执行文件执行,而不需要配置 script。
原理探究
下面来看看当我们在项目中直接执行 npm i
安装各个依赖包时,npm 在背后到底做了哪些事:
- 首先,根据项目中的 package.json 文件的 dependencies 和 devDependencies 属性 ,确定需要安装哪些包;
- 然后,查找项目中是否有 package-lock.json 文件。因为在 package.json 中记录的包的版本号一般都是以
^
开头的,与真实安装的版本不一定一样。而 package-lock.json 文件中则会记录各个包实际安装的准确版本(version),这样 npm 就可以根据 package-lock.json 的记录去安装准确版本的包:
如果项目中没有 package-lock.json 文件,则会构建需要下载的包构建依赖关系。比如我们要安装 webpack,而 webpack 本身在运行时又依赖一些包,我们可以从安装过 webpack 的项目的 package-lock.json 中看到,其运行需要(requires)的包还是很多的:
如果你去查看 node_modules 下的 webpack 里的 package.json,会发现上图中 requires 的这些包都是 dependencies 属性里记录的那些。而这些包要正常运行又可能依赖于其它一些包,npm 就会将它们通通从 registry 仓库下载下来,比如图 1 中 resolved 的值就是下载地址。下载的都是 .tgz 为后缀名的压缩文件,然后解压放到项目中的 node_modules 目录下 ,并且会添加到缓存文件中。另外还会生成 package-lock.json 文件,记录好真实安装各个包的版本。
如果项目中有 package-lock.json 文件,则会先看看要安装的包的版本是否符合 package-lock.json 的记录,并且不与 package.json 中的记录冲突,是就去看看缓存文件中是否有对应的压缩文件,有就进行解压后放到项目的 node_modules 目录下,如果没有则重新下载。如果符合 package.json 的记录(比如是 ^1.0.0),但是 registry 仓库的包更新了(比如更新到了1.1.0),与 package-lock.json 记录的不一样,那么一般会重新构建包的依赖关系再去 registry 仓库进行下载、添加缓存和解压这些步骤,并更新 package-lock.json 文件。如果想完全按照 package-lock.json 里记录的版本来安装依赖,可以使用 npm-ci
。
缓存
前面提到的缓存策略,是从 npm5 开始支持的。我们可以在命令行输入:
powershell
npm get cache
来查看 npm 缓存存放的位置,比如在我的电脑上查询的结果如下图(需要允许查看隐藏的项目):
当我来到上图指向的目录后,再点击进入 _cacache 目录,
里面的内容如下:
其中,tmp 里为临时文件,content 目录下存放的就是我们从 registry 仓库下载下来的压缩文件,index 目录下存放的是索引文件。通过 package-lock.json 文件中各个包的记录里的 integrity 属性的值(通过算法得到,比如图 1 这个包的 integrity 的值就为 'sha1-I7CNdA6D9JxeWZRfvxtD6Au/Tts='),找到的就是 index 目录下的索引文件,里面记录的也是些配置信息:
其中 _shasum 属性记录的值,才是指向 content 目录下的压缩文件:
对其进行解压后添加到我们项目中的 node_modules 目录下。
更换仓库地址为国内镜像源
想查看 registry 仓库的地址我们可以通过在命令行输入:
powershell
npm config get registry
默认是 https://registry.npmjs.org
。但因为它是个外网,有时候该网址的下载速度可能会很慢,所以我们可以改变这个 registry 地址,将它改为服务器位于国内的镜像:
powershell
npm config set registry https://registry.npmmirror.com
这样通过 npm 去下载包的时候速度就会比较快了。
发布自己的包
发布
之前总是用别人的包,有时候我们自己可能也有些项目或封装的组件需要发布到 registry 仓库去,其实很简单,只需要分 3 步:
1. 创建一个项目
比如我基于 element-ui 封装了一个可以用于批量搜索的组件,命名为 QyElementUiBatchSearch,那么就可以创建了一个名为 qy-element-ui-batch-search 的项目,package.json 内容如下:
json
{
"name": "qy-element-ui-batch-search",
"version": "1.0.0",
"description": "基于element-ui封装的批量搜索输入框组件",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/chaimHL/qy-element-ui-batch-search"
},
"keywords": [
"element-ui",
"批量搜索",
"输入框"
],
"author": "Chaim",
"license": "ISC"
}
下面对几个属性做下解释:
main
就是项目的主入口文件,别人下载安装我们的包后,使用时能够导入的就是 index.js 暴露的变量,index.js 里的内容如下,我是以插件的形式让我封装的组件成为全局组件:
javascript
// index.js
import QyElementUiBatchSearch from './src/index.vue'
const install = Vue => {
Vue.component('QyElementUiBatchSearch', QyElementUiBatchSearch)
}
export default {
install
}
src/index.vue 就是我自己封装的批量搜索组件,写法与普通项目中封装的组件一致,感兴趣的可以去 npm 官网或下载安装到本地查看。
repository
为我们项目的仓库地址,发布成功后这个信息会出现在 npm 官网中我们这个包的页面上。keywords
是为了方便他人在 npm 官网上搜索我们的包的关键词。
2. 登录 npm
注意登录前确保仓库地址是指向的 https://registry.npmjs.org
,如果之前改为了国内镜像,可以通过下列命令切换回官方地址:
powershell
npm config set registry https://registry.npmjs.org
如果没有账号,可以先去官网进行注册,之后在项目中使用命令行执行:
powershell
npm login
随后按提示输入登录信息,示意图如下:
当出现上图红框中所示内容即代表登录成功。
3. 发布
登录成功后执行:
powershell
npm publish
即可发布我们的项目到 npm 的 registry 仓库,发布成功后显示如下信息:
现在我们就可以在 npm 官网搜索到我们发布的包了:
使用
现在我们来试试能不能使用我们发布的包。 在另一个项目中先安装我们的包: npm i qy-element-ui-batch-search
。 然后在使用了 element-ui 的 vue2 项目中,在 src\main.js 引入并使用 Vue.use()
来全局安装组件:
javascript
import QyElementUiBatchSearch from 'qy-element-ui-batch-search'
Vue.use(QyElementUiBatchSearch)
在 .vue 文件中,即可直接使用:
vue
<el-form-item label="运单号">
<QyElementUiBatchSearch
ref="qyElementUiBatchSearchRef"
@change-input="onChangeInput('trackingNumberList', $event)"
@clear-input="onClearInput('trackingNumberList')"
/>
</el-form-item>
更新
如果想更新我们发布的包,比如添加了 README.md 文件,那么就需要先将 package.json 中的 version
(版本号)更改,然后再次 npm publish
即可。之后再去使用了我们的包的项目下再次执行 npm i qy-element-ui-batch-search
重新安装,就会安装更新后的包了。
删除和过期
删除发布的包:npm unpublish
; 让发布的包过期: npm deprecate
。
npm 其它命令(One More Thing)
除了 init、 install 和 uninstall 这些比较常见的,npm 还有很多其它的命令,可以参见官方文档。