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
版本号输出
// 当前create-react-app版本号输出 new commander.Command(packageJson.name) .version(packageJson.version) // 默认已经生成该命令选项 复制代码
--verbose
展示详细的logs
--info
展示当前系统以及环境的一些信息
// 源码中,如果命令中有这个参数, 则会执行 /** 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
运行脚本
--template
指定项目的模板,可以指定一个自己的模板
--use-pnp
使用pnp --> pnp是什么
--typescript
使用ts开发,之后版本会移除这个选项
这个选项即将被弃用,可以使用--template typescript
代替
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
, 低于这个版本会抛错
createApp
首先会先调用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
的范本,感兴趣的小伙伴可以自己阅读。文正如果有不正确的地方欢迎指正批评!