webpack 學習筆記
===

###### 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,直接安裝)

- 安裝完畢會自動產生一個`package.json` 的文件 (在這些之前,請先安裝 `node.js`)

- 接著開始安裝 webpack,請輸入 `npm i -D webpack webpack-cli`

- 到 `package.json` 查看是否安裝完畢

- 建立指令 - 告訴 webpack 開始打包

---
### 實際演練:
- 在 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());
```
- 此時會產生錯誤

- 這時就要靠 webpack 來幫我們將兩個 js 零件做組合!在終端機輸入剛剛設定的 script
`npm run build`

- 此時 dist 資料夾,會出現一個 `main.js` 的檔案,這就是透過 webpack 打包後的結果
- 把 dist 資料夾內的 index.html 裡的 script 路徑改為使用 main.js,就可以順利在 console 裡面看到結果,以下是 main.js 內打包過後的程式
```javascript=
// main.js
(()=>{"use strict";console.log("hahahahaha")})();
```

---
### 設定配置 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` 刪除

- 接著把 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`

- 當我們引入 scss/sass/css 檔案到 js 裡面時,需要 loader 為我們解析文件,因此需要回到配置裡面去設定,在設定前,先安裝 `npm i -D sass sass-loader style-loader css-loader`

- 接著到 `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`

- css 樣式順利解析的結果

---
### Plugin
- plugin 可以讓專案更彈性,例如自動產生 html 文件,我們以這個來實際演練一下
- 打開終端機,輸入 `npm i -D html-webpack-plugin`

- 到配置裡面將 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 資料夾

- 但透過這樣的方式建立的 html 文件,卻將原本的 Hello world h2 標籤自動刪除,也無法直接寫在這個檔案,因為每次運行 build 一次,內容就會被刪除

---
### 透過 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

- 成果:

---
### 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 檔

- 同樣在 dist 資料夾內的 `index.html` 檔案,也會自動將此 js 檔放進去

---
### devServer
- 透過默認瀏覽器並設定埠號開啟網頁
- 打開終端機,輸入 `npm i -D webpack-dev-server`

- 到配置檔案,進行配置
```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"],
},
],
},
};
```