npm start 是一个常见的命令,通常用于启动 Node.js 项目或前端应用的开发服务器。要理解这个命令背后的原理,我们需要从几个角度来分析:Node.js、npm 脚本和项目的配置。
npm 和 npm script
npm 是 Node.js
的包管理工具,用于管理项目的依赖和任务。npm 支持通过脚本(scripts)来自动化执行常见的开发任务。
在 package.json
文件中,scripts
字段定义了不同的任务和命令。例如:
{
"scripts": {
"start": "node server.js"
}
}
在这个例子中,npm start
将会执行 node server.js
命令。npm start
实际上是执行 scripts.start
字段中定义的命令。
React 项目中的 npm start
在 React 和 Vue 项目中执行 npm start 的机制虽然有些不同,但其原理都基于 npm 脚本配置以及开发工具链的工作。
React 项目通常使用 Create React App(CRA)工具来搭建。这是一个开箱即用的脚手架,自动化了很多开发流程,包括构建工具、开发服务器和热重载等。
首先我们来使用 CRA 脚手架来创建一个项目:
npx create-react-app start
对于 CRA 创建出来的 React 项目,npm start 执行的命令通常是:
"scripts": {
"start": "react-scripts start"
}
react-scripts
是 CRA
默认包含的一个工具包,包含了项目的启动、构建和测试等功能。当我们安装这个依赖包的时候,它会默认在 node_modules 上添加一个 .bin
目录:
react-scripts.cmd 和 react-scripts.ps1 这两个文件是与 React 项目开发相关的执行脚本,它们用于在不同操作系统上执行 react-script
命令:
- react-scripts.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%\..\react-scripts\bin\react-scripts.js" %*
- react-scripts.ps1 代码如下:
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../react-scripts/bin/react-scripts.js" $args
} else {
& "$basedir/node$exe" "$basedir/../react-scripts/bin/react-scripts.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../react-scripts/bin/react-scripts.js" $args
} else {
& "node$exe" "$basedir/../react-scripts/bin/react-scripts.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
当你运行 npm start 时,实际上是执行 package.json
文件中的 scripts.start 配置。例如,如果你的 package.json
配置如下:
"scripts": {
"start": "react-scripts start"
}
npm start 会运行 react-scripts start 命令。
react-scripts 实际上是通过 react-scripts.cmd 或 react-scripts.ps1 来执行的脚本,这两个文件分别用于 Windows 批处理和 PowerShell 环境。
react-scripts.cmd 的执行
在执行 npm start 时,首先会运行 react-scripts.cmd(如果是 Windows 系统)。其执行流程如下:
- 跳转到
find_dp0
:这部分代码用于获取当前脚本所在的路径,并将其存储在 dp0 变量中。
:find_dp0
SET dp0=%~dp0
EXIT /b
- 设置 node.exe 路径:接着检查 dp0 路径下是否存在 node.exe。如果存在,则使用该 node.exe,否则使用全局的 node 命令。
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
- 执行 react-scripts.js:在确保 node.exe 的路径后,脚本将会执行 react-scripts 的 JavaScript 文件 react-scripts.js,并传递给它 npm start 时传入的参数(
%*
)。
"%_prog%" "%dp0%\..\react-scripts\bin\react-scripts.js" %*
这行命令启动了 React 项目的开发服务器或者执行构建相关的任务。
react-scripts.ps1 文件的执行和上面的逻辑也是差不多一样,这两个脚本都会定位到 node 执行程序,并运行 react-scripts/bin/react-scripts.js
。
react-scripts.js
在前面的内容中讲到执行 react-scripts.js
文件实际上就是执行下图的文件:
'use strict';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', (err) => {
throw err;
});
const spawn = require('react-dev-utils/crossSpawn');
const args = process.argv.slice(2);
const scriptIndex = args.findIndex(
(x) => x === 'build' || x === 'eject' || x === 'start' || x === 'test',
);
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];
if (['build', 'eject', 'start', 'test'].includes(script)) {
const result = spawn.sync(
process.execPath,
nodeArgs.concat(require.resolve('../scripts/' + script)).concat(args.slice(scriptIndex + 1)),
{ stdio: 'inherit' },
);
if (result.signal) {
if (result.signal === 'SIGKILL') {
console.log(
'The build failed because the process exited too early. ' +
'This probably means the system ran out of memory or someone called ' +
'`kill -9` on the process.',
);
} else if (result.signal === 'SIGTERM') {
console.log(
'The build failed because the process exited too early. ' +
'Someone might have called `kill` or `killall`, or the system could ' +
'be shutting down.',
);
}
process.exit(1);
}
process.exit(result.status);
} else {
console.log('Unknown script "' + script + '".');
console.log('Perhaps you need to update react-scripts?');
console.log('See: https://facebook.github.io/create-react-app/docs/updating-to-new-releases');
}
在这里的代码中,主要还是获取后面的参数来判断具体执行哪个文件,也就是区分不用的环境来执行不同的流程来启动 webpack:
const scriptIndex = args.findIndex(
(x) => x === 'build' || x === 'eject' || x === 'start' || x === 'test',
);
也就是执行这些不同的文件:
设置不同的环境变量来运行。
在这里就是将我们的 webpack 配置交给 webpack-dev-server 来进行处理并对输出的结果进行清除和优化,最终让开发者能看到好看的控制台输出。
这是 webpack 默认的返回的控制台信息,这是肯定不好看的,我们就需要先清空这些信息并提取关键信息来美化控制台:
总结
通过 npm start 启动应用的原理主要依赖于 npm 脚本和 package.json 中的配置。理解这一机制有助于你优化项目的启动流程、提高开发效率,并更容易进行跨平台开发。掌握它的工作原理后,你还可以自定义启动命令,集成其他工具,甚至通过调试信息优化项目的启动过程。
在上面的内容中只是讲到了 react 的,其实 vue 的脚手架也是差不多的执行原理。
如果你想自己实现一个,那么你可以看一下我们的目前正在研发的前端脚手架,就是差不多这个思路来实现的: