当前位置 博文首页 > 木子草明:npm基本用法及原理(10000+)

    木子草明:npm基本用法及原理(10000+)

    作者:木子草明 时间:2021-06-08 18:29

    ? ?作为前端开发者,应该每个人都用过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文件打开:
    在这里插入图片描述
    从标红的地方可以看出,这其实就是一个脚本,它最终执行的是: 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的全部配置:
    在这里插入图片描述
    可能个人的系统和配置不同,以我个人电脑配置为例,其实就是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也可以执行:

    "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,则会报错,
    在这里插入图片描述
    这是因为系统找不到eslint的位置(没有配系统环境变量),但是既然cmd室npm 脚本执行的环境,为什么npm run eslint可以执行呢?
    这是因为当我们通过npm run xxx执行脚本的时候,会把当前目录的'node_modules/.bin'加入到环境变量,也就是说npm执行脚本的时候,会自动到node_modules/.bin目录下找,如果找到则可以正常执行,我们来看一下:
    在这里插入图片描述
    在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的参数都是指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可以查看缓存目录:
    在这里插入图片描述
    按照路径打开文件夹,会发现_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原理总结为下面的流程图:
    在这里插入图片描述

    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缓存浅析
    bk