前言 这段时间公司的事情变得比较少,空下了很多时间,作为一个刚刚毕业初入职场的菜鸟级程序员(去年是),一点都不敢放松,秉持着我为人人的思想也想为开源社区做点小小的贡献,但是一直又没有什么明确的目标,最近在努力的准备吃透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
本身已经对其中选项进行了说明,一共有四种情况,我并没有一一去试他,因为还挺麻烦的,以后如果用到了再来补,我先来大概推测一下他们的意思:
指定版本为0.8.2
在npm
发布自己的react-scripts
在自己的网站上设置一个.tgz
的下载包
在自己的网站上设置一个.tar.gz
的下载包
从上述看的出来create-react-app
对于开发者还是很友好的,可以自己去定义很多东西,如果你不想这么去折腾,它也提供了标准的react-scripts
供开发者使用,我一直也很好奇这个,之后我在来单独说官方标准的react
配置是怎么做的。
目录分析 随着它版本的迭代,源码肯定是会发生变化的,我这里下载的是v1.1.0
,大家可以自行在github
上下载这个版本,找不到的戳链接 。
主要说明 我们来看一下它的目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ├── .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
文件开始看,人家是项目的介绍文件,你不看它看啥。
它里面理论上应该是有名称、版本等等一些说明性信息,但是都没用,看几个重要的配置。
1 2 3 "workspaces" : [ "packages/*" ] ,
关于workspaces
一开始我在npm
的说明文档里面没找到,虽然从字面意思我们也能猜到它的意思是实际工作的目录是packages
,后来我查了一下是yarn
里面的东东,具体看这篇文章 ,用于在本地测试,具体不关注,只是从这里我们知道了真正的起作用的文件都在packages
里面。
重点关注 从上述我们知道现在真正需要关注的内容都在packages
里面,我们来看看它里面都是有什么东东:
1 2 3 4 5 6 ├── 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
文件,找到其中的重点:
1 2 3 "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目录解析 经过以上一系列的查找,我们终于艰难的找到了create-react-app
命令的中心入口,其他的都先不管,我们打开packages/create-react-app
目录,仔细一瞅,噢哟,只有四个文件,四个文件我们还搞不定吗?除了package.json
、README.md
就只剩两个能看的文件了,我们来看看这两个文件。
index.js 既然之前已经看到packages/create-react-app/package.json
中关于bin
的设置,就是执行index.js
文件,我们就从index.js
入手,开始瞅瞅源码到底都有些虾米。
除了一大串的注释以外,代码其实很少,全贴上来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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' );
咋一眼看过去其实你就知道它大概是什么意思了….检查Node.js
的版本,小于4
就不执行了,我们分开来看一下,这里他用了一个库chalk
,理解起来并不复杂,一行一行的解析。
chalk
:这个对这段代码的实际影响就是在命令行中,将输出的信息变色。也就引出了这个库的作用改变命令行中输出信息的样式。npm地址
其中有几个Node
自身的API
:
process.versions
返回一个对象,包含Node
以及它的依赖信息
process.exit
结束Node
进程,1
是状态码,表示有异常没有处理
在我们经过index.js
后,就来到了createReactApp.js
,下面再继续看。
createReactApp.js 当我们本机上的Node
版本大于4
的时候就要继续执行这个文件了,打开这个文件,代码还不少,大概700
多行吧,我们慢慢拆解。
这里放个小技巧,在读源码的时候,可以在开一个写代码的窗口,跟着写一遍,执行过的代码可以在源文件中先删除,这样700行
代码,当你读了200行
的时候,源文件就只剩500行
了,不仅有成就感继续阅读,也把不执行的逻辑先删除了,影响不到你读其他地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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' );
打开代码一排依赖,懵逼….我不可能挨着去查一个个依赖是用来干嘛的吧?所以,我的建议就是先不管,用到的时候在回来看它是干嘛的,理解更加透彻一些,继续往下看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let projectName; 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 );
在上面的代码中,我把无关紧要打印信息省略了,这段代码算是这个文件的关键入口地此处他new
了一个commander
,这是个啥东东呢?这时我们就返回去看它的依赖,找到它是一个外部依赖,这时候怎么办呢?不可能打开node_modules
去里面找撒,很简单,打开npm
官网查一下这个外部依赖。
commander
:概述一下,Node
命令接口,也就是可以用它代管Node
命令。npm地址
上述只是commander
用法的一种实现,没有什么具体好说的,了解了commander
就不难,这里的定义也就是我们在命令行中看到的那些东西,比如参数,比如打印信息等等,我们继续往下来。
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 if (typeof projectName === 'undefined' ) { if (program.info ) { envinfo.print ({ packages : ['react' , 'react-dom' , 'react-scripts' ], noNativeIDE : true , duplicates : true , }); process.exit (0 ); } 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!
这里我之前省略了一个东西,还是拿出来说一下:
1 2 3 4 5 6 7 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
,用来更改初始化目录的模板,这里他已经说了,供内部使用,应该是开发者们开发时候用的,所以不建议大家使用这个选项。
我们继续往下看,有几个提前定义的函数,我们不管,直接找到第一个被执行的函数:
1 2 3 4 5 6 7 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()
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 62 63 64 65 66 67 68 69 70 71 72 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 ); } console .log (`Creating a new React app in ${chalk.green(root)} .` ); console .log (); 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' )) { 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` ) ); version = '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` ) ); } version = 'react-scripts@0.9.x' ; } } run (root, appName, version, verbose, originalDirectory, template, useYarn); }
我这里先来总结一下这个函数都做了哪些事情,再来看看他用到的依赖有哪些,先说做了哪些事情,在我们的目录下创建了一个项目目录,并且校验了这个目录的名称是否合法,这个目录是否安全,然后往其中写入了一个package.json
的文件,并且判断了当前环境下应该使用的react-scripts
的版本,然后执行了run
函数。我们在来看看这个函数用了哪些外部依赖:
fs-extra
:外部依赖,Node
自带文件模块的外部扩展模块 npm地址
semver
:外部依赖,用于比较Node
版本 npm地址
之后函数的函数依赖我都会进行详细的解析,除了少部分特别简单的函数,然后我们来看看这个函数的函数依赖:
checkAppName()
:用于检测文件名是否合法,
isSafeToCreateProjectIn()
:用于检测文件夹是否安全
shouldUseYarn()
:用于检测yarn
在本机是否已经安装
checkThatNpmCanReadCwd()
:用于检测npm
是否在正确的目录下执行
checkNpmVersion()
:用于检测npm
在本机是否已经安装了
checkAppName()
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 function checkAppName (appName ) { 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()
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 function isSafeToCreateProjectIn (root, name ) { 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 (); 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.' ); return false ; }
他这个函数也算比较简单,就是判断创建的这个目录是否包含除了上述validFiles
里面的文件,至于这里面的文件是怎么来的,就是create-react-app
在发展至今,开发者们提出来的。
shouldUseYarn()
1 2 3 4 5 6 7 8 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()
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 function checkThatNpmCanReadCwd ( ) { const cwd = process.cwd (); let childOutput = null ; try { 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 = ' ; const line = lines.find (line => line.indexOf (prefix) === 0 ); if (typeof line !== 'string' ) { return true ; } const npmCWD = line.substring (prefix.length ); if (npmCWD === cwd) { return true ; } 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.` ) ); 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()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function checkNpmVersion ( ) { let hasMinNpm = false ; let npmVersion = null ; try { npmVersion = execSync ('npm --version' ) .toString () .trim (); hasMinNpm = semver.gte (npmVersion, '3.0.0' ); } catch (err) { } 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()
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 function run ( root, appName, version, verbose, originalDirectory, template, useYarn ) { 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 => { 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); 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()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function getInstallPackage (version, originalDirectory ) { let packageToInstall = 'react-scripts' ; const validSemver = semver.valid (version); if (validSemver) { packageToInstall += `@${validSemver} ` ; } else if (version && version.match (/^file:/ )) { packageToInstall = `file:${path.resolve( originalDirectory, version.match(/^file:(.*)?$/)[1 ] )} ` ; } else if (version) { packageToInstall = version; } return packageToInstall; }
这个方法接收两个参数version
版本号,originalDirectory
原始目录,主要的作用是判断react-scripts
应该安装的信息,具体看每一行。
这里create-react-app
本身提供了安装react-scripts
的三种机制,一开始初始化的项目是可以指定react-scripts
的版本或者是自定义这个东西的,所以在这里他就提供了这几种机制,其中用到的外部依赖只有一个semver
,之前就说过了,不多说。
getPackageName()
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 function getPackageName (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 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); }); } 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 ] ); } 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()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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()
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 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); 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 (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 (); }); }); }
又到了比较关键的地方了,仔细看每一行代码注释,此处函数的作用就是组合一个yarn
或者npm
的安装命令,把这些模块安装到项目的文件夹中,其中用到的外部依赖cross-spawn
前面有说了,就不说了。
其实执行到这里,create-react-app
已经帮我们创建好了目录,package.json
并且安装了所有的依赖,react
、react-dom
和react-scrpts
,复杂的部分已经结束,继续往下走。
checkNodeVersion()
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 function checkNodeVersion (packageName ) { const packageJsonPath = path.resolve ( process.cwd (), 'node_modules' , packageName, 'package.json' ); const packageJson = require (packageJsonPath); if (!packageJson.engines || !packageJson.engines .node ) { return ; } 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()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function setCaretRangeForRuntimeDeps (packageName ) { const packagePath = path.join (process.cwd (), 'package.json' ); const packageJson = require (packagePath); if (typeof packageJson.dependencies === 'undefined' ) { console .error (chalk.red ('Missing dependencies in package.json' )); process.exit (1 ); } const packageVersion = packageJson.dependencies [packageName]; if (typeof packageVersion === 'undefined' ) { console .error (chalk.red (`Unable to find ${packageName} in package.json` )); process.exit (1 ); } makeCaretRange (packageJson.dependencies , 'react' ); makeCaretRange (packageJson.dependencies , 'react-dom' ); 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
:其实其中只有一种类型的模板,这个选项的作用就是配置之前我说过的那个函数,测试模板
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 const ownPackageName = require (path.join (__dirname, '..' , 'package.json' )).name ;const ownPath = path.join (appPath, 'node_modules' , ownPackageName);const appPackage = require (path.join (appPath, 'package.json' ));const useYarn = fs.existsSync (path.join (appPath, 'yarn.lock' ));appPackage.dependencies = appPackage.dependencies || {}; appPackage.scripts = { start : 'react-scripts start' , build : 'react-scripts build' , test : 'react-scripts test --env=jsdom' , eject : 'react-scripts eject' , }; fs.writeFileSync ( path.join (appPath, 'package.json' ), JSON .stringify (appPackage, null , 2 ) ); 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, '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
的源码分析的文。
码字不易,可能出现错别字什么的,说的不清楚的,说错的,欢迎指正,多多包涵!