webpack 學習筆記 === ![](https://i.imgur.com/VB2pc65.png) ###### tags: `webpack` --- ## 什麼是 Webpack - webpack 是一種靜態模組打包工具,它會根據一個或多個入口來建構一個依賴圖(dependency graph),然後將應用程式內的模組打包成多個概念類似包裹的靜態資源,作為呈現應用程式的內容。 --- ## 安裝 - 建立一個專案資料夾,開啟編譯器,接著再建立兩個資料夾,分別為: - dist (此資料夾將作為 webpack 打包後的檔案,稱 [output](https://webpack.docschina.org/concepts/#output)) - src (此資料夾將作為 webpack 的入口,稱 [entry](https://webpack.docschina.org/concepts/#entry)) > 這邊想成一個工廠,每一個工廠都有輸入線,輸入線裡面為產品的零件,當零件都確認擺放完畢後,一起到輸出口,再由機器將零件組裝起來,進而產生一個完整的產品。 - 打開終端機,首先先輸入 `npm init -y`(npm init 在安裝過程詢問一些問題,y 表示默認 yes,這個指令會直接默認所有回答為 yes,直接安裝) ![](https://i.imgur.com/7slHvBr.png) - 安裝完畢會自動產生一個`package.json` 的文件 (在這些之前,請先安裝 `node.js`) ![](https://i.imgur.com/Y3nGBts.png) - 接著開始安裝 webpack,請輸入 `npm i -D webpack webpack-cli` ![](https://i.imgur.com/1WkdsSU.png) - 到 `package.json` 查看是否安裝完畢 ![](https://i.imgur.com/P3PiX92.png) - 建立指令 - 告訴 webpack 開始打包 ![](https://i.imgur.com/4wwdv1D.png) --- ### 實際演練: - 在 dist 資料夾建立 `index.html` - 在 src 資料夾建立 `index.js` ```htmlembedded= <!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h2>Hello World</h2> <script src="../src/index.js"></script> </body> </html> ``` ```javascript= // index.js console.log("Hello world") ``` - 打開 web-server 後,開啟開發者工具,點擊 console 之後,會顯示 Hello world - 現在加入另外一個 js 檔案到 src 資料夾 ```javascript= // joke.js function generateJoke() { return "hahahahaha"; } export default generateJoke; ``` - 把這個 joke.js 文件試圖引入到 index.js 內 ```javascript= // index.js import generateJoke from "./joke"; console.log(generateJoke()); ``` - 此時會產生錯誤 ![](https://i.imgur.com/erKrTja.png) - 這時就要靠 webpack 來幫我們將兩個 js 零件做組合!在終端機輸入剛剛設定的 script `npm run build` ![](https://i.imgur.com/R2zL5wF.png) - 此時 dist 資料夾,會出現一個 `main.js` 的檔案,這就是透過 webpack 打包後的結果 - 把 dist 資料夾內的 index.html 裡的 script 路徑改為使用 main.js,就可以順利在 console 裡面看到結果,以下是 main.js 內打包過後的程式 ```javascript= // main.js (()=>{"use strict";console.log("hahahahaha")})(); ``` ![](https://i.imgur.com/HWOTRcE.png) --- ### 設定配置 configuration - 在根目錄底下建立一個 `webpack.config.js` 檔案,這裡面我們要建立入口,出口跟模式的配置 ```javascript= // webpack.config.js // 引入 Path 模組 const path = require("path"); module.exports = { mode: "development", // setup entry entry: path.resolve(__dirname, "src/index.js"), // setup output output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", }, }; ``` - 回到 `package.json` 內,把稍早設定的 script 修改一下,把 `--mode production` 刪除 ![](https://i.imgur.com/4wwdv1D.png) - 接著把 dist 的 `main.js` 刪除,再重新運行 `npm run build` ,就會看見 `bundle.js` 在 dist 出現,但要記得把 `index.html` 的 js 檔案路徑改成 `bundle.js` - 配置內設定多個路徑: ```javascript= // webpack.config.js // 引入 Path 模組 const path = require("path"); module.exports = { mode: "development", // setup entry, if there're multiple files, set is as an object entry: { bundle: path.resolve(__dirname, "src/index.js"), <file name> : path.resolve(__dirname, "src/...") }, // setup output output: { path: path.resolve(__dirname, "dist"), // 這邊的 name 會看向 entry 設定的 bundle filename: "[name].js", }, }; ``` --- ### loader - [webpack loader](https://webpack.docschina.org/concepts/loaders/) 文件 - loader 可以將不同語言轉換成瀏覽器看得懂的語言,也可以透過 loader 來將 sass 透過 sass-loader 來轉譯,圖片則可以轉為 url 等 - loader 從右到左(或從下到上)來取值 (evaluate) / 執行(execute) --- ### 使用 style-loader, css-loader & sass-loader - style-loader: 將 css 嵌入 DOM,建議與 css-loader 搭配使用 - 參考: [Webpack-style-loader](https://webpack.docschina.org/loaders/style-loader/) - css-loader: css-loader 會對 `@import` 和 `url()` 進行處理,就像 js 解析 import / require() 一樣 - 參考:[Webpack-css-loader](https://webpack.docschina.org/loaders/css-loader/) - sass-loader: 載入 sass / scss 編譯成 css 文件 - 參考:[Webpack-sass-loader](https://webpack.docschina.org/loaders/sass-loader/) --- ### 實際練:加入 scss 檔案到專案內 - 在 src 資料夾內建立一個 styles 的資料夾,在該資料夾內建立一個 `main.scss` 的檔案 - 將 `main.scss` import 到 `index.js` 內 ```sass= // main.scss @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"); $primary-color: #2fa8cc; $secondary-color: #f4f4f4; $box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1), 0 6px 6px rgba(0, 0, 0, 0.1); * { box-sizing: border-box; } body { background-color: $primary-color; font-family: "Roboto", sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; overflow: hidden; margin: 0; padding: 20px; } .container { background-color: $secondary-color; border-radius: 10px; box-shadow: $box-shadow; padding: 50px 20px; text-align: center; max-width: 100%; width: 800px; } h3 { margin: 0; opacity: 0.5; letter-spacing: 2px; } img { width: 100px; margin-bottom: 20px; } .joke { font-size: 30px; letter-spacing: 1px; line-height: 40px; margin: 50px auto; max-width: 600px; } .btn { background-color: $primary-color; color: $secondary-color; border: 0; border-radius: 10px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 6px 6px rgba(0, 0, 0, 0.1); padding: 14px 40px; font-size: 16px; cursor: pointer; &:active { transform: scale(0.98); } &:focus { outline: 0; } } ``` - import 到 `index.js` ```javascript= // index.js import generateJoke from "./joke"; import "./styles/main.scss"; console.log(generateJoke()); ``` - 如果直接在終端機運行 `npm run build` 指令會得到錯誤訊息 `You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders` ![](https://i.imgur.com/3bmfPt8.png) - 當我們引入 scss/sass/css 檔案到 js 裡面時,需要 loader 為我們解析文件,因此需要回到配置裡面去設定,在設定前,先安裝 `npm i -D sass sass-loader style-loader css-loader` ![](https://i.imgur.com/1EYGkxh.png) - 接著到 `webpack.config.js` 內進行 loader 的配置 ```javascript= const path = require("path"); module.exports = { mode: "development", entry: { bundle: path.resolve(__dirname, "src/index.js") }, output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", }, module: { rules: [ { // any file ends with this extenstion will apply these laoders test: /\.scss$/, use: [ "style-loader", "css-loader", "sass-loader" ], }, ] } }; ``` - 配置完畢後,回到終端機再運行一次 `npm run build` ![](https://i.imgur.com/c8nl166.png) - css 樣式順利解析的結果 ![](https://i.imgur.com/xhQL9de.png) --- ### Plugin - plugin 可以讓專案更彈性,例如自動產生 html 文件,我們以這個來實際演練一下 - 打開終端機,輸入 `npm i -D html-webpack-plugin` ![](https://i.imgur.com/HACal2J.png) - 到配置裡面將 plugin 加進去 ```javascript= const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { mode: "development", entry: { bundle: path.resolve(__dirname, "src/index.js") }, output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", }, module: { rules: [ { // any file ends with this extenstion will applies these laoder test: /\.scss$/, use: [ "style-loader", "css-loader", "sass-loader" ], }, ] }, plugins: [ new HtmlWebpackPlugin({ title: "webpack App", filename: "index.html" }) ] }; ``` - 配置完後,可以直接刪除原本的 dist 資料夾,我們可以透過 `npm run build` 讓 webpack 建立包含 `bundle.js`, `index.html` 檔案的 dist 資料夾 ![](https://i.imgur.com/5z4yR8L.png) - 但透過這樣的方式建立的 html 文件,卻將原本的 Hello world h2 標籤自動刪除,也無法直接寫在這個檔案,因為每次運行 build 一次,內容就會被刪除 ![](https://i.imgur.com/OfnXKtU.png) --- ### 透過 template 來建構網頁架構 - 在 src 資料夾內建立一個 template.html ```htmlembedded= <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- 會顯示我們在配置檔案內的 title --> <title> <%= htmlWebpackPlugin.options.title %> </title> </head> <body> <div class="container"> <img id="laughImg" alt="" /> <h3>Don't Laugh Challenge</h3> <div id="joke" class="joke"></div> <button id="jokeBtn" class="btn">Get Another Joke</button> </div> </body> </html> ``` - 到配置檔案內,把 template 的路徑加上去 ```javascript= plugins: [ // create an instance new HtmlWebpackPlugin({ title: "webpack App", filename: "index.html", template: "src/template.html" }) ] ``` - 接著到終端機運行 build 指令,webpack 就會自動幫我們打包,在 `template.html` 也會轉譯到 dist 的 index.html ![](https://i.imgur.com/xI7ePpR.png) - 成果: ![](https://i.imgur.com/OiFDT9f.png) --- ### Caching - 透過設定`contenthash`,讓檔案名稱在部署後生成一組序號,被瀏覽器作為快取暫存,這樣就可以降低流量,讓網路速度快些;該序號會在每次版本更新時,重新部署(build)之後改變,以避免瀏覽器會誤以為內容並無改變。 >[快取](https://zh.wikipedia.org/wiki/%E7%BC%93%E5%AD%98): >當CPU處理資料時,它會先到Cache中去尋找,如果資料因之前的操作已經讀取而被暫存其中,就不需要再從隨機存取記憶體(Main memory)中讀取資料——由於CPU的執行速度一般比主記憶體的讀取速度快,主記憶體儲器周期(存取主記憶體儲器所需要的時間)為數個時鐘周期。因此若要存取主記憶體的話,就必須等待數個CPU周期從而造成浪費。 - 回到配置檔案,做以下更動: ```javascript= output: { path: path.resolve(__dirname, "dist"), //filename: "bundle.js", filename: "[name][contenthash].js" }, ``` - 再次將 dist 資料夾刪除,重新運行 `npm run build` 指令 - 生成的 js 檔 ![](https://i.imgur.com/YUtTEoC.png) - 同樣在 dist 資料夾內的 `index.html` 檔案,也會自動將此 js 檔放進去 ![](https://i.imgur.com/bVR9PRz.png) --- ### devServer - 透過默認瀏覽器並設定埠號開啟網頁 - 打開終端機,輸入 `npm i -D webpack-dev-server` ![](https://i.imgur.com/PPDQiYv.png) - 到配置檔案,進行配置 ```javascript= devServer: { static: { directory: path.resolve(__dirname, "dist"), }, port: 3000, open: true, hot: true, compress: true, historyApiFallback: true, }, ``` - `port:3000`: 指定埠號為 3000 - `open: true`:打開默認的瀏覽器 - 參考:[Webpack-dev-server-open](https://webpack.docschina.org/configuration/dev-server/#devserveropen) - `hot: true`:hot module reload,開發過程中,當專案越來越大,部署時間就會越長,設定 `hot:true` 的目的在於可以使用 hot module replacement,針對有更動的檔案進行更新,而不需要重新載入整個網頁 - `compress: true,`:啟用 [gzip compression](https://betterexplained.com/articles/how-to-optimize-your-site-with-gzip-compression/) --- ### 目前使用的 config - 下載的 packages: - React: `npm i react react-dom react-router@6` - webpack: `npm i webpack webpack-cli webpack-dev-server --save-dev` - Babel: `npm i @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react @babel/runtime babel-loader` - HTML plugin: `npm i html-webpack-plugin --save-dev` - sass: `npm i sass save--dev` / `npm i node-sass --save-dev` - loaders: `npm i css-loader style-loader sass-loader` - package.json script 設定: ```json= "scripts": { "start": "webpack-dev-server --mode development --open --hot", "build": "webpack --mode production" }, ``` - webpack.config.js 設定: ```javascript= const path = require("path"); const HTMLWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: { bundle: path.resolve(__dirname, "src/index.js"), }, output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", }, plugins: [ new HTMLWebpackPlugin({ title: "easyTodo | Manage your tasks", filename: "index.html", template: "./src/index.html", }), ], devServer: { static: { directory: path.resolve(__dirname, "dist"), }, port: 3000, open: true, hot: true, compress: true, historyApiFallback: true, }, module: { rules: [ { test: /.js$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/preset-react"], plugins: ["@babel/plugin-transform-runtime"], }, }, }, { test: /\.(sass|css)$/, use: ["style-loader", "css-loader", "sass-loader"], }, ], }, }; ```