npm半截子原理篇 | 青训营

639 阅读11分钟

前言

npm对大家来说已经使用过很多次了,但是npm的原理有仔细深究吗?我相信大部分人都仅仅停留在能用的阶段,那么就有我来阐述一下什么是npm,它的原理又是什么,且听我慢慢道来!

开始学习之前必须安装nodejs!

什么是npm?

npm全称为node package manager,是nodejs的包管理器,是一个基于命令行的工具,用于管理项目的依赖项.类似于JAVA里的Maven.

npm常用命令

// 初始化,会创建一个package.json文件和package-lock.json文件
npm init

image.png

# 安装包
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的好处包括:

  1. 减少在安装依赖时node_modules的体积,提升安装依赖的速度,节省线上及其的硬盘资源以及部署上线的时间。
  2. 开发依赖和生产依赖分离,使得项目结构更加清晰,职责更加明确。

注意:devDependencies只在开发环境中安装,不会在生产环境中安装。因此,如果需要在生产环境中使用某个依赖包,需要将其添加到dependencies中。

2.生产环境所需依赖:dependencies

dependencies是指在生产环境中项目运行所需的依赖包。这些依赖项包括在应用程序运行时所需的库、框架和其他软件包。

"dependencies": {  
  "dependency-name": "^1.0.0"  
}

这表示该项目的生产环境依赖包需要另一个名为"dependency-name"的包,版本为1.0.0或更高版本。当项目进行生产环境构建或部署时,npm将自动安装所需的dependencies。

使用dependencies的好处包括:

  1. 确保项目在生产环境中具备所有必需的依赖项,从而保证项目的稳定性和可移植性。
  2. 通过版本控制和依赖管理工具,可以有效地解决不同依赖项之间的冲突和兼容性问题。

注意: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还会根据依赖关系将依赖包进行关联和依赖计算,确保项目中使用的所有依赖包都是与项目中其他依赖包兼容的。

依赖树扁平化

依赖树扁平化是怎么做到的呢?

通过使用广度优先算法实现!但是扁平化并非真正地扁平化,很难做到完全扁平化,因为一个项目中的依赖可能存在不同的版本号,而且不能丢弃任何一个版本的依赖,所以不能完全扁平化!

image.png

若依赖能全部扁平化就会在项目的node_modules里同一级按照abcd一定的规则依次排列(如上图);当依赖不能全部扁平化即遇到同一个包名的不同版本依赖时,不会做扁平化处理,而是哪个依赖需要它就在哪个依赖下添加一个node_modules,并将这个不同版本依赖放进去.这就不可避免的导致了模块冗余.如下图所示.

image.png

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有大用!

image.png

index-v5文件存的就是content-v2的索引的位置,如果key能和index-v5文件的位置对上就会去content-v2文件里下载对应的缓存二级制文件,也就是一个依赖包,这就是依赖包缓存原理,有了这个缓存机制,依赖包就能实现快速安装!

image.png

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"
  }
}

服务就启动了!

image.png

为什么npm run dev就能启动这个服务呢?

首先我们想一下这个命令(vite)是从哪来的?注意一下,项目的所有可执行命令都存放在了node_modules下面的.bin目录里,比如下图中的vite文件是给Linux,MacOS去使用,而Windows有两套第一个是vite.cmd(CMD使用),第二个为vite.ps1(Powershell使用),从而实现了vite的跨平台使用.但是这个命令为什么会放在这里呢(放在node_modules下面的.bin目录里)?

image.png

原来啊,当我们运行 npm i vite 时会把vite的包下载下来放在项目的node_modules下面,然后nodejs去读取了vite包里的package.json文件(该文件如下图所示),该文件配置了bin,所以就会往项目的node_modules下面的.bin目录里添加上述三个文件,从而使得在项目中可以使用到vite命令.

image.png

那么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).

image.png

prefix就是我们全局node_modules的存放位置,如图所示.这些就是我全局安装的package.

image.png

再追问一下,那为什么全局的命令可以执行呢?因为我们配过环境变量,会把这些命令注册到环境变量里面去!

image.png

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结果:

image.png

实际上大多数项目都有可能会用到,比如说著名的vite就是用到了preinstall 和 postinstall.

image.png

总结

npm是nodejs的包管理器,用于管理项目的依赖项。通过读取项目中的package.json文件,然后根据其中声明的依赖关系去下载对应的依赖包,如果依赖关系中有依赖包的版本号,则下载对应版本的依赖包。下载完成后,会将这些依赖包放入node_modules目录中,供项目使用。npm脚本的运行过程中有一些生命周期事件,可以用来在特定的时刻执行一些自定义的操作,比如在npm包安装之前或之后执行某些操作,或者在执行npm test命令之前或之后执行某些操作等。 ✈✈✈✈✈

路漫漫其修远兮,吾将上下而求索!