2018-06-13 更新。昨天忽然好奇在Google上搜了一波关于create-react-app 源码
的关键词,发现掘金出现好几篇仿文,就连我开头前沿瞎几把啰嗦的话都抄,我还能说什么是吧?之后博客仍是首发在Github
上, 地址戳这里戳这里!!转载求大家注明出处、改编求大家贴一下参考连接...2018-01-26 更新。这两天我边读边思考我是否是真的懂了,我发现我有个重大的失误,我弄错了学习的顺序,学习一个新的东西,咱们应该是先学会熟练的使用它,而后在去探究它的原理,我竟然把第一步忽略了,这明显是错误的,因此我今天在开头新补充一节
使用说明
,同时对后面作一些修改和补充。javascript以前写了几篇关于搭建
react
环境的文,一直尚未完善它,此次撸完这波源码在从新完善以前的从零搭建完美的react
开发打包测试环境,若是你对如何从零搭建一个react
项目有兴趣,或者是尚未经验的小白,能够期待一下,做为我看完源码的成果做品。前端若是后续有更正或者更新的地方,会在顶部加以说明。vue
这段时间公司的事情变得比较少,空下了不少时间,做为一个刚刚毕业初入职场的菜鸟级程序员,一点都不敢放松,秉持着我为人人的思想也想为开源社区作点小小的贡献,可是一直又没有什么明确的目标,最近在努力的准备吃透react
,加上react
的脚手架工具create-react-app
已经很成熟了,初始化一个react
项目根本看不到它究竟是怎么给我搭建的这个开发环境,又是怎么作到的,我仍是想知道知道,因此就把他拖出来溜溜。java
文中如有错误或者须要指正的地方,多多指教,共同进步。node
就像我开头说的那样,学习一个新的东西,应该是先知道如何用,而后在来看他是怎么实现的。create-react-app
究竟是个什么东西,总结一句话来讲,就是官方提供的快速搭建一个新的react
项目的脚手架工具,相似于vue
的vue-cli
和angular
的angular-cli
,至于为何不叫react-cli
是一个值得深思的问题...哈哈哈,有趣!react
不说废话了,贴个图,直接看create-react-app
的命令帮助。webpack
毕竟它已是一个很成熟的工具了,说明也很完善,重点对其中--scripts-version
说一下,其余比较简单,大概说一下,注意有一行Only <project-directory> is required
,直译一下,仅仅只有项目名称是必须的,也就是说你在用create-react-app
命令的时候,必须在其后跟上你的项目名称,其实这里说的不许确,像--version --info --help
这三个选项是不须要带项目名称的,具体看下面:git
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
自己已经对其中选项进行了说明,一共有四种状况,我并无一一去试他,由于还挺麻烦的,之后若是用到了再来补,我先来大概推测一下他们的意思:github
npm
发布本身的react-scripts
.tgz
的下载包.tar.gz
的下载包从上述看的出来create-react-app
对于开发者仍是很友好的,能够本身去定义不少东西,若是你不想这么去折腾,它也提供了标准的react-scripts
供开发者使用,我一直也很好奇这个,以后我在来单独说官方标准的react
配置是怎么作的。
随着它版本的迭代,源码确定是会发生变化的,我这里下载的是v1.1.0
,你们能够自行在github
上下载这个版本,找不到的戳连接。
咱们来看一下它的目录结构
├── .github ├── packages ├── tasks ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .yarnrc ├── appveyor.cleanup-cache.txt ├── appveyor.yml ├── CHANGELOG-0.x.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── lerna.json ├── LICENSE ├── package.json ├── README.md └── screencast.svg
咋一看好多啊,个人天啊,到底要怎么看,其实仔细一晃,好像不少一眼就能看出来是什么意思,大概说一下每一个文件都是干吗的,具体的我也不知道啊,往下看,一步一步来。
.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": [ "packages/*" ],
关于workspaces
一开始我在npm
的说明文档里面没找到,虽然从字面意思咱们也能猜到它的意思是实际工做的目录是packages
,后来我查了一下是yarn
里面的东东,具体看这篇文章,用于在本地测试,具体不关注,只是从这里咱们知道了真正的起做用的文件都在packages
里面。
从上述咱们知道如今真正须要关注的内容都在packages
里面,咱们来看看它里面都是有什么东东:
├── babel-preset-react-app --> 暂不关注 ├── create-react-app ├── eslint-config-react-app --> 暂不关注 ├── react-dev-utils --> 暂不关注 ├── react-error-overlay --> 暂不关注 └── react-scripts --> 核心啊,仍是暂不关注
里面有六个文件夹,哇塞,又是6个单独的项目,这要看到何年何月.....是否是有这种感触,放宽心大胆的看,先想一下咱们在安装了create-react-app
后在,在命令行输入的是create-react-app
的命令,因此咱们大胆的推测关于这个命令应该都是存在了create-react-app
下,在这个目录下一样有package.json
文件,如今咱们把这6个文件拆分红6个项目来分析,上面也说了,看一个项目首先看package.json
文件,找到其中的重点:
"bin": { "create-react-app": "./index.js" }
找到重点了,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
命令的中心入口,其余的都先无论,咱们打开packages/create-react-app
目录,仔细一瞅,噢哟,只有四个文件,四个文件咱们还搞不定吗?除了package.json
、README.md
就只剩两个能看的文件了,咱们来看看这两个文件。
既然以前已经看到packages/create-react-app/package.json
中关于bin
的设置,就是执行index.js
文件,咱们就从index.js
入手,开始瞅瞅源码到底都有些虾米。
除了一大串的注释之外,代码其实不多,全贴上来了:
var chalk = require('chalk'); var currentNodeVersion = process.versions.node; // 返回Node版本信息,若是有多个版本返回多个版本 var semver = currentNodeVersion.split('.'); // 全部Node版本的集合 var major = 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');
咋一眼看过去其实你就知道它大概是什么意思了....检查Node.js
的版本,小于4
就不执行了,咱们分开来看一下,这里他用了一个库chalk
,理解起来并不复杂,一行一行的解析。
chalk
:这个对这段代码的实际影响就是在命令行中,将输出的信息变色。也就引出了这个库的做用改变命令行中输出信息的样式。npm地址 其中有几个Node
自身的API
:
process.versions
返回一个对象,包含Node
以及它的依赖信息process.exit
结束Node
进程,1
是状态码,表示有异常没有处理在咱们通过index.js
后,就来到了createReactApp.js
,下面再继续看。
当咱们本机上的Node
版本大于4
的时候就要继续执行这个文件了,打开这个文件,代码还很多,大概700
多行吧,咱们慢慢拆解。
这里放个小技巧,在读源码的时候,能够在开一个写代码的窗口,跟着写一遍,执行过的代码能够在源文件中先删除,这样700行
代码,当你读了200行
的时候,源文件就只剩500行
了,不只有成就感继续阅读,也把不执行的逻辑先删除了,影响不到你读其余地方。
const validateProjectName = require('validate-npm-package-name'); const chalk = require('chalk'); const commander = require('commander'); const fs = require('fs-extra'); const path = require('path'); const execSync = require('child_process').execSync; const spawn = require('cross-spawn'); const semver = require('semver'); const dns = require('dns'); const tmp = require('tmp'); const unpack = require('tar-pack').unpack; const url = require('url'); const hyperquest = require('hyperquest'); const envinfo = require('envinfo'); const packageJson = require('./package.json');
打开代码一排依赖,懵逼....我不可能挨着去查一个个依赖是用来干吗的吧?因此,个人建议就是先无论,用到的时候在回来看它是干吗的,理解更加透彻一些,继续往下看。
let projectName; // 定义了一个用来存储项目名称的变量 const program = new commander.Command(packageJson.name) .version(packageJson.version) // 输入版本信息,使用`create-react-app -v`的时候就用打印版本信息 .arguments('<project-directory>') // 使用`create-react-app <my-project>` 尖括号中的参数 .usage(`${chalk.green('<project-directory>')} [options]`) // 使用`create-react-app`第一行打印的信息,也就是使用说明 .action(name => { projectName = name; // 此处action函数的参数就是以前argument中的<project-directory> 初始化项目名称 --> 此处影响后面 }) .option('--verbose', 'print additional logs') // option配置`create-react-app -[option]`的选项,相似 --help -V .option('--info', 'print environment debug info') // 打印本地相关开发环境,操做系统,`Node`版本等等 .option( '--scripts-version <alternative-package>', 'use a non-standard version of react-scripts' ) // 这我以前就说过了,指定特殊的`react-scripts` .option('--use-npm') // 默认使用`yarn`,指定使用`npm` .allowUnknownOption() // 这个我没有在文档上查到,直译就是容许无效的option 大概意思就是我能够这样`create-react-app <my-project> -la` 其实 -la 并无定义,可是我仍是能够这么作而不会保存 .on('--help', () => { // 此处省略了一些打印信息 }) // on('--help') 用来定制打印帮助信息 当使用`create-react-app -h(or --help)`的时候就会执行其中的代码,基本都是些打印信息 .parse(process.argv); // 这个就是解析咱们正常的`Node`进程,能够这么理解没有这个东东,`commander`就不能接管`Node`
在上面的代码中,我把可有可无打印信息省略了,这段代码算是这个文件的关键入口地此处他new
了一个commander
,这是个啥东东呢?这时咱们就返回去看它的依赖,找到它是一个外部依赖,这时候怎么办呢?不可能打开node_modules
去里面找撒,很简单,打开npm
官网查一下这个外部依赖。
commander
:概述一下,Node
命令接口,也就是能够用它代管Node
命令。npm地址 上述只是commander
用法的一种实现,没有什么具体好说的,了解了commander
就不难,这里的定义也就是咱们在命令行中看到的那些东西,好比参数,好比打印信息等等,咱们继续往下来。
// 判断在命令行中执行`create-react-app <name>` 有没有name,若是没有就继续 if (typeof projectName === 'undefined') { // 当没有传name的时候,若是带了 --info 的选项继续执行下列代码,这里配置了--info时不会报错 if (program.info) { // 打印当前环境信息和`react`、`react-dom`, `react-scripts`三个包的信息 envinfo.print({ packages: ['react', 'react-dom', 'react-scripts'], noNativeIDE: true, duplicates: true, }); process.exit(0); // 正常退出进程 } // 在没有带项目名称又没带 --info 选项的时候就会打印一堆错误信息,像--version 和 --help 是commander自带的选项,因此不用单独配置 console.error('Please specify the project directory:'); console.log( ` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}` ); console.log(); console.log('For example:'); console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`); console.log(); console.log( `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.` ); process.exit(1); // 抛出异常退出进程 }
还记得上面把create-react-app <my-project>
中的项目名称赋予了projectName
变量吗?此处的做用就是看看用户有没有传这个<my-project>
参数,若是没有就会报错,并显示一些帮助信息,这里用到了另一个外部依赖envinfo
。
envinfo
:能够打印当前操做系统的环境和指定包的信息。 npm地址
到这里我还要吐槽一下
segmentfault
的编辑器...我同时打开视图和编辑好卡...捂脸.png!
这里我以前省略了一个东西,仍是拿出来讲一下:
const hiddenProgram = new commander.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);
create-react-app
在初始化一个项目的时候,会生成一个标准的文件夹,这里有一个隐藏的选项--internal-testing-template
,用来更改初始化目录的模板,这里他已经说了,供内部使用,应该是开发者们开发时候用的,因此不建议你们使用这个选项。
咱们继续往下看,有几个提早定义的函数,咱们无论,直接找到第一个被执行的函数:
createApp( projectName, program.verbose, program.scriptsVersion, program.useNpm, hiddenProgram.internalTestingTemplate );
一个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()
function createApp(name, verbose, version, useNpm, template) { const root = path.resolve(name); // 获取当前进程运行的位置,也就是文件目录的绝对路径 const appName = 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基础内容 const packageJson = { 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` const useYarn = useNpm ? false : shouldUseYarn(); // 取得当前node进程的目录,以前还懂为何要单独取一次,以后也明白了,下一句代码将会改变这个值,因此若是我后面要用这个值,后续其实取得值将不是这个 // 因此这里的目的就是提早存好,省得我后续使用的时候很差去找,这个地方就是我执行初始化项目的目录,而不是初始化好的目录,是初始化的上级目录,有点绕.. const originalDirectory = 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 4 version = 'react-scripts@0.9.x'; } // 若是没有使用yarn 也发出警告 // 这里以前也没有说全,还判断了`npm`的版本是否是在3以上,若是没有依然指定安装`react-scripts@0.9.x`版本 if (!useYarn) { const npmInfo = 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 3 version = 'react-scripts@0.9.x'; } } // 传入这些参数执行run函数 // 执行完毕上述代码之后,将执行`run`函数,可是我仍是先把上述用到的函数所有说完,在来下一个核心函数`run` run(root, appName, version, verbose, originalDirectory, template, useYarn); }
我这里先来总结一下这个函数都作了哪些事情,再来看看他用到的依赖有哪些,先说作了哪些事情,在咱们的目录下建立了一个项目目录,而且校验了这个目录的名称是否合法,这个目录是否安全,而后往其中写入了一个package.json
的文件,而且判断了当前环境下应该使用的react-scripts
的版本,而后执行了run
函数。咱们在来看看这个函数用了哪些外部依赖:
以后函数的函数依赖我都会进行详细的解析,除了少部分特别简单的函数,而后咱们来看看这个函数的函数依赖:
checkAppName()
:用于检测文件名是否合法,isSafeToCreateProjectIn()
:用于检测文件夹是否安全shouldUseYarn()
:用于检测yarn
在本机是否已经安装checkThatNpmCanReadCwd()
:用于检测npm
是否在正确的目录下执行checkNpmVersion()
:用于检测npm
在本机是否已经安装了checkAppName()
function checkAppName(appName) { // 使用 validateProjectName 检查包名是否合法返回结果,这个validateProjectName是外部依赖的引用,见下面说明 const validationResult = 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); } // 定义了三个开发依赖的名称 const dependencies = ['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); } }
它这个函数其实还蛮简单的,用了一个外部依赖来校验文件名是否符合npm
包文件名的规范,而后定义了三个不能取得名字react
、react-dom
、react-scripts
,外部依赖:
validate-npm-package-name
:外部依赖,检查包名是否合法。npm地址 其中的函数依赖:
printValidationResults()
:函数引用,这个函数就是我说的特别简单的类型,里面就是把接收到的错误信息循环打印出来,没什么好说的。isSafeToCreateProjectIn()
function isSafeToCreateProjectIn(root, name) { // 定义了一堆文件名 // 我今天早上仔细的看了一些,如下文件的来历就是咱们这些开发者在`create-react-app`中提的一些文件 const validFiles = [ '.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(); // 这里就是在咱们建立好的项目文件夹下,除了上述文件之外不包含其余文件就会返回true const conflicts = fs .readdirSync(root) .filter(file => !validFiles.includes(file)); if (conflicts.length < 1) { return true; } // 不然这个文件夹就是不安全的,而且挨着打印存在哪些不安全的文件 console.log( `The directory ${chalk.green(name)} contains files that could conflict:` ); console.log(); for (const file of conflicts) { console.log(` ${file}`); } console.log(); console.log( 'Either try using a new directory name, or remove the files listed above.' ); // 而且返回false return false; }
他这个函数也算比较简单,就是判断建立的这个目录是否包含除了上述validFiles
里面的文件,至于这里面的文件是怎么来的,就是create-react-app
在发展至今,开发者们提出来的。
shouldUseYarn()
function shouldUseYarn() { try { execSync('yarnpkg --version', { stdio: 'ignore' }); return true; } catch (e) { return false; } }
就三行...其中execSync
是由node
自身模块child_process
引用而来,就是用来执行命令的,这个函数就是执行一下yarnpkg --version
来判断咱们是否正确安装了yarn
,若是没有正确安装yarn
的话,useYarn
依然为false
,无论指没有指定--use-npm
。
execSync
:引用自child_process.execSync
,用于执行须要执行的子进程checkThatNpmCanReadCwd()
function checkThatNpmCanReadCwd() { const cwd = process.cwd(); // 这里取到当前的进程目录 let childOutput = null; // 定义一个变量来保存`npm`的信息 try { // 至关于执行`npm config list`并将其输出的信息组合成为一个字符串 childOutput = spawn.sync('npm', ['config', 'list']).output.join(''); } catch (err) { return true; } // 判断是不是一个字符串 if (typeof childOutput !== 'string') { return true; } // 将整个字符串以换行符分隔 const lines = childOutput.split('\n'); // 定义一个咱们须要的信息的前缀 const prefix = '; cwd = '; // 去整个lines里面的每一个line查找有没有这个前缀的一行 const line = lines.find(line => line.indexOf(prefix) === 0); if (typeof line !== 'string') { return true; } // 取出后面的信息,这个信息你们能够自行试一试,就是`npm`执行的目录 const npmCWD = line.substring(prefix.length); // 判断当前目录和执行目录是不是一致的 if (npmCWD === cwd) { return true; } // 不一致就打印如下信息,大概意思就是`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/` ) ); } return false; }
这个函数我以前竟然贴错了,实在是很差意思。我以前没有弄懂这个函数的意思,今天再来看的时候已经豁然开朗了,它的意思上述代码已经解析了,其中用到了一个外部依赖:
cross-spawn
:这个我以前说到了没有?忘了,用来执行node
进程。npm地址 为何用单独用一个外部依赖,而不是用node
自身的呢?来看一下cross-spawn
它本身对本身的说明,Node
跨平台解决方案,解决在windows
下各类问题。
checkNpmVersion()
function checkNpmVersion() { let hasMinNpm = false; let npmVersion = null; try { npmVersion = execSync('npm --version') .toString() .trim(); hasMinNpm = semver.gte(npmVersion, '3.0.0'); } catch (err) { // ignore } return { hasMinNpm: hasMinNpm, npmVersion: npmVersion, }; }
这个能说的也比较少,一眼看过去就知道什么意思了,返回一个对象,对象上面有两个对对,一个是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()
function run( root, appName, version, verbose, originalDirectory, template, useYarn ) { // 这里对`react-scripts`作了大量的处理 const packageToInstall = getInstallPackage(version, originalDirectory); // 获取依赖包信息 const allDependencies = ['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 => { // 接收到上述的包名和是否为离线模式 const isOnline = info.isOnline; const packageName = info.packageName; console.log( `Installing ${chalk.cyan('react')}, ${chalk.cyan( 'react-dom' )}, and ${chalk.cyan(packageName)}...` ); console.log(); // 安装依赖 return install(root, useYarn, allDependencies, verbose, isOnline).then( () => packageName ); }) .then(packageName => { // 检查当前`Node`版本是否支持包 checkNodeVersion(packageName); // 检查`package.json`的开发依赖是否正常 setCaretRangeForRuntimeDeps(packageName); // `react-scripts`脚本的目录 const scriptsPath = path.resolve( process.cwd(), 'node_modules', packageName, 'scripts', 'init.js' ); // 引入`init`函数 const init = 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(); // 出现异常的时候将删除目录下的这些文件 const knownGeneratedFiles = [ 'package.json', 'npm-debug.log', 'yarn-error.log', 'yarn-debug.log', 'node_modules', ]; // 挨着删除 const currentFiles = 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)); } }); }); // 判断当前目录下是否还存在文件 const remainingFiles = 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); }); }
他这里对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()
function getInstallPackage(version, originalDirectory) { let packageToInstall = 'react-scripts'; // 定义常量 packageToInstall,默认就是标准`react-scripts`包名 const validSemver = semver.valid(version); // 校验版本号是否合法 if (validSemver) { packageToInstall += `@${validSemver}`; // 合法的话执行,就安装指定版本,在`npm install`安装的时候指定版本为加上`@x.x.x`版本号,安装指定版本的`react-scripts` } else if (version && version.match(/^file:/)) { // 不合法而且版本号参数带有`file:`执行如下代码,做用是指定安装包为咱们自身定义的包 packageToInstall = `file:${path.resolve( originalDirectory, version.match(/^file:(.*)?$/)[1] )}`; } else if (version) { // 不合法而且没有`file:`开头,默认为在线的`tar.gz`文件 // for tar.gz or alternative paths packageToInstall = version; } // 返回最终须要安装的`react-scripts`的信息,或版本号或本地文件或线上`.tar.gz`资源 return packageToInstall; }
这个方法接收两个参数version
版本号,originalDirectory
原始目录,主要的做用是判断react-scripts
应该安装的信息,具体看每一行。
这里create-react-app
自己提供了安装react-scripts
的三种机制,一开始初始化的项目是能够指定react-scripts
的版本或者是自定义这个东西的,因此在这里他就提供了这几种机制,其中用到的外部依赖只有一个semver
,以前就说过了,很少说。
getPackageName()
function getPackageName(installPackage) { // 函数进来就根据上面的那个判断`react-scripts`的信息来安装这个包,用于返回正规的包名 // 此处为线上`tar.gz`包的状况 if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) { // 里面这段建立了一个临时目录,具体它是怎么设置了线上.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 packageName = require(path.join(obj.tmpdir, 'package.json')).name; obj.cleanup(); return packageName; }) .catch(err => { 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(assumedProjectName); }); // 此处为信息中包含`git+`信息的状况 } else if (installPackage.indexOf('git+') === 0) { return Promise.resolve(installPackage.match(/([^/]+)\.git(#.*)?$/)[1]); // 此处为只有版本信息的时候的状况 } else if (installPackage.match(/.+@/)) { return Promise.resolve( installPackage.charAt(0) + installPackage.substr(1).split('@')[0] ); // 此处为信息中包含`file:`开头的状况 } else if (installPackage.match(/^file:/)) { const installPackagePath = installPackage.match(/^file:(.*)?$/)[1]; const installPackageJson = require(path.join(installPackagePath, 'package.json')); return Promise.resolve(installPackageJson.name); } // 什么都没有直接返回包名 return Promise.resolve(installPackage); }
他这个函数的目标就是返回一个正常的依赖包名,好比咱们什么都不带就返回react-scripts
,在好比咱们是本身定义的包就返回my-react-scripts
,继续到了比较关键的函数了,接收一个installPackage
参数,从这函数开始就采用Promise
回调的方式一直执行到最后,咱们来看看这个函数都作了什么,具体看上面每一行的注释。
总结一句话,这个函数的做用就是返回正常的包名,不带任何符号的,来看看它的外部依赖:
hyperquest
:这个用于将http请求流媒体传输。npm地址 他自己还有函数依赖,这两个函数依赖我都不单独再说,函数的意思很好理解,至于为何这么作我还没想明白:
getTemporaryDirectory()
:不难,他自己是一个回调函数,用来建立一个临时目录。extractStream()
:主要用到node
自己的一个流,这里我真没懂为何药改用流的形式,就不发表意见了,在看其实我仍是没懂,要真正的明白是要去试一次,可是真的有点麻烦,不想去关注。PS:其实这个函数很好理解就是返回正常的包名,可是里面的有些处理我都没想通,之后理解深入了在回溯一下。
checkIfOnline()
function checkIfOnline(useYarn) { if (!useYarn) { return Promise.resolve(true); } return new Promise(resolve => { dns.lookup('registry.yarnpkg.com', err => { let proxy; if (err != null && (proxy = getProxy())) { dns.lookup(url.parse(proxy).hostname, proxyErr => { resolve(proxyErr == null); }); } else { resolve(err == null); } }); }); }
这个函数自己接收一个是否使用yarn
的参数来判断是否进行后续,若是使用的是npm
就直接返回true
了,为何会有这个函数是因为yarn
自己有个功能叫离线安装,这个函数来判断是否离线安装,其中用到了外部依赖:
dns
:用来检测是否可以请求到指定的地址。npm地址 install()
function install(root, useYarn, dependencies, verbose, isOnline) { // 封装在一个回调函数中 return new Promise((resolve, reject) => { let command; // 定义一个命令 let args; // 定义一个命令的参数 // 若是使用yarn if (useYarn) { command = 'yarnpkg'; // 命令名称 args = ['add', '--exact']; // 命令参数的基础 if (!isOnline) { args.push('--offline'); // 此处接上面一个函数判断是不是离线模式 } [].push.apply(args, dependencies); // 组合参数和开发依赖 `react` `react-dom` `react-scripts` 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(); } // 不使用yarn的状况使用npm } else { // 此处于上述同样,命令的定义 参数的组合 command = 'npm'; args = [ 'install', '--save', '--save-exact', '--loglevel', 'error', ].concat(dependencies); } // 由于`yarn`和`npm`均可以带这个参数,因此就单独拿出来了拼接到上面 if (verbose) { args.push('--verbose'); } // 这里就把命令组合起来执行 const child = spawn(command, args, { stdio: 'inherit' }); // 命令执行完毕后关闭 child.on('close', code => { // code 为0表明正常关闭,不为零就打印命令执行错误的那条 if (code !== 0) { reject({ command: `${command} ${args.join(' ')}`, }); return; } // 正常继续往下执行 resolve(); }); }); }
又到了比较关键的地方了,仔细看每一行代码注释,此处函数的做用就是组合一个yarn
或者npm
的安装命令,把这些模块安装到项目的文件夹中,其中用到的外部依赖cross-spawn
前面有说了,就不说了。
其实执行到这里,create-react-app
已经帮咱们建立好了目录,package.json
而且安装了全部的依赖,react
、react-dom
和react-scrpts
,复杂的部分已经结束,继续往下走。
checkNodeVersion()
function checkNodeVersion(packageName) { // 找到`react-scripts`的`package.json`路径 const packageJsonPath = path.resolve( process.cwd(), 'node_modules', packageName, 'package.json' ); // 引入`react-scripts`的`package.json` const packageJson = 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); } }
这个函数直译一下,检查Node
版本,为何要检查了?以前我已经说过了react-scrpts
是须要依赖Node
版本的,也就是说低版本的Node
不支持,其实的外部依赖也是以前的几个,没什么好说的。
setCaretRangeForRuntimeDeps()
function setCaretRangeForRuntimeDeps(packageName) { const packagePath = path.join(process.cwd(), 'package.json'); // 取出建立项目的目录中的`package.json`路径 const packageJson = require(packagePath); // 引入`package.json` // 判断其中`dependencies`是否存在,不存在表明咱们的开发依赖没有成功安装 if (typeof packageJson.dependencies === 'undefined') { console.error(chalk.red('Missing dependencies in package.json')); process.exit(1); } // 拿出`react-scripts`或者是自定义的看看`package.json`中是否存在 const packageVersion = packageJson.dependencies[packageName]; if (typeof packageVersion === 'undefined') { console.error(chalk.red(`Unable to find ${packageName} in package.json`)); process.exit(1); } // 检查`react` `react-dom` 的版本 makeCaretRange(packageJson.dependencies, 'react'); makeCaretRange(packageJson.dependencies, 'react-dom'); // 从新写入文件`package.json` fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2)); }
这个函数我也不想说太多了,他的做用并无那么大,就是用来检测咱们以前安装的依赖是否写入了package.json
里面,而且对依赖的版本作了检测,其中一个函数依赖:
makeCaretRange()
:用来对依赖的版本作检测我没有单独对其中的子函数进行分析,是由于我以为不难,并且对主线影响不大,我不想贴太多说不完。
到这里createReactApp.js
里面的源码都分析完了,咦!你可能会说你都没说init()
函数,哈哈哈,看到这里说明你很认真哦,init()
函数是放在packages/react-scripts/script
目录下的,可是我仍是要给他说了,由于它其实跟react-scripts
包联系不大,就是个copy
他自己定义好的模板目录结构的函数。
init()
它自己接收5
个参数:
appPath
:以前的root
,项目的绝对路径appName
:项目的名称verbose
:这个参数我以前说过了,npm
安装时额外的信息originalDirectory
:原始目录,命令执行的目录template
:其实其中只有一种类型的模板,这个选项的做用就是配置以前我说过的那个函数,测试模板// 当前的包名,也就是这个命令的包 const ownPackageName = require(path.join(__dirname, '..', 'package.json')).name; // 当前包的路径 const ownPath = path.join(appPath, 'node_modules', ownPackageName); // 项目的`package.json` const appPackage = require(path.join(appPath, 'package.json')); // 检查项目中是否有`yarn.lock`来判断是否使用`yarn` const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); appPackage.dependencies = appPackage.dependencies || {}; // 定义其中`scripts`的 appPackage.scripts = { start: 'react-scripts start', build: 'react-scripts build', test: 'react-scripts test --env=jsdom', eject: 'react-scripts eject', }; // 从新写入`package.json` fs.writeFileSync( path.join(appPath, 'package.json'), JSON.stringify(appPackage, null, 2) ); // 判断项目目录是否有`README.md`,模板目录中已经定义了`README.md`防止冲突 const readmeExists = fs.existsSync(path.join(appPath, 'README.md')); if (readmeExists) { fs.renameSync( path.join(appPath, 'README.md'), path.join(appPath, 'README.old.md') ); } // 是否有模板选项,默认为当前执行命令包目录下的`template`目录,也就是`packages/react-scripts/tempalte` const templatePath = template ? path.resolve(originalDirectory, template) : path.join(ownPath, 'template'); if (fs.existsSync(templatePath)) { // 拷贝目录到项目目录 fs.copySync(templatePath, appPath); } else { console.error( `Could not locate supplied template: ${chalk.green(templatePath)}` ); return; }
这个函数我就不把代码贴全了,里面的东西也蛮好理解,基本上就是对目录结构的修改和重名了那些,挑了一些来讲,到这里,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
的源码分析的文。
码字不易,可能出现错别字什么的,说的不清楚的,说错的,欢迎指正,多多包涵!