You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
varchalk=require('chalk');varcurrentNodeVersion=process.versions.node;// 返回Node版本信息,如果有多个版本返回多个版本varsemver=currentNodeVersion.split('.');// 所有Node版本的集合varmajor=semver[0];// 取出第一个Node版本信息// 如果当前版本小于4就打印以下信息并终止进程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);// 终止进程}// 没有小于4就引入以下文件继续执行require('./createReactApp');
consthiddenProgram=newcommander.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);
functioncreateApp(name,verbose,version,useNpm,template){constroot=path.resolve(name);// 获取当前进程运行的位置,也就是文件目录的绝对路径constappName=path.basename(root);// 返回root路径下最后一部分checkAppName(appName);// 执行 checkAppName 函数 检查文件名是否合法fs.ensureDirSync(name);// 此处 ensureDirSync 方法是外部依赖包 fs-extra 而不是 node本身的fs模块,作用是确保当前目录下有指定文件名,没有就创建// isSafeToCreateProjectIn 函数 判断文件夹是否安全if(!isSafeToCreateProjectIn(root,name)){process.exit(1);// 不合法结束进程}// 到这里打印成功创建了一个`react`项目在指定目录下console.log(`Creating a new React app in ${chalk.green(root)}.`);console.log();// 定义package.json基础内容constpackageJson={name: appName,version: '0.1.0',private: true,};// 往我们创建的文件夹中写入package.json文件fs.writeFileSync(path.join(root,'package.json'),JSON.stringify(packageJson,null,2));// 定义常量 useYarn 如果传参有 --use-npm useYarn就是false,否则执行 shouldUseYarn() 检查yarn是否存在// 这一步就是之前说的他默认使用`yarn`,但是可以指定使用`npm`,如果指定使用了`npm`,`useYarn`就是`false`,不然执行 shouldUseYarn 函数// shouldUseYarn 用于检测本机是否安装了`yarn`constuseYarn=useNpm ? false : shouldUseYarn();// 取得当前node进程的目录,之前还懂为什么要单独取一次,之后也明白了,下一句代码将会改变这个值,所以如果我后面要用这个值,后续其实取得值将不是这个// 所以这里的目的就是提前存好,免得我后续使用的时候不好去找,这个地方就是我执行初始化项目的目录,而不是初始化好的目录,是初始化的上级目录,有点绕..constoriginalDirectory=process.cwd();// 修改进程目录为底下子进程目录// 在这里就把进程目录修改为了我们创建的目录process.chdir(root);// 如果不使用yarn 并且checkThatNpmCanReadCwd()函数 这里之前说的不是很对,在重新说一次// checkThatNpmCanReadCwd 这个函数的作用是检查进程目录是否是我们创建的目录,也就是说如果进程不在我们创建的目录里面,后续再执行`npm`安装的时候就会出错,所以提前检查if(!useYarn&&!checkThatNpmCanReadCwd()){process.exit(1);}// 比较 node 版本,小于6的时候发出警告// 之前少说了一点,小于6的时候指定`react-scripts`标准版本为0.9.x,也就是标准的`react-scripts@1.0.0`以上的版本不支持`node`在6版本之下if(!semver.satisfies(process.version,'>=6.0.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 6 or higher for a better, fully supported experience.\n`));// Fall back to latest supported react-scripts on Node 4version='react-scripts@0.9.x';}// 如果没有使用yarn 也发出警告// 这里之前也没有说全,还判断了`npm`的版本是不是在3以上,如果没有依然指定安装`react-scripts@0.9.x`版本if(!useYarn){constnpmInfo=checkNpmVersion();if(!npmInfo.hasMinNpm){if(npmInfo.npmVersion){console.log(chalk.yellow(`You are using npm ${npmInfo.npmVersion} so the project will be boostrapped with an old unsupported version of tools.\n\n`+`Please update to npm 3 or higher for a better, fully supported experience.\n`));}// Fall back to latest supported react-scripts for npm 3version='react-scripts@0.9.x';}}// 传入这些参数执行run函数// 执行完毕上述代码以后,将执行`run`函数,但是我还是先把上述用到的函数全部说完,在来下一个核心函数`run`run(root,appName,version,verbose,originalDirectory,template,useYarn);}
functioncheckAppName(appName){// 使用 validateProjectName 检查包名是否合法返回结果,这个validateProjectName是外部依赖的引用,见下面说明constvalidationResult=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);}// 定义了三个开发依赖的名称constdependencies=['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);}}
functionisSafeToCreateProjectIn(root,name){// 定义了一堆文件名// 我今天早上仔细的看了一些,以下文件的来历就是我们这些开发者在`create-react-app`中提的一些文件constvalidFiles=['.DS_Store','Thumbs.db','.git','.gitignore','.idea','README.md','LICENSE','web.iml','.hg','.hgignore','.hgcheck','.npmignore','mkdocs.yml','docs','.travis.yml','.gitlab-ci.yml','.gitattributes',];console.log();// 这里就是在我们创建好的项目文件夹下,除了上述文件以外不包含其他文件就会返回trueconstconflicts=fs.readdirSync(root).filter(file=>!validFiles.includes(file));if(conflicts.length<1){returntrue;}// 否则这个文件夹就是不安全的,并且挨着打印存在哪些不安全的文件console.log(`The directory ${chalk.green(name)} contains files that could conflict:`);console.log();for(constfileofconflicts){console.log(` ${file}`);}console.log();console.log('Either try using a new directory name, or remove the files listed above.');// 并且返回falsereturnfalse;}
functioncheckThatNpmCanReadCwd(){constcwd=process.cwd();// 这里取到当前的进程目录letchildOutput=null;// 定义一个变量来保存`npm`的信息try{// 相当于执行`npm config list`并将其输出的信息组合成为一个字符串childOutput=spawn.sync('npm',['config','list']).output.join('');}catch(err){returntrue;}// 判断是否是一个字符串if(typeofchildOutput!=='string'){returntrue;}// 将整个字符串以换行符分隔constlines=childOutput.split('\n');// 定义一个我们需要的信息的前缀constprefix='; cwd = ';// 去整个lines里面的每个line查找有没有这个前缀的一行constline=lines.find(line=>line.indexOf(prefix)===0);if(typeofline!=='string'){returntrue;}// 取出后面的信息,这个信息大家可以自行试一试,就是`npm`执行的目录constnpmCWD=line.substring(prefix.length);// 判断当前目录和执行目录是否是一致的if(npmCWD===cwd){returntrue;}// 不一致就打印以下信息,大概意思就是`npm`进程没有在正确的目录下执行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/`));}returnfalse;}
functionrun(root,appName,version,verbose,originalDirectory,template,useYarn){// 这里对`react-scripts`做了大量的处理constpackageToInstall=getInstallPackage(version,originalDirectory);// 获取依赖包信息constallDependencies=['react','react-dom',packageToInstall];// 所有的开发依赖包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=>{// 接收到上述的包名和是否为离线模式constisOnline=info.isOnline;constpackageName=info.packageName;console.log(`Installing ${chalk.cyan('react')}, ${chalk.cyan('react-dom')}, and ${chalk.cyan(packageName)}...`);console.log();// 安装依赖returninstall(root,useYarn,allDependencies,verbose,isOnline).then(()=>packageName);}).then(packageName=>{// 检查当前`Node`版本是否支持包checkNodeVersion(packageName);// 检查`package.json`的开发依赖是否正常setCaretRangeForRuntimeDeps(packageName);// `react-scripts`脚本的目录constscriptsPath=path.resolve(process.cwd(),'node_modules',packageName,'scripts','init.js');// 引入`init`函数constinit=require(scriptsPath);// 执行目录的拷贝init(root,appName,verbose,originalDirectory,template);// 当`react-scripts`的版本为0.9.x发出警告if(version==='react-scripts@0.9.x'){console.log(chalk.yellow(`\nNote: the project was boostrapped with an old unsupported version of tools.\n`+`Please update to Node >=6 and npm >=3 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();// 出现异常的时候将删除目录下的这些文件constknownGeneratedFiles=['package.json','npm-debug.log','yarn-error.log','yarn-debug.log','node_modules',];// 挨着删除constcurrentFiles=fs.readdirSync(path.join(root));currentFiles.forEach(file=>{knownGeneratedFiles.forEach(fileToMatch=>{if((fileToMatch.match(/.log/g)&&file.indexOf(fileToMatch)===0)||file===fileToMatch){console.log(`Deleting generated file... ${chalk.cyan(file)}`);fs.removeSync(path.join(root,file));}});});// 判断当前目录下是否还存在文件constremainingFiles=fs.readdirSync(path.join(root));if(!remainingFiles.length){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);});}
functiongetInstallPackage(version,originalDirectory){letpackageToInstall='react-scripts';// 定义常量 packageToInstall,默认就是标准`react-scripts`包名constvalidSemver=semver.valid(version);// 校验版本号是否合法if(validSemver){packageToInstall+=`@${validSemver}`;// 合法的话执行,就安装指定版本,在`npm install`安装的时候指定版本为加上`@x.x.x`版本号,安装指定版本的`react-scripts`}elseif(version&&version.match(/^file:/)){// 不合法并且版本号参数带有`file:`执行以下代码,作用是指定安装包为我们自身定义的包packageToInstall=`file:${path.resolve(originalDirectory,version.match(/^file:(.*)?$/)[1])}`;}elseif(version){// 不合法并且没有`file:`开头,默认为在线的`tar.gz`文件// for tar.gz or alternative pathspackageToInstall=version;}// 返回最终需要安装的`react-scripts`的信息,或版本号或本地文件或线上`.tar.gz`资源returnpackageToInstall;}
functiongetPackageName(installPackage){// 函数进来就根据上面的那个判断`react-scripts`的信息来安装这个包,用于返回正规的包名// 此处为线上`tar.gz`包的情况if(installPackage.match(/^.+\.(tgz|tar\.gz)$/)){// 里面这段创建了一个临时目录,具体它是怎么设置了线上.tar.gz包我没试就不乱说了returngetTemporaryDirectory().then(obj=>{letstream;if(/^http/.test(installPackage)){stream=hyperquest(installPackage);}else{stream=fs.createReadStream(installPackage);}returnextractStream(stream,obj.tmpdir).then(()=>obj);}).then(obj=>{constpackageName=require(path.join(obj.tmpdir,'package.json')).name;obj.cleanup();returnpackageName;}).catch(err=>{console.log(`Could not extract the package name from the archive: ${err.message}`);constassumedProjectName=installPackage.match(/^.+\/(.+?)(?:-\d+.+)?\.(tgz|tar\.gz)$/)[1];console.log(`Based on the filename, assuming it is "${chalk.cyan(assumedProjectName)}"`);returnPromise.resolve(assumedProjectName);});// 此处为信息中包含`git+`信息的情况}elseif(installPackage.indexOf('git+')===0){returnPromise.resolve(installPackage.match(/([^/]+)\.git(#.*)?$/)[1]);// 此处为只有版本信息的时候的情况}elseif(installPackage.match(/.+@/)){returnPromise.resolve(installPackage.charAt(0)+installPackage.substr(1).split('@')[0]);// 此处为信息中包含`file:`开头的情况}elseif(installPackage.match(/^file:/)){constinstallPackagePath=installPackage.match(/^file:(.*)?$/)[1];constinstallPackageJson=require(path.join(installPackagePath,'package.json'));returnPromise.resolve(installPackageJson.name);}// 什么都没有直接返回包名returnPromise.resolve(installPackage);}
functioncheckNodeVersion(packageName){// 找到`react-scripts`的`package.json`路径constpackageJsonPath=path.resolve(process.cwd(),'node_modules',packageName,'package.json');// 引入`react-scripts`的`package.json`constpackageJson=require(packageJsonPath);// 在`package.json`中定义了一个`engines`其中放着`Node`版本的信息,大家可以打开源码`packages/react-scripts/package.json`查看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);}}
前言
这段时间公司的事情变得比较少,空下了很多时间,作为一个刚刚毕业初入职场的菜鸟级程序员,一点都不敢放松,秉持着我为人人的思想也想为开源社区做点小小的贡献,但是一直又没有什么明确的目标,最近在努力的准备吃透
react
,加上react
的脚手架工具create-react-app
已经很成熟了,初始化一个react
项目根本看不到它到底是怎么给我搭建的这个开发环境,又是怎么做到的,我还是想知道知道,所以就把他拖出来溜溜。文中若有错误或者需要指正的地方,多多指教,共同进步。
使用说明
就像我开头说的那样,学习一个新的东西,应该是先知道如何用,然后在来看他是怎么实现的。
create-react-app
到底是个什么东西,总结一句话来说,就是官方提供的快速搭建一个新的react
项目的脚手架工具,类似于vue
的vue-cli
和angular
的angular-cli
,至于为什么不叫react-cli
是一个值得深思的问题...哈哈哈,有趣!不说废话了,贴个图,直接看
create-react-app
的命令帮助。概略说明
毕竟它已经是一个很成熟的工具了,说明也很完善,重点对其中
--scripts-version
说一下,其他比较简单,大概说一下,注意有一行Only <project-directory> is required
,直译一下,仅仅只有项目名称是必须的,也就是说你在用create-react-app
命令的时候,必须在其后跟上你的项目名称,其实这里说的不准确,像--version --info --help
这三个选项是不需要带项目名称的,具体看下面:create-react-app -V(or --version)
:这个选项可以单独使用,打印版本信息,每个工具基本都有吧?create-react-app --info
:这个选项也可以单独使用,打印当前系统跟react
相关的开发环境参数,也就是操作系统是什么啊,Node
版本啊之类的,可以自己试一试。create-react-app -h(or --help)
:这个肯定是可以单独使用的,不然怎么打印帮助信息,不然就没有上面的截图了。也就是说除了上述三个参数选项是可以脱离必须参数项目名称以外来单独使用的,因为它们都跟你要初始化的
react
项目无关,然后剩下的参数就是对要初始化的react
项目进行配置的,也就是说三个参数是可以同时出现的,来看一下它们分别的作用:create-react-app <my-project> --verbose
:看上图,打印本地日志,其实他是npm
和yarn
安装外部依赖包可以加的选项,可以打印安装有错时的信息。create-react-app <my-project> --scripts-version
:由于它本身把创建目录初始化步骤和控制命令分离了,用来控制react
项目的开发、打包和测试都放在了react-scripts
里面,所以这里可以单独来配置控制的选项,可能这样你还不是很明白,我下面具体说。create-react-app <my-project> --use-npm
:这个选项看意思就知道了,create-react-app
默认使用yarn
来安装,运行,如果你没有使用yarn
,你可能就需要这个配置了,指定使用npm
。定制选项
关于
--scripts-version
我还要多说一点,其实在上述截图中我们已经可以看到,create-react-app
本身已经对其中选项进行了说明,一共有四种情况,我并没有一一去试他,因为还挺麻烦的,以后如果用到了再来补,我先来大概推测一下他们的意思:npm
发布自己的react-scripts
.tgz
的下载包.tar.gz
的下载包从上述看的出来
create-react-app
对于开发者还是很友好的,可以自己去定义很多东西,如果你不想这么去折腾,它也提供了标准的react-scripts
供开发者使用,我一直也很好奇这个,之后我在来单独说官方标准的react
配置是怎么做的。目录分析
随着它版本的迭代,源码肯定是会发生变化的,我这里下载的是
v1.1.0
,大家可以自行在github
上下载这个版本,找不到的戳链接。主要说明
我们来看一下它的目录结构
咋一看好多啊,我的天啊,到底要怎么看,其实仔细一晃,好像很多一眼就能看出来是什么意思,大概说一下每个文件都是干嘛的,具体的我也不知道啊,往下看,一步一步来。
.github
:这里面放着当你在这个项目提issue
和pr
时候的规范packages
:字面意思就是包们.....暂时不管,后面详说 ----> 重点tasks
:字面意思就是任务们.....暂时不管,后面详说 ----> 重点.eslintignore
:eslint
检查时忽略文件.eslintrc
:eslint
检查配置文件.gitignore
:git
提交时忽略文件.travis.yml
:travis
配置文件.yarnrc
:yarn
配置文件appveyor.cleanup-cache.txt
:里面有一行Edit this file to trigger a cache rebuild
编辑此文件触发缓存,具体干嘛的,暂时不议appveyor.yml
:appveyor
配置文件CHANGELOG-0.x.md
:版本0.X开头的变更说明文件CHANGELOG.md
:当前版本变更说明文件CODE_OF_CONDUCT.md
:facebook
代码行为准则说明CONTRIBUTING.md
:项目的核心说明lerna.json
:lerna
配置文件LICENSE
:开源协议package.json
:项目配置文件README.md
:项目使用说明screencast.svg
:图片...看了这么多文件,是不是打退堂鼓了?哈哈哈哈,好了好了,进入正题,其实上述对于我们阅读源码有用的只有
packages
、tasks
、package.json
三个文件而已,而且本篇能用到的也就packages
和package.json
,是不是想打我.....我也只是想告诉大家这些文件有什么用,它们都是有各自的作用的,如果还不了解,参考下面的参考链接。参考链接
eslint
相关的:eslint官网travis
相关的:travis官网 travis入门yarn
相关的:yarn官网appveyor
相关的:appveyor官网lerna
相关的:lerna官网工具自行了解,本文只说源码相关的
packages
、package.json
。寻找入口
现在的前端项目大多数都有很多别的依赖,不在像以前那些原生
javascript
的工具库,拿到源码文件,就可以开始看了,像jQuery
、underscore
等等,一个两个文件包含了它所有的内容,虽然也有很框架会有umd
规范的文件可以直接阅读,像better-scroll
等等,但是其实他在书写源码的时候还是拆分成了很多块,最后在用打包工具整合在一起了。但是像create-react-app
这样的脚手架工具好像不能像之前那种方法来看了,必须找到整个程序的入口,在逐步突破,所以最开始的工具肯定是寻找入口。开始关注
拿到一个项目我们应该从哪个文件开始看起呢?只要是基于
npm
管理的,我都推荐从package.json
文件开始看,人家是项目的介绍文件,你不看它看啥。它里面理论上应该是有名称、版本等等一些说明性信息,但是都没用,看几个重要的配置。
关于
workspaces
一开始我在npm
的说明文档里面没找到,虽然从字面意思我们也能猜到它的意思是实际工作的目录是packages
,后来我查了一下是yarn
里面的东东,具体看这篇文章,用于在本地测试,具体不关注,只是从这里我们知道了真正的起作用的文件都在packages
里面。重点关注
从上述我们知道现在真正需要关注的内容都在
packages
里面,我们来看看它里面都是有什么东东:里面有六个文件夹,哇塞,又是6个单独的项目,这要看到何年何月.....是不是有这种感触,放宽心大胆的看,先想一下我们在安装了
create-react-app
后在,在命令行输入的是create-react-app
的命令,所以我们大胆的推测关于这个命令应该都是存在了create-react-app
下,在这个目录下同样有package.json
文件,现在我们把这6个文件拆分成6个项目来分析,上面也说了,看一个项目首先看package.json
文件,找到其中的重点:找到重点了,
package.json
文件中的bin
就是在命令行中可以运行的命令,也就是说我们在执行create-react-app
命令的时候,就是执行create-react-app
目录下的index.js
文件。多说两句
关于
package.json
中的bin
选项,其实是基于node
环境运行之后的内容。举个简单的例子,在我们安装create-react-app
后,执行create-react-app
等价于执行node index.js
。create-react-app目录解析
经过以上一系列的查找,我们终于艰难的找到了
create-react-app
命令的中心入口,其他的都先不管,我们打开packages/create-react-app
目录,仔细一瞅,噢哟,只有四个文件,四个文件我们还搞不定吗?除了package.json
、README.md
就只剩两个能看的文件了,我们来看看这两个文件。index.js
既然之前已经看到
packages/create-react-app/package.json
中关于bin
的设置,就是执行index.js
文件,我们就从index.js
入手,开始瞅瞅源码到底都有些虾米。除了一大串的注释以外,代码其实很少,全贴上来了:
咋一眼看过去其实你就知道它大概是什么意思了....检查
Node.js
的版本,小于4
就不执行了,我们分开来看一下,这里他用了一个库chalk
,理解起来并不复杂,一行一行的解析。chalk
:这个对这段代码的实际影响就是在命令行中,将输出的信息变色。也就引出了这个库的作用改变命令行中输出信息的样式。npm地址其中有几个
Node
自身的API
:process.versions
返回一个对象,包含Node
以及它的依赖信息process.exit
结束Node
进程,1
是状态码,表示有异常没有处理在我们经过
index.js
后,就来到了createReactApp.js
,下面再继续看。createReactApp.js
当我们本机上的
Node
版本大于4
的时候就要继续执行这个文件了,打开这个文件,代码还不少,大概700
多行吧,我们慢慢拆解。打开代码一排依赖,懵逼....我不可能挨着去查一个个依赖是用来干嘛的吧?所以,我的建议就是先不管,用到的时候在回来看它是干嘛的,理解更加透彻一些,继续往下看。
在上面的代码中,我把无关紧要打印信息省略了,这段代码算是这个文件的关键入口地此处他
new
了一个commander
,这是个啥东东呢?这时我们就返回去看它的依赖,找到它是一个外部依赖,这时候怎么办呢?不可能打开node_modules
去里面找撒,很简单,打开npm
官网查一下这个外部依赖。commander
:概述一下,Node
命令接口,也就是可以用它代管Node
命令。npm地址上述只是
commander
用法的一种实现,没有什么具体好说的,了解了commander
就不难,这里的定义也就是我们在命令行中看到的那些东西,比如参数,比如打印信息等等,我们继续往下来。还记得上面把
create-react-app <my-project>
中的项目名称赋予了projectName
变量吗?此处的作用就是看看用户有没有传这个<my-project>
参数,如果没有就会报错,并显示一些帮助信息,这里用到了另外一个外部依赖envinfo
。envinfo
:可以打印当前操作系统的环境和指定包的信息。 npm地址这里我之前省略了一个东西,还是拿出来说一下:
create-react-app
在初始化一个项目的时候,会生成一个标准的文件夹,这里有一个隐藏的选项--internal-testing-template
,用来更改初始化目录的模板,这里他已经说了,供内部使用,应该是开发者们开发时候用的,所以不建议大家使用这个选项。我们继续往下看,有几个提前定义的函数,我们不管,直接找到第一个被执行的函数:
一个
createAPP
函数,接收了5个参数projectName
: 执行create-react-app <name>
name的值,也就是初始化项目的名称program.verbose
:这里在说一下commander
的option
选项,如果加了这个选项这个值就是true
,否则就是false
,也就是说这里如果加了--verbose
,那这个参数就是true
,至于verbose
是什么,我之前也说过了,在yarn
或者npm
安装的时候打印本地信息,也就是如果安装过程中出错,我们可以找到额外的信息。program.scriptsVersion
:与上述同理,指定react-scripts
版本program.useNpm
:以上述同理,指定是否使用npm
,默认使用yarn
hiddenProgram.internalTestingTemplate
:这个东东,我之前给他省略了,我在前面已经补充了,指定初始化的模板,人家说了内部使用,大家可以忽略了,应该是用于开发测试模板目录的时候使用。找到了第一个执行的函数
createApp
,我们就来看看createApp
函数到底做了什么?createApp()
我这里先来总结一下这个函数都做了哪些事情,再来看看他用到的依赖有哪些,先说做了哪些事情,在我们的目录下创建了一个项目目录,并且校验了这个目录的名称是否合法,这个目录是否安全,然后往其中写入了一个
package.json
的文件,并且判断了当前环境下应该使用的react-scripts
的版本,然后执行了run
函数。我们在来看看这个函数用了哪些外部依赖:fs-extra
:外部依赖,Node
自带文件模块的外部扩展模块 npm地址semver
:外部依赖,用于比较Node
版本 npm地址之后函数的函数依赖我都会进行详细的解析,除了少部分特别简单的函数,然后我们来看看这个函数的函数依赖:
checkAppName()
:用于检测文件名是否合法,isSafeToCreateProjectIn()
:用于检测文件夹是否安全shouldUseYarn()
:用于检测yarn
在本机是否已经安装checkThatNpmCanReadCwd()
:用于检测npm
是否在正确的目录下执行checkNpmVersion()
:用于检测npm
在本机是否已经安装了checkAppName()
它这个函数其实还蛮简单的,用了一个外部依赖来校验文件名是否符合
npm
包文件名的规范,然后定义了三个不能取得名字react
、react-dom
、react-scripts
,外部依赖:validate-npm-package-name
:外部依赖,检查包名是否合法。npm地址其中的函数依赖:
printValidationResults()
:函数引用,这个函数就是我说的特别简单的类型,里面就是把接收到的错误信息循环打印出来,没什么好说的。isSafeToCreateProjectIn()
他这个函数也算比较简单,就是判断创建的这个目录是否包含除了上述
validFiles
里面的文件,至于这里面的文件是怎么来的,就是create-react-app
在发展至今,开发者们提出来的。shouldUseYarn()
就三行...其中
execSync
是由node
自身模块child_process
引用而来,就是用来执行命令的,这个函数就是执行一下yarnpkg --version
来判断我们是否正确安装了yarn
,如果没有正确安装yarn
的话,useYarn
依然为false
,不管指没有指定--use-npm
。execSync
:引用自child_process.execSync
,用于执行需要执行的子进程checkThatNpmCanReadCwd()
这个函数我之前居然贴错了,实在是不好意思。我之前没有弄懂这个函数的意思,今天再来看的时候已经豁然开朗了,它的意思上述代码已经解析了,其中用到了一个外部依赖:
cross-spawn
:这个我之前说到了没有?忘了,用来执行node
进程。npm地址为什么用单独用一个外部依赖,而不是用
node
自身的呢?来看一下cross-spawn
它自己对自己的说明,Node
跨平台解决方案,解决在windows
下各种问题。checkNpmVersion()
这个能说的也比较少,一眼看过去就知道什么意思了,返回一个对象,对象上面有两个对对,一个是
npm
的版本号,一个是是否有最小npm
版本的限制,其中一个外部依赖,一个Node
自身的API我之前也都说过了,不说了。看到到这里
createApp()
函数的依赖和执行都结束了,接着执行了run()
函数,我们继续来看run()
函数都是什么,我又想吐槽了,算了,忍住!!!run()
函数在createApp()
函数的所有内容执行完毕后执行,它接收7个参数,先来看看。root
:我们创建的目录的绝对路径appName
:我们创建的目录名称version
;react-scripts
的版本verbose
:继续传入verbose
,在createApp
中没有使用到originalDirectory
:原始目录,这个之前说到了,到run
函数中就有用了tempalte
:模板,这个参数之前也说过了,不对外使用useYarn
:是否使用yarn
具体的来看下面
run()
函数。run()
他这里对
react-script
做了很多处理,大概是由于react-script
本身是有node
版本的依赖的,而且在用create-react-app init <project>
初始化一个项目的时候,是可以指定react-script
的版本或者是外部自身定义的东东。他在
run()
函数中的引用都是用Promise
回调的方式来完成的,从我正式接触Node
开始就习惯用async/await
,所以对Promise
还真不熟,恶补了一番,下面我们来拆解其中的每一句和每一个函数的作用,先来看一下用到外部依赖还是之前那些不说了,来看看函数列表:getInstallPackage()
:获取要安装的react-scripts
版本或者开发者自己定义的react-scripts
getPackageName()
:获取到正式的react-scripts
的包名checkIfOnline()
:检查网络连接是否正常install()
:安装开发依赖包checkNodeVersion()
:检查Node
版本信息setCaretRangeForRuntimeDeps()
:检查发开依赖是否正确安装,版本是否正确init()
:将事先定义好的目录文件拷贝到我的项目中知道了个大概,我们在来逐一分析每个函数的作用:
getInstallPackage()
这个方法接收两个参数
version
版本号,originalDirectory
原始目录,主要的作用是判断react-scripts
应该安装的信息,具体看每一行。这里
create-react-app
本身提供了安装react-scripts
的三种机制,一开始初始化的项目是可以指定react-scripts
的版本或者是自定义这个东西的,所以在这里他就提供了这几种机制,其中用到的外部依赖只有一个semver
,之前就说过了,不多说。getPackageName()
他这个函数的目标就是返回一个正常的依赖包名,比如我们什么都不带就返回
react-scripts
,在比如我们是自己定义的包就返回my-react-scripts
,继续到了比较关键的函数了,接收一个installPackage
参数,从这函数开始就采用Promise
回调的方式一直执行到最后,我们来看看这个函数都做了什么,具体看上面每一行的注释。总结一句话,这个函数的作用就是返回正常的包名,不带任何符号的,来看看它的外部依赖:
hyperquest
:这个用于将http请求流媒体传输。npm地址他本身还有函数依赖,这两个函数依赖我都不单独再说,函数的意思很好理解,至于为什么这么做我还没想明白:
getTemporaryDirectory()
:不难,他本身是一个回调函数,用来创建一个临时目录。extractStream()
:主要用到node
本身的一个流,这里我真没懂为什么药改用流的形式,就不发表意见了,在看其实我还是没懂,要真正的明白是要去试一次,但是真的有点麻烦,不想去关注。checkIfOnline()
这个函数本身接收一个是否使用
yarn
的参数来判断是否进行后续,如果使用的是npm
就直接返回true
了,为什么会有这个函数是由于yarn
本身有个功能叫离线安装,这个函数来判断是否离线安装,其中用到了外部依赖:dns
:用来检测是否能够请求到指定的地址。npm地址install()
又到了比较关键的地方了,仔细看每一行代码注释,此处函数的作用就是组合一个
yarn
或者npm
的安装命令,把这些模块安装到项目的文件夹中,其中用到的外部依赖cross-spawn
前面有说了,就不说了。其实执行到这里,
create-react-app
已经帮我们创建好了目录,package.json
并且安装了所有的依赖,react
、react-dom
和react-scrpts
,复杂的部分已经结束,继续往下走。checkNodeVersion()
这个函数直译一下,检查
Node
版本,为什么要检查了?之前我已经说过了react-scrpts
是需要依赖Node
版本的,也就是说低版本的Node
不支持,其实的外部依赖也是之前的几个,没什么好说的。setCaretRangeForRuntimeDeps()
这个函数我也不想说太多了,他的作用并没有那么大,就是用来检测我们之前安装的依赖是否写入了
package.json
里面,并且对依赖的版本做了检测,其中一个函数依赖:makeCaretRange()
:用来对依赖的版本做检测我没有单独对其中的子函数进行分析,是因为我觉得不难,而且对主线影响不大,我不想贴太多说不完。
到这里
createReactApp.js
里面的源码都分析完了,咦!你可能会说你都没说init()
函数,哈哈哈,看到这里说明你很认真哦,init()
函数是放在packages/react-scripts/script
目录下的,但是我还是要给他说了,因为它其实跟react-scripts
包联系不大,就是个copy
他本身定义好的模板目录结构的函数。init()
它本身接收
5
个参数:appPath
:之前的root
,项目的绝对路径appName
:项目的名称verbose
:这个参数我之前说过了,npm
安装时额外的信息originalDirectory
:原始目录,命令执行的目录template
:其实其中只有一种类型的模板,这个选项的作用就是配置之前我说过的那个函数,测试模板这个函数我就不把代码贴全了,里面的东西也蛮好理解,基本上就是对目录结构的修改和重名了那些,挑了一些来说,到这里,
create-react-app
从零到目录依赖的安装完毕的源码已经分析完毕,但是其实这只是个初始化目录和依赖,其中控制环境的代码都存在react-scripts
中,所以其实离我想知道的关键的地方还有点远,但是本篇已经很长了,不打算现在说了,多多包涵。希望本篇对大家有所帮助吧。
啰嗦两句
本来这篇我是打算把
create-react-app
中所有的源码的拿出来说一说,包括其中的webpack
的配置啊,eslint
的配置啊,babel
的配置啊.....等等,但是实在是有点多,他自己本身把初始化的命令和控制react
环境的命令分离成了packages/create-react-app
和packages/react-script
两边,这个篇幅才把packages/create-react-app
说完,更复杂的packages/react-script
在说一下这篇幅都不知道有多少了,所以我打算之后空了,在单独写一篇关于packages/react-script
的源码分析的文。码字不易,可能出现错别字什么的,说的不清楚的,说错的,欢迎指正,多多包涵!
The text was updated successfully, but these errors were encountered: