• 微信公众号:美女很有趣。 工作之余,放松一下,关注即送10G+美女照片!

npm基本用法及原理(10000+)

开发技术 开发技术 4小时前 5次浏览

   作为前端开发者,应该每个人都用过npm,那么npm到底是什么东西呢?npm run,npm install的时候发生了哪些事情呢?下面做详细说明。

1.npm是什么

npm是JavaScript语言的包管理工具,它由三个部分组成:

  • npm网站 进入
    npm官网上可以查找包,查看包信息。
  • 注册表
    一个巨大的数据库,存放包的信息
  • 命令行工具npm-cli
    开发者运行npm命令的工具

这三者中,与我们打交道最多的就是npm-cli,其实我们所说的npm的使用,就是指这个工具的使用,那它到底是个什么东西呢?我们先来看看它被放在哪里,在系统命令行(window cmd)工具中输入 where npm安装node会自带npm),就能找到它的位置:
npm基本用法及原理(10000+)
然后根据路径找到npm文件打开:
npm基本用法及原理(10000+)
从标红的地方可以看出,这其实就是一个脚本,它最终执行的是: node npm-cli.js

   所以到目前为止,我们可以知道当在命令行输入npm时,其实是在node环境中,执行了一段npm-cli.js代码,这是对npm的一个直观的认识。
   至于npm-cli.js里面的逻辑是什么,就是研究源码层面的事了,这里不涉及。我们主要来看npm的用法和功能层面的原理。首先来看npm的配置文件package.json。

2.package.json文件

当我们运行命令npm init,根据提示输入一些信息后(npm init -y不需输入信息),会在当前目录下生成一个package.json文件:

{
  "name": "testNpm",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

这里就是一个npm包的基本信息,包括包名name,版本version,描述description,作者author,主文件main,脚本scripts等等, 这里先主要来看下main

2.1 入口文件 main

   main配置项的值是一个js文件的路径,它将作为程序的主入口文件。也就是说当别人引用了这个包时import testNpm from 'testNpm',其实引入的就是testNpm/index.js文件所export出的模块。

2.2 脚本 scripts

npm scripts 脚本应该是我们打交道最多的一个配置项了,它一个json的对象,由脚本名称和脚本内容组成:

"scripts":{
	"star":"echo star npm",
	"echo":"echo hello npm"
}

一般用npm run xxx来运行,但是一些关键命令比如:start,test,stop,restart等等,可以直接npm xxx来执行。那scripts是如何执行脚本的呢?又可以执行哪些脚本呢?

npm 脚本可以执行的命令
其实当我们npm run xxx的时候,就是把xxx的内容生成了一个shell脚本,然后执行脚本,那么npm的shell具体是什么呢?我们可以运行npm config get -l来查看npm的全部配置:
npm基本用法及原理(10000+)
可能个人的系统和配置不同,以我个人电脑配置为例,其实就是cmd.exe,其实就是window系统的cmd命令行工具。所以在cmd中可以执行的命令,在npm的scripts中都可以执行,举例说明:

"scripts":{
	/*系统命令*/
	"echo":"echo hello npm",
	"dir":"dir",
	"ip":"ipconfig"
}

像dir,ipconfig,echo这些都是可以直接在cmd命令行中执行的命令,在npm的scripts中都可以通过npm run xxx来执行。这一类是系统cmd的内部命令,不需要安装额外的插件,就可以直接执行。
还有一种就是我们在cmd还可以执行外部命令,比如我们如果安装了node,git等客户端,可以直接在cmd窗口执行(需配置了系统的环境变量):
npm基本用法及原理(10000+)
这一类的命令npm也可以执行:

"scripts":{
	/*系统命令*/
    "echo":"echo hello npm",
    "dir":"dird",
    "ip":"ipconfig",
    /*全局外部命令*/
    "git":"git --version",
    "node":"node -v",
}

这是全局引入的外部命令,还有些项目内部才有的命令,比如我们在项目下安装eslint: npm install eslint --save-dev,在scripts中配置了脚本的话,我们可以直接运行npm run eslint

"scripts":{
	/*系统命令*/
    "echo":"echo hello npm",
    "dir":"dird",
    "ip":"ipconfig",
    /*全局外部命令*/
    "git":"git --version",
    "node":"node -v",
    /*项目内外部命令*/
    "eslint":"eslint -v"
}

但是如果我们直接在cmd窗口执行eslint -v,则会报错,
npm基本用法及原理(10000+)
这是因为系统找不到eslint的位置(没有配系统环境变量),但是既然cmd室npm 脚本执行的环境,为什么npm run eslint可以执行呢?
这是因为当我们通过npm run xxx执行脚本的时候,会把当前目录的’node_modules/.bin’加入到环境变量,也就是说npm执行脚本的时候,会自动到node_modules/.bin目录下找,如果找到则可以正常执行,我们来看一下:
npm基本用法及原理(10000+)
在node_modules/.bin目录下果然是eslint.cmd脚本的,而它作的其实就是node eslint.js,用node来执行eslint.js的代码。

npm 脚本可以执行的命令总结:

  • cmd内部命令,例如dir,ipconfig…
  • 外部命令
    • 全局命令,加入了系统环境变量
    • 项目下命令,这部分会放在node_modules/.bin目录下,而npm会自动链接到此目录。

2.3 npm脚本其他配置

路径通配符
我们在写脚本命令的时候,常常要匹配文件,这就要用到路径的通配符。
总的来说*表示任意字符串,在目录中表示1级目录,**表示0级或多级目录,例如:

src/*:src目录下的任意文件,匹配 src/a.js; src/b.json;不匹配src/aa/a.js
src/*.js:src目录下任何js文件,匹配 src/a.js; 不匹配 src/b.json;src/aa/a.js
src/*/*.js:src目录下一级的任意js文件,匹配 src/aa/a.js; 不匹配src/a.js;src/a/aa/a.js
src/**/*.js:src目录下的任意js文件,匹配 src/a.js; src/a/a.js; src/a/aa/a.js

命令参数
关于npm的参数,我们先来看一段代码:
node代码:

	//index.js
	
	console.log(process.env.npm_package_name)
	console.log(process.env.npm_config_env)
	console.log(process.argv)

npm配置:

	//package.json
	
{
  "name": "npm",
  "version": "1.0.0",
  "scripts": {
    "node":"node index.js --name=node age=28",
  },
}

然后我们执行命令npm run node --env=npmEnv,结果为:
npm基本用法及原理(10000+)

下面来做下说明,其实npm的参数都是指node环境下的参数,用node的全局变量process来获取。

  • npm内部变量
    当我们在执行npm命令的时候,就会把package.json的参数加上npm_package_前缀,加入到process.env的变量中,所以在上面的node代码可以通过process.env.npm_package_name获取到package.json里面配置的name属性。
  • 命令参数
    当我们在运行npm命令时,带上以双横线为后缀的参数:npm 命令 --xx=xx,npm就会把xx加上npm_config_前缀,加入到process.env变量中,如果原来有同名的,命令参数的优先级最高,会覆盖掉原来的,所以在上面的node代码可以通过process.env.npm_config_env获取到npm run node --env=npmEnv命令里的参数env的值,如果参数没有赋值:npm run node --env,则默认值为true
  • 脚本参数
    这个其实要根据脚本的内容来看,比如我们上面的脚本是node index.js --env=node,这其实是纯粹的node命令了,可以通过process.argv来获取node的命令参数,这是个数组,第一个为node命令路径,第二个为执行文件路径,后面的值为用空格隔开的其他参数,如上面打印的结果所示。

执行顺序
npm脚本的执行顺序分为两部分:

  • 命令钩子
    npm脚本有pre,post两类钩子,一个是执行前,一个是执行后。比如,当我们执行npm run start时,会按照以下顺序执行npm run prestart ->npm run start ->npm run poststart
  • 多任务并行
    如果要执行多个脚本,可以用&&&来连接

    • npm run aa & npm run bb 并行执行,没有先后关系
    • npm run aa && npm run bb 串行执行,先执行完aa再执行bb

3.npm 包管理

npm做完包管理工具,主要的作用还是包的安装及管理。

3.1 安装包 npm install xxx

npm install xxx 命令用于安装包。
我们先来运行npm install vuenpm install eslint --save-dev,会发现项目会有以下变化:

  • 添加了目录node_modules
    安装的包和包的依赖都存放在这里,引入的时候,会自动到此目录下找。
  • package.json文件自动添加了如下配置:
      "dependencies": {
        "vue": "^2.6.13"
      },
      "devDependencies": {
        "eslint": "^7.27.0"
      }
    

    npm 在安装包的同时,会把包的名称和版本加入到dependencies配置中,这表明这是项目必需的包。
    如果带上参数--save-dev,则加入到devDependencies配置中,这表明这是项目开发时才需要的工具包,不是项目必需的。

  • 添加了package-lock.json文件
    锁定包的版本和依赖结构。

3.2 从package.json配置文件安装包

包依赖类型
现在把node_modules目录和package-lock.json文件都删除,然后运行npm install,会发现项目会自动安装vue和eslint包。
如果我们执行npm install --production则表明我们只是想安装项目必须的包,用于生产环境,这是就只会安装dependencies对象下的包。
其实npm包除了这两种还有其他包的依赖类型:

  • dependencies
    业务依赖,是项目的必须包,是项目线上代码的一部分。npm install --production只会安装此配置下的包。
  • devDependencies
    开发环境依赖,只在开发环境需要。npm install --save-dev安装包并添加到此配置下。
  • peerDependencies
    同行依赖,当运行npm install,会提示安装此配置下的包。注意只是警告提示,不会自动安装。
  • optionalDependencies
    可选依赖,表明即使安装失败,也不影响项目的安装过程。会覆盖掉dependencies中的同名包。
  • bundledDependencies
    打包依赖,发布当前包的时候,会把此配置下的依赖包也一起打包。必须先在 dependenciesdevDependencies 声明过,否则打包会报错。

包版本说明
npm采用semver作为包版本管理规范。此规范规定软件版本由三个部分组成:

  • 主版本号做了不兼容的重大变更
  • 次版本号做了向下兼容的功能添加
  • 补丁版本号做了向下兼容的bug修复

除了版本号之外,还有一些版本修饰,后面可以带上数字:

  • alpha内测版 eg:3.0.0-alpha.1
  • beta公测版 eg:3.0.0-beta.10
  • rc正式版本的候选版 eg:3.0.0-rc.3

版本匹配

  • */x:匹配任意值
    1.1.* = >=1.1.0 <1.2.0
    1.x = >=1.0.0 <2.0.0
  • ^xxx: 最左侧非0版本号不变,不小于xxx
    ^1.2.3 = >=1.2.3 <2.0.0 主版本号不变
    ^0.1.2 = >=0.1.2 <0.2.0 主、次版本号不变
    ^0.0.2 = = 0.0.2 主、次、补丁版本号都不变
  • ~xxx:如果列出了次版本号,则次版本号不变,如果没有列出次版本号,则主版本号不变,均不小于xxx
    ~1.2.3 = >=1.2.3 <1.3.0 主、次版本号不变
    ~1 = >=1.0.0 <2.0.0 主版本号不变

3.3 package-lock.json作用

固定版本
当我们安装包的时候,会自动添加package-lock.json文件,那么这个文件的作用是什么呢?在这个问题之前,先来看看npm install的安装原理:

//package.json
{
  "name": "npm",
  "version": "1.0.0",
  "dependencies": {
    "vue": "^2.5.1"
  },
  "devDependencies": {
    "eslint": "^7.0.0"
  }
}

有上面一份npm配置文件,当npm install时会安装两个包:vue ^2.5.1,eslint ^7.0.0 ,符合所配置版本的包是一个范围多个,npm会会安装符合版本配置的最新版本。比如:
vue ^2.5.1 = >=2.5.1 <3.0.0, npm会选择安装2.6.13,因为它在匹配版本范围内,且是目前最新的vue2的版本,它不会选择2.5.03.0.0
那么如果只有一份package.json文件,就很可能导致项目依赖的版本不一样。比如开发时候vue2的最新版本是2.6.13,过了几个月项目要上线,部署的时候vue2的最新版本已经是2.7.0了,那么线上就会安装最新的版本。如果2.7.0有一些不兼容2.6.13的地方,或者有bug,那就会导致我们开发的一个经典问题:开发环境没问题,一上线就坏。如果项目是多个人协同开发,甚至会导致开发环境都不一样。
那么我们来看看package-lock.json文件怎么解决这个问题的:

//package-lock.json
{
  "name": "npm",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "vue": {
      "version": "2.6.13",
      "resolved": "https://registry.nlark.com/vue/download/vue-2.6.13.tgz?cache=0&sync_timestamp=1622664849693&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvue%2Fdownload%2Fvue-2.6.13.tgz",
      "integrity": "sha1-lLLBsx/d8d/MNPKOyEi6jwHqTFs="
    },
	.....
  }
}

