這篇文章不是聊React這門技術本身,而是關於如何維護好一個React專案。
文字可能會涉及一些Webpack的基礎知識,如果你還不太瞭解Webpack的用法的話,可以從我之前的一篇文章《Webpack 速成》入門,深入淺出,童叟無欺。
程式設計領域中的“腳手架(Scaffolding)”指的是能夠快速搭建專案“骨架”的一類工具。例如大多數的React專案都有src目錄,public目錄,webpack配置檔案,babel配置等等,而src目錄中又通常包含components目錄,reducers目錄等等。每次在新建專案時,你不得不手動建立這些固定的檔案目錄,繁瑣而累贅。腳手架的作用就是幫助你完成這些重複性的工作,包括一鍵生成主要的目錄結構、安裝依賴等等。Yeoman就是著名的腳手架工具。
當你進入一個公司參與React專案時,你要做的可能只是開發指定的元件,執行命令啟動專案檢視執行和除錯,最後釋出打包上線。你可能不會去思考為什麼目錄結構是這個樣子,那麼多配置檔案是幹什麼用的(我曾經也是這個樣子)。今天我選擇了兩個React專案的腳手架工具 create-react-app(以下簡稱CRA)和react-starter-kit(以下簡稱RSK),根據它們的說明文件以及一些個人的經驗,來逐個解析不同檔案的作用。
這些知識並不僅僅適用於React專案,檔案的背後代表的是工具,工具的背後代表的其實是要解決的問題。不同的公司不同團隊使用的工具可能會不同,將來也會有新的技術或者框架出現,但這些解決問題的思路同樣能夠複用。或者當你需要立項一個React專案而又不想依賴腳手架時,它們會是一份好的教科書。
因為CRA是Facebook官方推出的腳手架工具,所以我們以CRA為主線索展開,它的User Guide文件最為(特)豐(別)富(長),本文的大部分內容也都參(翻)考(譯)自這份文件,如果也理解不恰當之處還多多指教。其中也會穿插react-starter-kit的相關內容。
這個系列的文章會分為上下兩個部分。在這上篇中,講解的是一些常規專案搭建的基礎配置,而在下篇的計劃中,則會講解高階配置,涉及開發環境的後端功能以及測試和部署。
最後一句廢話想強調的是,任何腳手架生成的專案結構都僅供參考。實際的組織方式和使用工具都要依據實際情況而定。
專案目錄
首先讓我們從最基本的目錄資料夾開始
CRA中有兩個非常重要的目錄有兩個,src
和public
:
src
: 該目錄中存放的是你的指令碼和樣式原始檔,所有你需要經過Webpack打包的或者編譯的檔案都必須而且只能放在這個資料夾裡(反過來說Webpack也只會去這個資料夾裡找需要打包的檔案)。例如TypeScript、LESS、SASS、Stylus原始碼等等,也可能是你需要在元件中引用的圖片、SVG資源等等(之後會談到在元件中引用樣式和圖片的用法)。總之src
目錄顧名思義(src = source),存放的是程式原始碼。
當然,src
目錄之下子目錄的命名和組織就沒有那麼講究了。如果你開發的是redux專案,自然會有components、reducers、actions等資料夾,甚至在components中分別為container component和stateless component建立資料夾都沒問題。
最後,src
目錄的入口是src/index.js
,不妨可以看看index.js
的內容
import React from `react`;
import ReactDOM from `react-dom`;
import `./index.css`;
import App from `./App`;
import registerServiceWorker from `./registerServiceWorker`;
ReactDOM.render(<App />, document.getElementById(`root`));
registerServiceWorker();複製程式碼
很簡單,它將入口元件渲染至頁面上,並且註冊service worker。
public
:public
通常用於存放使用者能夠訪問的資源,例如打包後的指令碼、圖片、HTML檔案。但事實上並不僅限於此,從RSK專案中我們可以看到public資料夾中還有robots.txt
、humans.txt
、crossdomain.xml
、favicon.ico
等等
雖然public中存放的不是元件,public目錄同樣存在入口,即/index.html
,也即是使用者在域名根路徑下訪問到的頁面。在CRA中規定,只有public
資料夾內的資源才能被index.html
使用。而html引用靜態資源的方式也比較特別,並非是通過相對路徑或者絕對路徑的,而是通過全域性變數引用。這個話題我們放在後面資源使用環節再說。
public
資料夾有時候也被命名為assets
甚至resources
,這都沒有關係。如果更加規矩一點,你可以在public
中建立子資料夾dist
用於儲存釋出上線的指令碼和樣式(dist其實就是distribute的縮寫,也意味著釋出的意思),或者建立build
資料夾用於儲存開發中構建後的指令碼
src
和public
是最重要的兩個資料夾。CRA中的資料夾只有這兩個。我們不妨再可以看看RSK中的資料夾還有哪些:
docs
: 用於存放(markdown格式的)開發相關文件tools
: 用於存放“工具指令碼”的資料夾。“工具指令碼”即是那些用於完成指定工作的指令碼,在資料夾裡你會看到例如build.js
、deploy.js
、copy.js
等等。即使不展示這些指令碼的具體內容,通過檔名也很容易判斷這些指令碼的作用,依次為構建、部署、複製檔案等。這一部分指令碼也可以通過npm
命令執行,稍後詳談。src/server
: 如果你的專案是以Javascript全棧的形式開發的話,可以將服務端程式碼也放入src
中test
: 用於存放測試相關指令碼
各式各樣的配置檔案
越來越多的工具被發明來用於輔助我們的開發,但不同的工具配合不同的專案需要進行不同的配置。所以有各式各樣的配置檔案可能存在於我們的專案檔案中。這些工具和配置檔案你不一定都會用上,但至少你在過目之後不會再對它們陌生,或許在以後解決問題的過程中能夠派的上用場。
以下的配置檔案摘自RSK腳手架中(如果你第一次看到腳手架為你生成了這麼多從來沒有看到過的檔案你一定會感到害怕,反正我是這麼覺得的。)
.editorconfig
: 告訴編輯器該專案的程式碼規範。在團隊開發中可能涉及的一個問題是,不同的同學可能使用的開發工具和開發習慣並不相同,有的使用WebStorm,有的使用Visual Studio Code。所以有可能在你的編輯器中習慣縮排使用的是2個空格,在他的編輯器中縮排使用的是4個空格。該配置檔案就是用於儲存統一的樣式規範,告訴編輯器統一使用兩個空格,不允許空字串結尾等等。具體請參考editorconfig.org.eslintrc.js
: 這個很好理解,eslint工具的配置檔案。eslint是一款專業對js語法和格式進行檢測的工具,大部分的編輯器應該都進行了整合,或者當作外掛進行安裝。該配置檔案告訴eslint哪些檔案可以忽略,哪些規則可以忽略,哪些檔案適配哪些規則等等。具體請參考: eslint.org/docs/user-g….stylelintrc.js
: 同上,stylelint是對樣式檔案進行語法規範檢測的工具,該配置檔案則可以對檢測規則進行細節配置。具體規則請參考: stylelint.io/user-guide/….flowconfig
: flow是Facebook推出一款用於對JavaScript語法進行型別檢測的開源工具(有TypeScript的意思)。該檔案就是該工具的配置檔案,具體可以前往flow.org/en/docs/con….env
: 在啟動專案時難免會使用到環境變數,最著名的環境環境變數莫過於NODE_ENV
,例如告訴程式使用生產環境:NODE_ENV=production
。我們都知道可以在執行命令列時通過命令列引數的形式指定環境變數,例如NODE_ENV=production node app.js
,然後再從程式裡通過讀取命令列引數的方式間接讀取環境變數。而通過dotenv模組,我們可以將環境變數都放入.env
環境中統一管理統一讀取。.travis.yml
: 持續整合工具travis-ci的配置檔案,該工具github marketplace有售,更多配置可以參考docs.travis-ci.com/user/custom…circle.yml
: 持續整合工具circleci的配置檔案,該工具github marketplace有售,更多配置可以參考circleci.com/docs/1.0/co…jest.config.js
: Facebook 的測試工具jest的配置檔案,更多配置可以參考facebook.github.io/jest/docs/e…jsdoc.config.json
: jsDoc是一款能夠根據檔案內函式註釋生成文件的工具,該檔案是該工具的配置檔案,更多資訊可以參考usejsdoc.org/about-confi…Dockerfile
: Docker容器的配置檔案(對不起Docker我實在不熟,沒有什麼好補充的)
除此之外,還有一些你可能會用得上的一些檔案,比如
CHANGELOG.md
: 版本更新的日誌CONTRIBUTEING.md
: 關於如何向該專案做出貢獻
工具指令碼
還在我入行的時候,前端開發流程是很簡單的,手動建立一個靜態頁面,然後引入你需要的指令碼就可以開始了。然而到了現在,不僅引入指令碼的方式發生了改變,包括除錯過程,打包流程,釋出上線都變得複雜而且專業,而這一切都離不開NodeJS指令碼。指令碼帶來的好處是可複用、自動化以及批量化處理。
開發中需要使用指令碼處理的環節非常的多,例如將less編譯為css,將指令碼編譯、壓縮、拼接,壓縮圖片等等。這些工作可以交給Webpack或者Gulp或者Grunt去做。但這些第三方庫並不是萬能的,它們的運作也依賴它們所處生態裡的外掛。在這種複雜的依賴情況下,出錯的情況常容易發生,為什麼不建議再使用Gulp或者Grunt了呢,詳見這篇文章:Why we should stop using Grunt & Gulp。正所謂流水的工具,鐵打的指令碼
npm指令碼都存放在package.json
檔案裡的scripts
欄位裡
npm命令有機會我們能單獨拿出一篇文章來聊,但言歸正傳回到腳手架,CRA中只用到了四種npm命令,分別是
npm start
: 在開發模式下啟動app,預設使用使用3000埠,啟動後在瀏覽器中輸入http://localhost:3000就能訪問,如果應用發生了更改頁面會自動重新整理npm test
: 執行應用的測試指令碼npm run build
: 為生產環境編譯並且打包應用程式,打包到build
資料夾中npm run eject
: 如果你稍有心的觀察CRA的目錄裡的檔案,你會發現沒有.babel
檔案,沒有webpack.config.js
類似檔案。因為所有的這些瑣碎的配置腳手架都幫你搞定了。全部都在react-scripts
類庫中。所以你看到package.json
檔案裡npm start
實際執行的是react-scripts start
。當你不滿足於腳手架為了你預設的配置時,你就可以使用eject
命令將配置暴露出來(比如start
命令,還有webpack.config.dev.js
),這樣你就可以完全自定義這些配置。注意這個操作是不可逆的。當你執行完畢之後你會發現package.json
裡的scripts
的start
命令變為node scripts/start.js
順表說一下為什麼build
命令前需要加關鍵字run
,而start
和test
就不需要,因為npm start
是內建的預設命令,你可以理解為類似於巨集的東西。如果你沒有在package.json
裡自定義start命令的話而又執行npm start
的話,它實際上執行的是node server.js
。更多的內建命令請參考docs.npmjs.com/misc/script…
自動格式化程式碼
這裡所說的格式化程式碼並不是指美化和格式化已經壓縮過的程式碼以便於閱讀。而是在程式碼的提交階段(commit)強制對程式碼進行格式化。所以這裡用到了額外的三個庫
husky
: 便於以npm指令碼的形式呼叫git hooks(hook指的是在某一個特定情況下執行的程式碼,比如React的各個預留出來的生命週期函式就算是hook)lint-staged
: 便於我們對staged階段(準備提交階段)的檔案執行npm指令碼prettier
: 對程式碼進行格式化
核心類庫當然是prettier,為什麼在開發時仍然需要對程式碼格式化,prettier自己給出了幾個理由,比如強制對程式碼進行格式化避免PR時產生不必要的語法問題,比如幫助還不熟悉的新同學規範程式碼,總之仍然是有必要的。
prettier解決了how的問題,但是還需要husky
和lint-staged
解決when的問題,也就是什麼時候做格式化。在CRA中,格式化的工作時放在準備提交的階段(pre-commit),在實際專案中你還可以放在預備push的階段。
husky解決的問題是將pre-commit的hook暴露出來。預設情況下如果你想編寫pre-commit指令碼,你需要編輯你專案的.git/hooks/pre-commit
檔案,如果我沒有記錯的話應該是shell指令碼,並且在執行之前記得賦予它們執行許可權。
然而當你安裝完husky之後,你就可以把pre-commit階段需要執行的指令碼直接放在package.json
裡的scipts
裡的precommit
欄位裡,比如:
"scripts": {
"precommit": "eslint"
},複製程式碼
而lint-staged
解決的則是最後一公里的問題,即封裝在pre-commit階段需要執行的指令碼,同樣是在package.json
配置,例如:
"dependencies": {
// ...
},
"lint-staged": {
"src/**/*.{js,jsx,json,css}": [
"prettier --single-quote --write",
"git add"
]
},
"scripts": {複製程式碼
開發規範
進入到元件化的時代,一切都是元件,就連html也可以變身為元件。在RSK腳手架中,你甚至會看到一個名為Html.js
的元件(然後採用後端渲染)。我們希望用元件解決一切問題,而不是把需要維護的程式碼遺落在各個地方,甚至包括<head />
標籤裡的內容。<title />
、<meta />
就交給React Helmet解決吧。
開發元件和引用元件就不贅述了,全世界都一樣,相信大家也耳熟能詳了。
樣式
至於樣式,無論你是使用Less、Sass、還是Stylus都一樣,只要在Webpack中使用對應的loader就能將其編譯為css。需要注意的是組織樣式的方式。傳統專案中樣式和指令碼是分離的,放在不同的資料夾中。但是在React專案中,我們只有元件一個維度,元件同時包含樣式和指令碼,都放在components
資料夾中。例如:
components/
|--Button.js
|--Button.less複製程式碼
那麼在Button.js
中你可以直接引用樣式
import React, { Component } from `react`;
import `./Button.css`;複製程式碼
或者,你也可以把所有的樣式都在樣式入口src/index.css
中引入,然後在元件入口src/index.js
中又統一引入樣式入口src/index.css
。
除了編譯樣式之外還有一些額外的工作需要進行,例如壓縮,例如為某些樣式屬性新增瀏覽器字首。在CRA中會使用Autoprefixer或者postcss進行處理,當然這一切都整合在react-scripts中。你也可以獨立的使用npm指令碼進行處理,監視樣式的變化,當樣式檔案發生更改時自動的進行預處理和“後”處理,這個流程對指令碼檔案也同樣有效。
目前也有很多專門用於優化npm指令碼執行的類庫,在上述流程中你也能夠(或者說是必須)用上:
onchange
、watch
: 用於監視檔案修改,然後執行特定的npm指令碼,比如"scripts": { ... "watch:css": "onchange `src/scss/*.scss` -- npm run build:css", "watch:js": "onchange `src/js/*.js` -- npm run build:js", }複製程式碼
parallelshell
: 用於並行的執行多個npm指令碼,比如"scripts": { ... "watch:all": "parallelshell `npm run serve` `npm run watch:css` `npm run watch:js`" }複製程式碼
相信你能領悟到這些程式碼幹了什麼事情:P
新增圖片字型等額外資源
圖片與字型等資源也和樣式一樣都與要使用它們的元件放在同一層級,至少都應該屬於同一個components
資料夾中,在元件也是通過import
的關鍵字引入,例如
import React from `react`;
import logo from `./logo.png`; // Tell Webpack this JS file uses this image
console.log(logo); // /logo.84287d09.png
function Header() {
// Import result is the URL of your image
return <img src={logo} alt="Logo" />;
}複製程式碼
為了減少頁面的請求數,體積小於10000 bytes的圖片會返回data URI而不是實際的路徑。當專案需要(為生產環境)進行構建時,Webpack會把大於10000 bytes的圖片資源拷貝到最終構建的資料夾中(在CRA中的目錄是/build/static/media
),並且根據內容hash值進行重新命名。所以不用擔心資源發生修改之後因為瀏覽器的快取而不會生效
為什麼要採用import
的方式引用樣式和圖片,文件中給出了三條理由:
- 指令碼和樣式能夠得到壓縮以及打包在一起,以便減少額外的網路請求
- 在編譯階段如果發現檔案丟失就會及時報錯,而不是上線之後再呈現給使用者404錯誤
- 根據檔案內容的hash值對檔案進行重新命名而避免瀏覽器快取問題
如果一定要引用public
資料夾中的資源
並非所有的資源都能在元件中引用,又或者有的第三方類庫並不支援與React整合,此時你就需要把資源放入public
資料夾中,然後在html中引用,比如:
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">複製程式碼
那麼在構建時(npm run build
),Webpack會將%PUBLIC_URL%
替換為實際的public
目錄的絕對路徑。
在js檔案中也可以通過訪問process.env.PUBLIC_URL
變數來獲得public
資料夾的絕對路徑
render() {
// Note: this is an escape hatch and should be used sparingly!
// Normally we recommend using `import` for getting asset URLs
// as described in “Adding Images and Fonts” above this section.
return <img src={process.env.PUBLIC_URL + `/img/logo.png`} />;
}複製程式碼
這種訪問資源的方式有以下一些缺點(當然是相對於import
方式而言),請務必瞭解:
public
資料夾內的檔案不會做任何處理,包括壓縮或者拼接之類的- 如果有檔案丟失的話在編譯階段不會報錯,使用者可能會收到404的請求返回
- 你可能需要手動處理快取問題,例如對檔案發生修改時對檔案進行重新命名,或者修改
Etag
等快取條件。詳情可以參考我的這篇文章《設計一個無懈可擊的瀏覽器快取方案:關於思路,細節,ServiceWorker,以及HTTP/2》
但是在某些情況下可以考慮使用這種訪問資源的方式
- 你需要引用一些打包之外的額外指令碼,比如pace.js,比如google analytics指令碼
- 有一些指令碼和Webpack不相容
- 上千張圖片需要動態引用
- 構建時打包輸出的檔案需要指定檔名
新增自定義的環境變數
CRA腳手架還允許你在process.env
上新增自定義的環境變數供全域性訪問。
預設它會提供兩個環境變數供使用,一個是上一節用到的public
資料夾路徑PUBLIC_URL
。另一個是大家更加熟悉的NODE_ENV
。後者是一個代表當前開發環境的變數,當你執行npm start
時它等於development
;當你執行npm test
時它的值是test
;當你執行npm run build
時,它的值是production
。你無法手動的覆蓋它,它能夠防止開發者不小心打包了一個開發版本部署到線上。NODE_ENV
也能夠幫助你有針對性的除錯程式碼,比如你只希望非production
環境下停用分析指令碼:
if (process.env.NODE_ENV !== `production`) {
analytics.disable();
}複製程式碼
當然你也可以新增自己的環境變數,新增方式有兩種,一種是通過命令列的方式比如在Windows系統下set REACT_APP_SECRET_CODE=abcdef&&npm start
。另一種是通過.env
檔案(也就是通過dotenv類庫),把你所需要的環境變數都寫在這個檔案中:
REACT_APP_SECRET_CODE=abcdef複製程式碼
需要注意的事情是,所有的自定義環境變數都需要以REACT_APP_SECRET_CODE
開頭(至於理由我沒有看懂:Any other variables except NODE_ENV will be ignored to avoid accidentally exposing a private key on the machine that could have the same name. 我不是很理解 exposing a private key 是什麼意思)。
一旦環境變數定義完畢之後,就能在檔案中使用,比如在指令碼中:
render() {
return (
<div>
<small>You are running this application in <b>{process.env.NODE_ENV}</b> mode.</small>
</div>
);
}複製程式碼
比如在html中:
<title>%REACT_APP_WEBSITE_NAME%</title>複製程式碼
最後,不同開發環境中的環境變數不必都放在.env
檔案中,可以劃分為.env.development
, .env.test
, .env.production
等不同的檔案存放,並且不同檔案之間還存在優先順序的關係,詳情可以訪問dotenv的文件
編輯器除錯
目前比較流行的IDE比如Visual Studio Code和WebStorm都支援編輯器內的程式碼除錯,但是可能需要配置編輯器的環境變數,或者增加配置檔案,又或者給瀏覽器安裝外掛。我個人使用編輯器進行除錯的體驗並不好,並非所有的場景都支援除錯。同時因為腳手架和編輯器除錯時都會啟動後端環境,這之中可能需要解決衝突的地方。
具體的配置資訊可以參考這兩款編輯器的官方文件。
上篇完
本文也同時釋出在我的知乎專欄前端技術漫遊指南中,歡迎大家關注