手把手帶你搞懂如何用 Webpack 建置 React 開發環境

Lin
18 min readMay 5, 2021

Webpack 是前端開發中必定會接觸到的工具之一,透過他讓我們能以現代框架、套件或是新潮的 JavaScript 語法進行開發,將多樣的模組與資源編譯並打包輸出成瀏覽器能夠看得懂的 HTML、CSS 與 JS。除了產生最終用於部署的檔案,Webpack 也能在開發期間讓檔案的更改內容能夠即時在瀏覽器上被檢視。

相信剛接觸 React 的學習者一定都用過官方提供的 CRA(Create React App)來建立專案,他幫我們建置好了 React 的開發環境,當我們在終端機輸入 npm run build 的時候,React Scripts 這個套件就會自動幫我們產出部署用的檔案,這個工具對 Webpack 的設定進行了封裝,讓我們不用親自操作設定檔就可以快速的進行開發。雖然在使用上 CRA 非常方便,但身為前端開發者我們還是必須了解 Webpack 的運作模式,才能依照不同情境的需求去調整環境建置,例如使用 react-scripts eject 指令去將 CRA 的設定檔反編譯後進行修改,或是從頭開始用 Webpack 建置自己的開發環境。

會有這篇文章產出是因為自己在學習 Webpack 在網路上搜尋資源時,發現套件的版本迭代造成在操作指令與設定上有所異動,並且部分資源對於套件的選擇理由沒有很好的解釋,因此依照自己的需求整合成這篇文章,希望自己的經驗能夠對同為學習者的朋友們有一點幫助。

在閱讀這篇文章前你需要先具備套件管理系統的基本操作知識、對 JavaScript 的模組系統有初步的理解,並且知道 React 或其他前端框架在開發中扮演的角色是什麼。這篇文章會從頭開始建立專案,下載 Webpack 與 React 整合所需要的套件並進行環境設定,大致上可以歸類為以下幾個重點:

  1. 安裝 Webpack 以及建置其設定檔
  2. 透過 Babel 讓 Webpack 能夠編譯 React 檔案
  3. 打包 JS、HTML 與 CSS 檔案
  4. 用 dev server 啟用 hot reload

在開始前我們要先確保電腦上已經安裝了 Node.js,在這篇教學我們使用 npm 來下載套件。npm 是個套件管理工具,在下載 Node.js 的時候就會一併被安裝。

最後提醒一下讀者們,這篇文章寫於 2021 年 5 月,如果你是在距離撰文時間有些遙遠的未來閱讀的話,在操作上任何環節出現了問題,試著將錯誤訊息丟上網路搜尋,或是找到各個套件的文件確認一下指令或設定方式是否已經更動!

開始囉

首先在建立新的專案資料夾,資料夾的名字可以自訂,然後進入資料夾並初始化專案:

mkdir react_with_webpack
cd react_with_webpack
npm init -y

這邊用 -y 去跳過所有的專案設定並採用預設值。初始化後會在專案目錄中出現 package.json 的檔案,他會紀錄我們專案所依賴的套件名稱與版本資訊。

Webpack 與他的 CLI 套件,這兩者分別是 Webpack 的核心功能,以及 CLI 的操作套件。使用 -D 指令將這些套件設定為僅在開發環境使用,因為我們只會在開發環境中用到他們,所以不要忘了加上 -D

npm i webpack -D
npm i webpack-cli -D

在過去的 npm 版本中是使用 -dev,在新版本中只需要打上 -D 就可以囉。也說明一下像是 wedpack、gulp 這類的工具是不建議裝在全域環境下的,避免在未來碰到需要升級、切換套件版本或接手他人專案的時候遇上衝突。

安裝完之後可以在 package.json 中確認這兩個套件被加入到 devDependencies 的項目中。如果你剛剛安裝的時候忘記加上 -D,套件就會被放到 dependencies 欄位裡,這時候別緊張,手動的修改 package.json 就可以了:

// package.json..."devDependencies": {
"webpack": "^5.36.2",
"webpack-cli": "^4.6.0"
}

套件名稱後面所帶的字串是套件版號,「^」字元代表的是未來這個專案要重新下載套件時必須至少安裝這個版號以上的版本。但是套件更新時有可能會更改指令或 API,因此實務上會使用固定版號避免出現不相容的狀況,而鎖定版號的資訊則是會自動記錄在 package-lock.json 裡。

到這邊安裝套件的工作就到一個段落了,接下來就要建立 Webpack 設定與我們的檔案,這邊會分別建立一個 Webpack 的設定檔、一個進入點的 JS 檔案。

先來寫 JS 的部分,在專案目錄中建立一個 src 資料夾與名為 index.js 的檔案,在裡面我們簡單的印出一個字串:

接著在專案根目錄建立 webpack.config.js 檔:

我們在一開始用 CommonJS 的模組系統取得路徑模組,並設定好 output 的路徑,用 __dirname 取得當前環境的路徑再由 path.resolve() 將相對路徑或路徑片段轉為絕對路徑,以確保在不同作業系統底下都能產出正確的路徑位置。你可能會疑惑為什麼不使用 import 呢?那是因為 Node.js 先前的版本是不支援 ES module 的,而我們在 React 中寫的 import 事實上也是透過 Babel 幫我們轉換為 ES5 的語法,因此在 Node.js 環境中應用到模組的場合常常看到的還是 CommonJS 的寫法喔。

在 CommonJS 的系統中,module.exports 就等同於 ES Module 的 export default,將內容給導出文件,而這個內容就是我們 Webpack 的設定物件。

首先 mode 是 Webpack 在打包過程中所採用的模式,分為 developmentproduction 兩種,從字面上就可以理解這個設定是決定 webpack 該次的打包任務給出的檔案是用於開發或是上線專案。

entry 則是打包任務的進入點,因為我們現在只有一個 JS 檔要編譯,因此給這個設定值字串即可,這邊給予我們 JS 檔案的路徑 ./src/index.jsoutput 就是輸出點了,path 是編譯後檔案存放的路徑,我們設定一個 build 資料夾在我們的專案根目錄上,最後給予這個輸出的 JS 命名就完成設定了。此時我們的專案目錄應該會長這樣:

▶ node_module▼︎ src
⎿ index.js
package-lock.json
package.json
webpack.config.js

在儲存檔案之後,我們在 package.json 檔案中加上 build 指令:

// package.json
{
"scripts": {
"build": "webpack build",
},
}

然後在終端機輸入剛剛設定的 Webpack 打包指令:

npm run build

因為這個指令是 Webpack 的預設指令,所以也可以不用加上 build

// package.json
{
"scripts": {
"build": "webpack",
},
}

回車後 Webpack 會開始依據 webpack.config.js 的設定編譯檔案,此時注意一下終端機中的訊息,如果出現了下面這些訊息警告你現在採用的是生產模式,就代表此次的打包任務並沒有讀到上面我們在 webpack.config.js 中所 設定的 mode,檢查一下檔名是否有打錯,或是設定內容有沒有錯字吧!

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.

編譯完成後檢查一下,專案目錄中是否已經出現了 build 的資料夾呢?如果發現是 Webpack 預設的 dist,或是編譯出來的 JS 檔不是我們設定的 bundle.js,那麼原因跟上面一樣,回頭確認一下設定使否有誤。

我們現在已經完成 JS 檔的編譯了,但我們還少了 HTML。所以我們在根目錄上建立 public 資料夾後加入 index.html 檔案,並在 <head> 中加上:

<script defer src=”../build/bundle.js”></script>

注意別 React 寫太習慣讓這個標籤自我閉合了,<script> 這個標籤是不能這樣用的!

完成後我們的專案目錄應該會長下面這樣:

▼︎ build
⎿ bundle.js
▶ node_module▼︎ public
⎿ index.html
▼︎ src
⎿ index.js
package-lock.json
package.json
webpack.config.js

最後在瀏覽器開啟這個 HTML 檔,打開主控台後應該就可以看到我們在 index.js 中所 log 的文字囉!

加入 React

終於到 React 出場的時候了,我們用 npm 下載使用 React 所必須的兩個套件:

npm i react react-dom

因為 React 使用了 JSX 的語法,就算你的瀏覽器版本再新也沒辦法看得懂,這時候就是 Babel 介入的時候了!這邊我們會安裝 Babel 幾個不同用途的套件,因為編譯的這個動作只會在開發時進行,所以下載時別忘記加上 -D。

npm i babel-loader -D

Webpack 在轉譯某些檔案時會需要透過 loader,這個套件就是 Webpack 要使用 Babel 功能所需的 loader。

npm i @babel/core @babel/preset-env @babel/preset-react -D

上面下載的依序是 Babel 的核心函式庫、 轉譯 ES6+ 語法所需的套件、用於轉換 React 語法的函式庫。

你說為什麼要分成那麼多套件,通通包成一包可以一次下載不是很好嗎?那是因為在每種開發環境下的需求不一定相同,如果把所有功能都包在一起,會大大的影響在傳輸以及執行上的成本與效率。

安裝完成後我們的 package.json 中依賴項目應該會長這樣:

//package.json..."devDependencies": {
"@babel/core": "^7.14.0",
"@babel/preset-env": "^7.14.1",
"@babel/preset-react": "^7.13.13",
"babel-loader": "^8.2.2",
"webpack": "^5.36.2",
"webpack-cli": "^4.6.0"
},
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
}