我们看到package-lock.json文件里直接记录了vue的固定版本号和下载地址。

npm在执行install的时候,会把每个需要安装的包先在package-lock.json查找,如果找到并且版本符合package.json的配置范围(在范围内就行,不需要最新),就会直接按照package-lock.json里的地址安装。如果没找到或者不符合范围,则安装原本的逻辑安装(符合版本要求的最新版)。
这样就确保,不管时间过了多久,只要package-lock.json文件不变,npm install安装的包的版本都是一致的,避免代码运行的依赖环境不同。

固定依赖结构
我们的一个项目通常会有很多依赖包,而这些依赖包很可能又会依赖其他的包,那如何来避免重复安装呢?
比如:

//package.json
{
  "name": "npm",
  "version": "1.0.0",
  "dependencies": {
    "esquery": "^1.4.0",
    "esrecurse": "^4.3.0",
    "eslint-scope": "^5.1.1"
  }
}

依赖关系如下:

  • esquery : ^1.4.0,
    • estraverse : ^5.1.0
  • esrecurse : ^4.3.0
    • estraverse : ^5.2.0
  • eslint-scope :^5.1.1
    • esrecurse : ^4.3.0
      • estraverse :^5.2.0
    • estraverse :^4.1.1

如果按照这个嵌套结构来安装包的话也是可以的,而且npm原来的版本就是这么做的,这样可以保证每个包都安装完整,但是问题是会导致一些包重复安装,如果这个依赖很多的话,重复的数量也会很多。那npm是怎么处理的呢?
npm采用的是用扁平结构,包的依赖,不管是直接依赖,还是子依赖的依赖,都会优先放在第一级。
如果第一级有找到符合版本的包,就不重复安装,如果没找到,则在当前目录下安装。
比如上面的包会被安装成如下的结构:

  • esquery :1.4.0,
    • estraverse : 5.2.0
  • esrecurse : 4.3.0
    • estraverse : 5.2.0
  • eslint-scope : 5.1.1
  • estraverse : 4.3.1

