1. 1. 使用 HTML 插件来做页面展现
    1. 1.0.1. Template
    2. 1.0.2. 使用 JavaScript 模板引擎
  • 2. 多页项目配置
    1. 2.0.1. 多页面问题
    2. 2.0.2. 多入口问题
    3. 2.0.3. 最佳实践
  • 3. 小结
  • 辛苦是获得一切的定律。

                   ——牛顿

    在项目中我们除了需要 JavaScript、CSS 和图片等静态资源,还需要页面来承载这些内容和页面结构,怎么在 Webpack 中处理 HTML。并且我们项目也不仅仅是单页应用(Single-Page Application,SPA),也可能是多页应用,所以我们还需要使用 Webpack 来给多页应用做打包。本小节将讲解这俩问题。

    使用 HTML 插件来做页面展现

    有了 JavaScript 文件,还缺 HTML 页面,要让 Webpack 处理 HTML 页面需要只需要使用html-webpack-plugin插件即可,首先安装它:

    1
    npm i html-webpack-plugin --save-dev

    然后我们修改对应的 webpack.config.js 内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const HtmlWebPackPlugin = require('html-webpack-plugin');

    module.exports = {
    mode: 'development',
    entry: {
    main: './src/index.js'
    },
    plugins: [new HtmlWebPackPlugin()]
    };

    只需要简单配置,执行webpack打包之后,发现 log 中显示,在dist文件夹中生成一个index.html的文件:

    打开后发现 HTML 的内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8" />
    <title>Webpack App</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    </head>

    <body>
    <script src="main.js"></script>
    </body>
    </html>

    除了我们 HTML 外,我们的 entry 也被主动插入到了页面中,这样我们打开index.html就直接加载了main.js了。

    如果要修改 HTML 的title和名称,可以如下配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const HtmlWebPackPlugin = require('html-webpack-plugin');

    module.exports = {
    mode: 'development',
    entry: {
    main: './src/index.js'
    },
    plugins: [new HtmlWebPackPlugin({title: 'hello', filename: 'foo.html'})]
    };

    Template

    虽然我们可以简单的修改 Title 这里自定义内容,但是对于我们日常项目来说,这远远不够。我们希望 HTML 页面需要根据我们的意愿来生成,也就是说内容是我们来定的,甚至根据打包的entry最后结果来定,这时候我们就需要使用html-webpack-plugintemplate功能了。

    比如我在index.js中,给id=”app”的节点添加内容,这时候 HTML 的内容就需要我们自定义了,至少应该包含一个含有id="app"的 DIV 元素:

    1
    <div id="app"></div>

    我们可以创建一个自己想要的 HTML 文件,比如index.html,在里面写上我们想要的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Webpack</title>
    </head>
    <body>
    <h1>hello world</h1>
    <div id="app"></div>
    </body>
    </html>

    把 webpack.config.js 更改如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const HtmlWebPackPlugin = require('html-webpack-plugin');

    module.exports = {
    mode: 'development',
    entry: {
    main: './src/index.js'
    },
    plugins: [
    new HtmlWebPackPlugin({
    template: './src/index.html'
    })
    ]
    };

    这时候,打包之后的 HTML 内容就变成了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Webpack</title>
    </head>
    <body>
    <h1>hello world</h1>
    <div id="app"></div>
    <script src="main.js"></script></body>
    </html>

    也是添加上了main.js内容。

    使用 JavaScript 模板引擎

    HTML 毕竟还是有限,这时候还可以使用 JavaScript 模板引擎来创建html-webpack-plugin的 Template 文件。下面我以pug模板引擎为例,来说明下怎么使用模板引擎文件的 Template。

    首先创建个index.pug文件,内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    doctype html
    html(lang="en")
    head
    title="Hello Pug"
    script(type='text/javascript').
    console.log('pug inline js')
    body
    h1 Pug - node template engine
    #app
    include includes/footer.pug

    如果不理解 Pug 模板的语法,可以简单看下文档,我这里简单解释下,首先在头部加了 title 和一个script标签,然后在 body 中内容为 h1、id="app"的 div 和引入(include)了一个 footer.pug的文件:

    1
    2
    footer#footer
    p Copyright @Copyright 2019

    这时候我们需要修改 webpack.config.js 内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const HtmlWebPackPlugin = require('html-webpack-plugin');

    module.exports = {
    mode: 'development',
    entry: {
    main: './src/index.js'
    },
    plugins: [
    new HtmlWebPackPlugin({
    template: './src/index.pug'
    })
    ]
    };

    但是只修改template='src/index.pug'是不够的,因为.pug这样的文件 Webpack 是不会解析的,所以我们需要加上 Pug 的 loader:pug-html-loader,除了这个插件还需要安装html-loader。首先通过npm i -D pug-html-loader html-loader安装它们,然后修改 webpack.config.js 内容,添加 rule:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const HtmlWebPackPlugin = require('html-webpack-plugin');

    module.exports = {
    mode: 'development',
    entry: {
    main: './src/index.js'
    },
    plugins: [
    new HtmlWebPackPlugin({
    template: './src/index.pug'
    })
    ],
    module: {
    rules: [{test: /\.pug$/, loader: ['html-loader', 'pug-html-loader']}]
    }
    };

    最后,我们得到的index.html内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <title>Hello Pug</title>
    <script type="text/javascript">
    console.log('pug inline js');
    </script>
    </head>
    <body>
    <h1>Pug - node template engine</h1>
    <div id="app"></div>
    <footer id="footer"><p>Copyright @Copyright 2019</p></footer>
    <script src="main.js"></script>
    </body>
    </html>

    Pug 引擎被转换成 HTML代码,里面包含了:main.jsfooter.pug的内容。

    关于html-webpack-plugin的参数这里就不展开了,可以查阅它的 README 文档。

    Tips: 使用 JavaScript 模板引擎,还可以定义一些变量,通过 html-webpack-plugin 传入进去。

    多页项目配置

    要做一个多页项目的配置,那么需要考虑以下几个问题:

    1. 多页应用,顾名思义最后我们打包生成的页面也是多个,即 HTML 是多个;
    2. 多页应用不仅仅是页面多个,入口文件也是多个;
    3. 多页应用可能页面之间页面结构是不同的,比如一个网站项目,典型的三个页面是:首页、列表页和详情页,肯定每个页面都不一样。

    下面我们来一个一个的问题解决:

    多页面问题

    多页面就是指的多个 HTML 页面,这时候可以直接借助 html-webpack-plugin 插件来实现,我们只需要多次实例化一个 html-webpack-plugin 的实例即可,例如:

    下面是同一个 template,那么可以只修改filename输出不同名的 HTML 即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const HtmlWebPackPlugin = require('html-webpack-plugin');

    const indexPage = new HtmlWebPackPlugin({
    template: './src/index.html',
    filename: 'index.html'
    });
    const listPage = new HtmlWebPackPlugin({
    template: './src/index.html',
    filename: 'list.html'
    });
    module.exports = {
    mode: 'development',
    entry: {
    main: './src/index.js'
    },
    plugins: [indexPage, listPage]
    };

    对于页面结构不同的 HTML 页面的配置,使用不同的 template 即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const HtmlWebPackPlugin = require('html-webpack-plugin');

    const indexPage = new HtmlWebPackPlugin({
    template: './src/index.html',
    filename: 'index.html'
    });
    const listPage = new HtmlWebPackPlugin({
    template: './src/list.html',
    filename: 'list.html'
    });
    module.exports = {
    mode: 'development',
    entry: {
    main: './src/index.js'
    },
    plugins: [indexPage, listPage]
    };

    多入口问题

    上面的多页面解决是多次实例化 html-webpack-plugin,根据传入的参数不同(主要是 filename 不同),打包出两个文件,但是这两个文件的特点是引入的 JavaScript 文件都是一样的,即都是main.js。

    对于多入口,并且入口需要区分的情况,那么需要怎么处理呢?

    这时候就需要借助 html-webpack-plugin 的两个参数了:chunksexcludeChunks。chunks是当前页面包含的 chunk 有哪些,可以直接用 entry 的key来命名,excludeChunks则是排除某些 chunks。

    例如,现在有两个 entry,分别是index.js和list.js,我们希望index.html跟index.js是一组,list.html跟list.js是一组,那么 webpack.config.js 需要修改为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const HtmlWebPackPlugin = require('html-webpack-plugin');

    module.exports = {
    mode: 'development',
    entry: {
    index: './src/index.js',
    list: './src/list.js'
    },
    plugins: [
    new HtmlWebPackPlugin({template: './src/index.html', filename: 'index.html', chunks: ['index']}),
    new HtmlWebPackPlugin({template: './src/list.html', filename: 'list.html', chunks: ['list']})
    ]
    };

    最佳实践

    现在说下我在项目中的一般做法,个人认为这是多页应用的最佳实践。

    首先,需要规定下目录结构规范,一般我们项目会有下面的目录规范:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ├── package.json
    ├── webpack.config.json
    ├── src
    │ ├── libs
    │ └── pages
    │ ├── detail.js
    │ ├── index.js
    │ └── list.js
    ├── template
    │ ├── detail.html
    │ ├── index.html
    │ └── list.html

    保证 template 和实际的 entry 是固定的目录,并且名字都是对应的。

    这时候我们可以写个 Node.js 代码遍历对应的路径,然后生成 webpack.config.jsentry和html-webpack-plugin内容。

    这里我使用了globby这个 NPM 模块,先写了读取src/pages/*.js的内容,然后生成entry:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    const path = require('path');
    const globby = require('globby');

    const getEntry = (exports.getEntry = () => {
    // 异步方式获取所有的路径
    const paths = globby.sync('./pages/*.js', {
    cwd: path.join(__dirname, './src')
    });
    const rs = {};
    paths.forEach(v => {
    // 计算 filename
    const name = path.basename(v, '.js');
    let p = path.join('./src', v);
    if (!p.startsWith('.')) {
    // 转成相对地址
    p = './' + p;
    }

    rs[name] = p;
    });
    return rs;
    });

    // 输出内容
    console.log(getEntry());
    下一步就是遍历entry对象,然后生成 html-webpack-plugins 的数组了:

    const HtmlWebPackPlugin = require('html-webpack-plugin');

    exports.getHtmlWebpackPlugins = () => {
    const entries = getEntry();
    return Object.keys(entries).reduce((plugins, filename) => {
    plugins.push(
    new HtmlWebPackPlugin({
    template: entries[filename],
    filename: `${filename}.html`,
    chunks: [filename]
    })
    );
    return plugins;
    }, []);
    };

    我们在 webpack.config.js 用的时候,直接require引入刚刚写的这个文件,然后:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const {getEntry, getHtmlWebpackPlugins} = require('./scripts/utils');

    module.exports = {
    mode: 'development',
    getEntry(),
    plugins: [
    //...
    ...getHtmlWebpackPlugins()
    ]
    };

    小结

    我们写的代码最终还是需要页面来承载展现,本小节主要介绍 Webpack 的 html-webpack-plugin 插件的使用方法。通过 html-webpack-plugin 我们可以生成包含 Webpack 打包后资源的 HTML 页面。针对 Webpack 中多页应用的打包,我们可以配置多个 html-webpack-plugin 插件实例。

    我们还可以按照文章介绍的多页应用最佳实践的方案,通过约定目录规范来通过 Node.js 代码来自动生成 Webpack 的多页应用配置。html-webpack-plugin 是 Webpack 中很重要的一个插件,基于这个插件的 API 我们可以做很多跟页面相关的优化项目,比如预取资源、实现 modern 打包等,后面的实战章节会继续介绍。

    本小节 Webpack 相关面试题:

    • 怎么配置 Webpack 的多页面开发?
    • 你们项目中 Webpack 的多页面开发有什么最佳实践吗?