安裝完 Babel 後我們就可以在檔案中寫 JSX 囉!在 src 資料夾中建立 App.js 檔案:

會點進來看文章的人應該對 React 語法不會太陌生,這裡就不解說了。當然因為我們選擇了 root 節點渲染,我們還要去 index.html 中建立 root

可以開始編譯了嗎?在這之前我們得去修改 webpack.config.js

你剛才可能會有疑問,為什麼我在寫 App.js 的時候不要像 CRA 的範本一樣把 <App/> 匯入到 index.js 中呢?因為接下來要示範怎麼組合多個 js 檔案。首先在 entry 的地方把原來的字串改為陣列,並把 App.js 也放上去,最後編譯出來的 JS 檔就會包含這兩個檔案的內容:

如此一來 Webpack 在編譯的時候就會把這兩支檔案壓縮成同一支。接下來增加一個新設定 moduletest 的部分是正規表達式,/\.js$/ 代表只要專案中的檔案名稱用「.js」結尾都需經過編譯。如果你習慣使用「.jsx」的副檔名來寫 React,這個地方就改使用 /\.(js|jsx)/

exclude 則是指定不要編譯的檔案,這裡我們把 node_module 填上去,否則每次編譯都去撈 node_module 裡面本來就已經處理好的檔案,執行的效能會大大被拖慢啊!

再來就是 use 的部分,這邊指派了在 Webpack 使用 Babel 所必須的 babel-loader,並將 @babel/preset-react@babel/preset-env 填入 options,他們會在編譯前先執行,讓 Webpack 具備能解讀 JSX 與 ES6+ 的能力,然後才執行我們檔案的編譯與打包。

接著就可以執行編譯了,如果過程出現錯誤,可以回來看看是否剛剛的設定裡面有沒有漏掉或打錯什麼字元。

npm run build

接下來用瀏覽器打開 index.html 檔案就可以看到我們在 App.js 中所寫的字樣囉!

但是我們 build 資料中現在只有 bundle.js 一支檔案而已,HTML 檔案還放在開發用的 public 資料夾裡啊!另外還要幫網頁加上樣式,所以接下來我們就來處理 HTML 與 CSS 的部分吧!

打包 HTML 與 CSS

要讓 HTML 參與到編譯打包的過程,我們需要下載另一支套件:

npm i html-webpack-plugin -D

Webpack 透過html-webpack-plugin 套件可以在編譯過程中將打包後的 JS 檔案路徑自動加入到 <head> 標籤,並且將程式碼壓縮,對 HTML 進行最小化(minify)的處理,我們在 webpack.config.js 中用 CJS 將套件載入:

我們新增了 plugins 的設定,並且建立一個新的 html-webpack-plugin 實體,參數是個帶有設定值的物件。template 為要編譯的檔案來源,我們指定 public 中的 index.html 檔案;filename 為編譯後的檔案名稱,這邊我選擇不更動它;inject 是 Webpack 會幫我們把 JS 檔案的路徑幫我們自動嵌入 HTML 的設定值,所以要記得先去打開 index.html 檔把原來引入的 <script> 標籤刪掉,否則編譯出來會有兩行 <script> 程式碼;minfy 則是最小化編譯檔案的設定值。

接著是 CSS 的部分,我們先來撰寫樣式,在 public 資料夾新增 index.css

接下來安裝解析 CSS 的 loaderplugins

npm i css-loader -D
npm i mini-css-extract-plugin -D

插件的部分使用前需要匯入,接著在 plugin 的地方加入設定:

首先在 module 的地方加上 css-loader 的設定,並指定編譯符合副檔名為 .css 的檔案。再來於 plugin 用套件建立物件實體,並指定路徑與檔名。

如果你曾看過其他 Webpack 建置教學,可能會注意到我這邊使用的不是 style-loader 而是 mini-css-extract-plugin 套件,這兩個套件的功能存在一些差異。style-loader 在 Webpack 解析 CSS 檔案後會將樣式嵌入 HTML 的 <style> 標籤中;而 mini-css-extract-plugin則是可以在各個 JS 檔案中提取被匯入的 CSS 檔案,組合打包成同一支檔案匯出後再將路徑放入 HTML檔案的 <link> 中,所以我們接下來在 index.js 加入 CSS檔案:

如果你眼尖的話,還會發現我們還改了 js 檔案匯出的路徑,讓 JS 與 CSS 檔案被匯出到各自的資料夾裡。到這邊要回頭提醒一下,在使用 use 屬性設定 Loader 的時候,編譯的順序是由後至前的,以這邊的例子來說就是 css-loader 會先執行,其次才是 mini-css-extract-plugin

