一般我们用bootstrap呐,都是用的从官网或github下载下来build好了的版本,千人一脸呐多没意思。当然,官网也给我们提供了自定义的工具,如下图所示,但每次要改些什么就要重新在官网上打包一份,而且还是个国外的网站,甭提有多烦躁了。
那么,有没有办法让我们随时随地都能根据业务的需要来自定义bootstrap呢?答案自然是肯定的,webpack有啥干不了的呀(大误)[手动滑稽]
bootstrap主要由两部分组成:样式和jQuery插件。这里要说的是样式,bootstrap有less的方案,也有sass的方案,因此,也存在两个loader分别对应这两套方案:less <=> bootstrap-webpack 和 sass <=> bootstrap-loader 。
我个人惯用的是less,因此本文以bootstrap-webpack
为例来介绍如何打造一个自定义的bootstrap。
众所周知,bootstrap这货指明是要全局的jQuery的,甭以为现在用webpack打包的就有什么突破了。引入全局jQuery的方法请看这篇文章《老式jQuery插件还不能丢,怎么兼容?》(ProvidePlugin
+ expose-loader
),我的脚手架项目Array-Huang/webpack-seed也是使用的这套方案。
bootstrap-webpack提供一个默认选配下的bootstrap,不过默认的我要你何用(摔
好,言归正题,我们首先需要新建两个配置文件bootstrap.config.js
和bootstrap.config.less
,并将这俩文件放在同一级目录下(像我就把业务代码里用到的config全部丢到同一个目录里了哈哈哈)。
因为每个页面都需要,也只需要引用一次,因此我们可以找个每个页面都会加载的公共模块(用Array-Huang/webpack-seed来举例就是src/public-resource/logic/common.page.js
,我每个页面都会加载这个js模块)来加载bootstrap:
require('!!bootstrap-webpack!bootstrapConfig'); // bootstrapConfig是我在webpack配置文件中设好的alias,不设的话这里就填实际的路径就好了
上文已经说到,bootstrap-webpack其实就是一个webpack的loader,所以这里是用loader的语法。需要注意的是,如果你在webpack配置文件中针对js文件设置了loader(比如说babel),那么在加载bootstrap-webpack的时候请在最前面加个!!
,表示这个require
语句忽略webpack配置文件中所有loader的配置,还有其它的用法,看自己需要哈:
adding ! to a request will disable configured preLoaders adding !! to a request will disable all loaders specified in the configuration adding -! to a request will disable configured preLoaders and loaders but not the postLoaders
上文提到有两个配置文件,bootstrap.config.js
和bootstrap.config.less
,显然,它们的作用是不一样的。
bootstrap.config.js
bootstrap.config.js
的作用就是配置需要加载哪些组件的样式和哪些jQuery插件,可配置的内容跟官网是一致的,官方给出这样的例子:
module.exports = {
scripts: {
// add every bootstrap script you need
'transition': true
},
styles: {
// add every bootstrap style you need
"mixins": true,
"normalize": true,
"print": true,
"scaffolding": true,
"type": true,
}
};
当时我是一下子懵逼了,就这么几个?完整的例子/文档在哪里?后来终于被我找到默认的配置了,直接拿过来在上面改改就能用了:
var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
styleLoader: ExtractTextPlugin.extract('css?minimize&-autoprefixer!postcss!less'),
scripts: {
transition: true,
alert: true,
button: true,
carousel: true,
collapse: true,
dropdown: true,
modal: true,
tooltip: true,
popover: true,
scrollspy: true,
tab: true,
affix: true,
},
styles: {
mixins: true,
normalize: true,
print: true,
scaffolding: true,
type: true,
code: true,
grid: true,
tables: true,
forms: true,
buttons: true,
'component-animations': true,
glyphicons: false,
dropdowns: true,
'button-groups': true,
'input-groups': true,
navs: true,
navbar: true,
breadcrumbs: true,
pagination: true,
pager: true,
labels: true,
badges: true,
jumbotron: true,
thumbnails: true,
alerts: true,
'progress-bars': true,
media: true,
'list-group': true,
panels: true,
wells: true,
close: true,
modals: true,
tooltip: true,
popovers: true,
carousel: true,
utilities: true,
'responsive-utilities': true,
},
};
这里的scripts
项就是jQuery插件了,而styles
项则是样式,可以分别对照着bootstrap英文版文档来查看。
需要解释的是styleLoader
项,这表示用什么loader来加载bootstrap的样式,相当于webpack配置文件中针对.less
文件的loader配置项吧,这里我也是直接从webpack配置文件里抄过来的。
另外,由于我使用了iconfont作为图标的解决方案,因此就去掉了glyphicons
;如果你要使用glyphicons
的话,请务必在webpack配置中设置好针对各类字体文件的loader配置,否则可是会报错的哦。
bootstrap.config.less
bootstrap.config.less
配置的是less变量,bootstarp官网上也有相同的配置,这里就不多做解释了,直接放个官方例子:
@font-size-base: 24px;
@btn-default-color: #444;
@btn-default-bg: #eee;
需要注意的是,我一开始只用了bootstrap.config.js
而没建bootstrap.config.less
,结果发现报错了,还来建了个空的bootstrap.config.less
就编译成功了,因此,无论你有没有配置less变量的需要,都请新建一个bootstrap.config.less
。
至此,一个可自定义的bootstrap就出炉了,你想怎么折腾都行了,什么不用的插件不用的样式,统统给它去掉,把体积减到最小,哈哈哈。
此方案有个缺点:此方案相当于每次编译项目时都把整个bootstrap编译一遍,而bootstrap是一个庞大的库,每次编译都会耗费不少的时间,如果只是编译一次也就算了,每次都要耗这时间那可真恶心呢。所以,我打算折腾一下看能不能有所改进,在这里先记录下原始的方案,后面如果真能改进会继续写文的了哈。
书承上文《如何打造一个自定义的bootstrap》。
上文说到我们利用webpack来打包一个可配置的bootstrap,但文末留下一个问题:由于bootstrap十分庞大,因此每次编译都要耗费大部分的时间在打包bootstrap这一块,而换来的仅仅是配置的便利,十分不划算。
我也并非是故意卖关子,这的确是我自己开发中碰到的问题,而在撰写完该文后,我立即着手探索解决之道。终于,发现了webpack这一大杀器:DllPlugin
&DllReferencePlugin
,打包时间过长的问题得到完美解决。
DllPlugin
&DllReferencePlugin
这一方案,实际上也是属于代码分割的范畴,但与CommonsChunkPlugin不一样的是,它不仅仅是把公用代码提取出来放到一个独立的文件供不同的页面来使用,它更重要的一点是:把公用代码和它的使用者(业务代码)从编译这一步就分离出来,换句话说,我们可以分别来编译公用代码和业务代码了。这有什么好处呢?很简单,业务代码常改,而公用代码不常改,那么,我们在日常修改业务代码的过程中,就可以省出编译公用代码那一部分所耗费的时间了(是不是马上就联想到坑爹的bootstrap了呢)。
整个过程大概是这样的:
DllPlugin
把公用代码打包成一个“Dll文件”(其实本质上还是js,只是套用概念而已);除了Dll文件外,DllPlugin
还会生成一个manifest.json文件作为公用代码的索引供DllReferencePlugin
使用。DllReferencePlugin
并进行编译,达到利用DllReferencePlugin
让业务代码和Dll文件实现关联的目的。Dll文件里只适合放置不常改动的代码,比如说第三方库(谁也不会有事无事就升级一下第三方库吧),尤其是本身就庞大或者依赖众多的库。如果你自己整理了一套成熟的框架,开发项目时只需要在上面添砖加瓦的,那么也可以把这套框架也打包进Dll文件里,甚至可以做到多个项目共用这一份Dll文件。
我们需要专门为Dll文件建一份webpack配置文件,不能与业务代码共用同一份配置:
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const dirVars = require('./webpack-config/base/dir-vars.config.js'); // 与业务代码共用同一份路径的配置表
module.exports = {
output: {
path: dirVars.dllDir,
filename: '[name].js',
library: '[name]', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致
},
entry: {
/*
指定需要打包的js模块
或是css/less/图片/字体文件等资源,但注意要在module参数配置好相应的loader
*/
dll: [
'jquery', '!!bootstrap-webpack!bootstrapConfig',
'metisMenu/metisMenu.min', 'metisMenu/metisMenu.min.css',
],
},
plugins: [
new webpack.DllPlugin({
path: 'manifest.json', // 本Dll文件中各模块的索引,供DllReferencePlugin读取使用
name: '[name]', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与参数output.library保持一致
context: dirVars.staticRootDir, // 指定一个路径作为上下文环境,需要与DllReferencePlugin的context参数保持一致,建议统一设置为项目根目录
}),
/* 跟业务代码一样,该兼容的还是得兼容 */
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
'window.$': 'jquery',
}),
new ExtractTextPlugin('[name].css'), // 打包css/less的时候会用到ExtractTextPlugin
],
module: require('./webpack-config/module.config.js'), // 沿用业务代码的module配置
resolve: require('./webpack-config/resolve.config.js'), // 沿用业务代码的resolve配置
};
编译Dll文件的代码实际上跟编译业务代码是一样的,记得利用--config
指定上述专供Dll使用的webpack配置文件就好了:
$ webpack --progress --colors --config ./webpack-dll.config.js
另外,建议可以把该语句写到npm scripts
里,好记一点哈。
我们需要在供编译业务代码的webpack配置文件里设好DllReferencePlugin
的配置项:
new webpack.DllReferencePlugin({
context: dirVars.staticRootDir, // 指定一个路径作为上下文环境,需要与DllPlugin的context参数保持一致,建议统一设置为项目根目录
manifest: require('../../manifest.json'), // 指定manifest.json
name: 'dll', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致
});
配置好DllReferencePlugin
了以后,正常编译业务代码即可。不过要注意,必须要先编译Dll并生成manifest.json后再编译业务代码;而以后每次修改Dll并重新编译后,也要重新编译一下业务代码。
不需要刻意做些什么,该怎么require就怎么require,webpack都会帮你处理好的了。
在每个页面里,都要按这个顺序来加载js文件:Dll文件 => CommonsChunkPlugin
生成的公用chunk文件(如果没用CommonsChunkPlugin
那就忽略啦) => 页面本身的入口文件。
有两个注意事项:
HtmlWebpackPlugin
来生成HTML并自动加载chunk的话,请务必在
里手写
来加载Dll文件。file-loader
给原封不动搬运过去。下面以我的脚手架项目Array-Huang/webpack-seed为例,测试一下(使用开发环境的webpack配置文件webpack.dev.config.js
)使用这套Dll方案前后的webpack编译时间:
由于该项目只是一个脚手架,涉及到的第三方库并不多,我只把jQuery、bootstrap、metisMenu给打包进Dll文件里了,尽管如此,还是差了将近6秒了,相信在实际项目中,这套DllPlugin
&DllReferencePlugin
的方案能为你省下更多的时间来找女朋友(大误)。
按照我们前面的十一篇的内容来看,自己写一个HTML页面,然后在上面加载webpack打包的js或其它类型的资源,感觉不也用得好好的么?
是的没错,不用webpack用requireJs其实也可以啊,甚至于,传统那种人工管理模块依赖的做法也没有什么问题嘛。
但既然你都已经看到这一篇了,想必早已和我一样,追求着以下这几点吧:
那么,废话不多说,下面就来说说使用webpack生成HTML页面有哪些好处吧。
在实际项目的开发过程中,我们会发现,虽然一个项目里会有很多个页面,但这些页面总有那么几个部分是相同或相似的,尤其是页头页尾,基本上是完全一致的。那我们要怎么处理这些共有的部分呢?
不就是复制粘贴的事嘛?写好一份完整的HTML页面,做下个页面的时候,直接copy一份文件,然后直接在copy的文件上进行修改不就好了吗?
谁是这么想这么做的,放学留下来,我保证不打死你!我曾经接受过这么一套系统,顶部栏菜单想加点东西,就要每个页面都改一遍,可维护性烂到爆啊。
Iframe流常见于管理后台类项目,可维护性OK,就是缺陷比较多,比如说:
最近这几年,随着移动互联网的兴起,SPA也变得非常常见了。不过SPA的局限性也非常大,比如搜索引擎无法收录,但我个人最在意的,是它太复杂了,尤其是一些本来业务逻辑就多的系统,很容易懵圈。
这倒真是一个办法,只是,需要后端的配合,利用后端代码把页面的各个部分给拼合在一起,所以这方法对前端起家的程序员还是有点门槛的。
所谓“用webpack生成HTML页面”,其实也并不是webpack起的核心作用,实际上靠的还是前端的模板引擎将页面的各个部分给拼合在一起来达到公共区域的复用。webpack更多的是组织统筹整个生成HTML页面的过程,并提供更大的控制力。最终,webpack生成的到底是完整的页面,还是供后端渲染的模板,就全看你自己把控了,非常灵活,外人甚至察觉不出来这到底是你自己写的还是代码统一生成的。
如果你想用在文件名上加hash的方法作为缓存方案的话,那么用webpack生成HTML页面就成为你唯一的选择了,因为随着文件的变动,它的hash也会变化,那么整个文件名都会改变,你总不能在每次编译后都手动修改加载路径吧?还是放心交给webpack吧。
如果你使用webpack来生成HTML页面,那么,你可以配置好每个页面加载的chunk(webpack打包后生成的js文件),生成出来的页面会自动用来加载这些chunk,路径什么的你都不用管了哈(当然前提是你配置好了output.publicPath)。另外,用
extract-text-webpack-plugin
打包好的css文件,webpack也会帮你自动添加到里,相当方便。
使用webpack生成出来的HTML页面可以很安心地跟webpack打包好的其它资源放到一起,相对于另起一个目录专门存放HTML页面文件来说,整个文件目录结构更加合理:
build
- index
- index
- entry.js
- page.html
- login
- entry.js
- page.html
- styles.css
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
的配置项真不少,这里仅列出多页应用常用到的配置:
/
来控制文件目录结构的,其最终生成的路径,是基于webpack配置中的output.path的。html-webpack-plugin
默认支持ejs格式的模板文件,如果你想使用其它格式的模板文件,那么需要在webpack配置里设置好相应的loader,比如handlebars-loader
啊html-loader
啊之类的。如果不指定这个参数,html-webpack-plugin
会使用一份默认的ejs模板进行渲染。如果你做的是简单的SPA应用,那么这个参数不指定也行,但对于多页应用来说,我们就依赖模板引擎给我们拼装页面了,所以这个参数非常重要。
插入到哪里,默认是插到
的末端,如果设置为'head',则把
插入到
里。html-webpack-plugin
负责加载的js/css文件的网址末尾加个URL参数,此URL参数的值是代表本次编译的一个hash值,每次编译后该hash值都会变化,属于缓存解决方案。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,内容如下所示:
<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作为演示对象:
<%= htmlWebpackPlugin.options.title %>
这是一个用html-webpack-plugin生成的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文件会是这样的:
<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-pluginb>生成的HTML页面h1>
<p>大家仔细瞧好了p>
<script src="index_bundle.js">script>
body>
html>
到这里,我们已经可以控制整个HTML文件的内容了,那么生成后端渲染所需的模板也就不是什么难事了,以PHP的模板引擎smarty为例:
<%= htmlWebpackPlugin.options.title %>
这是一个用html-webpack-plugin生成的HTML页面
大家仔细瞧好了
这是用smarty生成的内容:{$articleContent}
接下来在上面例子的基础上,我们演示如何处理资源的动态路径:
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]',
},
},
};
<%= htmlWebpackPlugin.options.title %>
这是一个用html-webpack-plugin生成的HTML页面
大家仔细瞧好了
我们改动了什么呢?
output.filename
里,我们添了个变量[chunkhash],这个变量的值会随chunk内容的变化而变化,那么,这个chunk文件最终的路径就会是一个动态路径了。
,它的src是require一张图片,相应地,我们配置了针对图片的loader配置,如果图片比较小,require()
就会返回DataUrl,而如果图片比较大,则会拷贝到dist/static/img/
目录下,并返回新图片的路径。下面来看看,到底html-webpack-plugin
能不能处理好这些动态的路径。
<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-pluginb>生成的HTML页面h1>
<p>大家仔细瞧好了p>
<img src="data:image/jpeg;base64,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" />
<script src="index_bundle.c3a064486c8318e5e11a.js">script>
body>
html>
显然,html-webpack-plugin
成功地将chunk加载了,又处理好了转化为DataUrl格式的图片,这一切,都是我们手工难以完成的事情。
至此,我们实现了使用webpack生成HTML页面并尝到了它所带来的甜头,但我们尚未实现对多个页面共有的部分实现复用,下一篇《构建一个简单的模板布局系统》我们就来介绍这部分的内容。
上文《利用webpack生成HTML普通网页&页面模板》我们基本上已经搞清楚如何利用html-webpack-plugin
来生成HTML普通网页&页面模板,本文将以我的脚手架项目Array-Huang/webpack-seed
介绍如何在这基础上搭建一套简单的模板布局系统。
上文我们说到,利用模板引擎&模板文件,我们可以控制HTML的内容,但这种控制总体来说还是比较有限的,而且很大程度受限于你对该模板引擎的熟悉程度,那么,有没有更简单的方法呢?
有!我们可以就用我们最熟悉的js来肆意组装、拼接出我们想要的HTML!
首先来看一个上文提到的例子:
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-webpack-plugin
指定一个名为index.ejs
的ejs模板文件,来达到生成HTML页面文件的目的,从html-webpack-plugin
的文档我们可以看出,除了默认支持的ejs外,其实还可以使用其它模板引擎(例如jade
、handlebars
、underscore
),支持的方法是在webpack配置文件中配置好相应的loader即可。
因此,我们可以推理出,html-webpack-plugin
其实并不关心你用的是什么模板引擎,只要你的模板最后export出来的是一份完整的HTML代码(字符串)就可以了。于是,我做了一个大胆的尝试,给html-webpack-plugin
的template
参数指定一个js文件,然后在此js文件末尾export出一份完整的HTML代码来。这个js文件我命名为“模板接口”(上面架构图上有标识),意思是,不是光靠这一个js文件就能形成一份模板,“接口”之后是一套完整的模板布局体系。下面以webpack-seed项目里的src/pages/alert/index
(“消息通知”页)作为例子进行说明。
html-webpack-plugin
配置先来看看我是如何给html-webpack-plugin
指定一个js作为模板的:
/*
这是用来生成alert/index页的HtmlWebpackPlugin配置
在原项目中是循环批量new HtmlWebpackPlugin的,此处为了更容易理解,特别针对alert/index页做了修改
*/
new HtmlWebpackPlugin({
filename: `alert/index/page.html`,
template: path.resolve(dirVars.pagesDir, `./alert/index/html.js`), // 指定为一个js文件而非普通的模板文件
chunks: ['alert/index', 'commons'], // 自动加载上index/login的入口文件以及公共chunk
hash: true, // 为静态资源生成hash值
xhtml: true, // 需要符合xhtml的标准
});
下面来介绍这个作为模板接口的js文件:
/* 选自webpack-seed/pages/alert/index/html.js */
const content = require('./content.ejs'); // 调取存放本页面实际内容的模板文件
const layout = require('layout'); // 调用管理后台内部所使用的布局方案,我在webpack配置里定义其别名为'layout'
const pageTitle = '消息通知'; // 页面名称
// 给layout传入“页面名称”这一参数(当然有需要的话也可以传入其它参数),同时也传入页面实际内容的HTML字符串。content({ pageTitle })的意思就是把pageTitle作为模板变量传给ejs模板引擎并返回最终生成的HTML字符串。
module.exports = layout.init({ pageTitle }).run(content({ pageTitle }));
从代码里我们可以看出,模板接口的作用实际上就是整理好当前页面独有的内容,然后交与layout作进一步的渲染;另一方面,模板接口直接把layout最终返回的结果(完整的HTML文档)给export出来,供html-webpack-plugin
生成HTML文件使用。
<%= pageTitle %>
接着我们来看看整套模板布局系统的核心——layout。layout的主要功能就是接收各个页面独有的参数(比如说页面名称),并将这些参数传入各个公共组件生成各组件的HTML,然后根据layout本身的模板文件将各组件的HTML以及页面实际内容的HTML拼接在一起,最终形成一个完整的HTML页面文档。
/* 选自webpack-seed/src/public-resource/layout/layout/html.js */
const config = require('configModule');
const noJquery = require('withoutJqueryModule');
const layout = require('./html.ejs'); // 整个页面布局的模板文件,主要是用来统筹各个公共组件的结构
const header = require('../../components/header/html.ejs'); // 页头的模板
const footer = require('../../components/footer/html.ejs'); // 页脚的模板
const topNav = require('../../components/top-nav/html.ejs'); // 顶部栏的模板
const sideMenu = require('../../components/side-menu/html.ejs'); // 侧边栏的模板
const dirsConfig = config.DIRS;
/* 整理渲染公共部分所用到的模板变量 */
const pf = {
pageTitle: '',
constructInsideUrl: noJquery.constructInsideUrl,
};
const moduleExports = {
/* 处理各个页面传入而又需要在公共区域用到的参数 */
init({ pageTitle }) {
pf.pageTitle = pageTitle; // 比如说页面名称,会在或面包屑里用到
return this;
},
/* 整合各公共组件和页面实际内容,最后生成完整的HTML文档 */
run(content) {
const headerRenderData = Object.assign(dirsConfig, pf); // 页头组件需要加载css/js等,因此需要比较多的变量
const renderData = {
header: header(headerRenderData),
footer: footer(),
topNav: topNav(pf),
sideMenu: sideMenu(pf),
content,
};
return layout(renderData);
},
};
module.exports = moduleExports;
接下来看看layout本身的模板文件长啥样吧:
<%= header %>
<%= topNav %>
<%= sideMenu %>
<%= content %>
<%= footer %>
整个页面的公共部分,被我以区域的形式切分成一个一个的组件,下面以页头组件作为例子进行解释:
<% if (pageTitle) { %> <%= pageTitle %> - <% } %> XXXX后台
页头组件控制的范围基本上就是整个以及
的头部。
不要小看这的头部,由于webpack在使用
extract-text-webpack-plugin
生成CSS文件并自动加载时,会把放在
的最后,而众所周知,实现IE8下Media Queries特性的
respond.js
是需要放在css后面来加载的,因此,我们就只能把respond.js
放到的头部来加载了。
由于我的脚手架项目还是比较简单的,所以这些公共组件的HTML都是直接根据模板文件来输出的;如果组件本身要处理的逻辑比较多,可以使用跟模板接口一样的思路,利用js文件来拼接。
至于组件本身行为的逻辑(js),可以一并放到各组件的目录里,在公共chunk里调用便是了。本文实际上只关注于如何生成HTML,这里提到这个只是提示一下组件的文件目录结构。
这里稍微再解释一下BUILD_FILE.js.*
和BUILD_FILE.dll.*
是什么,这些其实都是没有用webpack打包起来的js/css,我用file-loader把这些文件从src目录搬到build目录了,这里模板变量输出的都是搬运后的路径,具体请看《听说webpack连图片和字体也能打包?》。启动搬运的代码放在webpack-seed/src/public-resource/config/build-file.config.js
。
有了这套模板布局系统,我们就可以轻松地生成具有相同布局的多个静态页面了,如何管理页面布局公共部分这一多页应用的痛点也就顺利解决了。