作为前端开发者,应该每个人都用过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基本用法及原理(10000+)的更多相关文章

  1. webpack基本用法及原理(10000+)

    1 webpack是什么 所有工具的出现,都是为了解决特定的问题,那么前端熟悉的webpack是为了解决什么问题呢? 1.1 为什么会出现webpack js模块化: 浏览器认识的语言是HTML,CS ...

  2. imadjust从用法到原理—Matlab灰度变换函数之一

    imadjust从用法到原理-Matlab灰度变换函数之一 转摘网址:http://blog.sina.com.cn/s/blog_14d1511ee0102ww6s.html imadjust函数是 ...

  3. Python 中 -m 的典型用法、原理解析与发展演变

    在命令行中使用 Python 时,它可以接收大约 20 个选项(option),语法格式如下: python [-bBdEhiIOqsSuvVWx?] [-c command | -m module- ...

  4. synchronized是什么,用法及原理

    文章转Hollis博客 大家可以关注下,很多技术类型的文章 在再有人问你Java内存模型是什么,就把这篇文章发给他.中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题,提 ...

  5. Java之反射 — 用法及原理

    Java之反射 - 用法及原理 定义 Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意方法和属性:这种动态获取信息以及动态调用对象 ...

  6. java this的用法以及原理

    /** * this存在方法中,在方法中被调用. * 且是非static方法中被调用.(this 表示这个类的当前实例,而静态方法不依赖于该类的任何实例,随着类产生而装载,因此方法内不能引用 this ...

  7. ASP.NET Core MVC 模型绑定用法及原理

    前言 查询了一下关于 MVC 中的模型绑定,大部分都是关于如何使用的,以及模型绑定过程中的一些用法和概念,很少有关于模型绑定的内部机制实现的文章,本文就来讲解一下在 ASP.NET Core MVC ...

  8. Linux常用性能工具功能、用法及原理(一)

    Linux性能观测工具按类别可分为系统级别和进程级别,系统级别对整个系统的性能做统计,而进程级别则具体到进程,为每个进程维护统计信息. 按实现原理分,可分为基于计数器和跟踪以及剖析.含义如下: 计数器 ...

  9. Spark SQL入门用法与原理分析

    Spark SQL是为了让开发人员摆脱自己编写RDD等原生Spark代码而产生的,开发人员只需要写一句SQL语句或者调用API,就能生成(翻译成)对应的SparkJob代码并去执行,开发变得更简洁 注 ...

随机推荐

  1. docker日志设置

    最近查看docker日志的时候,使用命令docker log -f 会出现日志无限翻滚的情况,这些日志都是打印到控制台的,但是都被docker收集了起来,放到了/var/lib/docker/cont ...

  2. hdu5251最小矩形覆盖

    题意(中问题直接粘吧)矩形面积 Problem Description 小度熊有一个桌面,小度熊剪了很多矩形放在桌面上,小度熊想知道能把这些矩形包围起来的面积最小的矩形的面积是多少.   Input ...

  3. 利用ICMP进行命令控制和隧道传输

    目录 使用ICMP进行命令控制 使用ICMP搭建隧道 使用ICMP进行命令控制 攻击机:Kali  192.168.10.11 靶机:Windows 192.168.10.1 使用的工具:icmpsh ...

  4. Win64 驱动内核编程-28.枚举消息钩子

    枚举消息钩子 简单粘贴点百度的解释,科普下消息钩子: 钩子是WINDOWS中消息处理机制的一个要点,通过安装各种钩子,应用程序能够设置相应的子例程来监视系统里的消息传递以及在这些消息到达目标窗口程序之 ...

  5. spring和mybatis整合时Access denied for user '***'@'localhost' (using password: YES)错误的解决方案

    参考文章:博客园文章 参考解决办法: 将数据库配置文件格式 key=value 改为 jdbc.key=value 以下为问题分析 使用Spring + Mybatis + Mysql整合时,测试报错 ...

  6. Insert Pictures In Hexo Blog

    After build my blog following the online course step by step , I began to try to write my own blog️ ...

  7. Blazor实现未登录重定向到登录页的方法

    今天研究了一下blazor,发现他默认启动就是类似于后台管理系统的界面,看到这个页面我就想给他写个登录,有登录就涉及到未登录重定向的问题,但是我没有找到blazor全局路由的设置,知道的老哥可以告诉我 ...

  8. Spring中声明式事务存在的优缺点以及注意事项!

    事务管理在系统开发中是不可缺少的一部分,Spring提供了很好事务管理机制,主要分为编程式事务和声明式事务两种. 关于事务的基础知识,如什么是事务,数据库事务以及Spring事务的ACID.隔离级别. ...

  9. Pytorch系列:(六)自然语言处理NLP

    这篇文章主要介绍Pytorch中常用的几个循环神经网络模型,包括RNN,LSTM,GRU,以及其他相关知识点. nn.Embedding 在使用各种NLP模型之前,需要将单词进行向量化,其中,pyto ...

  10. ES6学习-5 解构赋值(2)对象的解构赋值

    啥也不说,先举个栗子: 1 let { myname, myage } = { myage: 18, myname: "郭郭" }; 2 console.log(myname) / ...