create-react-app(v3.7.2)
能够很快很方便初始化一个
react
开发项目,这个东西究竟是怎样运做的,作了哪些处理呢?今天揭开内部秘密。源码用到的一些有用的第三方库也列了出来,方便之后你们在本身的
cli
中使用。
// CRA的全部的命令以下
/** npm package commander: 命令基础工具 */
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(
'--template <path-to-template>',
'specify a template for the created project'
)
.option('--use-npm')
.option('--use-pnp')
// TODO: Remove this in next major release.
.option(
'--typescript',
'(this option will be removed in favour of templates in the next major release of create-react-app)'
)
.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(` 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);
复制代码
-V, --version
版本号输出node
// 当前create-react-app版本号输出
new commander.Command(packageJson.name)
.version(packageJson.version) // 默认已经生成该命令选项
复制代码
--verbose
展现详细的logsreact
--info
展现当前系统以及环境的一些信息git
// 源码中,若是命令中有这个参数, 则会执行
/** npm package: envinfo: 快速获取当前各类软件环境的信息 */
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);
复制代码
--scripts-version
指定一个特定的react-scripts
运行脚本github
--template
指定项目的模板,能够指定一个本身的模板typescript
--use-pnp
使用pnp --> pnp是什么npm
--typescript
使用ts开发,以后版本会移除这个选项json
这个选项即将被弃用,可使用--template typescript
代替app
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';
}
}
复制代码
建立项目会调用createApp
方法, node版本要求>=8.10.0
, 低于这个版本会抛错less
createApp
dom
首先会先调用createApp
方法
createApp(
projectName, // 项目名称
program.verbose, // --verbose
program.scriptsVersion, // --scripts-version
program.template, // --template
program.useNpm, // --use-npm
program.usePnp, // --use-pnp
program.typescript // --typescript
);
复制代码
建立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
);
复制代码
若是有使用yarn
, 会先将当前目录下的yarn.lock.cached
文件拷贝到项目根目录下并重命名为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
接着调用run
,继续建立新项目
/** npm 包 semver: 版本号校验以及比较等的工具库 */
run(
root,
appName,
version, // scriptsVersion
verbose,
originalDirectory,
template,
useYarn,
usePnp
);
复制代码
处理react-scripts
引用脚本和--template
入参
// ...
let packageToInstall = 'react-scripts';
// ...
// 将所用到的依赖搜集
const allDependencies = ['react', 'react-dom', packageToInstall];
Promise.all([
getInstallPackage(version, originalDirectory),
getTemplateInstallPackage(template, originalDirectory),
])
复制代码
调用getInstallPackage
处理react-scripts
的使用 --scripts-version
选项的入参能够为多种:
react-scripts
的版本 -> react-scripts@x.x.x
typescript
模板,若指定scriptsVersion
为react-scripts-ts
,会有确认提示/** npm package inquirer: 输入输出交互处理工具 */
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;
});
}
}
复制代码
调用getTemplateInstallPackage
处理--template
的使用
://
或者tgz|tar.gz
压缩包@xxx/xxx/xxxx
或者@xxxx
的指定路径或者模板名字const packageMatch = template.match(/^(@[^/]+\/)?(.+)$/);
const scope = packageMatch[1] || '';
const templateName = packageMatch[2];
if (
templateName === templateToInstall ||
templateName.startsWith(`${templateToInstall}-`)
) {
// Covers:
// - cra-template
// - @SCOPE/cra-template
// - cra-template-NAME
// - @SCOPE/cra-template-NAME
templateToInstall = `${scope}${templateName}`;
} else if (templateName.startsWith('@')) {
// Covers using @SCOPE only
templateToInstall = `${templateName}/${templateToInstall}`;
} else {
// Covers templates without the `cra-template` prefix:
// - NAME
// - @SCOPE/NAME
templateToInstall = `${scope}${templateToInstall}-${templateName}`;
}
// cra-template: This is the official base template for Create React App.
复制代码
最终处理成@xxx/cra-template
或者@xxx/cra-template-xxx
、cra-template-xxx
、cra-template
, 官方指定的两个模板为cra-template-typescript
、cra-template
。模板具体内容你们能够去 官方仓库 去查看,能够本身自定义或者魔改一些东西
接着获取--scripts-version
和--template
处理后的安装包信息
/** npm package tem: 用于在node.js环境中建立临时文件和目录。 hyperquest: 将http请求转化为流(stream)输出 tar-pack: tar/gz的压缩或者解压缩 */
Promise.all([
getPackageInfo(packageToInstall),
getPackageInfo(templateToInstall),
])
// getPackageInfo是一个颇有用的工具函数
// Extract package name from tarball url or path.
function getPackageInfo(installPackage) {
if (installPackage.match(/^.+\.(tgz|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 { 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+')) {
// 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:/)) {
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 });
}
function extractStream(stream, dest) {
return new Promise((resolve, reject) => {
stream.pipe(
unpack(dest, err => {
if (err) {
reject(err);
} else {
resolve(dest);
}
})
);
});
}
复制代码
run
方法主要的工做就是处理--scripts-version
和--template
提供的包,搜集项目的依赖
install
run
处理收集完依赖后会调用install
方法
return install(
root, // 项目的名称
useYarn,
usePnp,
allDependencies,
verbose,
isOnline // 若使用yarn,dns.lookup检测registry.yarnpkg.com是否正常的结果
)
// 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);
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' });
child.on('close', code => {
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`,
});
return;
}
resolve();
});
});
}
复制代码
依赖安装后检查react-scripts
执行包的版本与当前的node版本是否匹配,检查react
、react-dom
是否正确安装,并在它们的版本号前面加^
(上面安装命令带有exact
选项,会精确安装依赖,版本号不带^
),将依赖从新写入package.json
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])); `
);
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();
});
});
}
复制代码
正确检查依赖后将执行提供的scripts脚本下的init
初始化:
package.json
添加scripts
/eslintConfig
/browserslist
等配置README.md
,将其重命名README.old.md
yarn
,将模板README.md
的命令说明给为yarn
ts
项目则初始化相关配置(verifyTypeScriptSetup
)node_modules
的模板git
相关到此整个项目建立完毕
package.json
package.json
的react/react-dom
依赖版本号,并校验node
版本是否符合要求react-scripts
依赖,并经过子进程调用依赖下的react-scripts/scripts/init.js
,进行项目模板初始化ts
则初始化其配置git
create-react-app
这个包的源码相对简单,可是很是细密精炼,整个流程很是清晰,绝对是一个cli
的范本,感兴趣的小伙伴能够本身阅读。文正若是有不正确的地方欢迎指正批评!