webpack5 入門:pnpm + webpack5

這篇文章主要參考 A Beginner’s Guide to WebpackWebpack 5 : Guide for beginners 兩篇教學文去設定webpack 5的組態檔案,但因為兩篇文章的內容有錯誤且部份是webpack 4的組態設定,所以config有按官方文件作修改,最後會推薦比較新的教學影片和系列文。


而這篇文章記錄Webpack 5的環境建置,使用到的工具有:


前置作業

由於我有安裝Windows的WSL,所以工作環境會比較"接近"Linux,而WSL2的安裝可以參考之前寫的文章

mkdir webpack-js
cd webpack-js

webpack

根據官網的定義,webpack是一種JavaScript應用的靜態模組打包工具:

webpack is a static module bundler for modern JavaScript applications.

所謂的靜態檔案就是指那些非伺服器產生的檔案,例如:CSS、JavaScript、image等

如果更進一步來看,官網有一小段說明webpack的機制:

When webpack processes your application, it internally builds a dependency graph from one or more entry points and then combines every module your project needs into one or more bundles, which are static assets to serve your content from.

簡單來說,專案可能會需要引入其他模組,並仰賴模組裡函式提供的功能,而這種提供功能的模組稱為dependency。

Dependency: Any time one file depends on another, webpack treats this as a dependency.

因為一個專案可能仰賴好幾個模組,而webpack在做的事情是 ─ 它會根據專案的需要串連這些模組,而串連模組之間關係的方式就是建立一個dependency graph來描述這些模組之間相互仰賴的關係。

最後webapck再根據這個dependency graph將需要的檔案打包成瀏覽器能載入(load)的JavaScript檔案、秀出你寫的應用程式,而且通常會打包成一個檔案讓瀏覽器去讀取、執行這個檔案。

如果還是不態清楚bundler在做什麼,這篇文章會有更淺顯易懂的解釋。

Install

webpack是基於Node.js開發的打包工具,所以要先確保電腦裡安裝Node.js。

Warning: The minimum supported Node.js version to run webpack 5 is 10.13.0 (LTS)

要注意,使用webpack最少需要10.13.0以上LTS版本的Node.js,另外webpack只支援符合ES5標準的瀏覽器,但因為webpack有些功能無法在更舊的瀏覽器被使用,所以可能會需要polyfill套件支援,詳細可見官方文件的browser compatibility

webpack安裝好以後,接下來都會使用模組管理工具來管理專案會用到的模組。你可以使用跟著Node.js一起安裝的npm,或者使用其他模組管理工具,像是yarnpnpm等,而這篇是使用pnpm。

Initialize the project as node modele project

在安裝模組以前,記得先用模組管理工具初始化專案環境、新增 package.json 檔案來管理之後要新增的模組

// npm
npm init
npm init -y    // Skip default setting


// pnpm
pnpm init

init

之後若有安裝任何模組都會顯示在這個 package.json 檔案,檔案內可以修改跟這個專案有關的資訊,像是name、version、description、author等,這邊就先忽略不修改。

Install webpack5

接著開始安裝webpack,由於webpack只是打包工具,所以webpack不用跟著打包後的檔案一起發佈到網路上,所以官方建議用 --save-dev 安裝 webpack

pnpm install --save-dev webpack

// 或者使用
pnpm install -D webpack    // -D為--save-dev的縮寫

接著開發時可能也需要在自己的電腦(本機, local)查看目前開發的狀況,所以需要安裝 webpack-dev-server

以上可以下指令一次安裝兩個工具:

pnpm install --save-dev webpack webpack-dev-server

// 或者
pnpm install -D webpack webpack-dev-server

另外有時候可能也需要用命令來呼叫webpack,因此也可以安裝 webpack-cli

pnpm install --save-dev webpack-cli

// 或者使用
pnpm install -D webpack-cli

安裝好以後會在 package.json 看到安裝的模組版本,也會看到一個 node_modules 資料夾裡面會有剛剛安裝的模組相關檔案:

webpack-init


Test local server

剛才安裝 webpack-dev-server 有說到開發時可能需要在本機端(自己的電腦)看目前的開發狀況和成果,所以先用本機端伺服器(local server)測試一下如何查看現在的開發成果。

因為webpack是從某個起始點開始將專案所有相關檔案打包成一個JavaScript檔案,然後webpack的local server預設是把 publuc 資料夾的 index.html 當作網站首頁,同時index.html 裡面會引入一個打包後的JavaScript檔案處理網站的互動行為。

因此,我們先建立一個 publuc 資料夾,並在 publuc 資料夾底下建立一個 index.html

