npm 是 node 捆绑的依赖管理器,常用程度可想而知。那么你每天都在 npm/yarn run 的命令到底是如何运行项目的呢?
前端项目中运行 npm run xxx 的时候发生了什么?
大家都知道目前的 node 是捆绑 npm 的。npm 是 node 的依赖管理器,虽然它不是唯一的选择,我们还有 pnpm/yarn/cnpm/ni 。

但是依赖管理器都是在解决 npm 的某个痛点。对于 npm 依赖声明文件 本身是基本没有变化的。
例如我们可以使用 npm run serve运行某个命令, 也可以使用yarn serve 运行某个命令。
可以看到在这个地方 yarn 可以省略 run 这个参数。
但是,他们都只是对 package.json 进行解析而已

  1. {
  2. "name": "h5",
  3. "version": "1.0.7",
  4. "private": true,
  5. "scripts": {
  6. "serve": "vue-cli-service serve"
  7. },
  8. "dependencies": {
  9. "axios": "^0.19.2",
  10. "vuex": "^3.4.0"
  11. },
  12. "devDependencies": {
  13. "node-sass": "^4.12.0"
  14. }
  15. }

通过 npm run 与直接运行命令的区别

  1. {
  2. "scripts": {
  3. "serve": "vue-cli-service serve"
  4. }
  5. }

npm 在运行 vue-cli-service serve 这条命令的时候,会先在当前 node_modules/.bin下面看有没有同名的可执行文件,如果有,则使用其运行。

这里我们可以打开这个目录看看:
2022/07/17 前端项目中运行 npm run xxx 的时候发生了什么? - 图1
如果直接在命令行中运行 vue-cli-service serve这条命令,是不会从 node_modules 中查找可执行程序的。(因为操作系统中没有存在vue-cli-service这一条指令)

运行可执行文件

那么什么叫可执行文件呢?上面的图中有很多个同名的 vue-cli-service ,到底是运行哪个?
我们先来分析这几个文件怎么来的?
例如 @vue/cli-service有package.json文件,注意 bin 字段,当我们运行 ·npm i @vue/cli-service
这条命令时,npm 就会在 node_modules/.bin/目录中创建好以 vue-cli-service 为名的几个可执行文件了。

  1. {
  2. "name": "@vue/cli-service",
  3. "version": "4.4.6",
  4. "description": "local service for vue-cli projects",
  5. "main": "lib/Service.js",
  6. "typings": "types/index.d.ts",
  7. "bin": {
  8. "vue-cli-service": "bin/vue-cli-service.js"
  9. }
  10. }

对于可执行这个定义,每个系统不一样。在 windows 系统上,可执行文件是通过组策略和环境变量决定的。使 ·set pathext 可以查看 pathext 这个环境变量,他定义了可以作为可执行文件的后缀。

  1. # 查看可执行文件后缀
  2. set pathext

由上面的配置可以发现,我们常见的 exe 也在其中,这个可执行文件在 windows 上,在命令行中输入文件名或双击时即可以运行。
可以 查看这个短视频窥探一波。
在 unix 系统上面,是通过设置文件的属性为可执行,再在文件中的第一行声明解释器来运行的。

如果我们在 cmd 里运行的时候,windows 一般是调用了 vue-cli-service.cmd

这个文件,这是 windows 下的批处理脚本:

  1. @ECHO off
  2. SETLOCAL
  3. CALL :find_dp0
  4. SET _maybeQuote="
  5. IF EXIST "%dp0%\node.exe" (
  6. SET "_prog=%dp0%\node.exe"
  7. ) ELSE (
  8. SET _maybeQuote=
  9. SET "_prog=node"
  10. SET PATHEXT=%PATHEXT:;.JS;=;%
  11. )
  12. %_maybeQuote%%_prog%%_maybeQuote% "%dp0%\..\_@vue_cli-service@4.4.6@@vue\cli-service\bin\vue-cli-service.js" %*
  13. ENDLOCAL
  14. EXIT /b %errorlevel%
  15. :find_dp0
  16. SET dp0=%~dp0
  17. EXIT /b

所以当我们运行 vue-cli-service serve这条命令的时候,就相当于运行 node_modules/.bin/vue-cli-service.cmd serve

然后这个脚本会使用 node 去运行 vue-cli-service.js这个 js 文件,由于 node 中可以使用一系列系统相关的 api ,所以在这个 js 中可以做很多事情,例如读取并分析运行这条命令的目录下的文件,根据模板生成文件等。

  1. # unix 系默认的可执行文件,必须输入完整文件名
  2. vue-cli-service
  3. # windows cmd 中默认的可执行文件,当我们不添加后缀名时,自动根据 pathext 查找文件
  4. vue-cli-service.cmd
  5. # Windows PowerShell 中可执行文件

这里多提了下,在 windows 中 cmd 脚本使用得比较多,兼容性也较好。 powerShell 虽然比较强大,但他运行命令的方式由于和 cmd 命令有较大不同,这会导致你常常搞不清什么命令应该在什么解释器里运行。

示例:运行命令的方式不兼容

2022/07/17 前端项目中运行 npm run xxx 的时候发生了什么? - 图2

示例:windows 很多系统会默认禁止此脚本运行,导致 npm 命令运行错误
2022/07/17 前端项目中运行 npm run xxx 的时候发生了什么? - 图3

所以如果遇到 powerShell 相关错误时建议用 cmd 试试。

注入相关运行时信息

这一节我们通过调试 npm 的源码来进行说明。

首先我们在 mockm 这个前端接口联调工具的源码中先来个 debugger, 注意有从 process.env 中获取 NPM_CONFIG_REGISTRY 这个环境变量,这是 npm 安装时可配置的镜像地址。2022/07/17 前端项目中运行 npm run xxx 的时候发生了什么? - 图4
然后我们再看一下这个环境变量,在当前系统中是没有定义的。

2022/07/17 前端项目中运行 npm run xxx 的时候发生了什么? - 图5
让我们开始调试 mockm package.json 中的 scripts npm run s2

  1. {
  2. "scripts": {
  3. "s2": "node run.js remote --config=D:/git2/mockm/server/example/full.mm.config.js",
  4. }
  5. }
  6. npm run s2

2022/07/17 前端项目中运行 npm run xxx 的时候发生了什么? - 图6
为了节省篇幅,这里直接断点在关键地点:2022/07/17 前端项目中运行 npm run xxx 的时候发生了什么? - 图7

这是 npm@v6.x 的源码,可以发现 npm 使用了 npm-lifecycle 这个依赖来运行的子进程调用我们的 run.js文件。
在通过 spawn 运行 run.js 的时候,同时设置了进程相关的一些信息,这是由 node 原生支持的。
2022/07/17 前端项目中运行 npm run xxx 的时候发生了什么? - 图8例如刚刚说到的 NPM_CONFIG_REGISTRY 环境变量。

下面把继续进入下一个断点, run.js 文件:2022/07/17 前端项目中运行 npm run xxx 的时候发生了什么? - 图9可以发现子进程成功获取了父进程给予的信息。

总结