Webpack是JavaScript的模組建置工具,運行在Node.js上,它可以將零散的JavaScript檔案用各式工具優化並打包起來,加快網頁的載入時間。Webpack也並不限於用在JavaScript上,舉凡網頁有用到的靜態資源(如JS、CSS、圖片檔等),甚至是HTML網頁,Webpack都有辦法打包。



Webpack官方網站:

官方網站上給的一張圖例,簡潔扼要地說明了Webpack的功用:

webpack

Webpack的Node.js環境

Webpack官方建議使用Node.js最新的LTS版本來運行Webpack。Windows的使用者可以直接到Node.js官方網站下載到Node.js的安裝檔,而Linux作業系統或是macOS的使用者可以參考這篇文章來安裝指定版本的Node.js。

建立Webpack專案

Webpack可以被加進現有的Node.js專案中,在終端機執行以下指令來安裝webpackwebpack-cli這兩個套件。

npm i -D webpack webpack-cli

指令中的-D參數即是--save-dev

接著在package.json中加入新的腳本指令:

{
    ...

    "scripts": {
        ...

        "build:webpack": "webpack --mode production",

        ...
      },

      ...
}

production其實就是Webpack預設的模式,它會將NODE_ENV設為production,並且會自動啟用它自帶的一些相關外掛。如果需要的話也可以使用development,它會將NODE_ENV設為development,也會自動啟用它自帶的一些相關外掛。

再來就是在Node.js專案根目錄中,新增Webpack的設定檔webpack.config.cjs,內容可以初始如下:

module.exports = {
    plugins: [

    ],
    module: {
        rules: [

        ]
    },
};

使用npm指令打包Webpack專案

由於我們已經事先在package.json中加入build:webpack腳本指令了,因此可以直接使用npm run build:webpack指令來執行它。

進入點(Entry Point)和輸出

Webpack會去執行某個或是某幾個JavaScript檔案作為進入點,預設的進入點只有一個,路徑為src/index.js,且其輸出的JS檔案的預設路徑為dist/main.js。搭配其它的載入器也可以使用非JavaScript作為進入點。

如果要修改進入點的路徑,要在webpack.config.cjs設定檔中,加上entry欄位,設定方式如下:

module.exports = {
    ...

    entry: "./path/to/js-file",

    ...
};

例如:

module.exports = {
    ...

    entry: "./src/lib.js",

    ...
};

至於進入點的JS程式寫法,稍候會再提到。

如果要修改輸出的JS檔案的路徑,要在webpack.config.cjs設定檔中,加上output欄位,設定方式如下:

module.exports = {
    ...

    output: {
        path: "/path/to/folder",
        filename: "./path/to/file",
    },

    ...
};

output欄位中的path欄位表示要儲存輸出檔(不限於JS檔)的目錄,注意這個路徑必須要是絕對路徑!而filename欄位則是輸出的JS檔(其它類型的檔案會用其它方式設定)要儲存的路徑,注意這個路徑必須要是相對路徑,才能使輸出檔存在於path欄位的絕對路徑下。當然,如果您想要讓輸出檔放置在Node.js專案的根目錄下的dist目錄的話,可以不必設定path欄位。

例如:

const path = require("node:path");

module.exports = {
    ...

    output: {
        path: path.join(__dirname, "output"),
        filename: "./js/bundle.min.js",
    },

    ...
};

以上的__dirname即表示目前執行的JS檔案(此處即webpack.config.cjs)的所在目錄之絕對路徑(此處即Node.js專案的根目錄)。

如果要設定多個進入點,entry欄位和output欄位的設定方式如下:

const path = require("node:path");

module.exports = {
    ...

    entry: {
        "name-1": "./path/to/js-file-1",
        "name-2": "./path/to/js-file-2",
        "name-3": "./path/to/non-js-file-2",
    },
    output: {
        path: path.join(__dirname, "output"),
        filename: "./js/[name].min.js",
    },

    ...
};

filename欄位的字串中加入[name],可以讓Webpack自動用進入點的名稱來取代它。如果entry欄位有設定多個進入點,但是沒有設定output欄位的話,Webpack預設的output欄位值如下:

const path = require("node:path");

module.exports = {
    ...

    output: {
        path: path.join(__dirname, "dist"),
        filename: "./[name].js",
    },

    ...
};

如果網頁的靜態資源有需要使用CDN(Content Delivery Network),output欄位值可以再加上publicPath欄位,來替輸出檔的網址連結加上前綴,如此一來在HTML或是CSS中有用到的靜態資源網址連結就可以被取代為CDN的網址連結。

例如:

const path = require("node:path");

module.exports = {
    ...

    output: {
        path: path.join(__dirname, "output"),
        publicPath: "https://cdn.example.com/",
        filename: "./js/bundle.min.js",
    },

    ...
};

為了確保HTML或是CSS的版本和其連結到的靜態資源的版本是一樣的,我們可以在pathpublicPath欄位值的字串中使用[fullhash],讓Webpack自動用專案的雜湊值來取代它。

例如:

const path = require("node:path");

module.exports = {
    ...

    output: {
        path: path.join(__dirname, "output", "[fullhash]"),
        publicPath: "https://cdn.example.com/[fullhash]/",
        filename: "./js/bundle.min.js",
    },

    ...
};

在打包前先清空目錄

Webpack在打包專案前預設並不會先將輸出目錄清空,因此一不小心就會留有前次產生出來但現在已經沒有用到的檔案。若要讓Webpack將輸出目錄清空,可以將output欄位的clean屬性設為true

如下:

module.exports = {
    ...

    output: {
        ...

        clean: true,

        ...
    },

    ...
};

打包JavaScript檔案

Babel

現在開發JavaScript程式大多是靠Node.js,Node.js能夠支援的ECMAScript版本很新(可以查看這個網頁),但是主流的網頁瀏覽器對於比較新的ECMAScript語法並沒有完全支援(可以查看這個網頁)。所以為了使Node.js開發的JavaScript程式能夠在各大網頁瀏覽器上正常運行,我們需要使用「Babel」這套工具來轉換JS語法。

在終端機執行以下指令來安裝babel-loader@babel/core@babel/register@babel/preset-env這四個套件。

npm i -D babel-loader @babel/core @babel/register @babel/preset-env

webpack.config.cjs設定檔中,於module欄位的rules陣列內,加入Babel的載入器(loader)規則。Webpack會去讀取進入點的JS檔案所require或是import的檔案,以及這些檔案再引用的檔案(例如被引用的JS檔案又去引用其它的JS檔案,或是被引用的SCSS檔案又去引用其它的SCSS檔案,又或是被引用的CSS檔案又去引用其它的圖片檔案),這些靜態資源的檔案路徑,除了JS檔案外,都必須要在rules陣列中被匹配到,並指派正確的載入器來負責處理,否則Webpack就無法成功打包專案。

雖然JS檔可不必寫在rules陣列中來匹配,但是如果我們要指定別的載入器就得要寫了。Babel的載入器規則寫法如下:

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.js$/i,
                use: {
                    loader: "babel-loader",
                    options: { presets: ["@babel/preset-env"] },
                },
            },

            ...
        ]
    },

    ...
};

混淆與最小化JS程式

為了避免我們辛苦開發的JavaScript程式碼被輕易盜用,同時也希望Webpack打包出來的JS檔案可以愈小愈好,那就要替Webpack加上terser-webpack-plugin外掛。Webpack預設就有啟用這個外掛,如果我們要更改它的設定值,就要手動在webpack.config.cjs設定檔中將其require進來,不過還要先執行以下指令將terser-webpack-plugin加入devDependencies

npm i -D terser-webpack-plugin

optimization欄位中,其底下還有個minimizer欄位,值為一個陣列,接著將terser-webpack-plugin的實體加進minimizer欄位的陣列中。

如下:

const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
    ...

    optimization: {
        minimizer: [
            new TerserPlugin({
                extractComments: false,
                terserOptions: { format: { comments: false } },
            }),
        ]
    },

    ...
};

以上設定可以讓npm run build:webpack指令所輸出的JS檔案中完全沒有註解。

打包任意檔案

將檔案引用至JavaScript中

