npm 细节与原理探究,顺便发布个自己的包

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 在背后到底做了哪些事:

  1. 首先,根据项目中的 package.json 文件的 dependencies 和 devDependencies 属性 ,确定需要安装哪些包;
  2. 然后,查找项目中是否有 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 还有很多其它的命令,可以参见官方文档

相关推荐
小白学习日记37 分钟前
【复习】HTML常用标签<table>
前端·html
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081352 小时前
前端之路-了解原型和原型链
前端
永远不打烊2 小时前
librtmp 原生API做直播推流
前端
北极小狐2 小时前
浏览器事件处理机制:从硬件中断到事件驱动
前端
无咎.lsy2 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec2 小时前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron