摘要:這個(gè)選項(xiàng)看意思就知道了,默認(rèn)使用來(lái)安裝,運(yùn)行,如果你沒(méi)有使用,你可能就需要這個(gè)配置了,指定使用。
2018-06-13 更新。昨天突然好奇在Google上搜了一波關(guān)于create-react-app 源碼的關(guān)鍵詞,發(fā)現(xiàn)掘金出現(xiàn)好幾篇仿文,就連我開(kāi)頭前沿瞎幾把啰嗦的話(huà)都抄,我還能說(shuō)什么是吧?以后博客還是首發(fā)在Github上,地址戳這里戳這里??!轉(zhuǎn)載求你們注明出處、改編求你們貼一下參考鏈接...前言2018-01-26 更新。這兩天我邊讀邊思考我是不是真的懂了,我發(fā)現(xiàn)我有個(gè)重大的失誤,我弄錯(cuò)了學(xué)習(xí)的順序,學(xué)習(xí)一個(gè)新的東西,我們應(yīng)該是先學(xué)會(huì)熟練的使用它,然后在去探究它的原理,我居然把第一步忽略了,這明顯是錯(cuò)誤的,所以我今天在開(kāi)頭新補(bǔ)充一節(jié)使用說(shuō)明,同時(shí)對(duì)后面做一些修改和補(bǔ)充。
之前寫(xiě)了幾篇關(guān)于搭建react環(huán)境的文,一直還沒(méi)有完善它,這次擼完這波源碼在重新完善之前的從零搭建完美的react開(kāi)發(fā)打包測(cè)試環(huán)境,如果你對(duì)如何從零搭建一個(gè)react項(xiàng)目有興趣,或者是還沒(méi)有經(jīng)驗(yàn)的小白,可以期待一下,作為我看完源碼的成果作品。
如果后續(xù)有更正或者更新的地方,會(huì)在頂部加以說(shuō)明。
這段時(shí)間公司的事情變得比較少,空下了很多時(shí)間,作為一個(gè)剛剛畢業(yè)初入職場(chǎng)的菜鳥(niǎo)級(jí)程序員,一點(diǎn)都不敢放松,秉持著我為人人的思想也想為開(kāi)源社區(qū)做點(diǎn)小小的貢獻(xiàn),但是一直又沒(méi)有什么明確的目標(biāo),最近在努力的準(zhǔn)備吃透react,加上react的腳手架工具create-react-app已經(jīng)很成熟了,初始化一個(gè)react項(xiàng)目根本看不到它到底是怎么給我搭建的這個(gè)開(kāi)發(fā)環(huán)境,又是怎么做到的,我還是想知道知道,所以就把他拖出來(lái)溜溜。
文中若有錯(cuò)誤或者需要指正的地方,多多指教,共同進(jìn)步。
使用說(shuō)明就像我開(kāi)頭說(shuō)的那樣,學(xué)習(xí)一個(gè)新的東西,應(yīng)該是先知道如何用,然后在來(lái)看他是怎么實(shí)現(xiàn)的。create-react-app到底是個(gè)什么東西,總結(jié)一句話(huà)來(lái)說(shuō),就是官方提供的快速搭建一個(gè)新的react項(xiàng)目的腳手架工具,類(lèi)似于vue的vue-cli和angular的angular-cli,至于為什么不叫react-cli是一個(gè)值得深思的問(wèn)題...哈哈哈,有趣!
不說(shuō)廢話(huà)了,貼個(gè)圖,直接看create-react-app的命令幫助。
概略說(shuō)明畢竟它已經(jīng)是一個(gè)很成熟的工具了,說(shuō)明也很完善,重點(diǎn)對(duì)其中--scripts-version說(shuō)一下,其他比較簡(jiǎn)單,大概說(shuō)一下,注意有一行Only
create-react-app -V(or --version):這個(gè)選項(xiàng)可以多帶帶使用,打印版本信息,每個(gè)工具基本都有吧?
create-react-app --info:這個(gè)選項(xiàng)也可以多帶帶使用,打印當(dāng)前系統(tǒng)跟react相關(guān)的開(kāi)發(fā)環(huán)境參數(shù),也就是操作系統(tǒng)是什么啊,Node版本啊之類(lèi)的,可以自己試一試。
create-react-app -h(or --help):這個(gè)肯定是可以多帶帶使用的,不然怎么打印幫助信息,不然就沒(méi)有上面的截圖了。
也就是說(shuō)除了上述三個(gè)參數(shù)選項(xiàng)是可以脫離必須參數(shù)項(xiàng)目名稱(chēng)以外來(lái)多帶帶使用的,因?yàn)樗鼈兌几阋跏蓟?b>react項(xiàng)目無(wú)關(guān),然后剩下的參數(shù)就是對(duì)要初始化的react項(xiàng)目進(jìn)行配置的,也就是說(shuō)三個(gè)參數(shù)是可以同時(shí)出現(xiàn)的,來(lái)看一下它們分別的作用:
create-react-app
create-react-app
create-react-app
關(guān)于--scripts-version我還要多說(shuō)一點(diǎn),其實(shí)在上述截圖中我們已經(jīng)可以看到,create-react-app本身已經(jīng)對(duì)其中選項(xiàng)進(jìn)行了說(shuō)明,一共有四種情況,我并沒(méi)有一一去試他,因?yàn)檫€挺麻煩的,以后如果用到了再來(lái)補(bǔ),我先來(lái)大概推測(cè)一下他們的意思:
指定版本為0.8.2
在npm發(fā)布自己的react-scripts
在自己的網(wǎng)站上設(shè)置一個(gè).tgz的下載包
在自己的網(wǎng)站上設(shè)置一個(gè).tar.gz的下載包
從上述看的出來(lái)create-react-app對(duì)于開(kāi)發(fā)者還是很友好的,可以自己去定義很多東西,如果你不想這么去折騰,它也提供了標(biāo)準(zhǔn)的react-scripts供開(kāi)發(fā)者使用,我一直也很好奇這個(gè),之后我在來(lái)多帶帶說(shuō)官方標(biāo)準(zhǔn)的react配置是怎么做的。
目錄分析隨著它版本的迭代,源碼肯定是會(huì)發(fā)生變化的,我這里下載的是v1.1.0,大家可以自行在github上下載這個(gè)版本,找不到的戳鏈接。
主要說(shuō)明我們來(lái)看一下它的目錄結(jié)構(gòu)
├── .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
咋一看好多啊,我的天啊,到底要怎么看,其實(shí)仔細(xì)一晃,好像很多一眼就能看出來(lái)是什么意思,大概說(shuō)一下每個(gè)文件都是干嘛的,具體的我也不知道啊,往下看,一步一步來(lái)。
.github:這里面放著當(dāng)你在這個(gè)項(xiàng)目提issue和pr時(shí)候的規(guī)范
packages:字面意思就是包們.....暫時(shí)不管,后面詳說(shuō) ----> 重點(diǎn)
tasks:字面意思就是任務(wù)們.....暫時(shí)不管,后面詳說(shuō) ----> 重點(diǎn)
.eslintignore: eslint檢查時(shí)忽略文件
.eslintrc:eslint檢查配置文件
.gitignore:git提交時(shí)忽略文件
.travis.yml:travis配置文件
.yarnrc:yarn配置文件
appveyor.cleanup-cache.txt:里面有一行Edit this file to trigger a cache rebuild編輯此文件觸發(fā)緩存,具體干嘛的,暫時(shí)不議
appveyor.yml: appveyor配置文件
CHANGELOG-0.x.md:版本0.X開(kāi)頭的變更說(shuō)明文件
CHANGELOG.md:當(dāng)前版本變更說(shuō)明文件
CODE_OF_CONDUCT.md:facebook代碼行為準(zhǔn)則說(shuō)明
CONTRIBUTING.md:項(xiàng)目的核心說(shuō)明
lerna.json:lerna配置文件
LICENSE:開(kāi)源協(xié)議
package.json:項(xiàng)目配置文件
README.md:項(xiàng)目使用說(shuō)明
screencast.svg:圖片...
看了這么多文件,是不是打退堂鼓了?哈哈哈哈,好了好了,進(jìn)入正題,其實(shí)上述對(duì)于我們閱讀源碼有用的只有packages、tasks、package.json三個(gè)文件而已,而且本篇能用到的也就packages和package.json,是不是想打我.....我也只是想告訴大家這些文件有什么用,它們都是有各自的作用的,如果還不了解,參考下面的參考鏈接。
參考鏈接eslint相關(guān)的:eslint官網(wǎng)
travis相關(guān)的:travis官網(wǎng) travis入門(mén)
yarn相關(guān)的:yarn官網(wǎng)
appveyor相關(guān)的:appveyor官網(wǎng)
lerna相關(guān)的:lerna官網(wǎng)
工具自行了解,本文只說(shuō)源碼相關(guān)的packages、package.json。
尋找入口現(xiàn)在的前端項(xiàng)目大多數(shù)都有很多別的依賴(lài),不在像以前那些原生javascript的工具庫(kù),拿到源碼文件,就可以開(kāi)始看了,像jQuery、underscore等等,一個(gè)兩個(gè)文件包含了它所有的內(nèi)容,雖然也有很框架會(huì)有umd規(guī)范的文件可以直接閱讀,像better-scroll等等,但是其實(shí)他在書(shū)寫(xiě)源碼的時(shí)候還是拆分成了很多塊,最后在用打包工具整合在一起了。但是像create-react-app這樣的腳手架工具好像不能像之前那種方法來(lái)看了,必須找到整個(gè)程序的入口,在逐步突破,所以最開(kāi)始的工具肯定是尋找入口。
開(kāi)始關(guān)注拿到一個(gè)項(xiàng)目我們應(yīng)該從哪個(gè)文件開(kāi)始看起呢?只要是基于npm管理的,我都推薦從package.json文件開(kāi)始看,人家是項(xiàng)目的介紹文件,你不看它看啥。
它里面理論上應(yīng)該是有名稱(chēng)、版本等等一些說(shuō)明性信息,但是都沒(méi)用,看幾個(gè)重要的配置。
"workspaces": [ "packages/*" ],
關(guān)于workspaces一開(kāi)始我在npm的說(shuō)明文檔里面沒(méi)找到,雖然從字面意思我們也能猜到它的意思是實(shí)際工作的目錄是packages,后來(lái)我查了一下是yarn里面的東東,具體看這篇文章,用于在本地測(cè)試,具體不關(guān)注,只是從這里我們知道了真正的起作用的文件都在packages里面。
重點(diǎn)關(guān)注從上述我們知道現(xiàn)在真正需要關(guān)注的內(nèi)容都在packages里面,我們來(lái)看看它里面都是有什么東東:
├── babel-preset-react-app --> 暫不關(guān)注 ├── create-react-app ├── eslint-config-react-app --> 暫不關(guān)注 ├── react-dev-utils --> 暫不關(guān)注 ├── react-error-overlay --> 暫不關(guān)注 └── react-scripts --> 核心啊,還是暫不關(guān)注
里面有六個(gè)文件夾,哇塞,又是6個(gè)多帶帶的項(xiàng)目,這要看到何年何月.....是不是有這種感觸,放寬心大膽的看,先想一下我們?cè)诎惭b了create-react-app后在,在命令行輸入的是create-react-app的命令,所以我們大膽的推測(cè)關(guān)于這個(gè)命令應(yīng)該都是存在了create-react-app下,在這個(gè)目錄下同樣有package.json文件,現(xiàn)在我們把這6個(gè)文件拆分成6個(gè)項(xiàng)目來(lái)分析,上面也說(shuō)了,看一個(gè)項(xiàng)目首先看package.json文件,找到其中的重點(diǎn):
"bin": { "create-react-app": "./index.js" }
找到重點(diǎn)了,package.json文件中的bin就是在命令行中可以運(yùn)行的命令,也就是說(shuō)我們?cè)趫?zhí)行create-react-app命令的時(shí)候,就是執(zhí)行create-react-app目錄下的index.js文件。
多說(shuō)兩句關(guān)于package.json中的bin選項(xiàng),其實(shí)是基于node環(huán)境運(yùn)行之后的內(nèi)容。舉個(gè)簡(jiǎn)單的例子,在我們安裝create-react-app后,執(zhí)行create-react-app等價(jià)于執(zhí)行node index.js。
create-react-app目錄解析經(jīng)過(guò)以上一系列的查找,我們終于艱難的找到了create-react-app命令的中心入口,其他的都先不管,我們打開(kāi)packages/create-react-app目錄,仔細(xì)一瞅,噢喲,只有四個(gè)文件,四個(gè)文件我們還搞不定嗎?除了package.json、README.md就只剩兩個(gè)能看的文件了,我們來(lái)看看這兩個(gè)文件。
index.js既然之前已經(jīng)看到packages/create-react-app/package.json中關(guān)于bin的設(shè)置,就是執(zhí)行index.js文件,我們就從index.js入手,開(kāi)始瞅瞅源碼到底都有些蝦米。
除了一大串的注釋以外,代碼其實(shí)很少,全貼上來(lái)了:
var chalk = require("chalk"); var currentNodeVersion = process.versions.node; // 返回Node版本信息,如果有多個(gè)版本返回多個(gè)版本 var semver = currentNodeVersion.split("."); // 所有Node版本的集合 var major = semver[0]; // 取出第一個(gè)Node版本信息 // 如果當(dāng)前版本小于4就打印以下信息并終止進(jìn)程 if (major < 4) { console.error( chalk.red( "You are running Node " + currentNodeVersion + ". " + "Create React App requires Node 4 or higher. " + "Please update your version of Node." ) ); process.exit(1); // 終止進(jìn)程 } // 沒(méi)有小于4就引入以下文件繼續(xù)執(zhí)行 require("./createReactApp");
咋一眼看過(guò)去其實(shí)你就知道它大概是什么意思了....檢查Node.js的版本,小于4就不執(zhí)行了,我們分開(kāi)來(lái)看一下,這里他用了一個(gè)庫(kù)chalk ,理解起來(lái)并不復(fù)雜,一行一行的解析。
chalk:這個(gè)對(duì)這段代碼的實(shí)際影響就是在命令行中,將輸出的信息變色。也就引出了這個(gè)庫(kù)的作用改變命令行中輸出信息的樣式。npm地址
其中有幾個(gè)Node自身的API:
process.versions 返回一個(gè)對(duì)象,包含Node以及它的依賴(lài)信息
process.exit 結(jié)束Node進(jìn)程,1是狀態(tài)碼,表示有異常沒(méi)有處理
在我們經(jīng)過(guò)index.js后,就來(lái)到了createReactApp.js,下面再繼續(xù)看。
createReactApp.js當(dāng)我們本機(jī)上的Node版本大于4的時(shí)候就要繼續(xù)執(zhí)行這個(gè)文件了,打開(kāi)這個(gè)文件,代碼還不少,大概700多行吧,我們慢慢拆解。
這里放個(gè)小技巧,在讀源碼的時(shí)候,可以在開(kāi)一個(gè)寫(xiě)代碼的窗口,跟著寫(xiě)一遍,執(zhí)行過(guò)的代碼可以在源文件中先刪除,這樣700行代碼,當(dāng)你讀了200行的時(shí)候,源文件就只剩500行了,不僅有成就感繼續(xù)閱讀,也把不執(zhí)行的邏輯先刪除了,影響不到你讀其他地方。
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");
打開(kāi)代碼一排依賴(lài),懵逼....我不可能挨著去查一個(gè)個(gè)依賴(lài)是用來(lái)干嘛的吧?所以,我的建議就是先不管,用到的時(shí)候在回來(lái)看它是干嘛的,理解更加透徹一些,繼續(xù)往下看。
let projectName; // 定義了一個(gè)用來(lái)存儲(chǔ)項(xiàng)目名稱(chēng)的變量 const program = new commander.Command(packageJson.name) .version(packageJson.version) // 輸入版本信息,使用`create-react-app -v`的時(shí)候就用打印版本信息 .arguments("") // 使用`create-react-app ` 尖括號(hào)中的參數(shù) .usage(`${chalk.green(" ")} [options]`) // 使用`create-react-app`第一行打印的信息,也就是使用說(shuō)明 .action(name => { projectName = name; // 此處action函數(shù)的參數(shù)就是之前argument中的 初始化項(xiàng)目名稱(chēng) --> 此處影響后面 }) .option("--verbose", "print additional logs") // option配置`create-react-app -[option]`的選項(xiàng),類(lèi)似 --help -V .option("--info", "print environment debug info") // 打印本地相關(guān)開(kāi)發(fā)環(huán)境,操作系統(tǒng),`Node`版本等等 .option( "--scripts-version ", "use a non-standard version of react-scripts" ) // 這我之前就說(shuō)過(guò)了,指定特殊的`react-scripts` .option("--use-npm") // 默認(rèn)使用`yarn`,指定使用`npm` .allowUnknownOption() // 這個(gè)我沒(méi)有在文檔上查到,直譯就是允許無(wú)效的option 大概意思就是我可以這樣`create-react-app -la` 其實(shí) -la 并沒(méi)有定義,但是我還是可以這么做而不會(huì)保存 .on("--help", () => { // 此處省略了一些打印信息 }) // on("--help") 用來(lái)定制打印幫助信息 當(dāng)使用`create-react-app -h(or --help)`的時(shí)候就會(huì)執(zhí)行其中的代碼,基本都是些打印信息 .parse(process.argv); // 這個(gè)就是解析我們正常的`Node`進(jìn)程,可以這么理解沒(méi)有這個(gè)東東,`commander`就不能接管`Node`
在上面的代碼中,我把無(wú)關(guān)緊要打印信息省略了,這段代碼算是這個(gè)文件的關(guān)鍵入口地此處他new了一個(gè)commander,這是個(gè)啥東東呢?這時(shí)我們就返回去看它的依賴(lài),找到它是一個(gè)外部依賴(lài),這時(shí)候怎么辦呢?不可能打開(kāi)node_modules去里面找撒,很簡(jiǎn)單,打開(kāi)npm官網(wǎng)查一下這個(gè)外部依賴(lài)。
commander:概述一下,Node命令接口,也就是可以用它代管Node命令。npm地址
上述只是commander用法的一種實(shí)現(xiàn),沒(méi)有什么具體好說(shuō)的,了解了commander就不難,這里的定義也就是我們?cè)诿钚兄锌吹降哪切〇|西,比如參數(shù),比如打印信息等等,我們繼續(xù)往下來(lái)。
// 判斷在命令行中執(zhí)行`create-react-app` 有沒(méi)有name,如果沒(méi)有就繼續(xù) if (typeof projectName === "undefined") { // 當(dāng)沒(méi)有傳name的時(shí)候,如果帶了 --info 的選項(xiàng)繼續(xù)執(zhí)行下列代碼,這里配置了--info時(shí)不會(huì)報(bào)錯(cuò) if (program.info) { // 打印當(dāng)前環(huán)境信息和`react`、`react-dom`, `react-scripts`三個(gè)包的信息 envinfo.print({ packages: ["react", "react-dom", "react-scripts"], noNativeIDE: true, duplicates: true, }); process.exit(0); // 正常退出進(jìn)程 } // 在沒(méi)有帶項(xiàng)目名稱(chēng)又沒(méi)帶 --info 選項(xiàng)的時(shí)候就會(huì)打印一堆錯(cuò)誤信息,像--version 和 --help 是commander自帶的選項(xiàng),所以不用多帶帶配置 console.error("Please specify the project directory:"); console.log( ` ${chalk.cyan(program.name())} ${chalk.green(" ")}` ); 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); // 拋出異常退出進(jìn)程 }
還記得上面把create-react-app
envinfo:可以打印當(dāng)前操作系統(tǒng)的環(huán)境和指定包的信息。 npm地址
到這里我還要吐槽一下segmentfault的編輯器...我同時(shí)打開(kāi)視圖和編輯好卡...捂臉.png!
這里我之前省略了一個(gè)東西,還是拿出來(lái)說(shuō)一下:
const hiddenProgram = new commander.Command() .option( "--internal-testing-template", "(internal usage only, DO NOT RELY ON THIS) " + "use a non-standard application template" ) .parse(process.argv);
create-react-app在初始化一個(gè)項(xiàng)目的時(shí)候,會(huì)生成一個(gè)標(biāo)準(zhǔn)的文件夾,這里有一個(gè)隱藏的選項(xiàng)--internal-testing-template,用來(lái)更改初始化目錄的模板,這里他已經(jīng)說(shuō)了,供內(nèi)部使用,應(yīng)該是開(kāi)發(fā)者們開(kāi)發(fā)時(shí)候用的,所以不建議大家使用這個(gè)選項(xiàng)。
我們繼續(xù)往下看,有幾個(gè)提前定義的函數(shù),我們不管,直接找到第一個(gè)被執(zhí)行的函數(shù):
createApp( projectName, program.verbose, program.scriptsVersion, program.useNpm, hiddenProgram.internalTestingTemplate );
一個(gè)createAPP函數(shù),接收了5個(gè)參數(shù)
projectName: 執(zhí)行create-react-app
program.verbose:這里在說(shuō)一下commander的option選項(xiàng),如果加了這個(gè)選項(xiàng)這個(gè)值就是true,否則就是false,也就是說(shuō)這里如果加了--verbose,那這個(gè)參數(shù)就是true,至于verbose是什么,我之前也說(shuō)過(guò)了,在yarn或者npm安裝的時(shí)候打印本地信息,也就是如果安裝過(guò)程中出錯(cuò),我們可以找到額外的信息。
program.scriptsVersion:與上述同理,指定react-scripts版本
program.useNpm:以上述同理,指定是否使用npm,默認(rèn)使用yarn
hiddenProgram.internalTestingTemplate:這個(gè)東東,我之前給他省略了,我在前面已經(jīng)補(bǔ)充了,指定初始化的模板,人家說(shuō)了內(nèi)部使用,大家可以忽略了,應(yīng)該是用于開(kāi)發(fā)測(cè)試模板目錄的時(shí)候使用。
找到了第一個(gè)執(zhí)行的函數(shù)createApp,我們就來(lái)看看createApp函數(shù)到底做了什么?
createApp()function createApp(name, verbose, version, useNpm, template) { const root = path.resolve(name); // 獲取當(dāng)前進(jìn)程運(yùn)行的位置,也就是文件目錄的絕對(duì)路徑 const appName = path.basename(root); // 返回root路徑下最后一部分 checkAppName(appName); // 執(zhí)行 checkAppName 函數(shù) 檢查文件名是否合法 fs.ensureDirSync(name); // 此處 ensureDirSync 方法是外部依賴(lài)包 fs-extra 而不是 node本身的fs模塊,作用是確保當(dāng)前目錄下有指定文件名,沒(méi)有就創(chuàng)建 // isSafeToCreateProjectIn 函數(shù) 判斷文件夾是否安全 if (!isSafeToCreateProjectIn(root, name)) { process.exit(1); // 不合法結(jié)束進(jìn)程 } // 到這里打印成功創(chuàng)建了一個(gè)`react`項(xiàng)目在指定目錄下 console.log(`Creating a new React app in ${chalk.green(root)}.`); console.log(); // 定義package.json基礎(chǔ)內(nèi)容 const packageJson = { name: appName, version: "0.1.0", private: true, }; // 往我們創(chuàng)建的文件夾中寫(xiě)入package.json文件 fs.writeFileSync( path.join(root, "package.json"), JSON.stringify(packageJson, null, 2) ); // 定義常量 useYarn 如果傳參有 --use-npm useYarn就是false,否則執(zhí)行 shouldUseYarn() 檢查yarn是否存在 // 這一步就是之前說(shuō)的他默認(rèn)使用`yarn`,但是可以指定使用`npm`,如果指定使用了`npm`,`useYarn`就是`false`,不然執(zhí)行 shouldUseYarn 函數(shù) // shouldUseYarn 用于檢測(cè)本機(jī)是否安裝了`yarn` const useYarn = useNpm ? false : shouldUseYarn(); // 取得當(dāng)前node進(jìn)程的目錄,之前還懂為什么要多帶帶取一次,之后也明白了,下一句代碼將會(huì)改變這個(gè)值,所以如果我后面要用這個(gè)值,后續(xù)其實(shí)取得值將不是這個(gè) // 所以這里的目的就是提前存好,免得我后續(xù)使用的時(shí)候不好去找,這個(gè)地方就是我執(zhí)行初始化項(xiàng)目的目錄,而不是初始化好的目錄,是初始化的上級(jí)目錄,有點(diǎn)繞.. const originalDirectory = process.cwd(); // 修改進(jìn)程目錄為底下子進(jìn)程目錄 // 在這里就把進(jìn)程目錄修改為了我們創(chuàng)建的目錄 process.chdir(root); // 如果不使用yarn 并且checkThatNpmCanReadCwd()函數(shù) 這里之前說(shuō)的不是很對(duì),在重新說(shuō)一次 // checkThatNpmCanReadCwd 這個(gè)函數(shù)的作用是檢查進(jìn)程目錄是否是我們創(chuàng)建的目錄,也就是說(shuō)如果進(jìn)程不在我們創(chuàng)建的目錄里面,后續(xù)再執(zhí)行`npm`安裝的時(shí)候就會(huì)出錯(cuò),所以提前檢查 if (!useYarn && !checkThatNpmCanReadCwd()) { process.exit(1); } // 比較 node 版本,小于6的時(shí)候發(fā)出警告 // 之前少說(shuō)了一點(diǎn),小于6的時(shí)候指定`react-scripts`標(biāo)準(zhǔn)版本為0.9.x,也就是標(biāo)準(zhǔn)的`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. ` + `Please update to Node 6 or higher for a better, fully supported experience. ` ) ); // Fall back to latest supported react-scripts on Node 4 version = "react-scripts@0.9.x"; } // 如果沒(méi)有使用yarn 也發(fā)出警告 // 這里之前也沒(méi)有說(shuō)全,還判斷了`npm`的版本是不是在3以上,如果沒(méi)有依然指定安裝`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. ` + `Please update to npm 3 or higher for a better, fully supported experience. ` ) ); } // Fall back to latest supported react-scripts for npm 3 version = "react-scripts@0.9.x"; } } // 傳入這些參數(shù)執(zhí)行run函數(shù) // 執(zhí)行完畢上述代碼以后,將執(zhí)行`run`函數(shù),但是我還是先把上述用到的函數(shù)全部說(shuō)完,在來(lái)下一個(gè)核心函數(shù)`run` run(root, appName, version, verbose, originalDirectory, template, useYarn); }
我這里先來(lái)總結(jié)一下這個(gè)函數(shù)都做了哪些事情,再來(lái)看看他用到的依賴(lài)有哪些,先說(shuō)做了哪些事情,在我們的目錄下創(chuàng)建了一個(gè)項(xiàng)目目錄,并且校驗(yàn)了這個(gè)目錄的名稱(chēng)是否合法,這個(gè)目錄是否安全,然后往其中寫(xiě)入了一個(gè)package.json的文件,并且判斷了當(dāng)前環(huán)境下應(yīng)該使用的react-scripts的版本,然后執(zhí)行了run函數(shù)。我們?cè)趤?lái)看看這個(gè)函數(shù)用了哪些外部依賴(lài):
fs-extra:外部依賴(lài),Node自帶文件模塊的外部擴(kuò)展模塊 npm地址
semver:外部依賴(lài),用于比較Node版本 npm地址
之后函數(shù)的函數(shù)依賴(lài)我都會(huì)進(jìn)行詳細(xì)的解析,除了少部分特別簡(jiǎn)單的函數(shù),然后我們來(lái)看看這個(gè)函數(shù)的函數(shù)依賴(lài):
checkAppName():用于檢測(cè)文件名是否合法,
isSafeToCreateProjectIn():用于檢測(cè)文件夾是否安全
shouldUseYarn():用于檢測(cè)yarn在本機(jī)是否已經(jīng)安裝
checkThatNpmCanReadCwd():用于檢測(cè)npm是否在正確的目錄下執(zhí)行
checkNpmVersion():用于檢測(cè)npm在本機(jī)是否已經(jīng)安裝了
checkAppName()function checkAppName(appName) { // 使用 validateProjectName 檢查包名是否合法返回結(jié)果,這個(gè)validateProjectName是外部依賴(lài)的引用,見(jiàn)下面說(shuō)明 const validationResult = validateProjectName(appName); // 如果對(duì)象中有錯(cuò)繼續(xù),這里就是外部依賴(lài)的具體用法 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); } // 定義了三個(gè)開(kāi)發(fā)依賴(lài)的名稱(chēng) const dependencies = ["react", "react-dom", "react-scripts"].sort(); // 如果項(xiàng)目使用了這三個(gè)名稱(chēng)都會(huì)報(bào)錯(cuò),而且退出進(jìn)程 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. ` + `Due to the way npm works, the following names are not allowed: ` ) + chalk.cyan(dependencies.map(depName => ` ${depName}`).join(" ")) + chalk.red(" Please choose a different project name.") ); process.exit(1); } }
它這個(gè)函數(shù)其實(shí)還蠻簡(jiǎn)單的,用了一個(gè)外部依賴(lài)來(lái)校驗(yàn)文件名是否符合npm包文件名的規(guī)范,然后定義了三個(gè)不能取得名字react、react-dom、react-scripts,外部依賴(lài):
validate-npm-package-name:外部依賴(lài),檢查包名是否合法。npm地址
其中的函數(shù)依賴(lài):
printValidationResults():函數(shù)引用,這個(gè)函數(shù)就是我說(shuō)的特別簡(jiǎn)單的類(lèi)型,里面就是把接收到的錯(cuò)誤信息循環(huán)打印出來(lái),沒(méi)什么好說(shuō)的。
isSafeToCreateProjectIn()function isSafeToCreateProjectIn(root, name) { // 定義了一堆文件名 // 我今天早上仔細(xì)的看了一些,以下文件的來(lái)歷就是我們這些開(kāi)發(fā)者在`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(); // 這里就是在我們創(chuàng)建好的項(xiàng)目文件夾下,除了上述文件以外不包含其他文件就會(huì)返回true const conflicts = fs .readdirSync(root) .filter(file => !validFiles.includes(file)); if (conflicts.length < 1) { return true; } // 否則這個(gè)文件夾就是不安全的,并且挨著打印存在哪些不安全的文件 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; }
他這個(gè)函數(shù)也算比較簡(jiǎn)單,就是判斷創(chuàng)建的這個(gè)目錄是否包含除了上述validFiles里面的文件,至于這里面的文件是怎么來(lái)的,就是create-react-app在發(fā)展至今,開(kāi)發(fā)者們提出來(lái)的。
shouldUseYarn()function shouldUseYarn() { try { execSync("yarnpkg --version", { stdio: "ignore" }); return true; } catch (e) { return false; } }
就三行...其中execSync是由node自身模塊child_process引用而來(lái),就是用來(lái)執(zhí)行命令的,這個(gè)函數(shù)就是執(zhí)行一下yarnpkg --version來(lái)判斷我們是否正確安裝了yarn,如果沒(méi)有正確安裝yarn的話(huà),useYarn依然為false,不管指沒(méi)有指定--use-npm。
execSync:引用自child_process.execSync,用于執(zhí)行需要執(zhí)行的子進(jìn)程
checkThatNpmCanReadCwd()function checkThatNpmCanReadCwd() { const cwd = process.cwd(); // 這里取到當(dāng)前的進(jìn)程目錄 let childOutput = null; // 定義一個(gè)變量來(lái)保存`npm`的信息 try { // 相當(dāng)于執(zhí)行`npm config list`并將其輸出的信息組合成為一個(gè)字符串 childOutput = spawn.sync("npm", ["config", "list"]).output.join(""); } catch (err) { return true; } // 判斷是否是一個(gè)字符串 if (typeof childOutput !== "string") { return true; } // 將整個(gè)字符串以換行符分隔 const lines = childOutput.split(" "); // 定義一個(gè)我們需要的信息的前綴 const prefix = "; cwd = "; // 去整個(gè)lines里面的每個(gè)line查找有沒(méi)有這個(gè)前綴的一行 const line = lines.find(line => line.indexOf(prefix) === 0); if (typeof line !== "string") { return true; } // 取出后面的信息,這個(gè)信息大家可以自行試一試,就是`npm`執(zhí)行的目錄 const npmCWD = line.substring(prefix.length); // 判斷當(dāng)前目錄和執(zhí)行目錄是否是一致的 if (npmCWD === cwd) { return true; } // 不一致就打印以下信息,大概意思就是`npm`進(jìn)程沒(méi)有在正確的目錄下執(zhí)行 console.error( chalk.red( `Could not start an npm process in the right directory. ` + `The current directory is: ${chalk.bold(cwd)} ` + `However, a newly started npm process runs in: ${chalk.bold( npmCWD )} ` + `This is probably caused by a misconfigured system terminal shell.` ) ); // 這里他對(duì)windows的情況作了一些多帶帶的判斷,沒(méi)有深究這些信息 if (process.platform === "win32") { console.error( chalk.red(`On Windows, this can usually be fixed by running: `) + ` ${chalk.cyan( "reg" )} delete "HKCUSoftwareMicrosoftCommand Processor" /v AutoRun /f ` + ` ${chalk.cyan( "reg" )} delete "HKLMSoftwareMicrosoftCommand Processor" /v AutoRun /f ` + chalk.red(`Try to run the above two lines in the terminal. `) + chalk.red( `To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/` ) ); } return false; }
這個(gè)函數(shù)我之前居然貼錯(cuò)了,實(shí)在是不好意思。我之前沒(méi)有弄懂這個(gè)函數(shù)的意思,今天再來(lái)看的時(shí)候已經(jīng)豁然開(kāi)朗了,它的意思上述代碼已經(jīng)解析了,其中用到了一個(gè)外部依賴(lài):
cross-spawn:這個(gè)我之前說(shuō)到了沒(méi)有?忘了,用來(lái)執(zhí)行node進(jìn)程。npm地址
為什么用多帶帶用一個(gè)外部依賴(lài),而不是用node自身的呢?來(lái)看一下cross-spawn它自己對(duì)自己的說(shuō)明,Node跨平臺(tái)解決方案,解決在windows下各種問(wèn)題。
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, }; }
這個(gè)能說(shuō)的也比較少,一眼看過(guò)去就知道什么意思了,返回一個(gè)對(duì)象,對(duì)象上面有兩個(gè)對(duì)對(duì),一個(gè)是npm的版本號(hào),一個(gè)是是否有最小npm版本的限制,其中一個(gè)外部依賴(lài),一個(gè)Node自身的API我之前也都說(shuō)過(guò)了,不說(shuō)了。
看到到這里createApp()函數(shù)的依賴(lài)和執(zhí)行都結(jié)束了,接著執(zhí)行了run()函數(shù),我們繼續(xù)來(lái)看run()函數(shù)都是什么,我又想吐槽了,算了,忍?。。?!
run()函數(shù)在createApp()函數(shù)的所有內(nèi)容執(zhí)行完畢后執(zhí)行,它接收7個(gè)參數(shù),先來(lái)看看。
root:我們創(chuàng)建的目錄的絕對(duì)路徑
appName:我們創(chuàng)建的目錄名稱(chēng)
version;react-scripts的版本
verbose:繼續(xù)傳入verbose,在createApp中沒(méi)有使用到
originalDirectory:原始目錄,這個(gè)之前說(shuō)到了,到run函數(shù)中就有用了
tempalte:模板,這個(gè)參數(shù)之前也說(shuō)過(guò)了,不對(duì)外使用
useYarn:是否使用yarn
具體的來(lái)看下面run()函數(shù)。
run()function run( root, appName, version, verbose, originalDirectory, template, useYarn ) { // 這里對(duì)`react-scripts`做了大量的處理 const packageToInstall = getInstallPackage(version, originalDirectory); // 獲取依賴(lài)包信息 const allDependencies = ["react", "react-dom", packageToInstall]; // 所有的開(kāi)發(fā)依賴(lài)包 console.log("Installing packages. This might take a couple of minutes."); getPackageName(packageToInstall) // 獲取依賴(lài)包原始名稱(chēng)并返回 .then(packageName => // 檢查是否離線(xiàn)模式,并返回結(jié)果和包名 checkIfOnline(useYarn).then(isOnline => ({ isOnline: isOnline, packageName: packageName, })) ) .then(info => { // 接收到上述的包名和是否為離線(xiàn)模式 const isOnline = info.isOnline; const packageName = info.packageName; console.log( `Installing ${chalk.cyan("react")}, ${chalk.cyan( "react-dom" )}, and ${chalk.cyan(packageName)}...` ); console.log(); // 安裝依賴(lài) return install(root, useYarn, allDependencies, verbose, isOnline).then( () => packageName ); }) .then(packageName => { // 檢查當(dāng)前`Node`版本是否支持包 checkNodeVersion(packageName); // 檢查`package.json`的開(kāi)發(fā)依賴(lài)是否正常 setCaretRangeForRuntimeDeps(packageName); // `react-scripts`腳本的目錄 const scriptsPath = path.resolve( process.cwd(), "node_modules", packageName, "scripts", "init.js" ); // 引入`init`函數(shù) const init = require(scriptsPath); // 執(zhí)行目錄的拷貝 init(root, appName, verbose, originalDirectory, template); // 當(dāng)`react-scripts`的版本為0.9.x發(fā)出警告 if (version === "react-scripts@0.9.x") { console.log( chalk.yellow( ` Note: the project was boostrapped with an old unsupported version of tools. ` + `Please update to Node >=6 and npm >=3 to get supported tools in new projects. ` ) ); } }) // 異常處理 .catch(reason => { console.log(); console.log("Aborting installation."); // 根據(jù)命令來(lái)判斷具體的錯(cuò)誤 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(); // 出現(xiàn)異常的時(shí)候?qū)h除目錄下的這些文件 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)); } }); }); // 判斷當(dāng)前目錄下是否還存在文件 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); }); }
他這里對(duì)react-script做了很多處理,大概是由于react-script本身是有node版本的依賴(lài)的,而且在用create-react-app init
他在run()函數(shù)中的引用都是用Promise回調(diào)的方式來(lái)完成的,從我正式接觸Node開(kāi)始就習(xí)慣用async/await,所以對(duì)Promise還真不熟,惡補(bǔ)了一番,下面我們來(lái)拆解其中的每一句和每一個(gè)函數(shù)的作用,先來(lái)看一下用到外部依賴(lài)還是之前那些不說(shuō)了,來(lái)看看函數(shù)列表:
getInstallPackage():獲取要安裝的react-scripts版本或者開(kāi)發(fā)者自己定義的react-scripts
getPackageName():獲取到正式的react-scripts的包名
checkIfOnline():檢查網(wǎng)絡(luò)連接是否正常
install():安裝開(kāi)發(fā)依賴(lài)包
checkNodeVersion():檢查Node版本信息
setCaretRangeForRuntimeDeps():檢查發(fā)開(kāi)依賴(lài)是否正確安裝,版本是否正確
init():將事先定義好的目錄文件拷貝到我的項(xiàng)目中
知道了個(gè)大概,我們?cè)趤?lái)逐一分析每個(gè)函數(shù)的作用:
getInstallPackage()function getInstallPackage(version, originalDirectory) { let packageToInstall = "react-scripts"; // 定義常量 packageToInstall,默認(rèn)就是標(biāo)準(zhǔn)`react-scripts`包名 const validSemver = semver.valid(version); // 校驗(yàn)版本號(hào)是否合法 if (validSemver) { packageToInstall += `@${validSemver}`; // 合法的話(huà)執(zhí)行,就安裝指定版本,在`npm install`安裝的時(shí)候指定版本為加上`@x.x.x`版本號(hào),安裝指定版本的`react-scripts` } else if (version && version.match(/^file:/)) { // 不合法并且版本號(hào)參數(shù)帶有`file:`執(zhí)行以下代碼,作用是指定安裝包為我們自身定義的包 packageToInstall = `file:${path.resolve( originalDirectory, version.match(/^file:(.*)?$/)[1] )}`; } else if (version) { // 不合法并且沒(méi)有`file:`開(kāi)頭,默認(rèn)為在線(xiàn)的`tar.gz`文件 // for tar.gz or alternative paths packageToInstall = version; } // 返回最終需要安裝的`react-scripts`的信息,或版本號(hào)或本地文件或線(xiàn)上`.tar.gz`資源 return packageToInstall; }
這個(gè)方法接收兩個(gè)參數(shù)version版本號(hào),originalDirectory原始目錄,主要的作用是判斷react-scripts應(yīng)該安裝的信息,具體看每一行。
這里create-react-app本身提供了安裝react-scripts的三種機(jī)制,一開(kāi)始初始化的項(xiàng)目是可以指定react-scripts的版本或者是自定義這個(gè)東西的,所以在這里他就提供了這幾種機(jī)制,其中用到的外部依賴(lài)只有一個(gè)semver,之前就說(shuō)過(guò)了,不多說(shuō)。
getPackageName()function getPackageName(installPackage) { // 函數(shù)進(jìn)來(lái)就根據(jù)上面的那個(gè)判斷`react-scripts`的信息來(lái)安裝這個(gè)包,用于返回正規(guī)的包名 // 此處為線(xiàn)上`tar.gz`包的情況 if (installPackage.match(/^.+.(tgz|tar.gz)$/)) { // 里面這段創(chuàng)建了一個(gè)臨時(shí)目錄,具體它是怎么設(shè)置了線(xiàn)上.tar.gz包我沒(méi)試就不亂說(shuō)了 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]); // 此處為只有版本信息的時(shí)候的情況 } else if (installPackage.match(/.+@/)) { return Promise.resolve( installPackage.charAt(0) + installPackage.substr(1).split("@")[0] ); // 此處為信息中包含`file:`開(kāi)頭的情況 } else if (installPackage.match(/^file:/)) { const installPackagePath = installPackage.match(/^file:(.*)?$/)[1]; const installPackageJson = require(path.join(installPackagePath, "package.json")); return Promise.resolve(installPackageJson.name); } // 什么都沒(méi)有直接返回包名 return Promise.resolve(installPackage); }
他這個(gè)函數(shù)的目標(biāo)就是返回一個(gè)正常的依賴(lài)包名,比如我們什么都不帶就返回react-scripts,在比如我們是自己定義的包就返回my-react-scripts,繼續(xù)到了比較關(guān)鍵的函數(shù)了,接收一個(gè)installPackage參數(shù),從這函數(shù)開(kāi)始就采用Promise回調(diào)的方式一直執(zhí)行到最后,我們來(lái)看看這個(gè)函數(shù)都做了什么,具體看上面每一行的注釋。
總結(jié)一句話(huà),這個(gè)函數(shù)的作用就是返回正常的包名,不帶任何符號(hào)的,來(lái)看看它的外部依賴(lài):
hyperquest:這個(gè)用于將http請(qǐng)求流媒體傳輸。npm地址
他本身還有函數(shù)依賴(lài),這兩個(gè)函數(shù)依賴(lài)我都不多帶帶再說(shuō),函數(shù)的意思很好理解,至于為什么這么做我還沒(méi)想明白:
getTemporaryDirectory():不難,他本身是一個(gè)回調(diào)函數(shù),用來(lái)創(chuàng)建一個(gè)臨時(shí)目錄。
extractStream():主要用到node本身的一個(gè)流,這里我真沒(méi)懂為什么藥改用流的形式,就不發(fā)表意見(jiàn)了,在看其實(shí)我還是沒(méi)懂,要真正的明白是要去試一次,但是真的有點(diǎn)麻煩,不想去關(guān)注。
PS:其實(shí)這個(gè)函數(shù)很好理解就是返回正常的包名,但是里面的有些處理我都沒(méi)想通,以后理解深刻了在回溯一下。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); } }); }); }
這個(gè)函數(shù)本身接收一個(gè)是否使用yarn的參數(shù)來(lái)判斷是否進(jìn)行后續(xù),如果使用的是npm就直接返回true了,為什么會(huì)有這個(gè)函數(shù)是由于yarn本身有個(gè)功能叫離線(xiàn)安裝,這個(gè)函數(shù)來(lái)判斷是否離線(xiàn)安裝,其中用到了外部依賴(lài):
dns:用來(lái)檢測(cè)是否能夠請(qǐng)求到指定的地址。npm地址
install()function install(root, useYarn, dependencies, verbose, isOnline) { // 封裝在一個(gè)回調(diào)函數(shù)中 return new Promise((resolve, reject) => { let command; // 定義一個(gè)命令 let args; // 定義一個(gè)命令的參數(shù) // 如果使用yarn if (useYarn) { command = "yarnpkg"; // 命令名稱(chēng) args = ["add", "--exact"]; // 命令參數(shù)的基礎(chǔ) if (!isOnline) { args.push("--offline"); // 此處接上面一個(gè)函數(shù)判斷是否是離線(xiàn)模式 } [].push.apply(args, dependencies); // 組合參數(shù)和開(kāi)發(fā)依賴(lài) `react` `react-dom` `react-scripts` args.push("--cwd"); // 指定命令執(zhí)行目錄的地址 args.push(root); // 地址的絕對(duì)路徑 // 在使用離線(xiàn)模式時(shí)候會(huì)發(fā)出警告 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 { // 此處于上述一樣,命令的定義 參數(shù)的組合 command = "npm"; args = [ "install", "--save", "--save-exact", "--loglevel", "error", ].concat(dependencies); } // 因?yàn)閌yarn`和`npm`都可以帶這個(gè)參數(shù),所以就多帶帶拿出來(lái)了拼接到上面 if (verbose) { args.push("--verbose"); } // 這里就把命令組合起來(lái)執(zhí)行 const child = spawn(command, args, { stdio: "inherit" }); // 命令執(zhí)行完畢后關(guān)閉 child.on("close", code => { // code 為0代表正常關(guān)閉,不為零就打印命令執(zhí)行錯(cuò)誤的那條 if (code !== 0) { reject({ command: `${command} ${args.join(" ")}`, }); return; } // 正常繼續(xù)往下執(zhí)行 resolve(); }); }); }
又到了比較關(guān)鍵的地方了,仔細(xì)看每一行代碼注釋?zhuān)颂幒瘮?shù)的作用就是組合一個(gè)yarn或者npm的安裝命令,把這些模塊安裝到項(xiàng)目的文件夾中,其中用到的外部依賴(lài)cross-spawn前面有說(shuō)了,就不說(shuō)了。
其實(shí)執(zhí)行到這里,create-react-app已經(jīng)幫我們創(chuàng)建好了目錄,package.json并且安裝了所有的依賴(lài),react、react-dom和react-scrpts,復(fù)雜的部分已經(jīng)結(jié)束,繼續(xù)往下走。
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`中定義了一個(gè)`engines`其中放著`Node`版本的信息,大家可以打開(kāi)源碼`packages/react-scripts/package.json`查看 if (!packageJson.engines || !packageJson.engines.node) { return; } // 比較進(jìn)程的`Node`版本信息和最小支持的版本,如果比他小的話(huà),會(huì)報(bào)錯(cuò)然后退出進(jìn)程 if (!semver.satisfies(process.version, packageJson.engines.node)) { console.error( chalk.red( "You are running Node %s. " + "Create React App requires Node %s or higher. " + "Please update your version of Node." ), process.version, packageJson.engines.node ); process.exit(1); } }
這個(gè)函數(shù)直譯一下,檢查Node版本,為什么要檢查了?之前我已經(jīng)說(shuō)過(guò)了react-scrpts是需要依賴(lài)Node版本的,也就是說(shuō)低版本的Node不支持,其實(shí)的外部依賴(lài)也是之前的幾個(gè),沒(méi)什么好說(shuō)的。
setCaretRangeForRuntimeDeps()function setCaretRangeForRuntimeDeps(packageName) { const packagePath = path.join(process.cwd(), "package.json"); // 取出創(chuàng)建項(xiàng)目的目錄中的`package.json`路徑 const packageJson = require(packagePath); // 引入`package.json` // 判斷其中`dependencies`是否存在,不存在代表我們的開(kāi)發(fā)依賴(lài)沒(méi)有成功安裝 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"); // 重新寫(xiě)入文件`package.json` fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2)); }
這個(gè)函數(shù)我也不想說(shuō)太多了,他的作用并沒(méi)有那么大,就是用來(lái)檢測(cè)我們之前安裝的依賴(lài)是否寫(xiě)入了package.json里面,并且對(duì)依賴(lài)的版本做了檢測(cè),其中一個(gè)函數(shù)依賴(lài):
makeCaretRange():用來(lái)對(duì)依賴(lài)的版本做檢測(cè)
我沒(méi)有多帶帶對(duì)其中的子函數(shù)進(jìn)行分析,是因?yàn)槲矣X(jué)得不難,而且對(duì)主線(xiàn)影響不大,我不想貼太多說(shuō)不完。
到這里createReactApp.js里面的源碼都分析完了,咦!你可能會(huì)說(shuō)你都沒(méi)說(shuō)init()函數(shù),哈哈哈,看到這里說(shuō)明你很認(rèn)真哦,init()函數(shù)是放在packages/react-scripts/script目錄下的,但是我還是要給他說(shuō)了,因?yàn)樗鋵?shí)跟react-scripts包聯(lián)系不大,就是個(gè)copy他本身定義好的模板目錄結(jié)構(gòu)的函數(shù)。
init()它本身接收5個(gè)參數(shù):
appPath:之前的root,項(xiàng)目的絕對(duì)路徑
appName:項(xiàng)目的名稱(chēng)
verbose:這個(gè)參數(shù)我之前說(shuō)過(guò)了,npm安裝時(shí)額外的信息
originalDirectory:原始目錄,命令執(zhí)行的目錄
template:其實(shí)其中只有一種類(lèi)型的模板,這個(gè)選項(xiàng)的作用就是配置之前我說(shuō)過(guò)的那個(gè)函數(shù),測(cè)試模板
// 當(dāng)前的包名,也就是這個(gè)命令的包 const ownPackageName = require(path.join(__dirname, "..", "package.json")).name; // 當(dāng)前包的路徑 const ownPath = path.join(appPath, "node_modules", ownPackageName); // 項(xiàng)目的`package.json` const appPackage = require(path.join(appPath, "package.json")); // 檢查項(xiàng)目中是否有`yarn.lock`來(lái)判斷是否使用`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", }; // 重新寫(xiě)入`package.json` fs.writeFileSync( path.join(appPath, "package.json"), JSON.stringify(appPackage, null, 2) ); // 判斷項(xiàng)目目錄是否有`README.md`,模板目錄中已經(jīng)定義了`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") ); } // 是否有模板選項(xiàng),默認(rèn)為當(dāng)前執(zhí)行命令包目錄下的`template`目錄,也就是`packages/react-scripts/tempalte` const templatePath = template ? path.resolve(originalDirectory, template) : path.join(ownPath, "template"); if (fs.existsSync(templatePath)) { // 拷貝目錄到項(xiàng)目目錄 fs.copySync(templatePath, appPath); } else { console.error( `Could not locate supplied template: ${chalk.green(templatePath)}` ); return; }
這個(gè)函數(shù)我就不把代碼貼全了,里面的東西也蠻好理解,基本上就是對(duì)目錄結(jié)構(gòu)的修改和重名了那些,挑了一些來(lái)說(shuō),到這里,create-react-app從零到目錄依賴(lài)的安裝完畢的源碼已經(jīng)分析完畢,但是其實(shí)這只是個(gè)初始化目錄和依賴(lài),其中控制環(huán)境的代碼都存在react-scripts中,所以其實(shí)離我想知道的關(guān)鍵的地方還有點(diǎn)遠(yuǎn),但是本篇已經(jīng)很長(zhǎng)了,不打算現(xiàn)在說(shuō)了,多多包涵。
希望本篇對(duì)大家有所幫助吧。
啰嗦兩句本來(lái)這篇我是打算把create-react-app中所有的源碼的拿出來(lái)說(shuō)一說(shuō),包括其中的webpack的配置啊,eslint的配置啊,babel的配置啊.....等等,但是實(shí)在是有點(diǎn)多,他自己本身把初始化的命令和控制react環(huán)境的命令分離成了packages/create-react-app和packages/react-script兩邊,這個(gè)篇幅才把packages/create-react-app說(shuō)完,更復(fù)雜的packages/react-script在說(shuō)一下這篇幅都不知道有多少了,所以我打算之后空了,在多帶帶寫(xiě)一篇關(guān)于packages/react-script的源碼分析的文。
碼字不易,可能出現(xiàn)錯(cuò)別字什么的,說(shuō)的不清楚的,說(shuō)錯(cuò)的,歡迎指正,多多包涵!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/92706.html
摘要:弄清之后,就去腳手架源代碼里面找。這樣更加靈活,而且復(fù)用性高,起新項(xiàng)目,如果差別不大,幾乎可以做到零配置,這樣開(kāi)發(fā)者壓根就不需要關(guān)心業(yè)務(wù)之外的東西從零開(kāi)始開(kāi)發(fā)一個(gè)腳手架三 上一篇已經(jīng)初步整了個(gè)kkk-react,這一篇不寫(xiě)代碼,粗略講解下create-react-app的部分源碼。 前沿:科普下看源碼的思路。以本人看過(guò)N多源碼的經(jīng)驗(yàn)總結(jié),想要看這種腳手架或者npm包的源碼,第一步就是看...
摘要:這里通過(guò)調(diào)用方法方法主要是通過(guò)來(lái)通過(guò)命令執(zhí)行下的方法。 原文地址Nealyang/personalBlog 前言 對(duì)于前端工程構(gòu)建,很多公司、BU 都有自己的一套構(gòu)建體系,比如我們正在使用的 def,或者 vue-cli 或者 create-react-app,由于筆者最近一直想搭建一個(gè)個(gè)人網(wǎng)站,秉持著呼吸不停,折騰不止的原則,編碼的過(guò)程中,還是不想太過(guò)于枯燥。在 coding 之前...
摘要:通過(guò)文件可以對(duì)圖標(biāo)名稱(chēng)等信息進(jìn)行配置。注意,注冊(cè)的只在生產(chǎn)環(huán)境中生效,并且該功能只有在下才能有效果該文件是過(guò)濾文件配置該文件是描述文件定義了項(xiàng)目所需要的各種模塊,以及項(xiàng)目的配置信息比如名稱(chēng)版本許可證等元數(shù)據(jù)。 一、 快速開(kāi)始: 全局安裝腳手架: $ npm install -g create-react-app 通過(guò)腳手架搭建項(xiàng)目: $ create-react-app 開(kāi)始項(xiàng)目: ...
摘要:通過(guò)文件可以對(duì)圖標(biāo)名稱(chēng)等信息進(jìn)行配置。注意,注冊(cè)的只在生產(chǎn)環(huán)境中生效,并且該功能只有在下才能有效果該文件是過(guò)濾文件配置該文件是描述文件定義了項(xiàng)目所需要的各種模塊,以及項(xiàng)目的配置信息比如名稱(chēng)版本許可證等元數(shù)據(jù)。 一、 快速開(kāi)始: 全局安裝腳手架: $ npm install -g create-react-app 通過(guò)腳手架搭建項(xiàng)目: $ create-react-app 開(kāi)始項(xiàng)目: ...
摘要:使用官方的的另外一種版本和一起使用自動(dòng)配置了一個(gè)項(xiàng)目支持。需要的依賴(lài)都在文件中。帶靜態(tài)類(lèi)型檢驗(yàn),現(xiàn)在的第三方包基本上源碼都是,方便查看調(diào)試。大型項(xiàng)目首選和結(jié)合,代碼調(diào)試維護(hù)起來(lái)極其方便。 showImg(https://segmentfault.com/img/bVbrTKz?w=1400&h=930); 阿特伍德定律,指的是any application that can be wr...
閱讀 1463·2021-11-25 09:43
閱讀 3710·2021-11-10 11:48
閱讀 5473·2021-09-23 11:21
閱讀 1651·2019-08-30 15:55
閱讀 3565·2019-08-30 13:53
閱讀 1298·2019-08-30 10:51
閱讀 925·2019-08-29 14:20
閱讀 2033·2019-08-29 13:11