# HTML处理

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

# html-webpack-plugin

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

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

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

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

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

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

image-20210805233522066

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

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"><script defer src="main.js"></script></head>
  <body>
  </body>
</html>

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

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

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

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

形成出来的foo.html的文件如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>hello</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"><script defer src="main.js"></script></head>
  <body>
  </body>
</html>

# Template

虽然可以简单的修改 Title 这里自定义内容,但是对于日常项目来说,这远远不够。

希望 HTML 页面需要根据的意愿来生成,也就是说内容是来定的,甚至根据打包的 entry 最后结果来定,这时候就需要使用html-webpack-plugintemplate功能了。

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

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

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

<!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 更改如下:

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

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

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

<!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>
  <script defer src="main.js"></script></head>
  <body>
    <h1>hello world</h1>
    <div id="app"></div>
  </body>
</html>

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

# 使用JS模板引擎

HTML 毕竟还是有限,这时候还可以使用 JavaScript 模板引擎来创建html-webpack-plugin的 Template 文件。

下面我以pug (opens new window)模板引擎为例,来说明下怎么使用模板引擎文件的 Template。

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

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的文件:

footer#footer
  p Copyright @Copyright 2019

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

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 (opens new window),除了这个插件还需要安装html-loader (opens new window)。首先通过npm i -D pug-html-loader html-loader安装它们,然后修改 webpack.config.js 内容,添加 rule:

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内容如下:

<!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 (opens new window)的参数这里就不展开了,可以查阅它的 README 文档。

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

# 多页项目配置

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

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

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

# 多页面问题

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

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

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 即可。

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.jslist.js,希望index.htmlindex.js是一组,list.htmllist.js是一组,那么 webpack.config.js 需要修改为:

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

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

打包出来内容:

image-20210806000721097

# 最佳实践

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

步骤与思路:

  • 约定的目录结构(方便写逻辑代码);
  • 使用js读取目录结构与文件;
  • 创建entries数组;
  • 创建HtmlWebPackPlugin数组;
  • 最后拼接成webpack配置。

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

.
├── dist
│   ├── index.html
│   ├── index.js
│   ├── list.html
│   └── list.js
├── package-lock.json
├── package.json
├── scripts
│   ├── resolve.js
│   └── utils.js
├── src
│   └── pages
├── template
│   ├── index.html
│   └── list.html
└── webpack.config.js

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

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

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

注意:

globby这个库,目前12.x的版本是ES Module的版本,用法发生了重大变化。

import {globby} from 'globby';

const paths = await globby(['*', '!cake']);

console.log(paths);
//=> ['unicorn', 'rainbow']

而上面的代码,可以参考如下命令安装:

npm i globby@11

scripts/resolve.js文件:

const path = require('path')

// 项目根目录
module.exports.resolve = (src) => path.join(path.resolve(__dirname, '../'), src)

// src目录
module.exports.src = (src) => path.join(path.resolve(__dirname, '../src'), src)

scripts/utils.js文件:

const path = require('path');
const globby = require('globby');
const src = require('./resolve').src
const HtmlWebPackPlugin = require('html-webpack-plugin');

const getEntry = (source = './pages', ext='.js') => {
  // 异步方式获取所有的路径
  const res = src(source)
  const paths = globby.sync(res, {
    cwd: src(source)
  });
  const rs = {};
  paths.forEach(v => {
    // 计算 filename
    const name = path.basename(v, ext);
    rs[name] = v;
  });
  return rs;
};

exports.getEntry = getEntry

// 遍历`entry`对象,然后生成 html-webpack-plugins 的数组了
exports.getHtmlWebpackPlugins = () => {
  const entries = getEntry('../template', '.html');
  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引入刚刚写的这个文件,然后:

const { getEntry, getHtmlWebpackPlugins } = require('./scripts/utils')

const entry = getEntry()
console.log('🚀 ~ file: webpack.config.js ~ line 4 ~ entry', entry)

const htmls = getHtmlWebpackPlugins()
console.log('🚀 ~ file: webpack.config.js ~ line 7 ~ htmls', htmls)

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

打包结果:

image-20210806103908100

这里还有一份webpack4中尝试的项目:项目地址 (opens new window)