JavaScript原先的require函數和import關鍵字只能夠引用JavaScript程式,但是在Webpack的框架下,它們要引用什麼樣的類型的檔案都是可以的。因此如果要打包非JS檔案的話,只要在進入點的JS檔案中,加上如以下的程式即可:

import './path/to/file';

檔案載入器

前面有提到過,靜態資源的檔案路徑,除了JS檔案外,都必須要在webpack.config.cjs設定檔中的rules陣列內被匹配到,並指派正確的載入器來負責處理,否則Webpack就無法成功打包專案。

Webpack內建檔案載入器,稱為「資產模組」(Asset Module),分為如下幾種類型:

  • asset/resource:在輸出目錄產生引用到的檔案並修改URL指到那個檔案。
  • asset/inline:將檔案內容轉成Data URI,嵌入至程式碼中。
  • asset/source:將檔案內容直接嵌入至程式碼中。
  • asset:根據檔案大小自動選擇要用asset/inline還是asset/resource來打包檔案。

webpack.config.cjs設定檔中,於module欄位的rules陣列內,加入資產規則。若是要載入如SVG、JPEG、PNG、GIF、WEBP等網頁常用的圖片格式,以及EOT、WOFF、WOFF2、TTF、OTF、SVG等網頁常用的字型格式的話,規則如下:

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.(eot|woff|woff2|[ot]tf)$/,
                type: "asset/resource",
                generator: { filename: "fonts/[name][ext]" },
            },
            {
                test: /.*font.*\.svg$/,
                type: "asset/resource",
                generator: { filename: "fonts/[name][ext]" },
            },
            {
                test: /^(?!.*font).*\.svg$/,
                type: "asset/resource",
                generator: { filename: "images/[name][ext]" },
            },
            {
                test: /\.(jpe?g|png|gif|webp)$/,
                type: "asset/resource",
                generator: { filename: "images/[name][ext]" },
            },

            ...
        ]
    },

    ...
};

如果想保留靜態資源檔案的檔名和副檔名,就如上面規則中,把generatorfilename欄位值設為xxx/[name][ext]。如果不設定filename欄位,就會以雜湊值當作是輸出檔名,將檔案輸出到輸出目錄底下。

打包SCSS/CSS檔案

SCSS是用來產生CSS的程式,提供比CSS還要更方便的語法結構,也相容原本的CSS。若您原先只用CSS,在使用Webpack時改用SCSS,可以讓CSS變得容易維護許多。SCSS(Sassy CSS)是Sass提供的一種新格式,而Sass原本在用的舊格式也稱為Sass。

Sass/SCSS的官方網站:

為了能夠讓Webpack在打包時候去解析SCSS/CSS檔案,我們不能直接指派檔案載入器來處理SCSS/CSS檔案。

Sass/SCSS/CSS載入器

sass-loader是一個能夠讀取Sass、SCSS和CSS檔案的載入器,檔案在被讀取後會被轉成CSS格式,可以在終端機執行以下指令來安裝。

npm i -D sass-loader node-sass

webpack.config.cjs設定檔中,於module欄位的rules陣列內,加入Sass/SCSS/CSS載入器的規則。如下:

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.(sa|sc|c)ss$/i,
                use: ["sass-loader"],
            },

            ...
        ]
    },

    ...
};

在這邊的規則中,use欄位值使用了陣列結構,表示要使用多個載入器。接下來繼續再介紹更多相關的載入器。

PostCSS載入器

postcss-loader是一個能對CSS做前處理的載入器。autoprefixerpostcss-loader的外掛,可以自動添加所有網頁瀏覽器的CSS前綴。cssnano也是postcss-loader的外掛,可以最小化CSS。在終端機執行以下指令來安裝這些套件:

npm i -D postcss postcss-loader autoprefixer cssnano

webpack.config.cjs設定檔中,於sass-loader之上,加入PostCSS載入器。如下:

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.(sa|sc|c)ss$/i,
                use: [
                    {
                        loader: "postcss-loader",
                        options: {
                            postcssOptions: {
                                plugins: [
                                    require("autoprefixer"),
                                    require("cssnano")({ preset: ["default", { discardComments: { removeAll: true } }] }),
                                ],
                            },
                        },
                    },
                    "sass-loader",
                ]
            },

            ...
        ]
    },

    ...
};

關於autoprefixer的作用,這邊再舉個例子。例如以下CSS語法:

.example {
    display: grid;
    transition: all .5s;
    user-select: none;
    background: linear-gradient(to bottom, white, black);
}

經過postcss-loaderautoprefixer處理後會變成:

.example {
    display: grid;
    -webkit-transition: all .5s;
    transition: all .5s;
    -webkit-user-select: none;
       -moz-user-select: none;
        -ms-user-select: none;
            user-select: none;
    background: -webkit-gradient(linear, left top, left bottom, from(white), to(black));
    background: linear-gradient(to bottom, white, black);
}

如此一來就不必太擔心同一個CSS到不同的網頁瀏覽器上顯示出來會有很大的不同啦!

CSS載入器

css-loader才是真正讀取CSS的載入器,它會去尋找CSS中使用@import或是url()引用的其它CSS檔案或是圖片檔案,將它們也丟給Webpack一起打包。在終端機執行以下指令來安裝這個套件:

npm i -D css-loader

webpack.config.cjs設定檔中,於postcss-loader之上,加入CSS載入器。如下:

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.(sa|sc|c)ss$/i,
                use: [
                    "css-loader",
                    {
                        loader: "postcss-loader",
                        options: {
                            postcssOptions: {
                                plugins: [
                                    require("autoprefixer"),
                                    require("cssnano")({ preset: ["default", { discardComments: { removeAll: true } }] }),
                                ],
                            },
                        },
                    },
                    "sass-loader",
                ]
            }

            ...
        ]
    },

    ...
};

輸出CSS

光有CSS載入器的話會把CSS檔案包進JS檔案中一起輸出(若再加上style-loader,就能讓CSS在JS執行之後被注入到HTML中,但筆者不太喜歡這樣的作法),如果要把CSS檔案從JS檔案中分離出來的話,我們還需要加上mini-css-extract-plugin這個外掛,可以在終端機執行以下指令來安裝。

npm i -D mini-css-extract-plugin

webpack.config.cjs設定檔中,將mini-css-extract-plugin的實體加進plugins欄位的陣列中。

如下:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
    ...

    plugins: [
        ...

        new MiniCssExtractPlugin({
            filename: "./path/to/css-file"
        }),

        ...
    ],

    ...
};

filename欄位用來設定CSS檔案的輸出路徑,其設定方式和JS檔案的filename是一樣的。如果不設定filename欄位,預設值會是./[name].css

接著也還需要在webpack.config.cjs設定檔中,於postcss-loader之上,加入mini-css-extract-plugin提供的載入器。

如下:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.(sa|sc|c)ss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    "css-loader",
                    {
                        loader: "postcss-loader",
                        options: {
                            postcssOptions: {
                                plugins: [
                                    require("autoprefixer"),
                                    require("cssnano")({ preset: ["default", { discardComments: { removeAll: true } }] }),
                                ],
                            },
                        },
                    },
                    "sass-loader",
                ]
            }

            ...
        ]
    },

    ...
};

移除無用的CSS

現在開發網頁很少自己從無到有把CSS寫出來,通常會直接套用如Bootstrap等的CSS框架,但是我們不太可能將CSS框架提供的CSS全都應用上,肯定會留有完全沒有被使用到CSS設定。這些沒有用到的CSS在沒有經過特殊處理的情況下也還是會被網頁瀏覽器給讀取,不僅會佔用伺服器流量,網頁的載入時間也會比較長。

為了移除沒有用到的CSS,我們需要安裝purgecss-webpack-plugin外掛,以及方便用來取得檔案路徑的glob套件。可以在終端機執行以下指令來安裝這些套件:

npm i -D purgecss-webpack-plugin glob

webpack.config.cjs設定檔中,將purgecss-webpack-plugin的實體加進plugins欄位的陣列中。

如下:

const path = require("node:path");
const glob = require("glob");
const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");
	 
const PATHS = { views: path.resolve(__dirname, "/path/to/html-or-templates-folder") };

module.exports = {
    ...

    plugins: [
        ...

        new PurgeCSSPlugin({
            paths: glob.globSync(`${PATHS.views}/**/*`, {nodir: true}),
            safelist() {
                return {
                    standard: [],
                    deep: [],
                    greedy: [],
                };
            }
        }),

        ...
    ],

    ...
};

以上程式中,用了PATHS變數來儲存一些固定、可能會被重複使用的路徑。views這個路徑是用來放置HTML網頁檔案或是HTML模板檔案的目錄。在purgecss-webpack-plugin外掛的選項中,paths欄位的值為多個路徑的字串陣列,一個路徑應指向一個純文字檔案,purgecss-webpack-plugin會自動從這些路徑指到的多個純文字檔案中分析出現過的字詞,並記錄下來。如果CSS的元素名稱並沒有出現在這些字詞中,該元素就會被刪除,除非該名稱存在於白名單之中。

purgecss-webpack-plugin的白名單分為三種,用safelist函數來設定,這三種會被同時使用。

首先是standard,這個是很單純的名稱對應。例如:

return {
    standard: ["example"],
    deep: [],
    greedy: [],
};

若輸入的CSS為:

.example {
    width: 100%;
}

.example p {
    font-size: 15px;
}

#example {
    width: 100%;
}

.example-2 {
    width: 100%;
}

經過白名單的檢查之後,只會剩下:

.example {
    width: 100%;
}

#example {
    width: 100%;
}

如果將白名單改為:

return {
    standard: ["example", "p"],
    deep: [],
    greedy: [],
};

繼續使用以上的例子,最後它被保留的CSS元素則為:

.example {
    width: 100%;
}

.example p {
    font-size: 15px;
}

#example {
    width: 100%;
}

再來是第二種白名單deep,這個也是用來對應名稱,但是可以使用正規表示式來匹配元素名稱。例如:

return {
    standard: [],
    deep: [/^example/],
    greedy: [],
};

繼續使用同樣的例子,最後它被保留的CSS元素為:

.example {
    width: 100%;
}

#example {
    width: 100%;
}

.example-2 {
    width: 100%;
}

注意這邊是.example p被刪除了!因為p這個名稱並沒有被匹配到。

如果想要讓.example p也被保留下來,可以使用第三種白名單greedy,用這個白名單的正規表示式匹配成功的元素名稱,其後面的其它所有名稱都會被忽略。

例如把剛才的deep白名單直接換成greedy,繼續使用同樣的例子,最後例子中所有的CSS元素都可以被保留下來。

HTML網頁

Webpack的HTML網頁大致上可以分為兩種,一種是將主要的CSS語法甚至是HTML語法都寫進JavaScript中,而HTML檔案內只有撰寫如下的內容:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Title</title>
        <!-- 可能會有簡單的CSS -->
    </head>
    <body>
        <!-- 可能會有讀取畫面 -->
        <script type="text/javascript" src="./path/to/bundle.min.js"></script>
    </body>
</html>

當網頁瀏覽器開啟這個HTML網頁時,網頁瀏覽器就會下載JavaScript檔案來執行,接著將HTML和CSS注入到HTML網頁中,此時網頁才會顯示出真正的內容。

這種網頁通常會利用大量的JavaScript程式來完成畫面的操作流程,讓使用者在操作網頁時不太需要載入新網頁,但這種網頁在開啟的時候通常會有一個等待時間。這部份的用法可以參考這篇文章:

這篇文章要介紹的是第二種的網頁,就是傳統將HTML語法直接寫在HTML檔案內的作法。例如:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Title</title>
        <script type="text/javascript" src="./path/to/bundle.min.js"></script>
        <link href="./path/to/bundle.min.css" rel="stylesheet">
    </head>
    <body>
        <main class="container">
            <h1 class="mt-5">歡迎</h1>
            <p class="lead">這是我用Webpack做的第一個網頁。</p>
            <p>如果你也有興趣的話可以瀏覽<a href="https://magiclen.org/webpack/">這篇文章</a>。</p>
        </main>
    
        <footer class="footer">
            <div class="container">
                <span class="text-muted">沒有著作權,歡迎分享</span>
            </div>
        </footer>
    </body>
</html>

其實這種網頁,HTML的部份也不太需要用Webpack來打包,不過放進Webpack框架中有個最大的好處,那就是可以讓它被最小化!

最小化HTML網頁

html-webpack-plugin外掛可以幫助我們產生HTML檔案(如剛才提到的第一種網頁),或是將現有的HTML檔案最小化。可以在終端機執行以下指令來安裝這個套件:

npm i -D html-webpack-plugin

webpack.config.cjs設定檔中,將html-webpack-plugin的實體加進plugins欄位的陣列中,有幾個HTML檔案就加幾個html-webpack-plugin的實體。

如下:

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    ...

    plugins: [
        ...

        new HtmlWebpackPlugin({
            template: "./path/to/original-html-file",
            filename: "./path/to/output-html-file",
            minify: {
                collapseBooleanAttributes: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true,
                removeComments: true,
                removeEmptyAttributes: true,
                removeRedundantAttributes: true,
                removeScriptTypeAttributes: true,
                removeStyleLinkTypeAttributes: true,
                minifyCSS: true,
                minifyJS: true,
                sortAttributes: true,
                useShortDoctype: true,
            },
            inject: false,
        }),

        ...
    ]

    ...
};

filename欄位用來設定HTML檔案的輸出路徑,其設定方式和JS檔案的filename是一樣的。

如此一來,經過Webpack打包後就會得到最小化的HTML檔案。

把HTML檔案當作進入點來用

事實上有個html-loader載入器,它可以去解析HTML中有連結用到的靜態資源網址,並且如css-loader載入器一樣可以做到取代網址的動作。換句話說,利用它可以使HTML檔案也能夠作為進入點,去打包其有用到的靜態檔案。不過這個載入器的問題有點多,不去用它還比較省時間。

舉一些例子來說明常見問題

關於libraryTarget

現在Node.js專案中有以下幾個檔案。

export function greet(name) {
    alert(`Hello, ${name}!`);
}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Greeting</title>
        <script src="./js/bundle.min.js"></script>
    </head>
    <body>
        You should see an alert message.
    
        <script>
            greet("Webpack");
        </script>
    </body>
</html>
const TerserPlugin = require("terser-webpack-plugin");
	 
const HtmlWebpackPlugin = require("html-webpack-plugin");
 
module.exports = {
    output: {
        clean: true,
        filename: "./js/bundle.min.js",
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: "./views/index.html",
            filename: "./index.html",
            minify: {
                collapseBooleanAttributes: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true,
                removeComments: true,
                removeEmptyAttributes: true,
                removeRedundantAttributes: true,
                removeScriptTypeAttributes: true,
                removeStyleLinkTypeAttributes: true,
                minifyCSS: true,
                minifyJS: true,
                sortAttributes: true,
                useShortDoctype: true,
            },
            inject: false,
        }),
    ],
    module: {
        rules: [
            {
                test: /\.js$/i,
                use: {
                    loader: "babel-loader",
                    options: { presets: ["@babel/preset-env"] },
                },
            },
        ],
    },
    optimization: {
        minimizer: [
            new TerserPlugin({
                extractComments: false,
                terserOptions: { format: { comments: false } },
            }),
        ]
    },
};

記得用以下指令安裝所需的套件:

npm i -D webpack webpack-cli babel-loader @babel/core @babel/register @babel/preset-env html-webpack-plugin terser-webpack-plugin

在執行npm run build:webpack之後,用網頁瀏覽器開啟dist/index.html。我們預期會看到一個對話框,上面寫著Hello, Webpack,然而實際上並沒有,而且網頁瀏覽器的主控台(console)還跳出錯誤訊息,說是greet函數沒有定義。

這個其實是因為我們在webpack.config.cjsoutput欄位中,並沒有去設定library欄位的值,如果我們將library欄位的值設為lib,如下:

module.exports = {
    ...

    output: {
        ...

        library: "lib",

        ...
    }

    ...
};

就可以將HTML語法改成以下這樣來呼叫到greet函數。