包安装的数量从开始的8个减少到了6个,虽然还是有重复,但是因为这个json的结构,又是以包名为键名,所以同一级下只能有一个同名的包,就像 estraverse : 5.2.0不能放在外层,因为外层已经有了以estraverse 为名的对象:estraverse : 4.3.1
package-lock.json记录的就是上面的依赖结构(上面只是简写,每一项还包含一些其他的信息,比如下载地址),这也是node_modules里面包的结构。
所以一个项目只要package-lock.json不变,它的依赖结构就不变,而且npm不用重新解析包的结构了,直接从package-lock.json文件就可以安装完整且正确的包依赖,也提高了重新安装的效率。

3.4 包缓存

npm安装包不是每一次都从服务器直接下载,而是有缓存机制。当npm安装包时,会在本地的缓存一份。执行npm config get cache可以查看缓存目录:
npm基本用法及原理(10000+)
按照路径打开文件夹,会发现_cacache缓存文件夹,打开文件夹会有index-v5content-v2两个目录。
其中index-v5存放的是包的索引,而content-v2则存放的是缓存的压缩包。

缓存查找
那么npm是如何找到缓存包的呢?以vue包为例:

  • 1.首先安装vue包: npm install vue
  • 2.查看package-lock.json文件,根据包信息获取resolved,integrity字段,构造字符串:
    pacote:range-manifest:{resolved}:{integrity}
  • 3.把上面字符串按SHA256加密,得到加密字符串:
    2686ae12fd03809c9e5704cd01db518f1d7d07efe5ab61e6ef386e95b8481360
  • 4.上面加密字符串的前4位就是_cacache/index-v5目录的下两级,索引文件的位置:
    _cacache/index-v5/26/86/ae12fd03809c9e5704cd01db518f1d7d07efe5ab61e6ef386e95b8481360
  • 5.打开按照上面路径找到的索引文件,在索引文件中找到_shasum字段:
    94b2c1b31fddf1dfcc34f28ec848ba8f01ea4c5b
  • 6.上面符串就是缓存包的位置,其前4位就是_cacache/content-v2/sha1目录的下两级,包位置:
    _cacache/content-v2/sha1/94/b2/c1b31fddf1dfcc34f28ec848ba8f01ea4c5b
  • 7.把按照上面路径找到的文件的拓展名改为.tgz,然后解压,会得到vue.tar包,再解压,就是我们熟悉的vue包了。

3.5 npm install 原理流程图

把npm install原理总结为下面的流程图:
npm基本用法及原理(10000+)

4.npm常用命令

  • npm init [-y] 创建package.json文件 [直接创建]
  • npm run xxx [--env] 运行脚本 [参数]
  • npm config get [-l] 查看npm配置 [全部配置]
  • npm install xxx [--save-dev] [-g] 安装npm包 [添加到开发依赖] [全局安装]
  • npm uninstall xxx [-g] 删除包 [删除全局包]
  • npm root [-g] npm包安装的目录 [全局包安装目录]
  • npm ls [-g] 查看项目安装的包 [全局安装的包]
  • npm install [--production] 安装项目 [只安装项目依赖]
  • npm ci 安装项目,不对比package.json,只从package-lock.json安装,并且会先删除node_modules目录
  • npm config get cache 查看缓存目录
  • npm cache clean --force 清除npm包缓存

参考

  • 前端工程化 – 剖析npm的包管理机制
  • 什么是 npm —— 写给初学者的编程教程
  • npm缓存浅析

程序员灯塔
转载请注明原文链接:npm基本用法及原理(10000+)
喜欢 (0)