webpack-init

然後在 index.html 引入打包後的檔案 bundle.js

webpack-init

接著在命令列輸入 pnpm webpack serve --mode development,可以看到server已經開啟並占用port8080:

webpack-init

可以輸入這串local server的網址開啟頁面,已經能看到剛剛建立的html檔案顯示斗大標題 Hello Webpack

local-server0test

但從develop tool可以看到沒辦法讀取 bundle.js 檔案,那是因為現在還沒真正打包專案並新增一個名為 bundle.js 的檔案,晚一點才會來產生 bundle.js

另外,每次都要下這~麼長一串指令 pnpm webpack serve --mode development 打開local server實在太麻煩了,可以在 package.json 檔案內設定指令縮寫:

scripts-start

以後只要輸入 pnpm start 這串指令替代 pnpm webpack serve --mode development,就方便許多了。


Configuration - Introduction

由於我們還沒要發布到真正的網路上,一切都先在本機端作業,而且還要對專案作一些設定,所以先修改一下剛剛測試用的東西:

  1. public 資料夾先改名為 dist

  2. package.jsonscriptsstart 指令改為: "start": "webpack serve --config ./webpack.config.js --mode development", 指令大致意思是用我們替專案作的設定來打包並且開啟伺服器查看

再來新增 src 資料夾並在內部新增 index.js 檔案,以及在 src 資料夾以外新增一個webpack的config檔案 ─ webpack.config.js

webpack-config

注意: 根據專案需求 webpack.config.js 可能放在不同位置,這裡是根據官方文件的步驟新增在 src 資料夾以外。

webpack.config.js 是用來替這份webpack專案作設定。

config檔案,中文稱 組態檔,所謂的組態檔是專門用來設定一個應用程式的組態設定、參數等等,以webpack來說,這些設定或參數可能會決定webpack如何打包你的檔案。

用比較通俗的角度來看(?),因為webpack是將多個檔案打包成一個檔案,所以可能需要知道哪個檔案是要讓瀏覽器第一個開啟的網站首頁檔?或者要從哪個檔案當作建立dependency graph的起始點?而這些資訊就是要寫在webpack的config檔案。

雖然從官方的教學文件大略知道webpack 4.0.0之後,其實不需要特別去新增一個config檔案來決定你的專案打包前後的配置需求。

Since version 4.0.0, webpack does not require a configuration file to bundle your project. Nevertheless, it is incredibly configurable to better fit your needs.

不過有時候將網站發布到網路上,可能要根據網路伺服器的要求修改config檔案,例如伺服器規定一定要從 public 資料夾底下的 index.html 才能順利開啟網站之類的,所以需要設定打包後檔案的首頁路徑是 /public/index.html

因此,webpack官方還是強烈推薦自己建立一個專案的config檔案

由於剛才已經把開發環境改成只在本機端作業,不能採用webpack專案的預設設定,這裡先在 webpack.config.js 裡對打包起始點(entry)、輸出(output)檔案路徑和名稱,以及伺服器(devServer)進入首頁的路徑修正一下:

dev-server

(註:webpack 4開啟 devServer 的路徑是 contentBase ;目前 webpack 5 已改為 static

可以再下一次指令 pnpm start 檢查是否正確顯示剛才看過的首頁。

爬了一下webpack其他的組態設定方式,發現有非常多的名詞需要知道😅,接下來就一步步來看這些名詞來了解如何在 webpack.config.js 內自訂Webpack專案的組態。


Configuration - Core Concepts

webpack 的組態設定和運作有幾個核心概念(core concepts)需要知道,分別是:

  • Entry

  • Output

  • Loaders

  • Plugins

  • Mode

Entry

Entry 或說 entry point 指的就是webpack要從哪一個模組(module)當作起始點開始建立dependency graph,並且一步一步連結跟這個起始點有直接和間接依賴關係的模組。

Entry的預設起始是 /src/index.js,由此可知使用webpack的專案至少要有一個稱為 src 的資料夾,而且底下要有一個 index.js 檔案。

不過也可以根據需求去修改你的起始點,修改方式可參考官方文件這篇,而我們剛剛已經有修改了起始點:

const path = require('path');

module.exports = {
  entry: {
    main: path.resolve(__dirname, 'src/index.js'),
  },
}

假如專案是伺服器根據需求給予相應網頁的應動態型應用網站,可能會有多個起始點要設定:

module.exports = {
    entry: {
        home: './home.js',
        about: './about.js',
        contact: './contact.js'
    },
    // other config...
}

Output

webpack打包所有檔案後會輸出給瀏覽器讀取的檔案,Output 可以給定輸出檔案的路徑 path 和檔案名稱 filename,預設路徑是 ./dist、檔案名稱則是 main.js

剛才其實已經修改成另一個輸出路徑和檔案名稱,而輸出檔案的路徑相當於 ./dist/bundle.js

const path = require('path');

module.exports = {
  // other config...
  output: {
    path: path.resolve(__dirname, 'dist'),    // ./dist
    filename: '[name].bundle.js',        // main.bundle.js
    publicPath: "/"   // resolve "relative path" on the browser or server
  },
};

此外,這邊多增加 publicPath: "/" 是因為實作有碰到問題,可參考另一篇筆記:[Note] webpack 5 problem: Refused to execute script because its MIME type ('text/html') is not executable

Loaders

Loaders是蠻重要的組態,因為事實上Webpack只看得懂JavaScript和JSON檔案,但專案需求可能會要引入這兩種類型以外的檔案。

如果要讓Webpack處理非JavaScript或者非JSON類型的檔案,需要設定Loaders將其他類型檔案轉換成Webpack看得懂的模組(modules),同時讓其他類型檔案在你的專案被Webpack打包以後還可以使用。

不過在Webpack裡有個例外檔案 ─ .css,因為CSS檔案是前端標配之一,所以在Webpack是可以直接將CSS檔案加入dependency graph。(不過其他bundlers有可能看不懂 .css 檔案😅)

因為前面有說過是要把其他類型檔案轉成webpack看得懂的 模組,而且加入Loaders也要引入相應模組,所以設定Loaders 是 module 開頭,並在 module 內部的 rules 屬性設定Loaders的引入規則。

這裡要注意 rules 只能接受陣列型態 [] 的值,並在陣列裡包住一個設定Loaders組態物件 {},所以設定Loaders的語法整理起來應該是長這樣:

module.exports = {
    // other config...,
    module: {
        rules: [
            {
                // Loader1 with its config
            },
            {
                // Loader2 with its config
            },
        ]
    }
}

設定Loaders組態有兩個非常重要的屬性 usetest

  1. use: 設定要轉換其他類型檔案所使用的 loaders

  2. test: 設定要轉換的其他類型檔案

注意Loaders本身也是模組,所以要先安裝Loaders模組。

以下就直接來看怎麼在config檔案中設定loaders:

const path = require('path');

module.exports = {
  // other config...

  // Set loaders
  module:{
      rules:[
        // styles
        {
            test: /\.css$/i,
            use: ["style-loader", "css-loader"],
          },
        {
            test: /\.s[ac]ss$/i,
            use: ['style-loader', 'css-loader', 'sass-loader'],
        },

        // images
        {
            test: /\.(jpe?g|gif|bmp|mp3|mp4|ogg|wav|eot|ttf|woff|woff2|png)$/,
            type: 'asset',
            parser:    {
                dataUrlCondition: {
                    maxSize: 8 * 1024,
                }
            },
            generator: {
              publicPath: 'assets/images/',
              outputPath: 'assets/images/',
              filename: '[hash][ext]',
            },
        },
        {
            test: /\.svg/,
            type: 'asset/inline',
            use: 'svgo-loader',
            generator: {
              publicPath: 'assets/images/',
              outputPath: 'assets/images/',
              filename: '[hash][ext]',
            },
        },

        // fonts
        {
            test: /\.(eot|ttf|woff|woff2)$/i,
            type: 'asset',
            parser:{
                dataUrlCondition: {
                    maxSize: 8 * 1024,
                }
            }
        }

        // babel
          {
            test: /\.(js)$/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'babel-loader',
                options: { 
                    presets: ['@babel/preset-env']
                },
              },
            ],
          },
    ]
  }
};

這次安裝以下這些loaders:

  • Style - CSS^1pnpm i -D css-loader style-loader sass-loader sass

  • Style - Sass/SCSS^1

    • sass-loader:把Sass/SCSS [^3]檔案編譯成CSS檔案(註:記得先安裝sass)

    • sass:即dart-sass的安裝縮寫,功能是把 .scss 檔案編譯成 .css(※註:過去常用的node-sass已經deprecated、沒有在更新,官方也已經建議用dart-sass取代,故這裡改使用dart-sass)

  • images & fonts

  • Babel (JavaScript)pnpm i -D babel-loader @babel/core @babel/preset-env