...

    <body>
        ...
    
        <script>
            lib.greet("Webpack");
        </script>
    </body>

...

為什麼會這樣?這是因為若我們沒有在webpack.config.cjsoutput欄位中設定libraryTarget欄位的值,這個值預設為var,在var模式下,用來設定全域變數的library欄位的值若沒有設定,就無法正常使用了,因為沒有一個JS全域變數有儲存到我們的JS模組。

如果要將我們的JS模組指派給網頁瀏覽器下的window物件來儲存的話,可以將libraryTarget欄位的值改為window,此時的library欄位的值就代表我們的JS模組會存在window物件的哪個欄位。例如:

module.exports = {
    ...

    output: {
        ...

        libraryTarget: "window",
        library: "lib"

        ...
    }

    ...
};
...

    <body>
        ...
    
        <script>
            window.lib.greet("Webpack");
        </script>
    </body>

...

不過由於網頁中的JavaScript會自動去看沒有被定義的變數是否是window物件的屬性,所以HTML中的JS程式其實也可以寫成最一開始的模樣:

...

    <body>
        ...
    
        <script>
            lib.greet("Webpack");
        </script>
    </body>

...

libraryTarget欄位的值為window,但沒有設定library欄位的值的話,我們的JS模組內有被暴露(export)的項目會被塞給window物件。如此一來就可以直接在網頁中呼叫greet函數了!

...

    <body>
        ...
    
        <script>
            greet("Webpack");
        </script>
    </body>

...

不過最多人喜歡使用的libraryTarget欄位值應該是umd,它除了有支援root(即windowglobal,要看webpack.config.cjstarget是設為Node.js還是網頁瀏覽器,預設為web,即網頁瀏覽器)外,還支援AMD(Asynchronous Module Definition,用define來引用模組;用return來暴露)、CommonJS(用require來引用模組;用module.exportsexports來暴露)等JS模組化的機制。

如果library欄位值為lib,它的JS輸出格式會像是這樣:

(function webpackUniversalModuleDefinition(root, factory) {
	if(typeof exports === 'object' && typeof module === 'object') // CommonJS
		module.exports = factory();
	else if(typeof define === 'function' && define.amd)           // AMD
		define([], factory);
	else if(typeof exports === 'object')                          // CommonJS
		exports["lib"] = factory();
	else                                                          // root
		root["lib"] = factory();
})(window /* or global */, ...);

如果library欄位沒有設定,它的JS輸出格式會像是這樣:

(function webpackUniversalModuleDefinition(root, factory) {
	if(typeof exports === 'object' && typeof module === 'object') // CommonJS
		module.exports = factory();
	else if(typeof define === 'function' && define.amd)           // AMD
		define([], factory);
	else {                                                        // root
		var a = factory();
		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
	}
})(window /* or global */, ...);

在Webpack中用Vue.js

Vue.js是一個非常方便、可在前端或後端運作的框架兼模板引擎。

現在Node.js專案中有以下幾個檔案。

import * as Vue from "vue";
	 
const main = {
    data() {
        return { message: "Hello, Webpack!" };
    },
    mounted() {
        alert("Mounted!");
    },
};

export function init() {
    Vue.createApp(main).mount("#main");
}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Greeting</title>
        <script src="./js/bundle.min.js"></script>
    </head>
    <body>
        <div id="main">
            {{message}}
        </div>

        <script>
            init();
        </script>
    </body>
</html>
const TerserPlugin = require("terser-webpack-plugin");

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    output: {
        clean: true,
        filename: "./js/bundle.min.js",
        libraryTarget: "window",
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: "./views/index.html",
            filename: "./index.html",
            minify: {
                collapseBooleanAttributes: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true,
                removeComments: true,
                removeEmptyAttributes: true,
                removeRedundantAttributes: true,
                removeScriptTypeAttributes: true,
                removeStyleLinkTypeAttributes: true,
                minifyCSS: true,
                minifyJS: true,
                sortAttributes: true,
                useShortDoctype: true,
            },
            inject: false,
        }),
    ],
    module: {
        rules: [
            {
                test: /\.js$/i,
                use: {
                    loader: "babel-loader",
                    options: { presets: ["@babel/preset-env"] },
                },
            },
        ],
    },
    optimization: {
        minimizer: [
            new TerserPlugin({
                extractComments: false,
                terserOptions: { format: { comments: false } },
            }),
        ]
    },
};

