之前在 create-react-app教程-使用篇 中介绍了create-react-app的基本使用,
为了便于理解一个脚手架脚本是如何运作的,现在来看一下 create-react-app v1.5.2 的源码
入口index.js
create-react-app 一般会作为全局命令,因为便于更新等原因,create-react-app 只会做初始化仓库 执行当前版本命令等操作。
找到 create-react-app
入口index文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 'use strict'; var chalk = require('chalk');
var currentNodeVersion = process.versions.node; var semver = currentNodeVersion.split('.'); var major = semver[0];
if (major < 4) { console.error( chalk.red( 'You are running Node ' + currentNodeVersion + '.\n' + 'Create React App requires Node 4 or higher. \n' + 'Please update your version of Node.' ) ); process.exit(1); }
require('./createReactApp');
|
可以看到 index 文件没有做什么,只是做为一个入口文件判断一下 node版本,小于 4.x的提示并终止程序, 如果正常则加载 ./createReactApp 这个文件,主要的逻辑在该文件实现。
createReactApp.js
虽然 createReactApp.js 有751行,但是里面有一大半是注释和错误友好信息。
除了声明的依赖。跟着执行顺序先看到的是第56行 program
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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('--use-npm') .allowUnknownOption() .on('--help', () => { }) .parse(process.argv);
|
这里用到 commander
的依赖,这是 node.js 命令行接口的解决方案,正如我们所看到的 处理用户输入的参数,输出友好的提示信息等。
接着到了第109行:
1 2 3 4 5 6 7 8 9 10 11 12 13
| if (typeof projectName === 'undefined') { if (program.info) { envinfo.print({ packages: ['react', 'react-dom', 'react-scripts'], noNativeIDE: true, duplicates: true, }); process.exit(0); } process.exit(1); }
|
这里的 projectName 就是我们要创建的web应用名称,如果没有输入的话,输出一些提示信息就终止程序。
createApp 检测判断
然后到了第148行 执行createApp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| createApp( projectName, program.verbose, program.scriptsVersion, program.useNpm, hiddenProgram.internalTestingTemplate );
function createApp(name, verbose, version, useNpm, template) { const root = path.resolve(name); const appName = path.basename(root);
checkAppName(appName); fs.ensureDirSync(name); if (!isSafeToCreateProjectIn(root, name)) { process.exit(1); }
const packageJson = { name: appName, version: '0.1.0', private: true, }; fs.writeFileSync( path.join(root, 'package.json'), JSON.stringify(packageJson, null, 2) );
const useYarn = useNpm ? false : shouldUseYarn(); const originalDirectory = process.cwd(); process.chdir(root); if (!useYarn && !checkThatNpmCanReadCwd()) { process.exit(1); } if (!semver.satisfies(process.version, '>=6.0.0')) { version = 'react-scripts@0.9.x'; }
if (!useYarn) { const npmInfo = checkNpmVersion(); if (!npmInfo.hasMinNpm) { version = 'react-scripts@0.9.x'; } } run(root, appName, version, verbose, originalDirectory, template, useYarn); }
|
可以了解到 createApp 主要做的事情就是做一些安全判断比如:检查项目名是否合法,检查新建的话是否安全,检查npm版本,处理react-script的版本兼容。然后看下在createApp
中用到的 checkAppName
checkAppName 检查项目名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function checkAppName(appName) { const validationResult = validateProjectName(appName); if (!validationResult.validForNewPackages) { process.exit(1); }
const dependencies = ['react', 'react-dom', 'react-scripts'].sort(); if (dependencies.indexOf(appName) >= 0) { process.exit(1); } }
|
run 安装依赖拷贝模版
在 createApp 方法体内调用了run方法,run方法体内完成主要的安装依赖 拷贝模板等功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| function run(root,appName,version,verbose,originalDirectory,template,useYarn) { const packageToInstall = getInstallPackage(version, originalDirectory); const allDependencies = ['react', 'react-dom', packageToInstall];
getPackageName(packageToInstall) .then(packageName => checkIfOnline(useYarn).then(isOnline => ({ isOnline: isOnline, packageName: packageName, })) ) .then(info => { const isOnline = info.isOnline; const packageName = info.packageName; return install(root, useYarn, allDependencies, verbose, isOnline).then( () => packageName ); }) .then(packageName => { checkNodeVersion(packageName); setCaretRangeForRuntimeDeps(packageName); const scriptsPath = path.resolve( process.cwd(), 'node_modules', packageName, 'scripts', 'init.js' ); const init = require(scriptsPath); init(root, appName, verbose, originalDirectory, template); }) .catch(reason => { process.exit(1); }); }
|
可以猜到其中最重要的逻辑是 install
安装依赖和 init
拷贝模板。
install 安装依赖
install 方法体中是根据参数拼装命令行,然后用node去跑安装脚本 ,执行完成后返回一个 Promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| function install(root, useYarn, dependencies, verbose, isOnline) { return new Promise((resolve, reject) => { let command; let args; if (useYarn) { command = 'yarnpkg'; args = ['add', '--exact']; if (!isOnline) { args.push('--offline'); } [].push.apply(args, dependencies); args.push('--cwd'); args.push(root); } else { command = 'npm'; args = [ 'install', '--save', '--save-exact', '--loglevel', 'error', ].concat(dependencies); }
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(); }); }); }
|
init
拷贝模板
init 方法默认是在 【当前web项目路径】/node_modules/react-scripts/script/init.js 中 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| module.exports = function( appPath, appName, verbose, originalDirectory, template ) { const ownPath = path.dirname( require.resolve(path.join(__dirname, '..', 'package.json')) ); const appPackage = require(path.join(appPath, 'package.json')); const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); appPackage.dependencies = appPackage.dependencies || {};
const useTypeScript = appPackage.dependencies['typescript'] != null;
appPackage.scripts = { start: 'react-scripts start', build: 'react-scripts build', test: 'react-scripts test', eject: 'react-scripts eject', }; appPackage.eslintConfig = { extends: 'react-app', }; appPackage.browserslist = defaultBrowsers;
fs.writeFileSync( path.join(appPath, 'package.json'), JSON.stringify(appPackage, null, 2) + os.EOL );
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; }
};
|
简化一下逻辑这里的主要内容就是 修改package.json信息和拷贝模板文件
END