<h1>create-react-app 的使用 </h1>
参数解析
参数 <br/> | 参数解析 | 默认值 | 是否必填 |
<project-directory> | 项目文件夹名 | 无 | 是 |
verbose | 会打印执行 (包括依赖包的安装信息等) 的详细信息 | false | 否 |
info | 打印环境的信息 (系统、cpu、npm、yarn、chrome、react 等) | false | 否 |
use-npm | 是否使用 npm | false, 默认使用 yarn | 否 |
use-pnp | yarn 解决 node_modules 的一种解决方案,将 node_modules 放在全局缓存,不放在单独项目中 | false | 否 |
typescript | 是否使用 typescript | false | 否 |
internal-testing-template | 这个参数的值将替代 react-script 中的 template 目录,一般用于开发者使用 | false | |
scripts-version |
直接传入版本号 <br/> –scripts-version=@6.10.2 |
react-scripts | 否 |
npm 方式 <br/> –scripts-version=douchi-react-script <br/> |
|||
git+https 方式 <br/> –scripts-version=git+https://github.com/mcfly001/d… <br/> |
|||
git+ssh 方式 <br/> –scripts-version=git+ssh://git@github.com:mcfly001/douchi-react-script.git <br/> |
|||
file + url 方式 <br/> –scripts-version=file:../file-react-script |
|||
压缩包的方式(以 tgz|tar.gz 结尾)<br/> –scripts-version=./file-react-script.tar.gz |
主要依赖包作用分析
包 | 包作用 |
---|---|
validate-npm-package-name | 判断给定的字符串是不是符合 npm 包名称的规范 |
chalk | 给命令行输入的 log 设置颜色 |
commander | TJ 写的,自定义 shell 命令的工具,具体使用可以查看 commander |
semver | 用于版本比较的工具,比如哪个版本大,版本命名是否合法等 |
envinfo | 输出当前环境的信息 |
如何断点调试
通过 vscode(推荐使用)
- 通过上述操作即可断点调试 create-react-app
通过 chrome
- 进入到 packages/create-react-app 目录
- 执行以下命令
// dirname 为目录名,需要添加其他参数只需要在命令后面相应添加即可
node --inspect-brk index.js dirname
- 打开 chrome 浏览器,输入以下地址
chrome://inspect/#devices
- 点击 Remote Target 下面的 inspect 即可
代码分析
环境 | 版本 |
---|---|
系统 | macOS 10.14.5 |
node | v8.10.0 |
npm | 6.10.2 |
yarn | 1.17.3 |
create-react-app | 3.0.1 |
入口文件在 packages/create-react-app/index.js
// 检查当前环境安装的 node 版本,如果版本是小于 8.0.0 就报错需要安装更高的 node 版本,同时结束进程。如果符合条件接下来正式进入 create-react-app
'use strict';
var currentNodeVersion = process.versions.node;
var semver = currentNodeVersion.split('.');
var major = semver[0];
if (major < 8) {
console.error(
'You are running Node' +
currentNodeVersion +
'.\n' +
'Create React App requires Node 8 or higher. \n' +
'Please update your version of Node.'
);
process.exit(1);
}
require('./createReactApp');
以下代码在 packages/create-react-app/createReactApp.js
/**
* 自定义 create-react-app 这个 shell 命令
* 首先从同级目录的 package.json 中获取字段 name,即 create-react-app)作为命令名称
* 同时将 package.json 中的 version 作为该命令的版本号
* 其他参数见上文详解
*/
let projectName;
const program = new commander.Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(name => {projectName = name;})
.option('--verbose', 'print additional logs')
.option('--info', 'print environment debug info')
.option(
'--scripts-version <alternative-package>',
'use a non-standard version of react-scripts'
)
.option('--use-npm')
.option('--use-pnp')
.option('--typescript')
.allowUnknownOption()
.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(` 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);
// 如果 crete-react-app 中传入了 -info,就将系统信息、npm、node、chrom、react 等基本信息输出
if (program.info) {console.log(chalk.bold('\nEnvironment Info:'));
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);
}
/**
* 检查是否有 <project-directory> 这个参数,如果没有就输入必填提示,同时结束进程
*/
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);
}
function printValidationResults(results) {if (typeof results !== 'undefined') {
results.forEach(error => {console.error(chalk.red(` * ${error}`));
});
}
}
/**
* 在 create-react-app 中还有一个参数 --internal-testing-template
* 该参数用于替换 react-scripts 中的 template
*/
const hiddenProgram = new commander.Command()
.option(
'--internal-testing-template <path-to-template>',
'(internal usage only, DO NOT RELY ON THIS)' +
'use a non-standard application template'
)
.parse(process.argv);
接下来正式进入 create-react-app 的具体内容
function createApp(
name,
verbose,
version,
useNpm,
usePnp,
useTypescript,
template
) {const root = path.resolve(name);
const appName = path.basename(root);
// 判断项目名是不是符合规范
checkAppName(appName);
// 创建文件夹
fs.ensureDirSync(name);
// 判断当前目录下是不是存在除了(.git、doc、DS_Store 等文件)外的文件
if (!isSafeToCreateProjectIn(root, name)) {process.exit(1);
}
console.log(`Creating a new React app in ${chalk.green(root)}.`);
console.log();
// 生成 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
);
const useYarn = useNpm ? false : shouldUseYarn();
const originalDirectory = process.cwd();
process.chdir(root);
// 检测 npm 是否在正确目录下执行
if (!useYarn && !checkThatNpmCanReadCwd()) {process.exit(1);
}
// 判断 node 版本是不是小于 8.10.0, 如果是就会警告,同时 react-scripts 包会变成 0.9.x
if (!semver.satisfies(process.version, '>=8.10.0')) {
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 8.10 or higher for a better, fully supported experience.\n`
)
);
// Fall back to latest supported react-scripts on Node 4
version = 'react-scripts@0.9.x';
}
if (!useYarn) {// 返回一个对象 {hasMinNpm: '是否大于 5.0.0', npmVersion: '具体版本信息'}
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 5 or higher for a better, fully supported experience.\n`
)
);
}
// Fall back to latest supported react-scripts for npm 3
version = 'react-scripts@0.9.x';
}
} else if (usePnp) {// 返回一个对象 {hasMinYarnPnp: '是否大于 1.12.0',yarnVersion: 'yarn 的具体版本'}
const yarnInfo = checkYarnVersion();
if (!yarnInfo.hasMinYarnPnp) {if (yarnInfo.yarnVersion) {
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;
}
}
// 如果默认 yarn 的代理就会生成 yarn.lock 文件
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')
);
}
}
// 安装个中依赖
run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp,
useTypescript
);
}
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp,
useTypescript
) {
// 获取 npm 或者 yarn 关于 react-scripts 的安装的地址
getInstallPackage(version, originalDirectory).then(packageToInstall => {const allDependencies = ['react', 'react-dom', packageToInstall];
if (useTypescript) {
allDependencies.push(
// TODO: get user's node version instead of installing latest'@types/node','@types/react','@types/react-dom',
// TODO: get version of Jest being used instead of installing latest
'@types/jest',
'typescript'
);
}
console.log('Installing packages. This might take a couple of minutes.');
// 获取安装的包名称
getPackageName(packageToInstall)
.then(packageName =>
checkIfOnline(useYarn).then(isOnline => ({
isOnline: isOnline,
packageName: packageName,
}))
)
.then(info => {
const isOnline = info.isOnline;
const packageName = info.packageName;
console.log(`Installing ${chalk.cyan('react')}, ${chalk.cyan('react-dom')}, and ${chalk.cyan(packageName)}...`
);
console.log();
// 安装依赖
return install(
root,
useYarn,
usePnp,
allDependencies,
verbose,
isOnline
).then(() => packageName);
})
.then(async packageName => {
// 判断 node_modules 下面所有包中对于 node 版本的最低要求
checkNodeVersion(packageName);
// 给 package.json 中的依赖添加 ^
setCaretRangeForRuntimeDeps(packageName);
const pnpPath = path.resolve(process.cwd(), '.pnp.js');
const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];
// 安装依赖同时 copy react-scripts 下面的 template 到当前目录
await executeNodeScript(
{cwd: process.cwd(),
args: nodeArgs,
},
[root, appName, verbose, originalDirectory, template],
`
var init = require('${packageName}/scripts/init.js');
console.log(33333)
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 >=8.10 and npm >=5 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 empty
console.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);
});
});
}
/**
* 1. 安装依赖(为什么要先安装依赖,因为第二步,要不然找不到 react-scripts 中的 scripts 中的 init.js 文件)* 2. 在依赖的 node_modules 中找到 react-scripts 中的 scripts 里面的 init.js 并执行其对外的方法
* 2.1 修改 package.json,写入一些 start、build、test、eject 方法
* 2.2 修改 README.md 文件,输入一些帮助信息
* 2.3 copy template 中的文件到项目文件夹下面
* 2.4 重新 install 一下
*/
function executeNodeScript({cwd, args}, data, source) {return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[...args, '-e', source, '--', JSON.stringify(data)],
{cwd, stdio: 'inherit'}
);
child.on('close', code => {if (code !== 0) {
reject({command: `node ${args.join(' ')}`,
});
return;
}
resolve();});
});
}