彻底搞懂 npm run 原理前言 这是 Vite 源码系列的开篇,在该系列中我们会根据 Vite 流程来阅读它的源码。
前言
在分析 Vite
原理之前,我们需要先了解一下 npm
包的全局安装、局部安装以及 npm run <command>
这个命令的原理。有了这些前置条件,我们才能找到 vite
的入口文件,从而开启 vite
世界大门,对它的实现细节一探究竟。
<command>
表示这是一个可选参数。如果提供了 command
,npm run
会执行package.json
文件中 scripts
对象上和 command
同名的脚本,否则会列出 package.json
文件中 scripts
对象上所有的脚本。
在分析完npm run command
原理后,我们会写一个 nodejs
脚本模拟这个命令执行的过程,来加深对其原理的理解。
最后我们在上面的前置条件的基础上,一起寻找 Vite
的入口文件,为分析 Vite
做准备。
全局安装
当使用 npm install -global [package]
安装包时,包会被安装在 nodejs
安装目录(安装 nodejs 时选择的安装路径)的 node_modules
文件夹下,同时会在 nodejs
安装目录创建和包同名的可执行文件。这些同名的可执行文件被执行时会通过 nodejs 来执行 node_modules
目录下对应包中的 js 文件。
由于 nodejs 安装目录在安装 nodejs 时已经被加入到 Path 环境变量中,所以在命令行中输入和可执行文件同名的命令就可以直接调用这些可执行文件。
看个例子,下面是在全局安装 Vite
时生成的 cmd
可执行文件内容:
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\vite\bin\vite.js" %*
可以看到该可执行文件的最后一行逻辑是通过 nodejs 来执行 ./node_modules/vite/bin/vite.js
这个 js 文件。
使用 nvm 管理 nodejs 时全局安装目录会有所变化。可通过 npm root -g
命令查看全局包的具体安装位置。
局部安装
安装包时如果不添加 -g
或者 -global
选项,包会被安装在当前目录下的 node_modules/
文件夹中。
如果被安装的包的 package.json
中有 bin
这个属性,npm
就会在当前目录下的 node_modules/.bin
文件夹中创建同名的可执行文件。这些同名的可执行文件和全局安装时生成的可执行文件作用相同,被执行时会通过 nodejs 来执行 当前目录下的node_modules
目录下的对应的包中的 js 文件(通常这个 js 文件放在包的 .bin
文件夹中)。
局部安装时当前目录下的 node_modules/.bin
路径没有被添加到 Path 环境变量中,所以无法在命令行直接输入同名的命令来执行。
但是 npm
给我们提供了一种简单的方式,就是 npm run command
这个命令。首先在 package.json
中的上 scripts
对象上添加一个属性,属性名对应 npm run command
命令中的 command
,属性值是你需要执行的具体命令。比如属性名是 dev
,属性值是 vite dev
。然后在当前目录下命令行中输入 npm run dev
就可以调用 Vite
了。
看个例子,下面是在局部安装 Vite
时生成的 cmd
可执行文件:
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\..\vite\bin\vite.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\..\vite\bin\vite.js" %*
)
可以看到该可执行文件的两个逻辑分支的最后都是通过 nodejs 来执行 ../vite/bin/vite.js
这个 js 文件。
npm run command 过程
先看流程图,如下:
主要流程有以下几个阶段。
-
解析命令行,获取
npm run
后面的command
。 -
读取 package.json 文件的内容。根据第一步获取的
command
来匹配package.json
中scripts
对象,拿到需要执行的命令。 -
将当前目录下的
node_modules/.bin
文件夹路径临时加入到Path
环境变量中这样就可以在命令行中输入和可执行文件同名的命令来执行可执行文件。临时加入的环境变量只会在npm run
运行期间有效,不会影响系统真正的Path
环境变量。
关于临时将node_modules/.bin
加入到 Path
环境变量看这里npm run script
In addition to the shell's pre-existing
PATH
,npm run
addsnode_modules/.bin
to thePATH
provided to scripts.
- 执行第二步拿到的命令,当前目录下
node_modules/.bin
文件夹下同名的可执行文件被执行,接着相应包中的 js 文件被执行。
用 nodejs 脚本模拟 npm run command
根据上面的几个步骤,我们写一个 nodejs
的脚本来模拟 npm run command
的过程。
新建文件夹 npmRun
,进入 npmRun
文件夹通过初始化命令 npm init
初始化项目,此时会在当前文件夹下生成一个 package.json
文件。
接着通过命令 npm install --save-dev vite@latest
将 Vite
局部安装到 npmRun
文件夹。
我们在nomRun
的根目录添加一个 index.html
文件。因为Vite
在做开发服务器时会默认启动一个以当前文件夹为根目录的服务,所以如果能在浏览器中正常访问我们添加的 html
文件,则说明 Vite
启动成功了。
接着在 package.json
文件中的 scripts
对象上添加 dev: vite --port 8090
属性,这里我们通过 --port
选项设置 vite
服务的端口号为 8090
。
最后新建一个 npm.js
文件,这个文件就是我们模拟 npm run command
过程的 nodejs 脚本。
到这我们的准备工作已经做完,接下来开始写 nodejs 脚本。
首先我们需要用到 nodejs 两个方法。fs
模块的 readFileSync
用来读取 package.json
文件内容,child_process
模块的 exec
用来执行命令行命令。
引入模块:
const { readFileSync } = require("node:fs")
const { exec } = require('child_process');
通过 process
对象的 argv
获取命令行中的参数 dev
。
const argv = process.argv;
const scriptKey = argv[3];
读取 package.json 文件,根据字符串 "dev",找到对应的 script
const packageString = readFileSync("package.json", "utf-8")
const packageObj = JSON.parse(packageString)
const scriptValue = packageObj.scripts[scriptKey]
将 node_modules/bin 加入到 PATH
const Path = process.env.Path;
const tempPath = __dirname + "/node_modules/.bin"
process.env.Path = Path + ";" + tempPath
执行字符串 "dev" 对应的命令
exec(scriptValue, (err, stdout, stderr) => {
console.log(stderr)
})
脚本编写完成,我们现在需要当前目录的命令行中输入 node npm.js run dev
,然后在浏览器中输入 localhost:8090/index.html
查看能否正常访问页面。
至此我们就找到了 Vite 启动的入口文件。下一节我们将探索 vite/bin/vite.js
这个文件到底做了什么。
补充
既然最终是执行 node_modules/vite/bin/vite.js
这个文件,那么我们是否可以直接在命令行用 node 来执行这个文件呢?当然是可以的。在当前目录下打开命令行,输入 node ./node_modules/vite/bin/vite.js
可以看到 Vite 服务也能正常启动起来。既然可以直接调用,为什么还要用 npm run command
呢?因为 npm run command
提供了一些额外的功能,比如命令的串行和并行。
转载自:https://juejin.cn/post/7290835160537481277