create-react-app
是一个react
官方支持的创建项目的脚手架,可以说大多数学过react
的同学都使用过create-react-app
创建项目。
最简单的创建命令,所有参数都默认,只提供☝🏻项目名称:
npx create-react-app demo
本文主要是讲解create-react-app
(后面简称cra)的执行过程和核心源码。
🤔思考
在看源码前,大致想了一下思路:
- 获取用户输入的项目名
- 创建一个空项目
- copy模板
- 安装依赖
接下来,看看cra的实际流程。
cra的核心库
从github上clone下来的cra项目是一个使用lerna
管理的包含多个package的项目,整个packages如下:

其中核心的package有三个:
- create-react-app:处理全局命令,创建React项目,安装依赖并调用react-scripts初始化。
- react-scripts:用来生成项目模板,包含项目package.json中scripts命令的执行文件。
- cra-template:为create-react-app提供默认模板。
在cra的入口文件,首先会检验node
版本是否大于等于14,如果不是会输出提升并结束程序:
const currentVersion = process.versions.node;
const semver = currentVersion.split(".");
const major = semver[0]; // 大版本
if (major < 14) {
console.error(
'You are running Node ' +
currentNodeVersion +
'.\n' +
'Create React App requires Node 14 or higher. \n' +
'Please update your version of Node.'
);
process.exit(1);
}
运行流程

- 检验项目名(用户是否输入以及是否符合npm命名规范)
- 检查npm上
create-react-app
最新版本号和本地版本号是否一致
function checkForLatestVersion() {
return new Promise((resolve, reject) => {
https
.get(
'https://registry.npmjs.org/-/package/create-react-app/dist-tags',
res => {
if (res.statusCode === 200) {
let body = '';
res.on('data', data => (body += data));
res.on('end', () => {
resolve(JSON.parse(body).latest);
});
} else {
reject();
}
}
)
.on('error', () => {
reject();
});
});
}
checkForLatestVersion()
.catch(() => {
try {
return execSync('npm view create-react-app version').toString().trim();
} catch (e) {
return null;
}
})
.then(latest => {
if (latest && semver.lt(packageJson.version, latest)) {
// ......输出提示
process.exit(1);
} else {
// 到这里才开始创建项目
createApp(
projectName,
program.verbose,
program.scriptsVersion,
program.template,
useYarn,
program.usePnp
);
}
});
本地的cra等于npm上cra版本后才会调用createApp
,接下来看一下createApp
到底做了什么。
新建项目和package.json
// ......
fs.ensureDirSync(name);
程序在执行完fs.ensureDirSync(name)
,一个空项目就建完了。
接着新建package.json,初始的package.json只有name、version和private三个key:
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
// 新建package.json
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
);
和上文的思考有点区别,package.json文件是单独新建的(而不是我以为的模板复制)。
安装依赖
cra预装的依赖有4个:cra-template
、react-scripts
、react
、react-dom
。
通过cross-spawn
模块的spawn
来执行安装命令:
const spawn = require('cross-spawn');
function install(root, dependencies) {
return new Promise((resolve, reject) => {
let command = 'npm';
let args = [
'install',
'--no-audit',
'--save',
'--save-exact',
'--loglevel',
'error',
].concat(dependencies);
const child = spawn(command, args, { stdio: 'inherit' });
child.on('close', code => {
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`,
});
return;
}
resolve();
});
})
}
断点调试结果:

执行完这一步package.json中添加了4个依赖:
{
"name": "demo",
"version": "0.1.0",
"private": true,
"dependencies": {
"cra-template": "1.1.3",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-scripts": "5.0.0"
}
}
运行init.js
init.js位于react-scripts目录下,主要是生成项目模板。在createReactApp.js中安装完依赖后执行:
await executeNodeScript(
{
cwd: process.cwd(),
args: nodeArgs,
},
[root, appName, verbose, originalDirectory, templateName],
`
const init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
运行流程如下:

上文cra的核心库提到cra-template包含一个模板项目,包括:
- template.json
- template目录
其中template目录是模板项目。
package.json写入scripts
默认的模板项目,package.json中scripts只有4个命令:
const templateScripts = templatePackage.scripts || {};
appPackage.scripts = Object.assign(
{
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test',
eject: 'react-scripts eject',
},
templateScripts
);
其他配置合并:
// Setup the eslint config
appPackage.eslintConfig = {
extends: 'react-app',
};
// Setup the browsers list
appPackage.browserslist = defaultBrowsers;
// Add templatePackage keys/values to appPackage, replacing existing entries
templatePackageToReplace.forEach(key => {
appPackage[key] = templatePackage[key];
});
合并后更新package.json:
// 更新项目package.json
fs.writeFileSync(
path.join(appPath, 'package.json'),
JSON.stringify(appPackage, null, 2) + os.EOL
);
copy整个template目录
复制整个模板项目到新建的项目目录下:
const templateDir = path.join(templatePath, 'template');
if (fs.existsSync(templateDir)) {
fs.copySync(templateDir, appPath);
}
安装package.json新增的依赖:
let command = 'npm';
let args = [
'install',
'--no-audit',
'--save',
];
// 收集依赖
const dependenciesToInstall = Object.entries({
...templatePackage.dependencies,
...templatePackage.devDependencies,
});
if (dependenciesToInstall.length) {
args = args.concat(
dependenciesToInstall.map(([dependency, version]) => {
return `${dependency}@${version}`;
})
);
}
// 安装依赖
const proc = spawn.sync(command, args, { stdio: 'inherit' });
此时的package.json如下:
{
// .....
"dependencies": {
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"cra-template": "1.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
// ......
}
注意我们的依赖cra-template
的作用就是作为模板,现在模板的依赖已经安装完成&已经拷贝到新项目下,所以cra-template
就可以删除。
let remove = 'uninstall';
const proc = spawn.sync(command, [remove, templateName], {
stdio: 'inherit',
});
断点调试结果:

到这一步整个cra的流程基本走完了。接下来看看上文提到的scripts中的4个命令:
scripts中的命令
package.json中有以下4个命令:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
这4个命令刚好对应react-scripts模块下scripts下的4个文件:

该模块的入口文件是bin/react-scripts.js:
"bin": {
"react-scripts": "./bin/react-scripts.js"
},
通过process.argv
判断用户使用了哪个命令:
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' }
);
// .....
}
总结
运行cra命令行创建项目,会在create-react-app模块处理用户输入和创建一个包含package.json的项目,然后执行react-scripts模块复制模板和更新package.json(包括dependencies、eslintConfig、browserslist和scripts)。
网友评论