create-react-app源码分析(一)
在使用react的时候,create-react-app这个脚手架非常的方便和友好,是搭建react开发环境的利器。我也一直想了解脚手架是的原理,于是就看了一眼create-react-app的源码。
目录分析
create-react-app的源码主要集中仓库的packages文件夹下。
- create-react-app
- react-dev-utils
- react-scripts
- react-error-overlay
- babel-preset-react-app
- babel-plugin-named-asset-import
这六个文件夹每一个都是独立的项目。
create-react-app项目的核心是create-react-app和react-scripts。
从create-react-app命令可以想到create-react-app是该项目的入口。
create-react-app
从create-react-app的packagejson找到命令的入口,执行create-react-app等价于执行node index.js。
"bin": {
"create-react-app": "./index.js"
},
index.js
入口文件代码很少
// 返回Node版本信息,如果有多个版本返回多个版本
var currentNodeVersion = process.versions.node;
// 所有Node版本的集合
var semver = currentNodeVersion.split('.');
// 取出第一个Node版本信息
var major = semver[0];
// 如果当前版本小于8就打印以下信息并终止进程
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');
createReactApp.js
createReactApp 功能比较简单
- 命令行初始化
- 检查项目名称
- 修改package.json
- 拷贝模板文件到项目目录
commander 命令行处理程序
et projectName;
const program = new commander.Command(packageJson.name)
.version(packageJson.version)// 输入版本信息,使用`create-react-app -v`的时候就用打印版本信息
.arguments('<project-directory>')//定义 project-directory ,必填项
.usage(`${chalk.green('<project-directory>')} [options]`)
.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(
'--scripts-version <alternative-package>',
'use a non-standard version of react-scripts'
)//指定特殊的react-scripts
.option('--use-npm')//默认使用yarn,指定使用npm
.option('--use-pnp')
.option('--typescript')
.allowUnknownOption()//允许无效的option
.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);
if (program.info) {
// 打印当前环境信息和`react`、`react-dom`, `react-scripts`三个包的信息
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);
}
// 判断在命令行中执行`create-react-app <projectName>` 有没有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);
}
create-react-app在初始化一个项目的时候,会生成一个标准的文件夹,这里有一个隐藏的选项--internal-testing-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);
接下来就是createApp方法
参数
- name,初始化项目的名称
- verbose,是否输出额外信息
- version, react-scripts版本
- useNpm,指定是否使用npm,默认使用yarn
- usePnp, 是否使用pnp
- useTypescript,是否使用ts
- template,指定模板
function createApp(
name,
verbose,
version,
useNpm,
usePnp,
useTypescript,
template
) {
// 获取当前进程运行的位置,也就是文件目录的绝对路径
const root = path.resolve(name);
// 返回root路径下最后一部分
const appName = path.basename(root);
checkAppName(appName);// 执行 checkAppName 函数 检查文件名是否合法
fs.ensureDirSync(name);//确保当前目录下有指定文件名,没有就创建
////判断新建这个文件夹是否安全,否则直接退出
if (!isSafeToCreateProjectIn(root, name)) {
process.exit(1);
}
//打印成功创建了一个`react`项目在指定目录下
console.log(`Creating a new React app in ${chalk.green(root)}.`);
console.log();
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
);
//判断是使用 yarn还是 npm
const useYarn = useNpm ? false : shouldUseYarn();
const originalDirectory = process.cwd();
process.chdir(root);
//如果是使用npm,检测npm是否在正确目录下执行
process.exit(1);
if (!useYarn && !checkThatNpmCanReadCwd()) {
process.exit(1);
}
//判断node环境,输出一些提示信息, 并采用旧版本的 react-scripts
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';
}
//关于 npm、pnp、yarn 的使用判断,版本校验等
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 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) {
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 (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方法
run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp,
useTypescript
);
}
checkAppName
使用validate-npm-package-name package 检查包名是否合法
function checkAppName(appName) {
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);
}
}
isSafeToCreateProjectIn
检查当前目录下是否存在已有文件
function isSafeToCreateProjectIn(root, name) {
const validFiles = [
'.DS_Store',
'Thumbs.db',
'.git',
'.gitignore',
'.idea',
'README.md',
'LICENSE',
'.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))
// 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 => !errorLogFilePatterns.some(pattern => file.indexOf(pattern) === 0)
);
if (conflicts.length > 0) {
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.'
);
return false;
}
// Remove any remnant files from a previous installation
const currentFiles = fs.readdirSync(path.join(root));
currentFiles.forEach(file => {
errorLogFilePatterns.forEach(errorLogFilePattern => {
// This will catch `(npm-debug|yarn-error|yarn-debug).log*` files
if (file.indexOf(errorLogFilePattern) === 0) {
fs.removeSync(path.join(root, file));
}
});
});
return true;
}
shouldUseYarn
执行 yarnpkg --version 来判断我们是否正确安装了yarn,如果没有正确安装yarn的话,useYarn依然为false
function shouldUseYarn() {
try {
execSync('yarnpkg --version', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
checkThatNpmCanReadCwd
function checkThatNpmCanReadCwd() {
// 这里取到当前的进程目录
const cwd = process.cwd();
let childOutput = null;
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 = ';
const line = lines.find(line => line.indexOf(prefix) === 0);
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(
`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
返回npm的版本号和最小npm版本的限制
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,
};
}
function checkYarnVersion() {
let hasMinYarnPnp = false;
let yarnVersion = null;
try {
yarnVersion = execSync('yarnpkg --version')
.toString()
.trim();
let trimmedYarnVersion = /^(.+?)[-+].+$/.exec(yarnVersion);
if (trimmedYarnVersion) {
trimmedYarnVersion = trimmedYarnVersion.pop();
}
hasMinYarnPnp = semver.gte(trimmedYarnVersion || yarnVersion, '1.12.0');
} catch (err) {
// ignore
}
return {
hasMinYarnPnp: hasMinYarnPnp,
yarnVersion: yarnVersion,
};
}
run
run 主要做的事情就是安装依赖、拷贝模板。
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp,
useTypescript
) {
// 获取依赖包信息
getInstallPackage(version, originalDirectory).then(packageToInstall => {
// 所有的开发依赖包 packageToInstall为react-scripts
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 => {
// 安装完 react, react-dom, react-scripts 之后检查当前环境运行的node版本是否符合要求
checkNodeVersion(packageName);
// 检查并设置`package.json`的开发依赖版本号
setCaretRangeForRuntimeDeps(packageName);
const pnpPath = path.resolve(process.cwd(), '.pnp.js');
const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];
// 执行scripts文件夹下的init函数 拷贝模板
await executeNodeScript(
{
cwd: process.cwd(),
args: nodeArgs,
},
[root, appName, verbose, originalDirectory, template],
`
var init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
//当`react-scripts`的版本为0.9.x发出警告
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();
// 出现异常的时候将删除目录下的这些文件
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);
});
});
}
checkIfOnline
yarn本身有个功能叫离线安装,这个函数来判断是否离线安装
dns.lookup用来检测是否能够请求到指定的地址
function checkIfOnline(useYarn) {
if (!useYarn) {
// Don't ping the Yarn registry.
// We'll just assume the best case.
return Promise.resolve(true);
}
return new Promise(resolve => {
dns.lookup('registry.yarnpkg.com', err => {
let proxy;
if (err != null && (proxy = getProxy())) {
// If a proxy is defined, we likely can't resolve external hostnames.
// Try to resolve the proxy name as an indication of a connection.
dns.lookup(url.parse(proxy).hostname, proxyErr => {
resolve(proxyErr == null);
});
} else {
resolve(err == null);
}
});
});
}
install
function install(root, useYarn, usePnp, dependencies, verbose, isOnline) {
return new Promise((resolve, reject) => {
let command; // 定义一个命令
let args;// 定义一个命令的参数
if (useYarn) {
command = 'yarnpkg';
args = ['add', '--exact'];
if (!isOnline) {
args.push('--offline');
}
if (usePnp) {
args.push('--enable-pnp');
}
[].push.apply(args, dependencies);
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 {
command = 'npm';
args = [
'install',
'--save',
'--save-exact',
'--loglevel',
'error',
].concat(dependencies);
if (usePnp) {
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');
}
//把命令组合起来执行
const child = spawn(command, args, { stdio: 'inherit' });
//监听close事件
child.on('close', code => {
// code 为0代表正常关闭
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`,
});
return;
}
resolve();
});
});
}
执行完install方法 create-react-app已经帮我们创建好了目录,package.json并且安装了所有的依赖,react、react-dom和react-scrpts。
checkNodeVersion
function checkNodeVersion(packageName) {
//找到`react-scripts`的`package.json`路径
const packageJsonPath = path.resolve(
process.cwd(),
'node_modules',
packageName,
'package.json'
);
if (!fs.existsSync(packageJsonPath)) {
return;
}
const packageJson = require(packageJsonPath);
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);
}
}
setCaretRangeForRuntimeDeps
function setCaretRangeForRuntimeDeps(packageName) {
const packagePath = path.join(process.cwd(), 'package.json');
const packageJson = require(packagePath);
// 判断`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) + os.EOL);
}
executeNodeScript
通过调用react-script下的 init方法来拷贝模板,executeNodeScript 方法主要是通过 spawn 来通过 node命令执行react-script下的 init 方法。
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();
});
});
}
init.js
参数
- 项目的绝对路径
- 项目的名称
- 额外的信息
- 命令执行的目录
- 模板
module.exports = function(
appPath,
appName,
verbose,
originalDirectory,
template
) {
//获取当前包中包含 package.json 所在的文件夹路径
const ownPath = path.dirname(
require.resolve(path.join(__dirname, '..', 'package.json'))
);
//项目目录下的 package.json
const appPackage = require(path.join(appPath, 'package.json'));
//通过判断目录下是否有 yarn.lock 来判断是否使用 yarn
const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
// Copy over some of the devDependencies
appPackage.dependencies = appPackage.dependencies || {};
const useTypeScript = appPackage.dependencies['typescript'] != null;
// 设置 script 命令
appPackage.scripts = {
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test',
eject: 'react-scripts eject',
};
// 设置 eslint 的配置
appPackage.eslintConfig = {
extends: 'react-app',
};
// 设置浏览器列表
appPackage.browserslist = defaultBrowsers;
//写入我们需要创建的目录下的 package.json 中
fs.writeFileSync(
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')
);
}
// 获取模板的路径
const templatePath = template
? path.resolve(originalDirectory, template)
: path.join(ownPath, useTypeScript ? 'template-typescript' : 'template');
if (fs.existsSync(templatePath)) {
// 拷贝目录到项目目录
fs.copySync(templatePath, appPath);
} else {
console.error(
`Could not locate supplied template: ${chalk.green(templatePath)}`
);
return;
}
// Rename gitignore after the fact to prevent npm from renaming it to .npmignore
// See: https://github.com/npm/npm/issues/1862
try {
fs.moveSync(
path.join(appPath, 'gitignore'),
path.join(appPath, '.gitignore'),
[]
);
} catch (err) {
// Append if there's already a `.gitignore` file there
if (err.code === 'EEXIST') {
const data = fs.readFileSync(path.join(appPath, 'gitignore'));
fs.appendFileSync(path.join(appPath, '.gitignore'), data);
fs.unlinkSync(path.join(appPath, 'gitignore'));
} else {
throw err;
}
}
let command;
let args;
if (useYarn) {
command = 'yarnpkg';
args = ['add'];
} else {
command = 'npm';
args = ['install', '--save', verbose && '--verbose'].filter(e => e);
}
args.push('react', 'react-dom');
// 安装其他模板依赖项(如果存在)
const templateDependenciesPath = path.join(
appPath,
'.template.dependencies.json'
);
if (fs.existsSync(templateDependenciesPath)) {
const templateDependencies = require(templateDependenciesPath).dependencies;
args = args.concat(
Object.keys(templateDependencies).map(key => {
return `${key}@${templateDependencies[key]}`;
})
);
fs.unlinkSync(templateDependenciesPath);
}
// 安装react和react-dom以便与旧CRA cli向后兼容
// 没有安装react和react-dom以及react-scripts
// 或模板是presetend(通过--internal-testing-template)
if (!isReactInstalled(appPackage) || template) {
console.log(`Installing react and react-dom using ${command}...`);
console.log();
const proc = spawn.sync(command, args, { stdio: 'inherit' });
if (proc.status !== 0) {
console.error(`\`${command} ${args.join(' ')}\` failed`);
return;
}
}
if (useTypeScript) {
verifyTypeScriptSetup();
}
if (tryGitInit(appPath)) {
console.log();
console.log('Initialized a git repository.');
}
// 显示最优雅的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
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!');
};
//检查react是否安装
function isReactInstalled(appPackage) {
const dependencies = appPackage.dependencies || {};
return (
typeof dependencies.react !== 'undefined' &&
typeof dependencies['react-dom'] !== 'undefined'
);
}
到这里项目初始化已经完成了,接下来分析一下react-scripts,关于项目启动和打包的代码
create-react-app源码分析(一)
在使用react的时候,create-react-app这个脚手架非常的方便和友好,是搭建react开发环境的利器。我也一直想了解脚手架是的原理,于是就看了一眼create-react-app的源码。
目录分析
create-react-app的源码主要集中仓库的packages文件夹下。
这六个文件夹每一个都是独立的项目。
create-react-app项目的核心是create-react-app和react-scripts。
从create-react-app命令可以想到create-react-app是该项目的入口。
create-react-app
从create-react-app的packagejson找到命令的入口,执行create-react-app等价于执行node index.js。
index.js
入口文件代码很少
createReactApp.js
createReactApp 功能比较简单
commander 命令行处理程序
create-react-app在初始化一个项目的时候,会生成一个标准的文件夹,这里有一个隐藏的选项--internal-testing-template,用来更改初始化目录的模板,
接下来就是createApp方法
参数
checkAppName
使用
validate-npm-package-namepackage 检查包名是否合法isSafeToCreateProjectIn
检查当前目录下是否存在已有文件
shouldUseYarn
执行
yarnpkg --version来判断我们是否正确安装了yarn,如果没有正确安装yarn的话,useYarn依然为falsecheckThatNpmCanReadCwd
checkNpmVersion
返回npm的版本号和最小npm版本的限制
run
run 主要做的事情就是安装依赖、拷贝模板。
checkIfOnline
yarn本身有个功能叫离线安装,这个函数来判断是否离线安装
dns.lookup用来检测是否能够请求到指定的地址
install
执行完install方法 create-react-app已经帮我们创建好了目录,package.json并且安装了所有的依赖,react、react-dom和react-scrpts。
checkNodeVersion
setCaretRangeForRuntimeDeps
executeNodeScript
通过调用react-script下的 init方法来拷贝模板,executeNodeScript 方法主要是通过 spawn 来通过 node命令执行react-script下的 init 方法。
init.js
参数
到这里项目初始化已经完成了,接下来分析一下react-scripts,关于项目启动和打包的代码