webpack多页应用架构系列(十二):利用webpack生成HTML普通网页&页面模板

本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000007126268
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang

为什么要用webpack来生成HTML页面

按照我们前面的十一篇的内容来看,自己写一个HTML页面,然后在上面加载webpack打包的js或其它类型的资源,感觉不也用得好好的么?

是的没错,不用webpack用requireJs其实也可以啊,甚至于,传统那种人工管理模块依赖的做法也没有什么问题嘛。

但既然你都已经看到这一篇了,想必早已和我一样,追求着以下这几点吧:

  • 更懒,能自动化的事情绝不做第二遍。
  • 更放心,调通的代码比人靠谱,更不容易出错。
  • 代码洁癖,什么东西该放哪,一点都不能含糊,混在一起我就要死了。

那么,废话不多说,下面就来说说使用webpack生成HTML页面有哪些好处吧。

对多个页面共有的部分实现复用

在实际项目的开发过程中,我们会发现,虽然一个项目里会有很多个页面,但这些页面总有那么几个部分是相同或相似的,尤其是页头页尾,基本上是完全一致的。那我们要怎么处理这些共有的部分呢?

复制粘贴流

不就是复制粘贴的事嘛?写好一份完整的HTML页面,做下个页面的时候,直接copy一份文件,然后直接在copy的文件上进行修改不就好了吗?

谁是这么想这么做的,放学留下来,我保证不打死你!我曾经接受过这么一套系统,顶部栏菜单想加点东西,就要每个页面都改一遍,可维护性烂到爆啊。

Iframe流

Iframe流常见于管理后台类项目,可维护性OK,就是缺陷比较多,比如说:

  • 点击某个菜单,页面是加载出来了但是浏览器地址栏上的URL没变,刷新的话又回到首页了。
  • 搜索引擎收录完蛋,前台项目一般不能用Iframe来布局。
  • 没有逼格,Low爆了,这是最重要的一点(大误)。

SPA流

最近这几年,随着移动互联网的兴起,SPA也变得非常常见了。不过SPA的局限性也非常大,比如搜索引擎无法收录,但我个人最在意的,是它太复杂了,尤其是一些本来业务逻辑就多的系统,很容易懵圈。

后端模板渲染

这倒真是一个办法,只是,需要后端的配合,利用后端代码把页面的各个部分给拼合在一起,所以这方法对前端起家的程序员还是有点门槛的。

利用前端模板引擎生成HTML页面

所谓“用webpack生成HTML页面”,其实也并不是webpack起的核心作用,实际上靠的还是前端的模板引擎将页面的各个部分给拼合在一起来达到公共区域的复用。webpack更多的是组织统筹整个生成HTML页面的过程,并提供更大的控制力。最终,webpack生成的到底是完整的页面,还是供后端渲染的模板,就全看你自己把控了,非常灵活,外人甚至察觉不出来这到底是你自己写的还是代码统一生成的。

处理资源的动态路径

如果你想用在文件名上加hash的方法作为缓存方案的话,那么用webpack生成HTML页面就成为你唯一的选择了,因为随着文件的变动,它的hash也会变化,那么整个文件名都会改变,你总不能在每次编译后都手动修改加载路径吧?还是放心交给webpack吧。

自动加载webpack生成的css、less

如果你使用webpack来生成HTML页面,那么,你可以配置好每个页面加载的chunk(webpack打包后生成的js文件),生成出来的页面会自动用<script>来加载这些chunk,路径什么的你都不用管了哈(当然前提是你配置好了output.publicPath)。另外,用extract-text-webpack-plugin打包好的css文件,webpack也会帮你自动添加到<link>里,相当方便。

彻底分离源文件目录和生成文件目录

使用webpack生成出来的HTML页面可以很安心地跟webpack打包好的其它资源放到一起,相对于另起一个目录专门存放HTML页面文件来说,整个文件目录结构更加合理:

build
  - index
    - index
      - entry.js
      - page.html
    - login
      - entry.js
      - page.html
      - styles.css

如何利用webpack生成HTML页面

webpack生成HTML页面主要是通过html-webpack-plugin来实现的,下面来介绍如何实现。

html-webpack-plugin的配置项

每一个html-webpack-plugin的对象实例都只针对/生成一个页面,因此,我们做多页应用的话,就要配置多个html-webpack-plugin的对象实例:

pageArr.forEach((page) => {
  const htmlPlugin = new HtmlWebpackPlugin({
    filename: `${page}/page.html`,
    template: path.resolve(dirVars.pagesDir, `./${page}/html.js`),
    chunks: [page, 'commons'],
    hash: true, // 为静态资源生成hash值
    minify: true,
    xhtml: true,
  });
  configPlugins.push(htmlPlugin);
});

pageArr实际上是各个chunk的name,由于我在output.filename设置的是'[name]/entry.js',因此也起到构建文件目录结构的效果(具体请看这里),附上pageArr的定义:

module.exports = [
  'index/login',
  'index/index',
  'alert/index',
  'user/edit-password', 'user/modify-info',
];

html-webpack-plugin的配置项真不少,这里仅列出多页应用常用到的配置:

  • filename,生成的网页HTML文件的文件名,注意可以利用/来控制文件目录结构的,其最终生成的路径,是基于webpack配置中的output.path的。
  • template,指定一个基于某种模板引擎语法的模板文件,html-webpack-plugin默认支持ejs格式的模板文件,如果你想使用其它格式的模板文件,那么需要在webpack配置里设置好相应的loader,比如handlebars-loaderhtml-loader啊之类的。如果不指定这个参数,html-webpack-plugin会使用一份默认的ejs模板进行渲染。如果你做的是简单的SPA应用,那么这个参数不指定也行,但对于多页应用来说,我们就依赖模板引擎给我们拼装页面了,所以这个参数非常重要。
  • inject,指示把加载js文件用的<script>插入到哪里,默认是插到<body>的末端,如果设置为'head',则把<script>插入到<head>里。
  • minify,生成压缩后的HTML代码。
  • hash,在html-webpack-plugin负责加载的js/css文件的网址末尾加个URL参数,此URL参数的值是代表本次编译的一个hash值,每次编译后该hash值都会变化,属于缓存解决方案。
  • chunks,以数组的形式指定由html-webpack-plugin负责加载的chunk文件(打包后生成的js文件),不指定的话就会加载所有的chunk。

生成一个简单的页面

下面提供一份供生成简单页面(之所以说简单,是因为不指定页面模板,仅用默认模板)的配置:

var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpackConfig = {
  entry: 'index.js',
  output: {
    path: 'dist',
    filename: 'index_bundle.js'
  },
  plugins: [new HtmlWebpackPlugin(
    title: '简单页面',
    filename: 'index.html',
  )],
};

使用这份配置编译后,会在dist目录下生成一个index.html,内容如下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>简单页面</title>
  </head>
  <body>
    <script src="index_bundle.js"></script>
  </body>
</html>

由于没有指定模板文件,因此生成出来的HTML文件仅有最基本的HTML结构,并不带实质内容。可以看出,这更适合React这种把HTML藏js里的方案。

利用模板引擎获取更大的控制力

接下来,我们演示如何通过制定模板文件来生成HTML的内容,由于html-webpack-plugin原生支持ejs模板,因此这里也以ejs作为演示对象:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" /> 
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <h1>这是一个用<b>html-webpack-plugin</b>生成的HTML页面</h1>
    <p>大家仔细瞧好了</p>
  </body>
</html>

'html-webpack-plugin'的配置里也要指定template参数:

var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpackConfig = {
  entry: 'index.js',
  output: {
    path: 'dist',
    filename: 'index_bundle.js'
  },
  plugins: [new HtmlWebpackPlugin(
    title: '按照ejs模板生成出来的页面',
    filename: 'index.html',
    template: 'index.ejs',
  )],
};

那么,最后生成出来的HTML文件会是这样的:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" /> 
    <title>按照ejs模板生成出来的页面</title>
  </head>
  <body>
    <h1>这是一个用<b>html-webpack-plugin</b>生成的HTML页面</h1>
    <p>大家仔细瞧好了</p>
    <script src="index_bundle.js"></script>
  </body>
</html>

到这里,我们已经可以控制整个HTML文件的内容了,那么生成后端渲染所需的模板也就不是什么难事了,以PHP的模板引擎smarty为例:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" /> 
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <h1>这是一个用<b>html-webpack-plugin</b>生成的HTML页面</h1>
    <p>大家仔细瞧好了</p>
    <p>这是用smarty生成的内容:<b>{$articleContent}</b></p>
  </body>
</html>

处理资源的动态路径

接下来在上面例子的基础上,我们演示如何处理资源的动态路径:

var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpackConfig = {
  entry: 'index.js',
  output: {
    path: 'dist',
    filename: 'index_bundle.[chunkhash].js'
  },
  plugins: [new HtmlWebpackPlugin(
    title: '按照ejs模板生成出来的页面',
    filename: 'index.html',
    template: 'index.ejs',
  )],
  module: {
    loaders: {
      // 图片加载器,雷同file-loader,更适合图片,可以将较小的图片转成base64,减少http请求
      // 如下配置,将小于8192byte的图片转成base64码
      test: /\.(png|jpg|gif)$/,
      loader: 'url?limit=8192&name=./static/img/[hash].[ext]',
    },
  },
};
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" /> 
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <h1>这是一个用<b>html-webpack-plugin</b>生成的HTML页面</h1>
    <p>大家仔细瞧好了</p>
    <img src="<%= require('./imgs/login-bg.jpg')  %>" />
  </body>
</html>

我们改动了什么呢?

  1. 参数output.filename里,我们添了个变量[chunkhash],这个变量的值会随chunk内容的变化而变化,那么,这个chunk文件最终的路径就会是一个动态路径了。
  2. 我们在页面上添加了一个<img>,它的src是require一张图片,相应地,我们配置了针对图片的loader配置,如果图片比较小,require()就会返回DataUrl,而如果图片比较大,则会拷贝到dist/static/img/目录下,并返回新图片的路径。

下面来看看,到底html-webpack-plugin能不能处理好这些动态的路径。

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" /> 
    <title>按照ejs模板生成出来的页面</title>
  </head>
  <body>
    <h1>这是一个用<b>html-webpack-plugin</b>生成的HTML页面</h1>
    <p>大家仔细瞧好了</p>
    ![](http://upload-images.jianshu.io/upload_images/2970956-47399d0cc987233e.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    <script src="index_bundle.c3a064486c8318e5e11a.js"></script>
  </body>
</html>

显然,html-webpack-plugin成功地将chunk加载了,又处理好了转化为DataUrl格式的图片,这一切,都是我们手工难以完成的事情。

还未结束

至此,我们实现了使用webpack生成HTML页面并尝到了它所带来的甜头,但我们尚未实现“对多个页面共有的部分实现复用”,下一节《webpack多页应用架构系列(十三):构建一个简单的模板布局系统》我们就来介绍这部分的内容。

示例代码

诸位看本系列文章,搭配我在Github上的脚手架项目食用更佳哦(笑):Array-Huang/webpack-seedhttps://github.com/Array-Huang/webpack-seed)。

附系列文章目录(同步更新)

本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000007126268
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容