本文基于 create-react-app v3.4.1版本
概述 - package.json
- 先看
package.json,下面👇是完整内容 ```json { “name”: “create-react-app”, “version”: “3.4.1”, “keywords”: [ “react” ], “description”: “Create React apps with no build configuration.”, “repository”: { “type”: “git”, “url”: “https://github.com/facebook/create-react-app.git“, “directory”: “packages/create-react-app” }, “license”: “MIT”, “engines”: { “node”: “>=10” }, “bugs”: { “url”: “https://github.com/facebook/create-react-app/issues“ }, “files”: [ “index.js”, “createReactApp.js”, “yarn.lock.cached” ], “bin”: { “create-react-app”: “./index.js” }, “dependencies”: { “chalk”: “4.1.0”, “commander”: “4.1.0”, “cross-spawn”: “7.0.3”, “envinfo”: “7.5.1”, “fs-extra”: “9.0.1”, “hyperquest”: “2.1.3”, “inquirer”: “7.2.0”, “semver”: “7.3.2”, “tar-pack”: “3.4.1”, “tmp”: “0.2.1”, “validate-npm-package-name”: “3.0.0” } }
- 除去基本描述,我们比较关注的主要有以下两个:```json{..."engines": {"node": ">=10"},..."bin": {"create-react-app": "./index.js"}}
其中: engines 指明了运行所需的 node 版本至少为10以上 bin 指明了入口文件为 ./index.js
入口 - index.js
#!/usr/bin/env node// 指明这个脚本的解释程序为 node,并告知系统在 PATH 中查找 node
/*** Copyright (c) 2015-present, Facebook, Inc.* 版权所有,脸书公司,2015至今** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.* 源代码遵循 MIT 开源许可,相关文件可以在根目录中找到*/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// /!\ DO NOT MODIFY THIS FILE /!\// /!\ 不要修改这个文件 /!\// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//// create-react-app is installed globally on people's computers. This means// that it is extremely difficult to have them upgrade the version and// because there's only one global version installed, it is very prone to// breaking changes.// create-react-app 在人们的电脑上都是全局安装的,这意味着版本升级会变得非常困难。// 也正因如此,哪怕仅仅是一点小小的修改也可能对其造成破坏性的改变。//// The only job of create-react-app is to init the repository and then// forward all the commands to the local version of create-react-app.// create-react-app 的唯一任务就是初始化一个仓库,// 然后按步执行本地版本的 create-react-app 中的每一条命令。//// If you need to add a new command, please add it to the scripts/ folder.// 如果你需要添加一条命令,请把它加在 scripts/ 文件夹中。//// The only reason to modify this file is to add more warnings and// troubleshooting information for the `create-react-app` command.// 只有当你想要在 `create-react-app` 命令行中添加一些警告或者错误提示信息的情况下,// 才应该考虑修改本文件。//// Do not make breaking changes! We absolutely don't want to have to// tell people to update their global version of create-react-app.// 不要擅自改动!我们完全不希望不得不让人们来升级 create-react-app 的版本//// Also be careful with new language features.// This file must work on Node 0.10+.// 还需注意的是新的语言特性// 这个文件必须运行在 node 10+ 版本//// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// /!\ DO NOT MODIFY THIS FILE /!\// /!\ 不要修改这个文件 /!\// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
主要是一些提示信息,总结起来基本都是在说:不要修改这个文件!!!
'use strict';// 使用严格模式
详见:严格模式 - MDN
var currentNodeVersion = process.versions.node;// 获取 node 的版本
其中 process.versions 返回的是一个对象,包含了 node 及相关依赖的版本信息(字符串形式) 详见nodejs - process.versions 例如我在 node 环境下输入 console.log(process.versions),打印值如下:

所以 process.versions.node 可以获取到 nodejs 的版本号(字符串形式)
var semver = currentNodeVersion.split('.');var major = semver[0];// 获取版本号第一个数字,例如,我的node版本是 v10.19.0,major的值即为10
major 的值用于后续判断 create-react-app 是否可以执行
if (major < 10) {console.error('You are running Node ' +currentNodeVersion +'.\n' +'Create React App requires Node 10 or higher. \n' +'Please update your version of Node.');process.exit(1);}// 如果node的版本小于10,会输出错误信息并终止程序执行。
❌ 错误提示信息:
- 你的node版本为x.x.x,
- 运行create-react-app需要node10或更新版本
- 请升级你node的版本
require('./createReactApp');
运行 ./createReactApp.js
核心 - createReactApp.js
提示信息
- 经过
index.js中版本确认后,开始执行关键代码构建脚手架了。/*** 提示信息,此处省略1409个字符* /
刚开始依旧是大段的提示信息
严格模式
'use strict';// 熟悉的严格模式
引入依赖
- 接下来是大量依赖的引入,对于第三方库,这里给出对应的 github 地址。
const chalk = require('chalk');
美化文字样式,详见:chalk
const commander = require('commander');
node.js 命令行接口的完整解决方案,详见:commander
const envinfo = require('envinfo');
envinfo:开发环境的信息(系统、编程语言、数据库等),详见:envinfo
const fs = require('fs-extra');
fs-extra:扩展文件系统能力,详见:node-fs-extra
const hyperquest = require('hyperquest');
hyperquest:优化HTTP请求,详见:hyperquest
const inquirer = require('inquirer');
inquirer:提升命令行交互能力,详见:inquirer
const semver = require('semver');
semver:语义化版本号,详见:semver
const spawn = require('cross-spawn');
cross-spawn:跨平台执行子进程的工具库,详见:node-cross-spawn
const tmp = require('tmp');
tmp:用于生成临时文件或目录,详见:node-tmp
const unpack = require('tar-pack').unpack;
tar-pack:压缩&解压缩工具,详见:tar-pack
const validateProjectName = require('validate-npm-package-name');
validate-npm-package-name:验证一个字符串是否为npm包名,详见:validate-npm-package-name
- 其他几个为内部定义的模块
const dns = require('dns');const execSync = require('child_process').execSync;const os = require('os');const path = require('path');const url = require('url');
定义关键数据
- 接下来定义了几个比较关键的数据 ```javascript const packageJson = require(‘./package.json’); // 获取到 package.json 中的信息
let projectName; // 一会可能要用到的由用户定义的项目名称
const program = new commander.Command(packageJson.name) // Command 对象是 node.js 中 EventEmitter 的扩展
- `program` 后面还接了一大堆东西,就是当我们执行 `create-react-app` 时对不同参数的处理。```javascriptconst program = new commander.Command(packageJson.name)// --version.version(packageJson.version)// 使用自定义项目名新建一个项目.arguments('<project-directory>').usage(`${chalk.green('<project-directory>')} [options]`).action(name => {projectName = name;}).option('--verbose', 'print additional logs')// --info 查看系统信息.option('--info', 'print environment debug info').option('--scripts-version <alternative-package>','use a non-standard version of react-scripts').option('--template <path-to-template>','specify a template for the created project').option('--use-npm').option('--use-pnp').allowUnknownOption()// --help.on('--help', () => {console.log(` Only ${chalk.green('<project-directory>')} is required.`);console.log();console.log(` A custom ${chalk.cyan('--scripts-version')} can be one of:`);console.log(` - a specific npm version: ${chalk.green('0.8.2')}`);console.log(` - a specific npm tag: ${chalk.green('@next')}`);console.log(` - a custom fork published on npm: ${chalk.green('my-react-scripts')}`);console.log(` - a local path relative to the current working directory: ${chalk.green('file:../my-react-scripts')}`);console.log(` - a .tgz archive: ${chalk.green('https://mysite.com/my-react-scripts-0.8.2.tgz')}`);console.log(` - a .tar.gz archive: ${chalk.green('https://mysite.com/my-react-scripts-0.8.2.tar.gz')}`);console.log(` It is not needed unless you specifically want to use a fork.`);console.log();console.log(` A custom ${chalk.cyan('--template')} can be one of:`);console.log(` - a custom template published on npm: ${chalk.green('cra-template-typescript')}`);console.log(` - a local path relative to the current working directory: ${chalk.green('file:../my-custom-template')}`);console.log(` - a .tgz archive: ${chalk.green('https://mysite.com/my-custom-template-0.8.2.tgz')}`);console.log(` - a .tar.gz archive: ${chalk.green('https://mysite.com/my-custom-template-0.8.2.tar.gz')}`);console.log();console.log(` If you have any problems, do not hesitate to file an issue:`);console.log(` ${chalk.cyan('https://github.com/facebook/create-react-app/issues/new')}`);console.log();}).parse(process.argv);
这里其实有用到
jQuery中的链式调用的思想,每次调用方法都会返回一个Command对象。当我们执行 create-react-app —help 时,会打印出上述一大段的
console.log,提示我们可以使用的参数,基本都可以在代码中找到,--version、--verbose、--script-version、--template、--use-npm、--use-pnp、--help。

两个判断
第一个判断(info)
- 接下来是两个判断,首先如果我们输入的是
create-react-app --info,会按照固定格式打印出系统信息if (program.info) {console.log(chalk.bold('\nEnvironment Info:'));console.log(`\n current version of ${packageJson.name}: ${packageJson.version}`);console.log(` running from ${__dirname}`);return envinfo.run({System: ['OS', 'CPU'],Binaries: ['Node', 'npm', 'Yarn'],Browsers: ['Chrome', 'Edge', 'Internet Explorer', 'Firefox', 'Safari'],npmPackages: ['react', 'react-dom', 'react-scripts'],npmGlobalPackages: ['create-react-app'],},{duplicates: true,showNotFound: true,}).then(console.log);}
例如,当我输入 create-react-app —info 时,控制台输出的信息如下:

可以看到,同代码中的一模一样,打印出了 create-react-app版本、System信息、node相关信息、浏览器信息、react相关包信息、create-react-app相关信息(可能因为我是用yarn安装的所以没找到?)
第二个判断(projectName 项目名)
- 判断我们是否给出了项目名,如果没给出,会输出错误提示并退出。
if (typeof projectName === 'undefined') {console.error('Please specify the project directory:');console.log(` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}`);console.log();console.log('For example:');console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`);console.log();console.log(`Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`);process.exit(1);}

开始创建(createApp)
调用 createApp()
- 如果我们按照格式给出了项目名,那么接下来就开始创建App。
createApp(projectName, // 用户定义的项目名program.verbose, // 额外日志program.scriptsVersion, // 使用其他的 react-scriptsprogram.template, // 明确模板program.useNpm, // 使用npmprogram.usePnp // 使用pnpm?);
createApp() 函数概述
createApp函数的主体如下function createApp(name, verbose, version, template, useNpm, usePnp) {...}
由于 createApp 函数代码比较长,我们抛弃一些细节,来试图分段理解一下。
1. 检查node版本
const unsupportedNodeVersion = !semver.satisfies(process.version, '>=10');if (unsupportedNodeVersion) {console.log(chalk.yellow(`You are using Node ${process.version} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +`Please update to Node 10 or higher for a better, fully supported experience.\n`));// Fall back to latest supported react-scripts on Node 4version = 'react-scripts@0.9.x';}
2. 检查用户定义的项目名是否符合要求
const root = path.resolve(name);const appName = path.basename(root);checkAppName(appName);fs.ensureDirSync(name);if (!isSafeToCreateProjectIn(root, name)) {process.exit(1);}console.log();
其中,值得注意的是 fs 作为文件读写的一个接口 另外一个值得注意的是
checkAppName()这个函数,在800行左右可以看到它的源码如下。
function checkAppName(appName) {const validationResult = validateProjectName(appName);if (!validationResult.validForNewPackages) {console.error(chalk.red(`Cannot create a project named ${chalk.green(`"${appName}"`)} because of npm naming restrictions:\n`));[...(validationResult.errors || []),...(validationResult.warnings || []),].forEach(error => {console.error(chalk.red(` * ${error}`));});console.error(chalk.red('\nPlease choose a different project name.'));process.exit(1);}// TODO: there should be a single place that holds the dependenciesconst dependencies = ['react', 'react-dom', 'react-scripts'].sort();if (dependencies.includes(appName)) {console.error(chalk.red(`Cannot create a project named ${chalk.green(`"${appName}"`)} because a dependency with the same name exists.\n` +`Due to the way npm works, the following names are not allowed:\n\n`) +chalk.cyan(dependencies.map(depName => ` ${depName}`).join('\n')) +chalk.red('\n\nPlease choose a different project name.'));process.exit(1);}}
可以看到,基本就是做了两个判断
- 一是判断用户自定义项目名是否符合命名规则,如果不符合,就会给出错误提示并退出程序:

- 二是判断用户自定义的项目名是否和依赖冲突,如果冲突,会给出错误提示并退出程序:

3. 提示开始创建
- 打印一行信息,告诉我们创建要开始了。
console.log(`Creating a new React app in ${chalk.green(root)}.`);console.log();
4. 初始化 package.json
- 初始化一些基本信息,例如 name(项目名)、version(0.1.0)、private(true),我们可以在生成的 cra-test 项目根目录下找到 package.json ,其中最开始几行就是这里初始化的。 ```javascript // package.json 最初的样子 const packageJson = { name: appName, version: ‘0.1.0’, private: true, };
// 写入文件 fs.writeFileSync( path.join(root, ‘package.json’), JSON.stringify(packageJson, null, 2) + os.EOL );
<a name="5dZ2e"></a>#### 5. yarn? npm? pnpm?- 接下来是判断到底使用 yarn、npm、pnpm中的哪一个,此处的细节就不去深究了。我使用的是 `yarn`。```javascriptconst useYarn = useNpm ? false : shouldUseYarn();const originalDirectory = process.cwd();process.chdir(root);if (!useYarn && !checkThatNpmCanReadCwd()) {process.exit(1);}if (!useYarn) {const npmInfo = checkNpmVersion();if (!npmInfo.hasMinNpm) {if (npmInfo.npmVersion) {console.log(chalk.yellow(`You are using npm ${npmInfo.npmVersion} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +`Please update to npm 6 or higher for a better, fully supported experience.\n`));}// Fall back to latest supported react-scripts for npm 3version = 'react-scripts@0.9.x';}} else if (usePnp) {const yarnInfo = checkYarnVersion();if (yarnInfo.yarnVersion) {if (!yarnInfo.hasMinYarnPnp) {console.log(chalk.yellow(`You are using Yarn ${yarnInfo.yarnVersion} together with the --use-pnp flag, but Plug'n'Play is only supported starting from the 1.12 release.\n\n` +`Please update to Yarn 1.12 or higher for a better, fully supported experience.\n`));// 1.11 had an issue with webpack-dev-middleware, so better not use PnP with it (never reached stable, but still)usePnp = false;}if (!yarnInfo.hasMaxYarnPnp) {console.log(chalk.yellow('The --use-pnp flag is no longer necessary with yarn 2 and will be deprecated and removed in a future release.\n'));// 2 supports PnP by default and breaks when trying to use the flagusePnp = false;}}}if (useYarn) {let yarnUsesDefaultRegistry = true;try {yarnUsesDefaultRegistry =execSync('yarnpkg config get registry').toString().trim() ==='https://registry.yarnpkg.com';} catch (e) {// ignore}if (yarnUsesDefaultRegistry) {fs.copySync(require.resolve('./yarn.lock.cached'),path.join(root, 'yarn.lock'));}}
如果使用的是 yarn,会生成 yarn.lock 文件。
6. 执行 run() 函数(核心逻辑)
run()函数应该是整个 create-react-app.js 的核心,其中用到了大量的 Promise,Promise 我目前理解地不太深入,所以打算先简单介绍一下,后续再回来补充。run()最重要的目的就是安装各种 packages,我们通过run()对其它函数的调用粗略地看一下整个过程。
run() 函数(核心中的核心)
Promise.all([getInstallPackage(version, originalDirectory),getTemplateInstallPackage(template, originalDirectory),]).then(([packageToInstall, templateToInstall]) => {const allDependencies = ['react', 'react-dom', packageToInstall];console.log('Installing packages. This might take a couple of minutes.');Promise.all([// 获取包名getPackageInfo(packageToInstall),getPackageInfo(templateToInstall),]).then(([packageInfo, templateInfo]) =>checkIfOnline(useYarn).then(isOnline => ({// 检查是否在线isOnline,packageInfo,templateInfo,}))).then(({ isOnline, packageInfo, templateInfo }) => {let packageVersion = semver.coerce(packageInfo.version);const templatesVersionMinimum = '3.3.0';// Assume compatibility if we can't test the version.// 查看npm包是否存在if (!semver.valid(packageVersion)) {packageVersion = templatesVersionMinimum;}// Only support templates when used alongside new react-scripts versions.const supportsTemplates = semver.gte(packageVersion,templatesVersionMinimum);if (supportsTemplates) {allDependencies.push(templateToInstall);} else if (template) {console.log('');console.log(`The ${chalk.cyan(packageInfo.name)} version you're using ${packageInfo.name === 'react-scripts' ? 'is not' : 'may not be'} compatible with the ${chalk.cyan('--template')} option.`);console.log('');}console.log(`Installing ${chalk.cyan('react')}, ${chalk.cyan('react-dom')}, and ${chalk.cyan(packageInfo.name)}${supportsTemplates ? ` with ${chalk.cyan(templateInfo.name)}` : ''}...`);console.log();// 开始安装进程return install(root,useYarn,usePnp,allDependencies,verbose,isOnline).then(() => ({packageInfo,supportsTemplates,templateInfo,}));}).then(async ({ packageInfo, supportsTemplates, templateInfo }) => {const packageName = packageInfo.name;const templateName = supportsTemplates ? templateInfo.name : undefined;checkNodeVersion(packageName);setCaretRangeForRuntimeDeps(packageName);const pnpPath = path.resolve(process.cwd(), '.pnp.js');const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];await executeNodeScript({cwd: process.cwd(),args: nodeArgs,},[root, appName, verbose, originalDirectory, templateName],`var init = require('${packageName}/scripts/init.js');init.apply(null, JSON.parse(process.argv[1]));`);if (version === 'react-scripts@0.9.x') {console.log(chalk.yellow(`\nNote: the project was bootstrapped with an old unsupported version of tools.\n` +`Please update to Node >=10 and npm >=6 to get supported tools in new projects.\n`));}}).catch(reason => {console.log();console.log('Aborting installation.');if (reason.command) {console.log(` ${chalk.cyan(reason.command)} has failed.`);} else {console.log(chalk.red('Unexpected error. Please report it as a bug:'));console.log(reason);}console.log();// On 'exit' we will delete these files from target directory.const knownGeneratedFiles = ['package.json','yarn.lock','node_modules',];const currentFiles = fs.readdirSync(path.join(root));currentFiles.forEach(file => {knownGeneratedFiles.forEach(fileToMatch => {// This removes all knownGeneratedFiles.if (file === fileToMatch) {console.log(`Deleting generated file... ${chalk.cyan(file)}`);fs.removeSync(path.join(root, file));}});});const remainingFiles = fs.readdirSync(path.join(root));if (!remainingFiles.length) {// Delete target folder if emptyconsole.log(`Deleting ${chalk.cyan(`${appName}/`)} from ${chalk.cyan(path.resolve(root, '..'))}`);process.chdir(path.resolve(root, '..'));fs.removeSync(path.join(root));}console.log('Done.');process.exit(1);});});