記得用以下指令安裝所需的套件:

npm i -D webpack webpack-cli babel-loader @babel/core @babel/register @babel/preset-env html-webpack-plugin terser-webpack-plugin

也記得要安裝Vue.js:

npm i vue

在執行npm run build:webpack之後,用網頁瀏覽器開啟dist/index.html。我們會看到一個對話框,上面寫著Mounted!

用Webpack打包Font Awesome

Font Awesome是非常多人用的Icon Font,但是它在Webpack下使用時有個地方需要注意,否則無法正常打包。

現在Node.js專案中有以下幾個檔案。

@import "~@fortawesome/fontawesome-free/scss/fontawesome";
@import "~@fortawesome/fontawesome-free/scss/solid";
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Apple</title>
    <link href="./css/bundle.min.css" rel="stylesheet">
</head>
<body>
    <i class="fas fa-apple-alt fa-10x"></i>
</body>
</html>
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    entry: "./src/index.scss",
    output: {
        clean: true,
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: "./views/index.html",
            filename: "./index.html",
            minify: {
                collapseBooleanAttributes: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true,
                removeComments: true,
                removeEmptyAttributes: true,
                removeRedundantAttributes: true,
                removeScriptTypeAttributes: true,
                removeStyleLinkTypeAttributes: true,
                minifyCSS: true,
                minifyJS: true,
                sortAttributes: true,
                useShortDoctype: true,
            },
            inject: false,
        }),
        new MiniCssExtractPlugin({ filename: "./css/bundle.min.css" }),
    ],
    module: {
        rules: [
            {
                test: /\.(sa|sc|c)ss$/i,
                use: [
                    MiniCssExtractPlugin.loader,
                    "css-loader",
                    {
                        loader: "postcss-loader",
                        options: {
                            postcssOptions: {
                                plugins: [
                                    require("autoprefixer"),
                                    require("cssnano")({ preset: ["default", { discardComments: { removeAll: true } }] }),
                                ],
                            },
                        },
                    },
                    "sass-loader",
                ],
            },
            {
                test: /\.(eot|woff|woff2|[ot]tf)$/,
                type: "asset/resource",
                generator: { filename: "fonts/[name][ext]" },
            },
            {
                test: /.*font.*\.svg$/,
                type: "asset/resource",
                generator: { filename: "fonts/[name][ext]" },
            },
        ],
    },
};

記得用以下指令安裝所需的套件:

npm i -D webpack webpack-cli sass-loader node-sass postcss-loader autoprefixer cssnano css-loader mini-css-extract-plugin html-webpack-plugin

也記得要安裝Font Awesome:

npm i -D @fortawesome/fontawesome-free

但在執行npm run build:webpack時,會出現如下的錯誤訊息:

ModuleNotFoundError: Module not found: Error: Can't resolve '../webfonts/fa-solid-900.eot'

這個是因為Sass/SCSS在使用@import關鍵字引用其它Sass/SCSS檔案(不包含CSS檔案)時,貌似是直接拿目標檔案的內容來取代掉那行@import關鍵字,所以如果目標檔案的內容有用到相對路徑就會出問題。解決的方法有兩種,第一種就是改成去@importCSS檔案(如果有的話),如下:

@import "~@fortawesome/fontawesome-free/css/fontawesome.css";
@import "~@fortawesome/fontawesome-free/css/solid.css";

第二種就是去設定Sass/SCSS中儲存著路徑前綴的變數,以Font Awesome來說,這個變數為$fa-font-path。如下:

$fa-font-path: "~@fortawesome/fontawesome-free/webfonts/";
@import "~@fortawesome/fontawesome-free/scss/fontawesome";
@import "~@fortawesome/fontawesome-free/scss/solid";

改寫之後npm run build:webpack就可以正常被執行。用網頁瀏覽器開啟dist/index.html,就可以看到大大的蘋果圖示。