再往下看就是执行了一个 createApp 了,看这名字就知道最关键的方法就是它了。
function createApp(name, verbose, version, template, useNpm, usePnp) { // 此处省略 100 行代码 }createApp 传入了 6 个参数,对应的是 CRA 命令行传入的一些配置。
我在思考为啥这里不设计成一个 options 对象来接受这些参数?如果后期需要增删一些参数,是不是比较不好维护?这样的想法是我过度设计吗?
4. 检查应用名CRA 会检查输入的 project name 是否符合以下两条规范:
检查是否符合 npm 命名规范
检查是否含有 react/react-dom/react-scripts 等关键字
不符合规范则直接 process.exit(1) 退出进程。
和一般脚手架不同的是,CRA 会在创建项目时新创建一个 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 ); 6. 选择模板 function getTemplateInstallPackage(template, originalDirectory) { let templateToInstall = 'cra-template'; if (template) { // 一些处理逻辑 doTemplate(template); templateToInstall = doTemplate(template); } return Promise.resolve(templateToInstall); }默认使用 cra-template 模板,如果传入 template 参数,则使用对用的模板,该方法主要是给额外的 template 加 scope 和 prefix,比如 @scope/cra-template-${template},具体逻辑不展开。
这里 CRA 的核心思想是通过 npm 来对模板进行管理,这样方便扩展和管理。
7. 安装依赖CRA 会自动给项目安装 react、react-dom 和 react-scripts 以及模板。
command = 'npm'; args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat( dependencies ); const child = spawn(command, args, { stdio: 'inherit' }); 8. 初始化代码CRA 的功能其实不多,安装完依赖之后,实际上初始化代码的工作还没做。
接着往下看,看到这样一段代码代码:
await executeNodeScript( { cwd: process.cwd(), }, [root, appName, verbose, originalDirectory, templateName], ` var init = require('${packageName}/scripts/init.js'); init.apply(null, JSON.parse(process.argv[1])); ` );除此之外,CRA 貌似看不到任何复制代码的代码了,那我们需要的“初始化代码”的工作应该就是在这里完成了。
为了分析方便,忽略了上下文代码,说明一下,这段代码中的 packageName 的值是 react-scripts。也就是这里执行了 react-scripts 包中的 scripts/init 方法,并传入了几个参数。
8.1 react-scripts/init.js老规矩,只分析主流程代码,主流程主要就做了四件事:
处理 template 里的 packages.json
处理 package.json 的 scripts:默认值和 template 合并
写入 package.json
拷贝 template 文件
除此之外还有一些 git 和 npm 相关的操作,这里就不展开了。
// init.js // 删除了不影响主流程的代码 module.exports = function( appPath, appName, verbose, originalDirectory, templateName ) { const appPackage = require(path.join(appPath, 'package.json')); // 通过一些判断来处理 template 中的 package.json // 返回 templatePackage const templateScripts = templatePackage.scripts || {}; // 修改实际 package.json 中的 scripts // start、build、test 和 eject 是默认的命令,如果模板里还有其它 script 就 merge appPackage.scripts = Object.assign( { start: 'react-scripts start', build: 'react-scripts build', test: 'react-scripts test', eject: 'react-scripts eject', }, templateScripts ); // 写 package.json fs.writeFileSync( path.join(appPath, 'package.json'), JSON.stringify(appPackage, null, 2) + os.EOL ); // 拷贝 template 文件 const templateDir = path.join(templatePath, 'template'); if (fs.existsSync(templateDir)) { fs.copySync(templateDir, appPath); } };到这里,CRA 的主流程就基本走完了,关于 react-scripts 的命令,比如 start 和 build,后续会单独有文章进行讲解。
9. 从 CRA 中借鉴的工具方法CRA 的代码和思路其实并不复杂,但是不影响我们读它的代码,并且从中学习到一些好的想法。(当然,有一些代码我们也是可以拿来直接用的 ~
9.1 npm 相关 9.1.1 获取 npm 包版本号 const https = require('https'); function getDistTags(pkgName) { return new Promise((resolve, reject) => { https .get( `https://registry.npmjs.org/-/package/${pkgName}/dist-tags`, res => { if (res.statusCode === 200) { let body = ''; res.on('data', data => (body += data)); res.on('end', () => { resolve(JSON.parse(body)); }); } else { reject(); } } ) .on('error', () => { reject(); }); }); } // 获取 react 的版本信息 getDistTags('react').then(res => { const tags = Object.keys(res); console.log(tags); // ['latest', 'next', 'experimental', 'untagged'] console.log(res.latest]); // 17.0.1 }); 9.1.2 比较 npm 包版本号