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
的源碼分析的文。
碼字不易,可能出現錯別字什麼的,說的不清楚的,說錯的,歡迎指正,多多包涵!