文章结构:
引言
为了快速地进行构建使用 React 的项目,FaceBook 官方发布了一个无需配置的、用于快速构建开发环境的脚手架工具 create-react-app。
使用的原因以及特性:
- 无需配置;
- 集成了对 React, JSX, ES6 和 Flow 的支持;
- 集成了开发服务器;
- 配置好了浏览器热加载的功能;
- 在 JavaScript 中可以直接 import CSS 和图片;
- 自动处理 CSS 的兼容问题,无需添加
-webkit
前缀; - 集成好了编译命令,编译后直接发布成产品,并且还包含了 sourcemaps。
其他react脚手架:
- react-boilerplate
- react-redux-starter-kit
文件结构
如上图,大部分文件我们都不需要关注,是一些工具(如eslint,travis,yarn,appveyor,lerna)的配置文件,说明文件等等,我们只需要关注packages和package.json(项目的配置文件),主要的都在packages里,如下图所示,里面有六个文件,其中webpack的配置,eslint的配置,babel的配置等就不需介绍了,这里只需要关注create-react-app和react-scripts,另外react-dev-utils里面的源码也非常有用,它主要是设计webpack插件的编写。create-react-app是一个全局的命令行工具用来创建一个新的项目,react-script帮我们写好了项目所需要的开发依赖和配置文件中的配置属性,比如react-scripts已经自动下载需要的webpack-dev-server依赖,然后react-scripts自己写了一个node服务端的脚本代码start.js来实例化webpackDevServer,并且运行时启动了一个express的http服务器,我们只需要专注写源码即可了。即create-react-app主要是初始化目录和依赖,其中控制环境的代码都在react-scripts中,这里先介绍create-react-app的源码,react-scripts和react-dev-utils的源码分析放在下一篇文章中。
用到的npm包
validate-npm-package-name: 检验一个字符串是否是一个有效变得npm包名
chalk: 让命令行字符串字体变色,背景变色,加下划线,改变字体等等
commander: 仿照Ruby的commander,是node命令行界面的完整解决方案
fs-extra: 增加node原生fs模块没有的文件系统方法,而且为fs方法增加了promise支持
path: 跟node的path一样,将node的path发布到npm库中
cross-spawn: node中spawn和spawnSync的一种跨平台解决方案
semver: 对version号进行操作的库,如比较,验证版本号是否有效等
dns: 一个带有Web UI的DNS服务器,使用Redis配置存储
tmp: 创建暂时的文件和文件夹
tar-pack: 打包和解包模块成某种类型(在tar/gz中)
hyperquest: 将HTTP请求视为流式传输
envinfo: 用于调试的DEV环境信息
createReactApp.js
const validateProjectName = require('validate-npm-package-name');
const chalk = require('chalk');
const commander = require('commander');
const fs = require('fs-extra');
const path = require('path');
const execSync = require('child_process').execSync;
const spawn = require('cross-spawn');
const semver = require('semver');
const dns = require('dns');
const tmp = require('tmp');
const unpack = require('tar-pack').unpack;
const url = require('url');
const hyperquest = require('hyperquest');
const envinfo = require('envinfo');
const os = require('os');
const findMonorepo = require('react-dev-utils/workspaceUtils').findMonorepo;
const packageJson = require('./package.json');
复制代码
首先是引入包,包括npm社区里的和本地的
let projectName; // 定义了一个用来存储项目名称的变量
const program = new commander.Command(packageJson.name)
.version(packageJson.version) // 输入版本信息,使用`create-react-app -v`的时候就用打印版本信息
.arguments('<project-directory>') // 使用`create-react-app <my-project>` 尖括号中的参数
.usage(`${chalk.green('<project-directory>')} [options]`) // 使用`create-react-app`第一行打印的信息,也就是使用说明
.action(name => {
projectName = name; // 此处action函数的参数就是之前argument中的<project-directory> 初始化项目名称 --> 此处影响后面
})
.option('--verbose', 'print additional logs') // option配置`create-react-app -[option]`的选项,类似 --help -V
.option('--info', 'print environment debug info') // 打印本地相关开发环境,操作系统,`Node`版本等等
.option(
'--scripts-version <alternative-package>',
'use a non-standard version of react-scripts'
) // 这我之前就说过了,指定特殊的`react-scripts`
.option('--use-npm') // 默认使用`yarn`,指定使用`npm`
.allowUnknownOption() // 这个我没有在文档上查到,直译就是允许无效的option 大概意思就是我可以这样`create-react-app <my-project> -la` 其实 -la 并没有定义,但是我还是可以这么做而不会保存
.on('--help', () => {
// 此处省略了一些打印信息
}) // on('--help') 用来定制打印帮助信息 当使用`create-react-app -h(or --help)`的时候就会执行其中的代码,基本都是些打印信息
.parse(process.argv); // 这个就是解析我们正常的`Node`进程,可以这么理解没有这个东东,`commander`就不能接管`Node`
复制代码
这里的定义也就是我们在命令行中看到的那些东西,比如参数,打印信息等
// 判断在命令行中执行`create-react-app <name>` 有没有name,如果没有就继续
if (typeof projectName === 'undefined') {
// 当没有传name的时候,如果带了 --info 的选项继续执行下列代码,这里配置了--info时不会报错
if (program.info) {
// 打印当前环境信息和`react`、`react-dom`, `react-scripts`三个包的信息
envinfo.print({
packages: ['react', 'react-dom', 'react-scripts'],
noNativeIDE: true,
duplicates: true,
});
process.exit(0); // 正常退出进程
}
// 在没有带项目名称又没带 --info 选项的时候就会打印一堆错误信息,像--version 和 --help 是commander自带的选项,所以不用单独配置
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); // 抛出异常退出进程
}
复制代码
就是看用户有没有传入projectName变量,即create-react-app 中的参数,如果没有就会报错,并显示一些帮助信息。
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在初始化一个项目的时候,会生成一个标准的文件夹,这里有一个隐藏的选项--internal-testing-template,用来更改初始化目录的模板,不过只是供内部使用,应该是开发者们开发时候用到的,所以我们可以直接略过此选项。
createApp函数
createApp(
projectName,
program.verbose,
program.scriptsVersion,
program.useNpm,
hiddenProgram.internalTestingTemplate
);
复制代码
一个createAPP
函数,接收了5个参数
-
projectName
: 执行create-react-app
name的值,也就是初始化项目的名称 -
program.verbose
:这里在说一下commander
的option
选项,如果加了这个选项这个值就是true
,否则就是false
,也就是说这里如果加了--verbose
,那这个参数就是true
,至于verbose
是什么,我之前也说过了,在yarn
或者npm
安装的时候打印本地信息,也就是如果安装过程中出错,我们可以找到额外的信息。 -
program.scriptsVersion
:与上述同理,指定react-scripts
版本 -
program.useNpm
:以上述同理,指定是否使用npm
,默认使用yarn
-
hiddenProgram.internalTestingTemplate
:指定初始化的模板,人家说了内部使用,大家可以忽略了,应该是用于开发测试模板目录的时候使用。function createApp(name, verbose, version, useNpm, template) { const root = path.resolve(name); // 获取当前进程运行的位置,也就是文件目录的绝对路径 const appName = path.basename(root); // 返回root路径下最后一部分 checkAppName(appName); // 执行 checkAppName 函数 检查文件名是否合法 fs.ensureDirSync(name); // 此处 ensureDirSync 方法是外部依赖包 fs-extra 而不是 node本身的fs模块,作用是确保当前目录下有指定文件名,没有就创建 // isSafeToCreateProjectIn 函数 判断文件夹是否安全 if (!isSafeToCreateProjectIn(root, name)) { process.exit(1); // 不合法结束进程 } // 到这里打印成功创建了一个`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) ); // 定义常量 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); // 如果不使用yarn 并且checkThatNpmCanReadCwd()函数 这里之前说的不是很对,在重新说一次 // checkThatNpmCanReadCwd 这个函数的作用是检查进程目录是否是我们创建的目录,也就是说如果进程不在我们创建的目录里面,后续再执行`npm`安装的时候就会出错,所以提前检查 if (!useYarn && !checkThatNpmCanReadCwd()) { process.exit(1); } // 比较 node 版本,小于6的时候发出警告 // 之前少说了一点,小于6的时候指定`react-scripts`标准版本为0.9.x,也就是标准的`react-scripts@1.0.0`以上的版本不支持`node`在6版本之下 if (!semver.satisfies(process.version, '>=6.0.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 6 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'; } // 如果没有使用yarn 也发出警告 // 这里之前也没有说全,还判断了`npm`的版本是不是在3以上,如果没有依然指定安装`react-scripts@0.9.x`版本 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 boostrapped with an old unsupported version of tools.\n\n` + `Please update to npm 3 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'; } } // 传入这些参数执行run函数 // 执行完毕上述代码以后,将执行`run`函数,但是我还是先把上述用到的函数全部说完,在来下一个核心函数`run` run(root, appName, version, verbose, originalDirectory, template, useYarn); } 复制代码
这个函数首先在我们的目录下创建了一个项目目录,并且校验了这个目录的名称是否合法,这个目录是否安全,然后往其中写入了一个package.json的文件,并且判断了当前环境下应该使用的react-scripts的版本,然后执行了run函数。
其中有这些小函数:
-
checkAppName()
:用于检测文件名是否合法 -
isSafeToCreateProjectIn()
:用于检测文件夹是否安全 -
shouldUseYarn()
:用于检测yarn
在本机是否已经安装 -
checkThatNpmCanReadCwd()
:用于检测npm
是否在正确的目录下执行 -
checkNpmVersion()
:用于检测npm
在本机是否已经安装了
checkAppName()
function checkAppName(appName) {
// 使用 validateProjectName 检查包名是否合法返回结果,这个validateProjectName是外部依赖的引用,见下面说明
const validationResult = validateProjectName(appName);
// 如果对象中有错继续,这里就是外部依赖的具体用法
if (!validationResult.validForNewPackages) {
console.error(
`Could not create a project called ${chalk.red(
`"${appName}"`
)} because of npm naming restrictions:`
);
printValidationResults(validationResult.errors);
printValidationResults(validationResult.warnings);
process.exit(1);
}
// 定义了三个开发依赖的名称
const dependencies = ['react', 'react-dom', 'react-scripts'].sort();
// 如果项目使用了这三个名称都会报错,而且退出进程
if (dependencies.indexOf(appName) >= 0) {
console.error(
chalk.red(
`We cannot create a project called ${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。
isSafeToCreateProjectIn()
function isSafeToCreateProjectIn(root, name) {
// 定义了一些文件名
const validFiles = [
'.DS_Store',
'Thumbs.db',
'.git',
'.gitignore',
'.idea',
'README.md',
'LICENSE',
'web.iml',
'.hg',
'.hgignore',
'.hgcheck',
'.npmignore',
'mkdocs.yml',
'docs',
'.travis.yml',
'.gitlab-ci.yml',
'.gitattributes',
];
console.log();
// 这里就是在我们创建好的项目文件夹下,除了上述文件以外不包含其他文件就会返回true
const conflicts = fs
.readdirSync(root)
.filter(file => !validFiles.includes(file));
if (conflicts.length < 1) {
return true;
}
// 否则这个文件夹就是不安全的,并且挨着打印存在哪些不安全的文件
console.log(
`The directory ${chalk.green(name)} contains files that could conflict:`
);
console.log();
for (const file of conflicts) {
console.log(` ${file}`);
}
console.log();
console.log(
'Either try using a new directory name, or remove the files listed above.'
);
// 并且返回false
return false;
}
复制代码
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()
function checkThatNpmCanReadCwd() {
const cwd = process.cwd(); // 这里取到当前的进程目录
let childOutput = null; // 定义一个变量来保存`npm`的信息
try {
// 相当于执行`npm config list`并将其输出的信息组合成为一个字符串
childOutput = spawn.sync('npm', ['config', 'list']).output.join('');
} catch (err) {
return true;
}
// 判断是否是一个字符串
if (typeof childOutput !== 'string') {
return true;
}
// 将整个字符串以换行符分隔
const lines = childOutput.split('\n');
// 定义一个我们需要的信息的前缀
const prefix = '; cwd = ';
// 去整个lines里面的每个line查找有没有这个前缀的一行
const line = lines.find(line => line.indexOf(prefix) === 0);
if (typeof line !== 'string') {
return true;
}
// 取出后面的信息,这个信息大家可以自行试一试,就是`npm`执行的目录
const npmCWD = line.substring(prefix.length);
// 判断当前目录和执行目录是否是一致的
if (npmCWD === cwd) {
return true;
}
// 不一致就打印以下信息,大概意思就是`npm`进程没有在正确的目录下执行
console.error(
chalk.red(
`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.`
)
);
// 这里他对windows的情况作了一些单独的判断,没有深究这些信息
if (process.platform === 'win32') {
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;
}
复制代码
checkNpmVersion()
function checkNpmVersion() {
let hasMinNpm = false;
let npmVersion = null;
try {
npmVersion = execSync('npm --version')
.toString()
.trim();
hasMinNpm = semver.gte(npmVersion, '3.0.0');
} catch (err) {
// ignore
}
return {
hasMinNpm: hasMinNpm,
npmVersion: npmVersion,
};
}
复制代码
这个函数返回一个对象,对象上面有两个键值对,一个是npm的版本号,一个是是否有最小npm版本的限制
run函数
它接收7个参数:
root
:我们创建的目录的绝对路径appName
:我们创建的目录名称version
;react-scripts
的版本verbose
:继续传入verbose
,在createApp
中没有使用到originalDirectory
:原始目录,这个之前说到了,到run
函数中就有用了tempalte
:模板,这个参数之前也说过了,不对外使用useYarn
:是否使用yarn
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn
) {
// 这里对`react-scripts`做了大量的处理
const packageToInstall = getInstallPackage(version, originalDirectory); // 获取依赖包信息
const allDependencies = ['react', 'react-dom', packageToInstall]; // 所有的开发依赖包
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, allDependencies, verbose, isOnline).then(
() => packageName
);
})
.then(packageName => {
// 检查当前`Node`版本是否支持包
checkNodeVersion(packageName);
// 检查`package.json`的开发依赖是否正常
setCaretRangeForRuntimeDeps(packageName);
// `react-scripts`脚本的目录
const scriptsPath = path.resolve(
process.cwd(),
'node_modules',
packageName,
'scripts',
'init.js'
);
// 引入`init`函数
const init = require(scriptsPath);
// 执行目录的拷贝
init(root, appName, verbose, originalDirectory, template);
// 当`react-scripts`的版本为0.9.x发出警告
if (version === 'react-scripts@0.9.x') {
console.log(
chalk.yellow(
`\nNote: the project was boostrapped with an old unsupported version of tools.\n` +
`Please update to Node >=6 and npm >=3 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();
// 出现异常的时候将删除目录下的这些文件
const knownGeneratedFiles = [
'package.json',
'npm-debug.log',
'yarn-error.log',
'yarn-debug.log',
'node_modules',
];
// 挨着删除
const currentFiles = fs.readdirSync(path.join(root));
currentFiles.forEach(file => {
knownGeneratedFiles.forEach(fileToMatch => {
if (
(fileToMatch.match(/.log/g) && file.indexOf(fileToMatch) === 0) ||
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) {
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);
});
}
复制代码
这里主要对react-script做了处理,因为react-script本身是有node版本的依赖的,而且在用create-react-app init 初始化一个项目的时候,是可以指定react-script的版本。
其中有这些小函数:
getInstallPackage()
:获取要安装的react-scripts
版本或者开发者自己定义的react-scripts
getPackageName()
:获取到正式的react-scripts
的包名checkIfOnline()
:检查网络连接是否正常install()
:安装开发依赖包checkNodeVersion()
:检查Node
版本信息setCaretRangeForRuntimeDeps()
:检查发开依赖是否正确安装,版本是否正确init()
:将事先定义好的目录文件拷贝到我的项目中
getInstallPackage()
function getInstallPackage(version, originalDirectory) {
let packageToInstall = 'react-scripts'; // 定义常量 packageToInstall,默认就是标准`react-scripts`包名
const validSemver = semver.valid(version); // 校验版本号是否合法
if (validSemver) {
packageToInstall += `@${validSemver}`; // 合法的话执行,就安装指定版本,在`npm install`安装的时候指定版本为加上`@x.x.x`版本号,安装指定版本的`react-scripts`
} else if (version && version.match(/^file:/)) {
// 不合法并且版本号参数带有`file:`执行以下代码,作用是指定安装包为我们自身定义的包
packageToInstall = `file:${path.resolve(
originalDirectory,
version.match(/^file:(.*)?$/)[1]
)}`;
} else if (version) {
// 不合法并且没有`file:`开头,默认为在线的`tar.gz`文件
// for tar.gz or alternative paths
packageToInstall = version;
}
// 返回最终需要安装的`react-scripts`的信息,或版本号或本地文件或线上`.tar.gz`资源
return packageToInstall;
}
复制代码
这里create-react-app
本身提供了安装react-scripts
的三种机制,一开始初始化的项目是可以指定react-scripts
的版本或者是自定义,所以在这里他就提供了这几种机制。
getPackageName()
function getPackageName(installPackage) {
// 函数进来就根据上面的那个判断`react-scripts`的信息来安装这个包,用于返回正规的包名
// 此处为线上`tar.gz`包的情况
if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {
// 里面这段创建了一个临时目录,具体它是怎么设置了线上.tar.gz包我没试就不乱说了
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 packageName = require(path.join(obj.tmpdir, 'package.json')).name;
obj.cleanup();
return packageName;
})
.catch(err => {
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(assumedProjectName);
});
// 此处为信息中包含`git+`信息的情况
} else if (installPackage.indexOf('git+') === 0) {
return Promise.resolve(installPackage.match(/([^/]+)\.git(#.*)?$/)[1]);
// 此处为只有版本信息的时候的情况
} else if (installPackage.match(/.+@/)) {
return Promise.resolve(
installPackage.charAt(0) + installPackage.substr(1).split('@')[0]
);
// 此处为信息中包含`file:`开头的情况
} else if (installPackage.match(/^file:/)) {
const installPackagePath = installPackage.match(/^file:(.*)?$/)[1];
const installPackageJson = require(path.join(installPackagePath, 'package.json'));
return Promise.resolve(installPackageJson.name);
}
// 什么都没有直接返回包名
return Promise.resolve(installPackage);
}
复制代码
这个函数的作用就是返回正常的包名,不带任何符号的,
checkIfOnline()
function checkIfOnline(useYarn) {
if (!useYarn) {
return Promise.resolve(true);
}
return new Promise(resolve => {
dns.lookup('registry.yarnpkg.com', err => {
let proxy;
if (err != null && (proxy = getProxy())) {
dns.lookup(url.parse(proxy).hostname, proxyErr => {
resolve(proxyErr == null);
});
} else {
resolve(err == null);
}
});
});
}
复制代码
这个函数本身接收一个是否使用yarn
的参数来判断是否进行后续,如果使用的是npm
就直接返回true
了,为什么会有这个函数是由于yarn
本身有个功能叫离线安装,这个函数来判断是否离线安装。
install()
function install(root, useYarn, dependencies, verbose, isOnline) {
// 封装在一个回调函数中
return new Promise((resolve, reject) => {
let command; // 定义一个命令
let args; // 定义一个命令的参数
// 如果使用yarn
if (useYarn) {
command = 'yarnpkg'; // 命令名称
args = ['add', '--exact']; // 命令参数的基础
if (!isOnline) {
args.push('--offline'); // 此处接上面一个函数判断是否是离线模式
}
[].push.apply(args, dependencies); // 组合参数和开发依赖 `react` `react-dom` `react-scripts`
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();
}
// 不使用yarn的情况使用npm
} else {
// 此处于上述一样,命令的定义 参数的组合
command = 'npm';
args = [
'install',
'--save',
'--save-exact',
'--loglevel',
'error',
].concat(dependencies);
}
// 因为`yarn`和`npm`都可以带这个参数,所以就单独拿出来了拼接到上面
if (verbose) {
args.push('--verbose');
}
// 这里就把命令组合起来执行
const child = spawn(command, args, { stdio: 'inherit' });
// 命令执行完毕后关闭
child.on('close', code => {
// code 为0代表正常关闭,不为零就打印命令执行错误的那条
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`,
});
return;
}
// 正常继续往下执行
resolve();
});
});
}
复制代码
checkNodeVersion()
function checkNodeVersion(packageName) {
// 找到`react-scripts`的`package.json`路径
const packageJsonPath = path.resolve(
process.cwd(),
'node_modules',
packageName,
'package.json'
);
// 引入`react-scripts`的`package.json`
const packageJson = require(packageJsonPath);
// 在`package.json`中定义了一个`engines`其中放着`Node`版本的信息,大家可以打开源码`packages/react-scripts/package.json`查看
if (!packageJson.engines || !packageJson.engines.node) {
return;
}
// 比较进程的`Node`版本信息和最小支持的版本,如果比他小的话,会报错然后退出进程
if (!semver.satisfies(process.version, packageJson.engines.node)) {
console.error(
chalk.red(
'You are running Node %s.\n' +
'Create React App requires Node %s or higher. \n' +
'Please update your version of Node.'
),
process.version,
packageJson.engines.node
);
process.exit(1);
}
}
复制代码
这个函数直译一下,检查Node
版本,为什么要检查了?之前已经说过了react-scrpts
是需要依赖Node
版本的,也就是说低版本的Node
不支持。
setCaretRangeForRuntimeDeps()
function setCaretRangeForRuntimeDeps(packageName) {
const packagePath = path.join(process.cwd(), 'package.json'); // 取出创建项目的目录中的`package.json`路径
const packageJson = require(packagePath); // 引入`package.json`
// 判断其中`dependencies`是否存在,不存在代表我们的开发依赖没有成功安装
if (typeof packageJson.dependencies === 'undefined') {
console.error(chalk.red('Missing dependencies in package.json'));
process.exit(1);
}
// 拿出`react-scripts`或者是自定义的看看`package.json`中是否存在
const packageVersion = packageJson.dependencies[packageName];
if (typeof packageVersion === 'undefined') {
console.error(chalk.red(`Unable to find ${packageName} in package.json`));
process.exit(1);
}
// 检查`react` `react-dom` 的版本
makeCaretRange(packageJson.dependencies, 'react');
makeCaretRange(packageJson.dependencies, 'react-dom');
// 重新写入文件`package.json`
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
}
复制代码
这个函数就是用来检测我们之前安装的依赖是否写入了package.json
里面,并且对依赖的版本做了检测,其中一个函数依赖:
makeCaretRange()
:用来对依赖的版本做检测
init()
init()
函数是放在packages/react-scripts/script
目录下的,但它其实跟react-scripts
包联系不大,就是个copy
他本身定义好的模板目录结构的函数,所以放在这里讲。
接收5
个参数:
appPath
:之前的root
,项目的绝对路径appName
:项目的名称verbose
:这个参数我之前说过了,npm
安装时额外的信息originalDirectory
:原始目录,命令执行的目录template
:测试模板
// 当前的包名,也就是这个命令的包
const ownPackageName = require(path.join(__dirname, '..', 'package.json')).name;
// 当前包的路径
const ownPath = path.join(appPath, 'node_modules', ownPackageName);
// 项目的`package.json`
const appPackage = require(path.join(appPath, 'package.json'));
// 检查项目中是否有`yarn.lock`来判断是否使用`yarn`
const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
appPackage.dependencies = appPackage.dependencies || {};
// 定义其中`scripts`的
appPackage.scripts = {
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test --env=jsdom',
eject: 'react-scripts eject',
};
// 重新写入`package.json`
fs.writeFileSync(
path.join(appPath, 'package.json'),
JSON.stringify(appPackage, null, 2)
);
// 判断项目目录是否有`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')
);
}
// 是否有模板选项,默认为当前执行命令包目录下的`template`目录,也就是`packages/react-scripts/tempalte`
const templatePath = template
? path.resolve(originalDirectory, template)
: path.join(ownPath, 'template');
if (fs.existsSync(templatePath)) {
// 拷贝目录到项目目录
fs.copySync(templatePath, appPath);
} else {
console.error(
`Could not locate supplied template: ${chalk.green(templatePath)}`
);
return;
}
复制代码