前言
最近想写一些东西,学习学习全栈开发,就从熟悉的react开始,一直开发模式都是组件形式,对create-react-app
源码不了解,需要重新学习下!
网上找了很多资料,刷官网...
找到一个写的很全面的,但是create-react-app
已更新版本,我下载的是v3.3.0版本,版本不一样源码也会有些出入,所以这里也记录下自己的学习内容,方便后期自我查看和学习。
文中都是借鉴和一些自我理解,有些错误或者理解上的偏差,欢迎指正。
原文链接:https://segmentfault.com/a/11...
create-react-app 源码**
- 版本: v3.3.0github
- githu下载地址: https://github.com/mintsweet/...
目录
├── .github --- 项目中提的issue
和pr
的规范
├── docusaurus--- facebook的开源的用于简化构建,部署,和维护的博客网站
├── packages --- 包 项目核心
├── tasks--- 任务
├── test--- 测试文件
├── .eslintignore --- eslint
检查时忽略文件
├── .eslintrc.json --- eslint
配置文件
├── .gitignore --- git
提交时忽略文件
├── .travis.yml --- travis
配置文件
├── .yarnrc --- yarn
配置文件
├── azure-pipelines-test-job.yml--- Azure Pipelines
test配置
├── azure-pipelines.yml --- Azure Pipelines
配置,用于在Linux
,Windows
和macOS
上构建和测试create-react-app
├── CHANGELOG-0.x.md --- 版本变更说明文件
├── CHANGELOG-1.x.md --- 版本变更说明文件
├── CHANGELOG-2.x.md --- 版本变更说明文件
├── CHANGELOG.md --- 当前版本变更说明文件
├── CODE_OF_CONDUCT.md --- facebook
代码行为准则说明
├── CONTRIBUTING.md--- 项目的核心说明
├── lerna.json--- lerna
配置文件
├── LICENSE --- 开源协议
├── netlify.toml --- 可以理解为docusaurus
的配置设置
├── package.json --- 项目配置文件
├── README.md --- 项目使用说明
├── screencast-error.svg
└── screencast.svg
packages 文件
├── babel-plugin-named-asset-import
├── babel-preset-react-app
├── confusing-browser-globals
├── cra-template
├── cra-template-typescript
├── create-react-app
├── eslint-config-react-app
├── react-app-polyfill
├── react-dev-utils
├── react-error-overlay
└── react-scripts
代码的入口在packages/create-react-app/index.js
下,核心代码在createReactApp.js
中
断点调试:配置 vscode 的 debug 文件
这里添加三种环境,是 create-react-app 的不同种使用方式
create-react-app study-create-react-app-source
create-react-app
create-react-app study-create-react-app-source-ts --typescript
依赖包作用分析
包 | 包作用 |
---|---|
validate-npm-package-name | 判断给定的字符串是不是符合npm包名称的规范,检查包名是否合法 validate |
chalk | 给命令行输入的log设置颜色 chalk |
commander | 自定义shell命令的工具,也就是可以用它代管Node命令,具体使用可以查看commander |
semver | 用于版本比较的工具,比如哪个版本大,版本命名是否合法等 |
envinfo | 输出当前环境的信息,用于比较Node 版本 envinfo |
fs-extra | Node 自带文件模块的外部扩展模块 fs-extra |
cross-spawn | 用来执行node 进程,Node 跨平台解决方案,解决在windows 下各种问题,详细看 cross-spawn |
hyperquest | 用于将http请求流媒体传输,详细看 hyperquest |
dns | 用来检测是否能够请求到指定的地址 dns |
index.js
'use strict';
var currentNodeVersion = process.versions.node; // 返回Node版本信息,如果有多个版本返回多个版本
var semver = currentNodeVersion.split('.'); // 所有Node版本的集合
var major = semver[0]; // 取出第一个Node版本信息
if (major < 8) { // 校验当前node版本是否低于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');
index.js 的代码非常的简单,其实就是对 node 的版本做了一下校验,如果版本号低于 8,就退出应用程序,否则直接进入到核心文件中,createReactApp.js
中
createReactApp.js
createReactApp 的功能也非常简单其实,大概流程:
- 命令初始化,比如自定义
create-react-app --info
的输出等 - 判断是否输入项目名称,如果有,则根据参数去跑安装,如果没有,给提示,然后退出程序
- 修改 package.json
- 拷贝
react-script
下的模板文件
let projectName; //定义了一个用来存储项目名称的变量
const program = new commander.Command(packageJson.name)
.version(packageJson.version) //create-react-app -v 时候输出的值 packageJson 来自上面 const packageJson = require('./package.json');
.arguments('')//定义 project-directory ,必填项
.usage(`${chalk.green('')} [options]`) // 使用`create-react-app`第一行打印的信息,也就是使用说明
.action(name => {
projectName = name; //获取用户的输入,存为 projectName
})
.option('--verbose', 'print additional logs') // option配置`create-react-app -[option]`的选项,类似 --help -V
.option('--info', 'print environment debug info') // 打印本地相关开发环境,操作系统,`Node`版本等等
.option( // 指定 react-scripts
'--scripts-version ',
'use a non-standard version of react-scripts'
)
.option(// 项目创建template
'--template ',
'specify a template for the created project'
)
.option('--use-npm') // 指定使用npm 默认使用yarn
.option('--use-pnp') //指定使用 pnp
// TODO: Remove this in next major release.
.option( // 指定使用ts,不过这里也有备注下一个版本就删除这项
'--typescript',
'(this option will be removed in favour of templates in the next major release of create-react-app)'
)
.allowUnknownOption()
.on('--help', () => {
// on('option', cb) 语法,输入 create-react-app --help 自动执行后面的操作输出帮助
console.log(` Only ${chalk.green('')} 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 fork 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);// commander 文档传送门,解析正常的`node
// 判断当前环境信息,退出
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);
}
//判断 projectName 是否为 undefined,然后输出相关提示信息,退出~
if (typeof projectName === 'undefined') {
//没有项目名称也没有--info 会抛出异常
console.error('Please specify the project directory:');
console.log(
` ${chalk.cyan(program.name())} ${chalk.green('')}`
);
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);
}
create-react-app
中的项目名称赋予了projectName
变量,此处的作用就是看看用户有没有传这个
参数,如果没有就会报错,并显示一些帮助信息,这里用到了另外一个外部依赖envinfo
。
createApp
修改vscode 配置文件
{
"type": "node",
"request": "launch",
"name": "CreateReactAppTs",
"program": "${workspaceFolder}/packages/create-react- app/index.js",
"args": \[
"study-create-react-app-source-ts",
"--typescript",
"--use-npm"
\]
}
commander
的option
选项,如果加了这个选项这个值就是true
,否则就是false
,也就是说这里如果加了--typescript
和--use-npm
,那这个参数就是true
,这些也都是我们在commander
中定义的 options,在源码里面 createApp 中,传入的参数分别为:
- projectName :项目名称
- program.verbose 是否输出额外信息
- program.scriptsVersion 传入的脚本版本
- program.useNpm 是否使用
npm
,默认使用yarn
- program.usePnp 是否使用
Pnp
,默认使用yarn
- program.typescript 是否使用
ts
- hiddenProgram.internalTestingTemplate 给开发者用的调试模板路径
function createApp(
name,
verbose,
version,
template,
useNpm,
usePnp,
useTypeScript
) {
// 检测node版本
const unsupportedNodeVersion = !semver.satisfies(process.version, '>=8.10.0');
// 判断node版本,如果结合TS一起使用就需要8.10或者更高版本,具体的方法在semver.js
if (unsupportedNodeVersion && useTypeScript) {
console.error(
chalk.red(
`You are using Node ${process.version} with the TypeScript template. Node 8.10 or higher is required to use TypeScript.\n`
)
);
process.exit(1);
} else 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 8.10 or higher for a better, fully supported experience.\n`
)
);
// Fall back to latest supported react-scripts on Node 4
version = '[email protected]';
}
const root = path.resolve(name); //path 拼接路径
const appName = path.basename(root); //获取文件名
checkAppName(appName); //检查传入的文件名合法性
fs.ensureDirSync(name); //确保目录存在,如果不存在则创建一个
if (!isSafeToCreateProjectIn(root, name)) {//判断新建这个文件夹是否安全,否则直接退出
process.exit(1);
}
console.log();
// 到这里打印成功创建了一个`react`项目在指定目录下
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,
};
// 往我们创建的文件夹中写入package.json文件
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
);
// 定义常量 useYarn 如果传参有 --use-npm useYarn就是false,否则执行 shouldUseYarn() 检查yarn是否存在
// 这一步就是之前说的他默认使用`yarn`,但是可以指定使用`npm`,如果指定使用了`npm`,`useYarn`就是`false`,不然执行 shouldUseYarn 函数
// shouldUseYarn 用于检测本机是否安装了`yarn`
const useYarn = useNpm ? false : shouldUseYarn();
// 取得当前node进程的目录,提前获取保存下来,接下来这个值会被改变,如果后面需要用到这个值,后续其实取得值将不是这个
// 所以这里的目的就是提前存好,免得我后续使用的时候不好去找,这个地方就是我执行初始化项目的目录,而不是初始化好的目录,是初始化的上级目录
const originalDirectory = process.cwd();
// 修改进程目录为底下子进程目录
// 在这里就把进程目录修改为了我们创建的目录
process.chdir(root);
// checkThatNpmCanReadCwd 这个函数的作用是检查进程目录是否是我们创建的目录,也就是说如果进程不在我们创建的目录里面,后续再执行`npm`安装的时候就会出错,所以提前检查
if (!useYarn && !checkThatNpmCanReadCwd()) {
process.exit(1);
}
if (!useYarn) { //关于 npm、pnp、yarn 的使用判断,版本校验等
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 = '[email protected]';
}
} else if (usePnp) {
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;
}
}
if (useTypeScript) {
console.log(
chalk.yellow(
'The --typescript option has been deprecated and will be removed in a future release.'
)
);
console.log(
chalk.yellow(
`In future, please use ${chalk.cyan('--template typescript')}.`
)
);
console.log();
if (!template) {
template = 'typescript';
}
}
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
);
}
代码非常简单,部分注释已经加载代码中,简单的说就是对一个本地环境的一些校验,版本检查、目录创建等,如果创建失败,则退出,如果版本较低,则使用对应低版本的create-react-app
,最后调用 run 方法
-
checkAppName()
:用于检测文件名是否合法, -
isSafeToCreateProjectIn()
:用于检测文件夹是否安全 -
shouldUseYarn()
:用于检测yarn
在本机是否已经安装 -
checkThatNpmCanReadCwd()
:用于检测npm
是否在正确的目录下执行 -
checkNpmVersion()
:用于检测npm
在本机是否已经安装了
checkAppName
checkAPPName 方法主要的核心代码是validate-npm-package-name
package,从名字即可看出,检查是否为合法的 npm 包名
function checkAppName(appName) {
// 关于validateProjectName方法 在插件`validate-npm-package-name`index文件
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 dependencies
const 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);
}
}
这个函数用了一个外部依赖来校验文件名是否符合npm包文件名的规范,然后定义了三个不能取得名字react、react-dom、react-scripts,红色框出来的,之前引用的是一个函数依赖printValidationResults()
,最新的代码里面换成了ES6
属性spread
,不过这个属性在ES9
里也有新的用法
printValidationResults()
:函数引用,这个函数就是我说的特别简单的类型,里面就是把接收到的错误信息循环打印出来
isSafeToCreateProjectIn
function isSafeToCreateProjectIn(root, name) {
const validFiles = [
'.DS_Store',
'.git',
'.gitattributes',
'.gitignore',
'.gitlab-ci.yml',
'.hg',
'.hgcheck',
'.hgignore',
'.idea',
'.npmignore',
'.travis.yml',
'docs',
'LICENSE',
'README.md',
'mkdocs.yml',
'Thumbs.db',
];
// These files should be allowed to remain on a failed install, but then
// silently removed during the next create.
const errorLogFilePatterns = [
'npm-debug.log',
'yarn-error.log',
'yarn-debug.log',
];
const isErrorLog = file => {
return errorLogFilePatterns.some(pattern => file.startsWith(pattern));
};
const conflicts = fs
.readdirSync(root)
.filter(file => !validFiles.includes(file))
// IntelliJ IDEA creates module files before CRA is launched
.filter(file => !/\.iml$/.test(file))
// Don't treat log files from previous installation as conflicts
.filter(file => !isErrorLog(file));
if (conflicts.length > 0) {
console.log(
`The directory ${chalk.green(name)} contains files that could conflict:`
);
console.log();
for (const file of conflicts) {
try {
const stats = fs.lstatSync(path.join(root, file));
if (stats.isDirectory()) {
console.log(` ${chalk.blue(`${file}/`)}`);
} else {
console.log(` ${file}`);
}
} catch (e) {
console.log(` ${file}`);
}
}
console.log();
console.log(
'Either try using a new directory name, or remove the files listed above.'
);
return false;
}
// Remove any log files from a previous installation.
fs.readdirSync(root).forEach(file => {
if (isErrorLog(file)) {
fs.removeSync(path.join(root, file));
}
});
return true;
}
安全性校验,检查当前目录下是否存在已有文件,判断创建的这个目录是否包含除了上述validFiles
里面的文件
shouldUseYarn
function shouldUseYarn() {
try {
execSync('yarnpkg --version', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
execSync
是由node
自身模块child_process
引用而来,用来执行命令的,这个函数执行yarnpkg --version
来判断我们是否正确安装了yarn
,如果没有正确安装yarn
的话,useYarn
依然为false
,不管指没有指定--use-npm
。
-
execSync
:引用自child_process.execSync
,用于执行需要执行的子进程
checkThatNpmCanReadCwd
// See https://github.com/facebook/create-react-app/pull/3355
function checkThatNpmCanReadCwd() {
const cwd = process.cwd();// 当前的进程目录
let childOutput = null; // 保存npm信息
try {
// Note: intentionally using spawn over exec since
// the problem doesn't reproduce otherwise.
// `npm config list` is the only reliable way I could find
// to reproduce the wrong path. Just printing process.cwd()
// in a Node process was not enough.
childOutput = spawn.sync('npm', ['config', 'list']).output.join('');
// 相当于执行`npm config list`并将其输出的信息组合成为一个字符串
} catch (err) {
// Something went wrong spawning node.
// Not great, but it means we can't do this check.
// We might fail later on, but let's continue.
return true;
}
if (typeof childOutput !== 'string') {
return true;
}
//字符串换行分割
const lines = childOutput.split('\n');
// `npm config list` output includes the following line:
// "; cwd = C:\path\to\current\dir" (unquoted)
// I couldn't find an easier way to get it.
const prefix = '; cwd = ';// 定义需要的信息前缀
const line = lines.find(line => line.startsWith(prefix));// 取整个lines里面的每个line查找有没有这个前缀的一行
if (typeof line !== 'string') {
// Fail gracefully. They could remove it.
return true;
}
const npmCWD = line.substring(prefix.length);
if (npmCWD === cwd) {// 判断当前目录和执行目录是否一致
return true;
}
console.error(
chalk.red( // 不一致就打印以下信息,大概意思就是`npm`进程没有在正确的目录下执行
`Could not start an npm process in the right directory.\n\n` +
`The current directory is: ${chalk.bold(cwd)}\n` +
`However, a newly started npm process runs in: ${chalk.bold(
npmCWD
)}\n\n` +
`This is probably caused by a misconfigured system terminal shell.`
)
);
if (process.platform === 'win32') {// 对window的情况作了单独判断
console.error(
chalk.red(`On Windows, this can usually be fixed by running:\n\n`) +
` ${chalk.cyan(
'reg'
)} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
` ${chalk.cyan(
'reg'
)} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
chalk.red(`Try to run the above two lines in the terminal.\n`) +
chalk.red(
`To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`
)
);
}
return false;
}
上述代码表示已经解析了,其中用到了一个外部依赖:
cross-spawn
:用来执行node进程,Node跨平台解决方案,解决在windows下各种问题npm地址
checkNpmVersion
function checkNpmVersion() {
let hasMinNpm = false;
let npmVersion = null;
try {
npmVersion = execSync('npm --version')
.toString()
.trim();
hasMinNpm = semver.gte(npmVersion, '5.0.0');
} catch (err) {
// ignore
}
return {
hasMinNpm: hasMinNpm,
npmVersion: npmVersion,
};
}
检测node
版本,react-scrpts
是需要依赖Node
版本的,低版本的Node
不支持,版本比较使用的是一个semver package.
run
run 主要做的事情就是安装依赖、拷贝模板。run()
函数在createApp()
函数的所有内容执行完毕后执行,它接收7个参数。
-
root
:我们创建的目录的绝对路径 -
appName
:我们创建的目录名称 -
version
;react-scripts
的版本 -
verbose
:继续传入verbose
,在createApp
中没有使用到 -
originalDirectory
:原始目录,这个之前说到了,到run
函数中就有用了 -
tempalte
:模板,这个参数之前也说过了,不对外使用 -
useYarn
:是否使用yarn
-
usePnp
: 是否使用pnp
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp
) {
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([
// 从网址或路径中提取软件包名称,也就是获取安装包名称这里之前用的getPackageName()
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.
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('');
}
// TODO: Remove with next major release.
if (!supportsTemplates && (template || '').includes('typescript')) {
allDependencies.push(
'@types/node',
'@types/react',
'@types/react-dom',
'@types/jest',
'typescript'
);
}
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);// 判断node_modules下面所有包中对于node版本的最低要求
setCaretRangeForRuntimeDeps(packageName);// 给package.json中的依赖添加^
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, templateName],
`
var init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
if (version === '[email protected]') {
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);
});
});
}
run在这里对react-script
做了很多处理,大概是由于react-script
本身是有node
版本的依赖的,而且在用create-react-app init
初始化一个项目的时候,是可以指定react-script
的版本;简单来说run 主要做的事情就是安装依赖、拷贝模板。
其中函数列表:
-
getInstallPackage()
:获取要安装的react-scripts
版本或者开发者自己定义的react-scripts
。
-
getTemplateInstallPackage
: 获取模板安装包信息,根据选择的模板创建模板安装包名
-
getPackageInfo()
: 从网址或路径中提取软件包名称,也就是获取安装包名称这里之前用的getPackageName()
-
checkIfOnline()
:检查网络连接是否正常 -
install()
:安装开发依赖包, -
checkNodeVersion()
:检查Node
版本信息,判断node_modules下面所有包中对于node版本的最低要求 -
setCaretRangeForRuntimeDeps()
:检查发开依赖是否正确安装,版本是否正确 -
executeNodeScript()
: 安装依赖同时copy react-scripts下面的template到当前目录,在create-react-app
之前的版本中,这里是通过调用react-script
下的init
方法来执行后续动作的。这里通过调用executeNodeScript
方法
getInstallPackage
function getInstallPackage(version, originalDirectory) {
let packageToInstall = 'react-scripts';
const validSemver = semver.valid(version);
if (validSemver) {
packageToInstall += `@${validSemver}`;
} else if (version) {
if (version[0] === '@' && !version.includes('/')) {
packageToInstall += version;
} else if (version.match(/^file:/)) {
packageToInstall = `file:${path.resolve(
originalDirectory,
version.match(/^file:(.*)?$/)[1]
)}`;
} else {
// for tar.gz or alternative paths
packageToInstall = version;
}
}
const scriptsToWarn = [
{
name: 'react-scripts-ts',
message: chalk.yellow(
`The react-scripts-ts package is deprecated. TypeScript is now supported natively in Create React App. You can use the ${chalk.green(
'--template typescript'
)} option instead when generating your app to include TypeScript support. Would you like to continue using react-scripts-ts?`
),
},
];
for (const script of scriptsToWarn) {
if (packageToInstall.startsWith(script.name)) {
return inquirer
.prompt({
type: 'confirm',
name: 'useScript',
message: script.message,
default: false,
})
.then(answer => {
if (!answer.useScript) {
process.exit(0);
}
return packageToInstall;
});
}
}
return Promise.resolve(packageToInstall);
}
getInstallPackage
根据传入的 version 和原始路径 originalDirectory 去获取要安装的 package 列表,默认情况下version 为 undefined,获取到的 packageToInstall 为react-scripts
,也就是我们如上图的 resolve 回调
getTemplateInstallPackage
function getTemplateInstallPackage(template, originalDirectory) {
let templateToInstall = 'cra-template';
if (template) {
if (template.match(/^file:/)) {
templateToInstall = `file:${path.resolve(
originalDirectory,
template.match(/^file:(.*)?$/)[1]
)}`;
} else if (
template.includes('://') ||
template.match(/^.+\.(tgz|tar\.gz)$/)
) {
// for tar.gz or alternative paths
templateToInstall = template;
} else {
// Add prefix 'cra-template-' to non-prefixed templates, leaving any
// @scope/ intact.
const packageMatch = template.match(/^(@[^/]+\/)?(.+)$/);
const scope = packageMatch[1] || '';
const templateName = packageMatch[2];
const name = templateName.startsWith(templateToInstall)
? templateName
: `${templateToInstall}-${templateName}`;
templateToInstall = `${scope}${name}`;
}
}
return Promise.resolve(templateToInstall);
}
getTemplateInstallPackage
创建一个cra-template
模板前缀,把传入的模板名称合并返回一个新的模板名称,
getPackageInfo
// Extract package name from tarball url or path.
// 从网址或路径中提取软件包名称
function getPackageInfo(installPackage) {
if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {// 判断`react-scripts`的信息来安装这个包,用于返回正规的包名
return getTemporaryDirectory() //创建一个临时目录
.then(obj => {
let stream;
if (/^http/.test(installPackage)) {
stream = hyperquest(installPackage);
} else {
stream = fs.createReadStream(installPackage);
}
return extractStream(stream, obj.tmpdir).then(() => obj);
})
.then(obj => {
const { name, version } = require(path.join(
obj.tmpdir,
'package.json'
));
obj.cleanup();
return { name, version };
})
.catch(err => {
// The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz
// However, this function returns package name only without semver version.
console.log(
`Could not extract the package name from the archive: ${err.message}`
);
const assumedProjectName = installPackage.match(
/^.+\/(.+?)(?:-\d+.+)?\.(tgz|tar\.gz)$/
)[1];
console.log(
`Based on the filename, assuming it is "${chalk.cyan(
assumedProjectName
)}"`
);
return Promise.resolve({ name: assumedProjectName });
});
} else if (installPackage.startsWith('git+')) { // 此处为信息中包含`git+`信息的情况
// Pull package name out of git urls e.g:
// git+https://github.com/mycompany/react-scripts.git
// git+ssh://github.com/mycompany/react-scripts.git#v1.2.3
return Promise.resolve({
name: installPackage.match(/([^/]+)\.git(#.*)?$/)[1],
});
} else if (installPackage.match(/.+@/)) { // 此处为只有版本信息的时候的情况
// Do not match @scope/ when stripping off @version or @tag
return Promise.resolve({
name: installPackage.charAt(0) + installPackage.substr(1).split('@')[0],
version: installPackage.split('@')[1],
});
} else if (installPackage.match(/^file:/)) { // 此处为信息中包含`file:`开头的情况
const installPackagePath = installPackage.match(/^file:(.*)?$/)[1];
const { name, version } = require(path.join(
installPackagePath,
'package.json'
));
return Promise.resolve({ name, version });
}
return Promise.resolve({ name: installPackage });
}
这个函数的作用就是返回正常的包名,不带任何符号的,这里使用了一个外部依赖hyperquest
,看了下以前版本的源码,这个函数之前是getPackageName()
同一个方法
install
function install(root, useYarn, usePnp, dependencies, verbose, isOnline) {
return new Promise((resolve, reject) => { // 封装在一个回调函数中
let command;// 定义一个命令
let args;// 定义一个命令的参数
if (useYarn) { // 如果使用yarn
command = 'yarnpkg';
args = ['add', '--exact'];
if (!isOnline) {
args.push('--offline'); // 是否离线模式
}
if (usePnp) {
args.push('--enable-pnp');
}
[].push.apply(args, dependencies); // 组合参数和开发依赖 `react` `react-dom` `react-scripts`
// Explicitly set cwd() to work around issues like
// https://github.com/facebook/create-react-app/issues/3326.
// Unfortunately we can only do this for Yarn because npm support for
// equivalent --prefix flag doesn't help with this issue.
// This is why for npm, we run checkThatNpmCanReadCwd() early instead.
args.push('--cwd');// 指定命令执行目录的地址
args.push(root);// 地址的绝对路径
if (!isOnline) { // 离线模式时候会发出警告
console.log(chalk.yellow('You appear to be offline.'));
console.log(chalk.yellow('Falling back to the local Yarn cache.'));
console.log();
}
} else { //使用npm 的情况
command = 'npm';
args = [
'install',
'--save',
'--save-exact',
'--loglevel',
'error',
].concat(dependencies);
if (usePnp) { // 如果是pnp发出警告npm 不支持pnp
console.log(chalk.yellow("NPM doesn't support PnP."));
console.log(chalk.yellow('Falling back to the regular installs.'));
console.log();
}
}
if (verbose) {
args.push('--verbose');
}
// 在spawn执行完命令后会有一个回调,判断code是否为 0,然后 resolve Promise,
const child = spawn(command, args, { stdio: 'inherit' });
child.on('close', code => {
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`,
});
return;
}
resolve();
});
});
}
install
就是把梳理好的package
交给npm
或者yarn
安装依赖
executeNodeScript
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();
});
});
}
executeNodeScript
方法主要是通过 spawn 来通过 node命令执行react-script
下的 init 方法
react-script/init.js
module.exports = function(
appPath,
appName,
verbose,
originalDirectory,
templateName
) {
const appPackage = require(path.join(appPath, 'package.json')); //项目目录下的 package.json
const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); //通过判断目录下是否有 yarn.lock 来判断是否使用 yarn
if (!templateName) { // 判断是否有模板名称
console.log('');
console.error(
`A template was not provided. This is likely because you're using an outdated version of ${chalk.cyan(
'create-react-app'
)}.`
);
console.error(
`Please note that global installs of ${chalk.cyan(
'create-react-app'
)} are no longer supported.`
);
return;
}
const templatePath = path.join( //获取模板的路径
require.resolve(templateName, { paths: [appPath] }),
'..'
);
let templateJsonPath;
if (templateName) { //
templateJsonPath = path.join(templatePath, 'template.json');
} else {
// TODO: Remove support for this in v4.
templateJsonPath = path.join(appPath, '.template.dependencies.json');
}
let templateJson = {};
if (fs.existsSync(templateJsonPath)) { //判断路径是否正确
templateJson = require(templateJsonPath);
}
// Copy over some of the devDependencies
appPackage.dependencies = appPackage.dependencies || {};
// Setup the script rules 定义scripts
const templateScripts = templateJson.scripts || {};
appPackage.scripts = Object.assign(
{
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test',
eject: 'react-scripts eject',
},
templateScripts
);
// Update scripts for Yarn users
if (useYarn) {// 判断是否使用yarn,如果是替换成你怕吗 npm
appPackage.scripts = Object.entries(appPackage.scripts).reduce(
(acc, [key, value]) => ({
...acc,
[key]: value.replace(/(npm run |npm )/, 'yarn '),
}),
{}
);
}
// Setup the eslint config 设置eslint配置
appPackage.eslintConfig = {
extends: 'react-app',
};
// Setup the browsers list 这是浏览器 openBrowser
appPackage.browserslist = defaultBrowsers;
fs.writeFileSync( //写入我们需要创建的目录下的 package.json 中
path.join(appPath, 'package.json'),
JSON.stringify(appPackage, null, 2) + os.EOL
);
// 判断项目目录是否有`README.md`,模板目录中已经定义了`README.md`防止冲突
const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
if (readmeExists) {
fs.renameSync(
path.join(appPath, 'README.md'),
path.join(appPath, 'README.old.md')
);
}
// Copy the files for the user
const templateDir = path.join(templatePath, 'template');
if (fs.existsSync(templateDir)) {
fs.copySync(templateDir, appPath);
} else {
console.error(
`Could not locate supplied template: ${chalk.green(templateDir)}`
);
return;
}
// modifies README.md commands based on user used package manager.
if (useYarn) {// 判断是否使用yarn,如果是替换成你怕吗 npm 默认使用npm
try {
const readme = fs.readFileSync(path.join(appPath, 'README.md'), 'utf8');
fs.writeFileSync(
path.join(appPath, 'README.md'),
readme.replace(/(npm run |npm )/g, 'yarn '),
'utf8'
);
} catch (err) {
// Silencing the error. As it fall backs to using default npm commands.
}
}
const gitignoreExists = fs.existsSync(path.join(appPath, '.gitignore'));
if (gitignoreExists) {
// Append if there's already a `.gitignore` file there
const data = fs.readFileSync(path.join(appPath, 'gitignore'));
fs.appendFileSync(path.join(appPath, '.gitignore'), data);
fs.unlinkSync(path.join(appPath, 'gitignore'));
} else {
// Rename gitignore after the fact to prevent npm from renaming it to .npmignore
// See: https://github.com/npm/npm/issues/1862
fs.moveSync(
path.join(appPath, 'gitignore'),
path.join(appPath, '.gitignore'),
[]
);
}
let command;
let remove;
let args;
if (useYarn) {
command = 'yarnpkg';
remove = 'remove';
args = ['add'];
} else {
command = 'npm';
remove = 'uninstall';
args = ['install', '--save', verbose && '--verbose'].filter(e => e);
}
// Install additional template dependencies, if present 安装其他模板依赖项如果有
const templateDependencies = templateJson.dependencies;
if (templateDependencies) {
args = args.concat(
Object.keys(templateDependencies).map(key => {
return `${key}@${templateDependencies[key]}`;
})
);
}
// Install react and react-dom for backward compatibility with old CRA cli
// which doesn't install react and react-dom along with react-scripts
if (!isReactInstalled(appPackage)) {
args = args.concat(['react', 'react-dom']);
}
// 安装react和react-dom以便与旧CRA cli向后兼容
// 没有安装react和react-dom以及react-scripts
// 或模板是presetend(通过--internal-testing-template)
// Install template dependencies, and react and react-dom if missing.
if ((!isReactInstalled(appPackage) || templateName) && args.length > 1) {
console.log();
console.log(`Installing template dependencies using ${command}...`);
const proc = spawn.sync(command, args, { stdio: 'inherit' });
if (proc.status !== 0) {
console.error(`\`${command} ${args.join(' ')}\` failed`);
return;
}
}
if (args.find(arg => arg.includes('typescript'))) {
console.log();
verifyTypeScriptSetup();
}
// Remove template
console.log(`Removing template package using ${command}...`);
console.log();
const proc = spawn.sync(command, [remove, templateName], {
stdio: 'inherit',
});
if (proc.status !== 0) {
console.error(`\`${command} ${args.join(' ')}\` failed`);
return;
}
if (tryGitInit(appPath)) {
console.log();
console.log('Initialized a git repository.');
}
// Display the most elegant way to cd.
// This needs to handle an undefined originalDirectory for
// backward compatibility with old global-cli's.
// 显示最优雅的CD方式。
// 这需要处理一个未定义的originalDirectory
// 与旧的global-cli的向后兼容性。
let cdpath;
if (originalDirectory && path.join(originalDirectory, appName) === appPath) {
cdpath = appName;
} else {
cdpath = appPath;
}
// Change displayed command to yarn instead of yarnpkg
// 将显示的命令更改为yarn而不是yarnpkg
const displayedCommand = useYarn ? 'yarn' : 'npm';
console.log();
console.log(`Success! Created ${appName} at ${appPath}`);
console.log('Inside that directory, you can run several commands:');
console.log();
console.log(chalk.cyan(` ${displayedCommand} start`));
console.log(' Starts the development server.');
console.log();
console.log(
chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}build`)
);
console.log(' Bundles the app into static files for production.');
console.log();
console.log(chalk.cyan(` ${displayedCommand} test`));
console.log(' Starts the test runner.');
console.log();
console.log(
chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}eject`)
);
console.log(
' Removes this tool and copies build dependencies, configuration files'
);
console.log(
' and scripts into the app directory. If you do this, you can’t go back!'
);
console.log();
console.log('We suggest that you begin by typing:');
console.log();
console.log(chalk.cyan(' cd'), cdpath);
console.log(` ${chalk.cyan(`${displayedCommand} start`)}`);
if (readmeExists) {
console.log();
console.log(
chalk.yellow(
'You had a `README.md` file, we renamed it to `README.old.md`'
)
);
}
console.log();
console.log('Happy hacking!');
}
初始化方法主要做的事情就是修改目标路径下的 package.json,添加一些配置命令,然后 copy!react-script 下的模板到目标路径下
简单总结下
- 判断 node 版本,如果大版本小于 8 ,则直接退出(截止目前是 8)
- createReactApp.js 初始化一些命令参数,然后再去判断是否传入了 packageName,否则直接退出
- 各种版本的判断,然后通过
cross-spawn
来用命令行执行所有的安装 - 当所有的依赖安装完后,依旧通过命令行,初始化 node 环境,来执行 react-script 下的初始化方法:修改 package.json 中的一些配置、以及 copy 模板文件
- 处理完成,给出用户友好提示
看完大佬的分享,在自己跑一遍果然理顺很多,感谢大佬的分享,写出来也是记录自我一个学习的过程,方便后期查看