Webpack是JavaScript的模組建置工具,運行在Node.js上,它可以將零散的JavaScript檔案用各式工具優化並打包起來,加快網頁的載入時間。Webpack也並不限於用在JavaScript上,它除了還能打包網頁有用到的靜態資源(如JS、CSS、圖片檔等)外,也還能透過HTML模板來產生HTML網頁。



本篇文章不會介紹用Webpack打包檔案的方式,如果您還不熟悉這部份的話,請先閱讀這篇文章:

https://magiclen.org/webpack/

Webpack的HTML網頁大致上可以分為兩種,一種是傳統將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 role="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打包。

一種是將主要的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注入(inject)到HTML網頁中,此時網頁才會顯示出真正的內容。本篇文章將會介紹多種不同的HTML模板引擎來應用這種方式產生出HTML網頁。

Handlebars

Handlebars是個HTML模板引擎,被廣泛用於JavaScript生態圈,由於它相容於原本的HTML,而且又多了模板引用機制(partial),因此非常適合拿來切版。

Handlebars的官方網站:

https://handlebarsjs.com/

這篇文章介紹過的Webpack基本用法,這邊就不再詳細說明了。現在就讓我們把Handlebars模板引擎加進Webpack專案吧!

Handlebars 載入器

為了能夠讓Webpack打包並處理Handlebars的模板檔案,我們需要使用handlebars-loader這個Handlebars載入器。可以在終端機執行以下指令來安裝:

npm i -D handlebars-loader handlebars

webpack.config.js設定檔中,於module欄位的rules陣列內,加入Handlebars載入器的規則。若是要載入副檔名為.hbs.handlebars的檔案的話,規則如下:

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.(hbs|handlebars)$/,
                use: 'handlebars-loader'
            }

            ...
        ]
    }

    ...
};

加入html-webpack-plugin外掛

html-webpack-plugin外掛可以幫助我們產生HTML檔案,或是將現有的HTML檔案最小化。這個在先前的文章中也有介紹過。

可以在終端機執行以下指令來安裝html-webpack-plugin

npm i -D html-webpack-plugin

webpack.config.js設定檔中,將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
        })

        ...
    ]

    ...
};

至於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網頁後,會去讀取並執行./path/to/bundle.min.js,而這個JavaScript程式內就有我們用Handlebars產生出來的HTML語法,其會被JavaScript程式注入到現有的HTML網頁中。

HTML注入

我們必須要自己撰寫HTML注入的JavaScript程式。最基本的方式就是替網頁瀏覽器的document加上DOMContentLoaded事件,並在此事件的回呼(callback)函數中去建立或搜尋要修改的元素,然後修改其擁有的innerHTML欄位值。

例如要將某個Handlebars的模板檔案編譯出來的HTML語法注入到HTML的<body>中,Webpack的JavaScript進入點可以這樣寫:

import template from './path/to/handlebars-file';

document.addEventListener("DOMContentLoaded", () => {
    document.body.innerHTML = template();
});

例如要在HTML的<body>中新增一個id為main<div>,並且把某個Handlebars的模板檔案編譯出來的HTML語法注入到這個<div>中,Webpack的JavaScript進入點可以這樣寫:

import template from './path/to/handlebars-file';

document.addEventListener("DOMContentLoaded", () => {
    let divMain = document.createElement('div');

    divMain.id = 'main';
    divMain.innerText = template();

    document.body.appendChild(divMain);
});

Handlebars的上下文(context)

如果Handlebars的模板有挖空格的話,例如:

Hello, {{something}}!

我們可以在JavaScript程式中,將Handlebars的上下文物件,作為參數傳給引用Handlebars的模板檔案後所得到的函數。如下:

import template from '../views/index.hbs';

document.addEventListener("DOMContentLoaded", () => {
    let context = {
        something: 'world'
    };

    document.body.innerHTML = template(context);
});

更改模板引用(partial)的目錄

如果要使用Handlebars的模板引用機制,預設的partial模板搜尋路徑是在使用這個partial模板的Handlebars模板的同一個目錄下。由於許多人會把partial模板分開來存放,若要修改partial模板的存放目錄,可以在webpack.config.js設定檔的Handlebars載入器規則中,添加query欄位,這個欄位值是一個物件,該物件的partialDirs欄位可以用來設定partial模板的多個存放目錄。

如下:

const path = require('path');

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.(hbs|handlebars)$/,
                use: {
                    loader: 'handlebars-loader',
                    query: {
                        partialDirs: [
                            path.join(__dirname, 'views', 'partials')
                        ]
                    }
                }
            }

            ...
        ]
    }

    ...
};

自訂Handlebars的Helper

handlebars-loader提供了Handlebars的Helper的自動引用機制,並將Helper模組化。如果想使用自訂的Helper,可以在webpack.config.js設定檔的Handlebars載入器規則中,添加query欄位,這個欄位值是一個物件,該物件的helperDirs欄位可以用來設定Helper模組的多個存放目錄。

如下:

const path = require('path');

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.(hbs|handlebars)$/,
                use: {
                    loader: 'handlebars-loader',
                    query: {
                        helperDirs: [
                            path.join(__dirname, 'views', 'helpers')
                        ]
                    }
                }
            }

            ...
        ]
    }

    ...
};

只要存在於Helper目錄中的JavaScript模組,都會被自動引用。Helper模組的實作方式如下:

import Handlebars from 'handlebars/dist/handlebars.runtime';

export default function link(text, url) {
    text = Handlebars.escapeExpression(text);
    url = Handlebars.escapeExpression(url);

    return new Handlebars.SafeString(
        "<a href='" + url + "'>" + text + "</a>"
    );
}

如果將以上模組存成link.js,則這個Helper的名稱為link。這個Helper可以用來將一個文字和一個網址組成擁有超連結的HTML文字。

呼叫方式如下:

My website is {{{link 'MagicLen' 'https://magiclen.org'}}}.

React

React用的JSX是一種將JavaScript程式和HTML語法結合在一起的模板,非常適合用於大型專案。

React的官方網站:

https://reactjs.org/

我們也可以讓Webpack直接支援JSX。不過在這篇文章介紹過的Webpack基本用法,這邊就不再詳細說明了。現在就讓我們把JSX加進Webpack專案吧!

替Babel加上@babel/preset-react套件

在終端機執行以下指令來安裝@babel/preset-react套件:

npm i -D @babel/preset-react

修改webpack.config.js設定檔中的Babel載入器的規則,如下:

module.exports = {
    ...

    module: {
        rules: [
            ...

            {
                test: /\.jsx?$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            '@babel/preset-env',
                            '@babel/preset-react'
                        ]
                    }
                }
            }

            ...
        ]
    }

    ...
};

如此一來就可以直接在.js檔案中撰寫JSX了,或者也可以用.jsx檔案。

安裝React套件

為了要能夠在JSX中使用到React的模組,我們還需要安裝reactreact-dom這兩個套件。指令如下:

npm i --save react react-dom

加入html-webpack-plugin外掛

html-webpack-plugin外掛的加入方式可以參考文章上面的Handlebars部份。

至於HTML的話,React似乎比較習慣弄一個頂層的<div>(id通常會叫作root或是app)來操作底下的DOM,而不是直接去控制<body>

所以HTML網頁可以寫成如下這樣:

<!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8"/>
        <title>Title</title>
    </head>
    <body>
        <div id="root"></div>
        <script type="text/javascript" src="./path/to/bundle.min.js"></script>
    </body>
</html>

JSX的Hello World

我們可以直接在Webpack的進入點試寫一個JSX的Hello World網頁,程式碼如下:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
    <h1>Hello, world!</h1>,
    document.getElementById('root')
);