接下來我們就可以執行編譯了:

npm run build

打包完成後我們可以看到 build 資料夾中有了新建置的兩個資料夾,而其中就存放著編譯好的 CSS 與 HTML 檔案:

▼︎ build
⎿ ▼︎ css
⎿ index.css
⎿ ▼︎ js
⎿ bundle.js
⎿ index.html
▶ node_module▼︎ public
⎿ index.html
⎿ index.css
▼︎ src
⎿ index.js
⎿ App.js
package-lock.json
package.json
webpack.config.js

接著把 build 中的 index.html 打開,如果操作都沒有錯誤的話應該可以看到我們的網頁如預期的顯示了:

本地端伺服器

終於把環境建置好了,但是等等。所以我們每一次修改檔案後,就要重新輸入指令編譯才能看到更動的結果嗎?

Webpack 其實提供了一個工具幫我們建置本地端的伺服器,並且內建 hot reload 的功能,當我們檔案修改並按下存檔時,就自動偵測到檔案改變,並且針對修改的部分進行更新,而不是整個重新打包編譯,最後幫我們更新畫面,大大減少了我們一來一往在編輯器、終端機與瀏覽器上點擊重整頁面按鈕的時間!

npm i webpack-dev-server -D

安裝完後其實就可以直接啟用了,但是我們可以對套件進行一些自訂,在 webpack.config.js 中加入設定:

我們指定了伺服器的端口設定,在開發時有時候會需要啟用多個伺服器,自訂端口可以避免啟用服務的時候連接埠被佔用的問題。然後在 package.json 新增指令:

// package.json"scripts": {
"build": "webpack build",
"start": "webpack serve --open",
},

接下來只要在終端機鍵入指令:

npm run start

webpack-dev-server 就會幫你在瀏覽器自動打開新分頁,並且將伺服器啟用在我們設定的 3000 端口顯示我們根目錄下的 index.html 檔案。你現在可以嘗試修改一下在我們 CSS 檔案中 <h1> 的顏色,當我們按下儲存,就可以看到頁面非常迅速的就幫我們改變畫面了。當然如果修改的是 JS 的部分就會需要一些時間處理,但是比起自己鍵入指令編譯已經是方便到不行了!

這邊有幾點要提醒一下,首先因為 hot reload 是針對編譯的檔案做偵測與重載,因此對於 webpack.config.js 的更動是需要關閉伺服器重新載入的喔。針對不同的作業系統,關閉伺服器的指令是 Ctrl + C⌃ + C。再來有些教學所提供 webpack-dev-server 作為啟用伺服器的指令,但這個指令已經在新版本的套件中改為 webpack serve 了,如果鍵入了舊的指令,終端機只會不停地告訴你 command not found 喔。

另外我們也不想要在每次 Webpack 重新建置檔案的時候還要人工的手動去刪除資料夾,我們可以透過套件搭配指令幫我們做到這件事,首先在專案上安裝:

npm i rimraf -D

Rimraf 這個套件可以幫我們刪除檔案與資料夾,我們在 package.json 加入他們,並透過 && 來串連兩個指令:

// package.json"scripts": {
"start": "webpack serve --open",
"clean-build-folder": "rimraf ./build/*",
"build": "npm run clean-build-folder && webpack build"
},

如此一來只要每當我們在終端機鍵入指令:

npm run build

Rimraf 就會先幫我們清除 build 資料夾裡下的所有檔案,然後 Webpack 才開始進行打包編譯。

另外如果你嫌 webpack-dev-server 的 hot reload 速度還是太慢的話,或許可以研究看看有別於 Webpack 的另一套 build tool + dev server 工具 Vite。因為他選擇在開發環境中不預先打包,並透過現代瀏覽器原生所支持的 ES Module 來處理模組相依,因此能達到非常快的重載速度,是個很值得花時間嘗試看看的工具。

到這邊我們已經完成初步的建置了!但是這樣的配置並不能滿足所有專案的開發需求,依照個別專案所選擇的工具我們可能還需要 SASS 或 PostCSS 的 loader、處理檔案與圖片的 loader;你可能也選擇了使用 Pug 、 TypeScript 或是 ESLint 等工具來開發,這些都需要載入對應的套件。這篇文章僅是帶你建立基本的 React 編譯環境,但相信看到這邊的你對於 Webpack 要怎麼進行擴充套件的設定也有初步的認識,能夠依照需求再對自己的專案進行擴充。

最後提供我們在這篇文章最終完成的 Webpack 設定檔,完整的專案則會放到 Github 上給有需要的人參考。如果你發現這篇文章有不正確的地方,也歡迎來訊讓我知道 :)

所以我說為什麼那個 Gist 的縮排都會自動跳成八格啦!!!

--

--