注意事項

  1. 實際上可先安裝CSS相關的樣式(style)loaders就好,因為我之前有在用Sass/SCSS,所以這次才會跟著CSS loaders一起安裝;

  2. 剛開始可能搞不清楚要用哪些Loaders,所以webpack官網有貼心整理一些Loaders,可以來這邊看官網介紹的Loaders

  3. module的屬性設定建議參照官方文件:Module

  4. webpack 5 才出現的asset module可以參考官方文件中的 Asset Managementasset modules 以及 Module 來設定

  5. svg圖檔不一定要用 'asset/inline',也有其他處理方式:

    • 可以改成 'asset/resource':以module形式引入,但也是要用 <img src={ /* svg module */ } /> 一般的image圖檔加入到檔案中;

    • 或者用@svgr/webpack:這個套件以 <svg><path>....</path></svg> 的形式引入,這種方式可以用css去部分修改svg圖檔的樣子比較彈性一點。

Plugins

Loaders是負責讓Webpack讀取 .js.json 以外類型的檔案,而plugins則是用來擴充webpack沒有的功能,這篇只會介紹一個原則上一定會用到的plugin ─ Html Webpack Plugin

前面webpack bundle後的JavaScript檔案,其實是在 /dist 資料夾下新增一個 index.html 再用 <script> 加入bundle後的JS檔案;但如果一個網頁應用程式會產生多個HTML檔案,還要手動新增HTML再加入JavaScript檔案實在是太麻煩了,這時候就可以用 Html Webpack Plugin 自動建立HTML檔案並且會在這份HTML檔案加入相對應的JavaScript檔。

Html Webpack Plugin 的組態設定如下:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');    // *

module.exports = {
    // other config...
    plugins: [ new HtmlWebpackPlugin() ],
}

new HtmlWebpackPlugin() 的括號內可以新增一些跟這個HTML檔案相關的屬性,譬如新增HTML的title標籤(title)、產生HTML的樣板(template)、產生後的檔案名稱(filename),以及要注入的JavaScript script位置(inject):

new HtmlWebpackPlugin({
    title: 'Hello Webapck 5',
    template: './src/index.html',
    filename: 'index.html',
    inject: 'body',    // 表示會放在<body></body>標籤內的最下方
})

同樣地,webpack官網也有整理一些Plugins,可以視專案需求新增。

Mode

Mode可以被設定為none、development或是production,預設值是development。

Mode主要是用來配置webpack bundle時的最佳化設定,這邊最佳化的白話文意思是「檔案被bundle的時候會不會加入某些開發用功能或是被最小化?」,而最小化 (minification) 就是指移除不必要與冗餘的程式碼。

  • none:表示會檔案被bundle後的程式碼格式和平常沒兩樣(有空格就有空格、有空行就會有空行),這種模式產生的檔案程式碼可讀性最高,可以了解檔案被bundle後會被webpack加入那些程式碼;

  • development:這種模式bundle的檔案會加入一些比較好除錯的功能,若檔案比較大,bundle速度可能會比較慢;

  • production:將檔案bundle成最小化格式,也就是儘可能刪除不必要的程式碼(像是註解、空格、空行等),因為空白處都會被刪除,所以bundle後的檔案只會剩一行程式碼。

當然,config檔案的這些配置選項不只有這些,以後可能會另外新增文章整理其他會用到的組態配置,或者詳細的組態配置可先參考官方文件各個組態的章節

webpack-doc

最後推薦一部近幾個月才發佈的webpach 5教學影片,內容比較新也正確: Webpack 5 Crash Course | Frontend Development Setup 另外還有一系列深入淺出、說明很親切的webpack介紹文章,共有三篇:


devServer

在用webpack dev server看目前開發成果的時候,建議可以增加以下設定:

module.exports = {
    static: { directory: path.resolve(__dirname, 'build') },
    hot: true,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: {
          host: '0.0.0.0',
          protocol: 'https:',
          port: 8080,
        },
        pathRewrite: {
          '^/api': '',
        },
      },
    },
};

historyApiFallback 主要是要解決後來實作遇到的問題(可參考這篇)。

proxy 則可能是在使用第三方API時需要的組態,如果沒加這個設定可能會碰到 local server 跨域存取的問題(CORS可參考這篇筆記)。


參考資料

[^2]: 如果要使用 .css 檔案,得安裝css-loader,且通常跟著安裝style-loader。 [^3]: 如果要使用Sass/SCSS,則要安裝sass-loader,且必定要跟著安裝dart-sass或node-sass或sass-embedded。 [^4]: 過去都要使用raw-loader、file-loader、url-loader之類的loaders處理images和fonts,但在webpack5的官方文件說明內建的assets module已經提供這樣的功能