前言
npm对大家来说已经使用过很多次了,但是npm的原理有仔细深究吗?我相信大部分人都仅仅停留在能用的阶段,那么就有我来阐述一下什么是npm,它的原理又是什么,且听我慢慢道来!
开始学习之前必须安装nodejs!
什么是npm?
npm全称为node package manager,是nodejs的包管理器,是一个基于命令行的工具,用于管理项目的依赖项.类似于JAVA里的Maven.
npm常用命令
// 初始化,会创建一个package.json文件和package-lock.json文件
npm init
# 安装包
npm install
# 从npm仓库下载并安装指定的包,如果指定了版本号,则安装特定版本。
npm install <package-name>
# 全局安装指定的包,可以在命令行中直接使用。
npm install -g <package-name>
# 搜索包
npm search <package-name>
# 发布包
npm publish <package-directory>
# 清理未使用的包
npm prune
# 设置npm镜像
npm config set registry <registry-url>
# 获取npm配置信息
npm config list
依赖分类
1.开发依赖:devDependencies
devDependencies用于指定在开发环境中所需的依赖包及其版本,以确保开发环境的稳定性和一致性。
devDependencies是软件开发中的一种依赖关系管理机制。它用于指定在开发环境中所需的依赖包及其版本,以确保开发环境的稳定性和一致性。
在Node.js的npm包管理系统中,devDependencies可以在package.json文件中进行声明。例如,如果一个项目需要使用某个开发工具或库,但这个工具或库只在开发环境中使用,可以在package.json文件中添加以下内容:
"devDependencies": {
"dev-dependency-name": "^1.0.0"
}
这表示该项目的开发依赖包需要另一个名为"dev-dependency-name"的包,版本为1.0.0或更高版本。当项目进行开发时,npm将自动安装所需的devDependencies。
使用devDependencies的好处包括:
- 减少在安装依赖时node_modules的体积,提升安装依赖的速度,节省线上及其的硬盘资源以及部署上线的时间。
- 开发依赖和生产依赖分离,使得项目结构更加清晰,职责更加明确。
注意:devDependencies只在开发环境中安装,不会在生产环境中安装。因此,如果需要在生产环境中使用某个依赖包,需要将其添加到dependencies中。
2.生产环境所需依赖:dependencies
dependencies是指在生产环境中项目运行所需的依赖包。这些依赖项包括在应用程序运行时所需的库、框架和其他软件包。
"dependencies": {
"dependency-name": "^1.0.0"
}
这表示该项目的生产环境依赖包需要另一个名为"dependency-name"的包,版本为1.0.0或更高版本。当项目进行生产环境构建或部署时,npm将自动安装所需的dependencies。
使用dependencies的好处包括:
- 确保项目在生产环境中具备所有必需的依赖项,从而保证项目的稳定性和可移植性。
- 通过版本控制和依赖管理工具,可以有效地解决不同依赖项之间的冲突和兼容性问题。
注意:dependencies依赖的包不仅开发环境能使用,生产环境也能使用。因此,在开发过程中,如果需要使用某个依赖包进行测试或开发,但这个包只在生产环境中使用,应该将其添加到devDependencies中。
3.插件运行所需依赖:peerDependencies
插件通常需要一些特定的依赖项才能正常工作。这些依赖项被称为"peerDependencies",这些依赖项对于插件的运行是必要的,但它们不是由插件本身提供的,而是由使用插件的应用程序提供的。
当一个应用程序安装一个具有peerDependencies的插件时,这些依赖项也会被自动安装。这是因为插件本身没有提供这些依赖项,而是期望应用程序已经安装了这些依赖项。
在Node.js的npm包管理系统中,插件的peerDependencies可以在package.json文件中进行声明。例如,如果一个插件需要另一个插件作为其peerDependency,可以在package.json文件中添加以下内容:
"peerDependencies": {
"dependency-name": "^1.0.0"
}
这表示该插件需要另一个名为"dependency-name"的插件的版本1.0.0或更高版本。当应用程序安装该插件时,npm将自动安装所需的依赖项。
注意:如果应用程序没有提供所需的peerDependencies,插件可能无法正常工作。因此,在使用插件时,确保应用程序已经安装了所需的依赖项是非常重要的。
npm install原理
在执行npm install时发生了什么事?
在执行npm install时,npm会读取项目中的package.json文件,然后根据其中声明的依赖关系去下载对应的依赖包。如果依赖关系中有依赖包的版本号,则下载对应版本的依赖包。下载完成后,会将这些依赖包以扁平化的方式以及有着一定的排序规则放入node_modules目录中,供项目使用。npm还会根据依赖关系将依赖包进行关联和依赖计算,确保项目中使用的所有依赖包都是与项目中其他依赖包兼容的。
依赖树扁平化
依赖树扁平化是怎么做到的呢?
通过使用广度优先算法实现!但是扁平化并非真正地扁平化,很难做到完全扁平化,因为一个项目中的依赖可能存在不同的版本号,而且不能丢弃任何一个版本的依赖,所以不能完全扁平化!
若依赖能全部扁平化就会在项目的node_modules里同一级按照abcd一定的规则依次排列(如上图);当依赖不能全部扁平化即遇到同一个包名的不同版本依赖时,不会做扁平化处理,而是哪个依赖需要它就在哪个依赖下添加一个node_modules,并将这个不同版本依赖放进去.这就不可避免的导致了模块冗余.如下图所示.
package-lock.json
package-lock.json文件是npm 5及以上版本引入的新特性,它用于锁定当前项目的依赖关系,防止因为依赖更新导致项目出现问题。当我们执行npm install时,npm会根据package-lock.json文件中的信息来安装依赖(如果package-lock.json和package.json的依赖包信息不一致,优先以package.json为准),保证所有依赖的版本都是一致的。如果我们手动修改了package.json文件中的依赖信息,那么npm install时会自动更新package-lock.json文件中的依赖信息。
package-lock.json文件中包含了当前项目的所有依赖及其版本号,以及依赖之间的关系,还包括了具体的安装路径和安装文件的哈希值等信息。这些信息可以帮助npm快速安装依赖,并且保证了所有开发者在安装依赖时得到的都是一致的结果。
依赖包缓存原理
version,integrity和包名通过一个算法生成出一个key,这个key有大用!
index-v5文件存的就是content-v2的索引的位置,如果key能和index-v5文件的位置对上就会去content-v2文件里下载对应的缓存二级制文件,也就是一个依赖包,这就是依赖包缓存原理,有了这个缓存机制,依赖包就能实现快速安装!
npm run原理
npm run命令是用来运行package.json文件中scripts字段下的脚本的。它会在当前项目的根目录下查找package.json文件,读取其中的scripts字段,然后根据用户输入的参数来执行对应的脚本。
比如,如果package.json文件中有一个名为"start"的脚本,我们可以使用"npm run start"来运行它。这个命令会执行"start"脚本中定义的命令。
在执行脚本时,npm会创建一个新的Shell,在其中执行定义的命令。这个Shell的环境变量会从当前Shell中继承而来,但是npm会添加一些额外的变量,比如NODE_ENV和npm_package_XXX等。
另外,我们也可以在运行脚本时传递参数,这些参数会被传递给Shell中的命令。例如,"npm run build -- --watch"会将"--watch"作为参数传递给"build"脚本中定义的命令。
举个例子:
项目初始化后运行npm i vite获取到package.json文件,我们在终端运行npm run dev
// package.json
{
"name": "npm-demo",
"version": "1.0.0",
"description": "npm test",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "vite"
},
"repository": {
"type": "git",
"url": "test"
},
"keywords": [
"test"
],
"author": "lyfashions",
"license": "ISC",
"dependencies": {
"vite": "^4.4.9"
}
}
服务就启动了!
为什么npm run dev就能启动这个服务呢?
首先我们想一下这个命令(vite)是从哪来的?注意一下,项目的所有可执行命令都存放在了node_modules下面的.bin目录里,比如下图中的vite文件是给Linux,MacOS去使用,而Windows有两套第一个是vite.cmd(CMD使用),第二个为vite.ps1(Powershell使用),从而实现了vite的跨平台使用.但是这个命令为什么会放在这里呢(放在node_modules下面的.bin目录里)?
原来啊,当我们运行 npm i vite 时会把vite的包下载下来放在项目的node_modules下面,然后nodejs去读取了vite包里的package.json文件(该文件如下图所示),该文件配置了bin,所以就会往项目的node_modules下面的.bin目录里添加上述三个文件,从而使得在项目中可以使用到vite命令.
那么npm run dev 的运行机制是怎么样的呢,它有着一套自己的查找规则.
npm run dev 实际上执行的是项目的package.json文件里配置的scripts,也就相当于执行npm run vite.
1.首先去当前项目的node_modules下面的.bin目录里查找vite,
2.如果没有就会继续去全局的node_modules里查找,
3.如果还是没有,就会去环境变量里面查找,
4,最后实在是没有就会报错了!
// 全局安装package
npm install -g ts-node
// 当前项目里安装package
npm install vite
举个例子,比如我在终端中运行ts-node -v,打印 v10.7.0.那么nodejs就会先去当前项目的node_modules下面的.bin目录里查找ts-node,发现没有则去全局的node_modules里查找,最终在全局中找到了这个ts-node命令(查看全局node_modules可以运行npm config list).
prefix就是我们全局node_modules的存放位置,如图所示.这些就是我全局安装的package.
再追问一下,那为什么全局的命令可以执行呢?因为我们配过环境变量,会把这些命令注册到环境变量里面去!
npm生命周期
npm脚本的运行过程中有一些生命周期事件,可以用来在特定的时刻执行一些自定义的操作,以下是npm生命周期事件的列表:
- preinstall 和 postinstall - 在包安装之前和之后运行
- preuninstall 和 postuninstall - 在包卸载之前和之后运行
- preversion 和 postversion - 在执行npm version命令之前和之后运行
- pretest 和 posttest - 在运行npm test命令之前和之后运行
- prestart 和 poststart - 在运行npm start命令之前和之后运行
- prerestart 和 postrestart - 在运行npm restart命令之前和之后运行
npm的生命周期简而言之就是命令执行前,执行时,执行后三阶段!
通过在package.json文件中的scripts字段中定义这些事件,我们可以在运行npm命令时自动执行一些自定义的操作,比如在npm包安装之前或之后执行某些操作,或者在执行npm test命令之前或之后执行某些操作等。
举个简单例子:
// package.json文件
"scripts": {
"predev": "node prev.js",
"test": "node index.js",
"posttest": "node post.js"
},
// prev.js文件
console.log('prev')
// index.js文件
console.log('test')
// post.js文件
console.log('post')
// 运行npm run test结果:
> npm-demo@1.0.0 pretest
> node prev.js
prev
> npm-demo@1.0.0 test
> node index.js
test
> npm-demo@1.0.0 posttest
> node post.js
post
运行npm run test结果:
实际上大多数项目都有可能会用到,比如说著名的vite就是用到了preinstall 和 postinstall.
总结
npm是nodejs的包管理器,用于管理项目的依赖项。通过读取项目中的package.json文件,然后根据其中声明的依赖关系去下载对应的依赖包,如果依赖关系中有依赖包的版本号,则下载对应版本的依赖包。下载完成后,会将这些依赖包放入node_modules目录中,供项目使用。npm脚本的运行过程中有一些生命周期事件,可以用来在特定的时刻执行一些自定义的操作,比如在npm包安装之前或之后执行某些操作,或者在执行npm test命令之前或之后执行某些操作等。 ✈✈✈✈✈
路漫漫其修远兮,吾将上下而求索!