npm install 到npm run xxx深度解读

2,620 阅读10分钟

npm:Bring the best of open source to you, your team and your company

npm全称“node package manager”,即node的包管理器;我们可以将npm看做是一个庞大的服务器,上面放置类大量的第三方开源库,开发者只需要通过这个服务器就可以下载自己开发所需的任何第三方依赖。如今,npm已经是node的内置能力,下载安装node时默认会安装npm,依托node的火爆,npm得到越来越多的开发者的青睐。

npm 实现思路

  • 1、买个服务器作为代码仓库(registry),在里面放所有需要被共享的代码;
  • 2、通知开源库的作者使用 npm publish 把代码提交到 registry 上;
  • 3、想使用这些代码,就把第三方依赖(package)的name写到 package.json 里,当运行 npm install ,npm 就会帮忙下载代码;
  • 4、下载完的代码出现在 node_modules 目录里,可以在项目中随意使用。

npm文件

执行npm init后生成一个文件和一个文件夹:package.jsonnode_modules

package.json

package.json 用来描述项目及项目所依赖的模块信息,安装包依赖关系都由package.json来管理,开发者几乎不必考虑它们,但是有必要了解package.json中各个字段的含义和使用方法。

例如如下package.json:

{
  "name": "xxx",
  "version": "0.1.0",
  "author": "xxxx@163.com>",
  "description": "xxxx",
  "keywords":["node.js","javascript"],
  "private": true,
  "bugs":{"url":"xxx","email":"xxx"},
  "contributors":[{"name":"xx","email":"xxx@example.com"}],
  "repository": {},
  "homepage": "",
  "license":"MIT",
  "dependencies": {
    "react": "^16.8.6",
  },
  "devDependencies": {
    "browserify": "~13.0.0",
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "bin": {
  	"webpack": "./bin/webpack.js"
  },
  "main": "lib/webpack.js",
  "module": "es/index.js",
  "eslintConfig": {
    "extends": "react-app"
  },
  "engines" : { 
    "node" : ">=0.10.3 <0.12" 
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "style": ["./node_modules/tipso/src/tipso.css"],
  "files": [
    "lib/",
    "SECURITY.md"
  ]
}
  • name:项目/模块名称,长度必须小于等于214个字符,不能以"."(点)或者""(下划线)开头,不能包含大写字母。
  • version:项目版本
  • author:项目开发者,它的值是你在npmjs.org网站的有效账户名,遵循“账户名<邮件>”的规则,例如:zhangsan zhangsan@163.com
  • description:项目描述,是一个字符串。它可以帮助人们在使用npm search时找到这个包。
  • keywords:项目关键字,是一个字符串数组。它可以帮助人们在使用npm search时找到这个包。
  • private:是否私有,设置为 true 时,npm 拒绝发布。
  • license:软件授权条款,让用户知道他们的使用权利和限制。
  • bugs:bug 提交地址。
  • contributors:项目贡献者 。
  • repository:项目仓库地址。
  • homepage:项目包的官网 URL。
  • dependencies:生产环境下,项目运行所需依赖。
  • devDependencies:开发环境下,项目所需依赖。
  • scripts:执行 npm 脚本命令简写,比如 “start”: “react-scripts start”, 执行 npm start 就是运行 “react-scripts start”。
  • bin:内部命令对应的可执行文件的路径。
  • main:项目默认执行文件,比如 require(‘webpack’);就会默认加载 lib 目录下的 webpack.js 文件,如果没有设置,则默认加载项目跟目录下的 index.js 文件。
  • module:是以 ES Module(也就是 ES6)模块化方式进行加载,因为早期没有 ES6 模块化方案时,都是遵循 CommonJS 规范,而 CommonJS 规范的包是以 main 的方式表示入口文件的,为了区分就新增了 module 方式,但是 ES6 模块化方案效率更高,所以会优先查看是否有 module 字段,没有才使用 main 字段。
  • eslintConfig:EsLint 检查文件配置,自动读取验证。
  • engines:项目运行的平台/环境。
  • browserslist:供浏览器使用的版本列表。
  • style:供浏览器使用时,样式文件所在的位置;样式文件打包工具parcelify,通过它知道样式文件的打包位置。
  • files:打包是被项目包含的文件名数组;

version

版本号由三部分组成:major.minor.patch,主版本号.次版本号.修补版本号。安装一些依赖包的时候,版本号前面都会带 ^ 或者 ~ 的符号,这两个符号代表意思如下:

  • ~ :会匹配最近的小版本依赖包,比如 ~1.2.3 会匹配所有 1.2.x 版本,但是不包括 1.3.0;

  • ^ :会匹配最新的大版本依赖包,比如 ^1.2.3 会匹配所有 1.x.x 的包,包括 1.3.0,但是不包括 2.0.0;

  • * :安装最新版本的依赖包,比如 *1.2.3 会匹配 x.x.x;

  • 指定特定的版本号:直接写1.2.3,前面什么前缀都没有

需要注意 ^ 版本更新可能比较大,会造成项目代码错误,所以 建议使用 ~ 来标记版本号,这样可以保证项目不会出现大的问题,也能保证包中的小bug可以得到修复。

dependencies

生产环境下,项目运行所需依赖,但是开发环境的依赖模块也可以配置到这里。

devDependencies

开发环境下,项目所需依赖。

有一些包有可能你只是在开发环境中用到,例如你用于检测代码规范的 eslint ,用于进行测试的 jest 。用户开发项目即使不安装这些依赖也可以正常运行,当打包上线时反而安装他们会耗费更多的时间和资源,所以你可以把这些依赖添加到 devDependencies 中,这些依赖照样会在你本地进行 npm install 时被安装和管理,但是不会被安装到生产环境。

script

执行 npm 脚本命令简写。

配置格式:"xxx": "node ./index.js"

在终端输入npm run xxx,相当于在终端中输入 node ./index.js,即使用node执行当前目录下的index.js文件。

bin

内部命令对应的可执行文件的路径。

许多软件包都具有一个或多个要安装到 PATH 中的可执行文件。 bin 字段是命令名到本地文件名的映射。在安装时,npm 会将文件软链接/符号连接prefix/bin 以进行全局安装或./node_modules/.bin/本地安装。

node_modules

node_modules中包含的文件分为两类:

第三方依赖的package文件。

在运行npm i 或者 npm install时,根据**当前项目package.json **配置dependenciesdevDependencies中的第三方依赖信息,从npm服务器中下载对应版本的package到node_modules目录下。

.bin文件

命令对应的可执行文件的软链接文件,指向对应第三方依赖的bin文件。

npm install

安装第三方依赖的命令。

npm 在执行install的时候,会根据项目的package.json信息,现在相应的第三方依赖(package)到node_modules目录。如果在执行npm install时没有lock文件,会生成package-lock.json文件,如果存在lock文件,则根据依赖的版本肯能会更新package-lock.json文件。

package-lock.json

package-lock.json 是在 npm(^5.x.x.x)后才有。

官方文档是这样解释的:

package-lock.json 它会在 npm 更改 node_modules 目录树 或者 package.json 时自动生成的 ,它准确的描述了当前项目npm包的依赖树,并且在随后的安装中会根据 package-lock.json 来安装,保证是相同的一个依赖树,不考虑这个过程中是否有某个依赖有小版本的更新。

它的产生就是来对整个依赖树进行版本固定的(锁死)。

当我们在一个项目中npm install时候,会自动生成一个package-lock.json文件,和package.json在同一级目录下。package-lock.json记录了项目的一些信息和所依赖的模块。这样在每次安装都会出现相同的结果. 不管你在什么机器上面或什么时候安装。

当我们下次再npm install时候,npm 发现如果项目中有 package-lock.json 文件,会根据 package-lock.json 里的内容来处理和安装依赖而不再根据 package.json

package-lock.json 生成逻辑

简单描述一下 package-lock.json 生成的逻辑。假设我们现在有三个 package,在项目 test中,安装依赖A,A项目面有B,B项目面有C,A、B、C之间依赖如下:

// package test
{ "name": "test", "dependencies": { "A": "^1.0.0" }}
// package A
{ "name": "A", "version": "1.0.0", "dependencies": { "B": "^1.0.0" }}
// package B
{ "name": "B", "version": "1.0.0", "dependencies": { "C": "^1.0.0" }}
// package C
{ "name": "C", "version": "1.0.0" }

在这种情况下 package-lock.json, 会生成类似下面铺平的结构:

// package-lock.json
{ 
    "name": "test",  
    "version": "1.0.0",  
    "dependencies": {    
        "A": { "version": "1.0.0" },
        "B": { "version": "1.0.0" },
        "C": { "version": "1.0.0" }  
    }
}

如果后续无论是直接依赖的 A 发版,或者间接依赖的B, C 发版,只要我们不动 package.json, package-lock.json 都不会重新生成。

版本更新

  • 若A 发布了新版本 1.1.0,虽然项目 package.json 写的是 ^1.0.0 但是因为 package-lock.json 的存在,npm i 并不会自动升级,

    此时可以手动运行 npm i A@1.1.0 来实现升级。

    因为 1.1.0 package-lock.json 里记录的 A@1.0.0是不一致的,因此会更新 package-lock.json 里的 A 的版本为 1.1.0。

  • 若B 发布了新版本 1.0.1, 此刻如果我们不做操作是不会自动升级 B 的版本的,但如果此刻 A 发布了 1.1.1,虽然并没有升级 B 的依赖,但是如果我们项目里升级 A@1.1.1,此时 package-lock.json 里会把 B 直接升到 1.1.0,因为此刻^1.0.0的最新版本就是 1.1.0。

经过升级操作后 项目 test 的 package.json 变成:

// package 
test{ "dependencies": { "A": "^1.1.0" }}

对应的 package-lock.json 文件

{  
    "name": "test",  
    "version": "1.0.0",
    "dependencies": {  
        "A": { "version": "1.1.0" },
        "B": { "version": "1.1.0" },
        "C": { "version": "1.0.0" }
    }
}
  • 这个时候将 B 直接加入我们test 项目的依赖,B@^1.0.0,package.json如下:
{ "dependencies": { "A": "^1.1.0", "B": "^1.0.0" }}

执行这个操作后,package-lock.json 并没有被改变,因为现在 package-lock.jsonB@1.1.0满足 ^1.0.0 的要求

  • 但是如果将test直接引入的 B 的版本固定到 2.x 版本, package-lock.json 就会发生改变
{ "dependencies": { "A": "^1.1.0", "B": "^2.0.0" }}

因为存在了两个冲突的B版本,package-lock.json 文件会变成如下形式

{  
    "name": "lock-test",
    "version": "1.0.0",  
    "dependencies": {    
        "A": {      
            "version": "1.1.0",      
            "dependencies": {        
                "B": { "version": "1.1.0" }      
            }    
        },    
        "B": { "version": "2.0.0" },    
        "C": { "version": "1.0.0" }  
    }
}

因为 B 的版本出现了冲突,npm 使用嵌套描述了这种行为

实际开发中并不需要关注这种生成的算法逻辑,我们只需要了解,package-lock.json 的生成逻辑是为了能够精准的反映出我们 node_modules 的结构,并保证能够这种结构被还原。

package-lock.json 被更改

1、package.json 文件改动;

2、挪动了包的位置;

将部分包的位置从 dependencies 移动到 devDependencies 这种操作,虽然包未变,但是也会影响 package-lock.json,会将部分包的 dv 字段设置为 true

3、registry 的影响

在 node_modules 文件夹下的包中下载时,就算版本一样,安装源 registry 不同,执行 npm i 时也会修改 package-lock.json

可能还存在其他的原因,但是 package-lock.json 是不会无缘无故被更改的,一定是因为 package.json 或者 node_modules 被更改了,因为 正如上面提到的 package-lock.json 为了能够精准的反映出我们 node_modules 的结构

抽象的执行流程

  • 1、遍历出项目的依赖树

    • a. 首先确定项目的首层依赖(dependencies和devDependencies下的模块);

    • b. 从首层依赖的每个模块开始,npm开启多进程遍历更深层级的模块,最终生成一颗完整的依赖树;

    • c. 获取模块,并更新依赖树:获取版本信息 --> 获取模块实体 --> 获取模块依赖 (如果有依赖的话,再来一遍) 将依赖树扁平化处理,并去除重复的模块

      重复模块的定义:模块的模块名相同且semver有交集(每个semver都对应一个版本的允许范围,如果两个模块的semver有交集,就能找到一个兼容的版本)

  • 2、安装模块:更新node_modules;

  • 3、生成或者更新版本描述文件(package.json和package-lock.json);

具体的执行流程

npm详细执行流程如下:

当package.json和package-lock.json中下载信息不一致,npm不同版处理:

  • v5.0.x:根据package-lock.json下载
  • v5.1.0-v5.4.2:根据package.json下载,然后更新package-lock.json
  • v5.4.2以上:当package.json与package-lock.json版本有兼容,根据package-lock.json下载;两者不兼容根据package.json下载,然后更新package-lock.json;

npm run xxx

执行特定脚本的命令。

npm 在执行install的时候,会根据第三方依赖中的package.json里面的bin配置,在node_modules下面的.bin目录生成一个可执行文件。生成的可执行文件其实是一个替身文件(软链接文件),这个替身文件真正指向的就是第三方依赖package.json里面的bin配置指向的那个文件。

npm运行script脚本时会自动给脚本路径都加上了node_moudles/.bin前缀,这意味着:你在试图运行script中某个脚本时,实际上是运行node_moudles/.bin下的对应软链接文件。

如下图,以vite/vue项目为例,执行npm run dev的大致流程如下:


参考文档