create-react-app是Facebook提供的一个用于快速构建React前端项目的工具,只需简单运行几个命令即可创建一个React应用。它的特点就是零配置,开箱即用。
那么,在执行create-react-app命令之后,究竟发生了什么呢?接下来就通过源码来一探究竟。先通过流程图大致看一下完整的流程:
首先要知道的是,一般create-react-app这个npm包是全局安装的(也可以选择非全局安装,这种情况暂不讨论),因此可以在任意目录下通过bash或者CMD直接调用create-react-app命令,在以下目录可以找到create-react-app的执行入口:
C:/Users/{用户名}/AppData/Roaming/npm/
在此目录下可以看到有create-react-app和create-react-app.cmd两个文件,它们分别可以被bash和CMD中执行的“create-react-app app”命令调用(为方便讲解,假设填入的参数值为“app”),这两个文件的作用是一样的,那就是用Node调用一份js文件,并传入字符串“app”作为参数。以下则是create-react-app.cmd文件内容。
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\node_modules\create-react-app\index.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\node_modules\create-react-app\index.js" %*
)
由以上代码可以发现,它所执行的js文件就位于“C:/Users/{用户名}/AppData/Roaming/npm/node_modules/create-react-app/index.js”。如果打开此文件,会发现其代码比较简短,主要就是判断了一下当前Node的版本,如果主版本号小于4就终止执行,否则就调用同一相同目录下的“createReactApp.js”。至此,程序流程已转入“createReactApp.js”。
在“createReactApp.js”中,首先通过"commander"库解析出项目名参数值为"app"并保存到变量,然后解析出其他几个参数用这些参数调用“createApp”函数,代码如下:
createApp(
projectName,
program.verbose,
program.scriptsVersion,
program.useNpm,
hiddenProgram.internalTestingTemplate
);
在“createApp()”中,程序作了若干检查后,会在最开始执行"create-react-app"命令的位置以"app"为名创建一个新目录作为项目目录,同时“app”也是项目名。在创建目录后,程序会在app目录内创建“package.json”文件,并向其写入以下内容:
{
"name": "app",
"version": "0.1.0",
"private": true
}
而后又对Node版本进行了检查,满足条件后确定依赖库react-scripts的版本,然后调用“run”函数:
run(root, appName, version, verbose, originalDirectory, template, useYarn);
在“run()”函数中,主要是通过Promise连续then()来处理一些流程,主要有确定react-scripts包名,检查当前环境是否联网等,然后调用“install()”函数安装react、react-dom、react-scripts这三个依赖包,在安装完成后,调用react-scripts包内的init.js文件,关键代码为:
const scriptsPath = path.resolve(
process.cwd(),
'node_modules',
packageName,
'scripts',
'init.js'
);
const init = require(scriptsPath);
init(root, appName, verbose, originalDirectory, template);
至此,程序流程已转移到项目目录app/node_modules/react-scripts/scripts/init.js中。
在init.js中,首先给package.json添加了"scripts"字段,其内容中包含了start、build、test、eject这几个关键命令,代码为:
appPackage.scripts = {
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test --env=jsdom',
eject: 'react-scripts eject',
};
fs.writeFileSync(
path.join(appPath, 'package.json'),
JSON.stringify(appPackage, null, 2)
);
若已有README.md文件,则将其重命名为README.old.md,然后将react-scripts中template目录下的内容复制到当前项目目录,这些文件里包含了一个简单的react项目的代码。因为要复制过来的文件中含“README.md”文件,故刚刚要先对已有的README.md文件重命名。
最后,程序会检测项目目录中是否已安装了react和react-dom这两个依赖包,若未安装,则使用yarn或npm安装它们。
至此,create-react-app的全部流程执行完毕。
从源码来看,整个程序的执行流程就像一个流水线,一步接一步地向后执行。其所需完成的功能并不是很多,但整个代码量却不少,原因就在于在执行这套流程的过程中,要对执行环境、Node环境等进行大量的检查以保障程序的健壮性和对多种场景的适应性。
另外,在react-scripts包的目录中可以看到,除了存放示例项目代码的template目录,还有bin、config、scripts这几个目录。
其中,config目录中存放了webpack的配置文件,而bin和scripts目录中则是一些js文件。
在项目的package.json文件中可以看到:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
这就意味着执行“yarn start”等命令时,实际是执行react-scripts命令,而这个命令会通过上面提到的bin目录中的react-scripts文件根据不同的动作将程序流程分发到scripts目录中对应的js文件,例如“yarn start”实际最终执行的是react-scripts/scripts/start.js文件。