webpack多页应用架构专题系列 4

2024-02-20 01:58

本文主要是介绍webpack多页应用架构专题系列 4,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

第四章:webpack的进阶应用

如何打造一个自定义的bootstrap?

前言

一般我们用bootstrap呐,都是用的从官网或github下载下来build好了的版本,千人一脸呐多没意思。当然,官网也给我们提供了自定义的工具,如下图所示,但每次要改些什么就要重新在官网上打包一份,而且还是个国外的网站,甭提有多烦躁了。

bootstrap官网 - 自定义打包

那么,有没有办法让我们随时随地都能根据业务的需要来自定义bootstrap呢?答案自然是肯定的,webpack有啥干不了的呀(大误)[手动滑稽]

sass/less的两套方案

bootstrap主要由两部分组成:样式和jQuery插件。这里要说的是样式,bootstrap有less的方案,也有sass的方案,因此,也存在两个loader分别对应这两套方案:less <=> bootstrap-webpack 和 sass <=> bootstrap-loader 。

我个人惯用的是less,因此本文以bootstrap-webpack为例来介绍如何打造一个自定义的bootstrap。

开工了!

先引入全局的jQuery

众所周知,bootstrap这货指明是要全局的jQuery的,甭以为现在用webpack打包的就有什么突破了。引入全局jQuery的方法请看这篇文章《老式jQuery插件还不能丢,怎么兼容?》(ProvidePlugin + expose-loader),我的脚手架项目Array-Huang/webpack-seed也是使用的这套方案。

如何加载bootstrap配置?

bootstrap-webpack提供一个默认选配下的bootstrap,不过默认的我要你何用(摔

好,言归正题,我们首先需要新建两个配置文件bootstrap.config.jsbootstrap.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?

上文提到有两个配置文件,bootstrap.config.jsbootstrap.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是一个庞大的库,每次编译都会耗费不少的时间,如果只是编译一次也就算了,每次都要耗这时间那可真恶心呢。所以,我打算折腾一下看能不能有所改进,在这里先记录下原始的方案,后面如果真能改进会继续写文的了哈。

预打包Dll,实现webpack音速编译

前言

书承上文《如何打造一个自定义的bootstrap》。

上文说到我们利用webpack来打包一个可配置的bootstrap,但文末留下一个问题:由于bootstrap十分庞大,因此每次编译都要耗费大部分的时间在打包bootstrap这一块,而换来的仅仅是配置的便利,十分不划算。

我也并非是故意卖关子,这的确是我自己开发中碰到的问题,而在撰写完该文后,我立即着手探索解决之道。终于,发现了webpack这一大杀器:DllPlugin&DllReferencePlugin,打包时间过长的问题得到完美解决。

解决方案的机制和原理

DllPlugin&DllReferencePlugin这一方案,实际上也是属于代码分割的范畴,但与CommonsChunkPlugin不一样的是,它不仅仅是把公用代码提取出来放到一个独立的文件供不同的页面来使用,它更重要的一点是:把公用代码和它的使用者(业务代码)从编译这一步就分离出来,换句话说,我们可以分别来编译公用代码和业务代码了。这有什么好处呢?很简单,业务代码常改,而公用代码不常改,那么,我们在日常修改业务代码的过程中,就可以省出编译公用代码那一部分所耗费的时间了(是不是马上就联想到坑爹的bootstrap了呢)。

整个过程大概是这样的:

  1. 利用DllPlugin把公用代码打包成一个“Dll文件”(其实本质上还是js,只是套用概念而已);除了Dll文件外,DllPlugin还会生成一个manifest.json文件作为公用代码的索引供DllReferencePlugin使用。
  2. 在业务代码的webpack配置文件中配置好DllReferencePlugin并进行编译,达到利用DllReferencePlugin让业务代码和Dll文件实现关联的目的。
  3. 在各个页面
    中,先加载Dll文件,再加载业务代码文件。

适用范围

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文件?

编译Dll文件的代码实际上跟编译业务代码是一样的,记得利用--config指定上述专供Dll使用的webpack配置文件就好了:

$ webpack --progress --colors --config ./webpack-dll.config.js

另外,建议可以把该语句写到npm scripts里,好记一点哈。

如何让业务代码关联Dll文件?

我们需要在供编译业务代码的webpack配置文件里设好DllReferencePlugin的配置项:

new webpack.DllReferencePlugin({context: dirVars.staticRootDir, // 指定一个路径作为上下文环境,需要与DllPlugin的context参数保持一致,建议统一设置为项目根目录manifest: require('../../manifest.json'), // 指定manifest.jsonname: 'dll',  // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致
});

配置好DllReferencePlugin了以后,正常编译业务代码即可。不过要注意,必须要先编译Dll并生成manifest.json后再编译业务代码;而以后每次修改Dll并重新编译后,也要重新编译一下业务代码。

如何在业务代码里使用Dll文件打包的module/资源?

不需要刻意做些什么,该怎么require就怎么require,webpack都会帮你处理好的了。

如何整合Dll?

在每个页面里,都要按这个顺序来加载js文件:Dll文件 => CommonsChunkPlugin生成的公用chunk文件(如果没用CommonsChunkPlugin那就忽略啦) => 页面本身的入口文件。

有两个注意事项:

  • 如果你是像我一样利用HtmlWebpackPlugin来生成HTML并自动加载chunk的话,请务必在<head>里手写<script>来加载Dll文件。
  • 为了完全分离源文件和编译后生成的文件,也为了方便在编译前可以清空build目录,不应直接把Dll文件编译生成到build目录里,我建议可以先生成到源文件src目录里,再用file-loader给原封不动搬运过去。

光说不练假把式,来个跑分啊大兄弟!

下面以我的脚手架项目Array-Huang/webpack-seed为例,测试一下(使用开发环境的webpack配置文件webpack.dev.config.js)使用这套Dll方案前后的webpack编译时间:

  • 使用Dll方案前的编译时间为:10秒17
  • 使用Dll方案后的编译时间为:4秒29

由于该项目只是一个脚手架,涉及到的第三方库并不多,我只把jQuery、bootstrap、metisMenu给打包进Dll文件里了,尽管如此,还是差了将近6秒了,相信在实际项目中,这套DllPlugin&DllReferencePlugin的方案能为你省下更多的时间来找女朋友(大误)。

利用webpack生成HTML普通网页&页面模板

为什么要用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><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外,其实还可以使用其它模板引擎(例如jadehandlebarsunderscore),支持的方法是在webpack配置文件中配置好相应的loader即可。

因此,我们可以推理出,html-webpack-plugin其实并不关心你用的是什么模板引擎,只要你的模板最后export出来的是一份完整的HTML代码(字符串)就可以了。于是,我做了一个大胆的尝试,给html-webpack-plugintemplate参数指定一个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的入口文件以及公共chunkhash: 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文件使用。

页面实际内容长啥样?

<!-- 选自webpack-seed/pages/alert/index/content.ejs -->
<div id="page-wrapper"><div class="container-fluid" ><h2 class="page-header"><%= pageTitle %></h2><!-- ...... --></div>
</div>

消息通知页

layout

接着我们来看看整套模板布局系统的核心——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; // 比如说页面名称,会在<title>或面包屑里用到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本身的模板文件长啥样吧:

<!-- 选自webpack-seed/src/public-resource/layout/layout/html.ejs -->
<%= header %>
<div id="wrapper"><%= topNav %><%= sideMenu %><%= content %>
</div>
<%= footer %>

组件

整个页面的公共部分,被我以区域的形式切分成一个一个的组件,下面以页头组件作为例子进行解释:

<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title><% if (pageTitle) { %> <%= pageTitle %> - <% } %> XXXX后台</title><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" /> <meta name="renderer" content="webkit" /><link rel="stylesheet" type="text/css" href="<%= BUILD_FILE.dll.css %>"><script type="text/javascript" src="<%= BUILD_FILE.dll.js %>"></script><!--[if lt IE 10]><script src="<%= BUILD_FILE.js.xdomain %>" slave="<%= SERVER_API_URL %>cors-proxy.html"></script><script src="<%= BUILD_FILE.js.html5shiv %>"></script><![endif]-->
</head>
<body><!--[if lt IE 9]><script src="<%= BUILD_FILE.js.respond %>"></script><![endif]-->

页头组件控制的范围基本上就是整个<head>以及<body>的头部。

不要小看这<body>的头部,由于webpack在使用extract-text-webpack-plugin生成CSS文件并自动加载时,会把<link>放在<head>的最后,而众所周知,实现IE8下Media Queries特性的respond.js是需要放在css后面来加载的,因此,我们就只能把respond.js放到<body>的头部来加载了。

由于我的脚手架项目还是比较简单的,所以这些公共组件的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

总结

有了这套模板布局系统,我们就可以轻松地生成具有相同布局的多个静态页面了,如何管理页面布局公共部分这一多页应用的痛点也就顺利解决了。



这篇关于webpack多页应用架构专题系列 4的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/726690

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

mybatis的整体架构

mybatis的整体架构分为三层: 1.基础支持层 该层包括:数据源模块、事务管理模块、缓存模块、Binding模块、反射模块、类型转换模块、日志模块、资源加载模块、解析器模块 2.核心处理层 该层包括:配置解析、参数映射、SQL解析、SQL执行、结果集映射、插件 3.接口层 该层包括:SqlSession 基础支持层 该层保护mybatis的基础模块,它们为核心处理层提供了良好的支撑。

百度/小米/滴滴/京东,中台架构比较

小米中台建设实践 01 小米的三大中台建设:业务+数据+技术 业务中台--从业务说起 在中台建设中,需要规范化的服务接口、一致整合化的数据、容器化的技术组件以及弹性的基础设施。并结合业务情况,判定是否真的需要中台。 小米参考了业界优秀的案例包括移动中台、数据中台、业务中台、技术中台等,再结合其业务发展历程及业务现状,整理了中台架构的核心方法论,一是企业如何共享服务,二是如何为业务提供便利。

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu1394(线段树点更新的应用)

题意:求一个序列经过一定的操作得到的序列的最小逆序数 这题会用到逆序数的一个性质,在0到n-1这些数字组成的乱序排列,将第一个数字A移到最后一位,得到的逆序数为res-a+(n-a-1) 知道上面的知识点后,可以用暴力来解 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#in

【专题】2024飞行汽车技术全景报告合集PDF分享(附原数据表)

原文链接: https://tecdat.cn/?p=37628 6月16日,小鹏汇天旅航者X2在北京大兴国际机场临空经济区完成首飞,这也是小鹏汇天的产品在京津冀地区进行的首次飞行。小鹏汇天方面还表示,公司准备量产,并计划今年四季度开启预售小鹏汇天分体式飞行汽车,探索分体式飞行汽车城际通勤。阅读原文,获取专题报告合集全文,解锁文末271份飞行汽车相关行业研究报告。 据悉,业内人士对飞行汽车行业

zoj3820(树的直径的应用)

题意:在一颗树上找两个点,使得所有点到选择与其更近的一个点的距离的最大值最小。 思路:如果是选择一个点的话,那么点就是直径的中点。现在考虑两个点的情况,先求树的直径,再把直径最中间的边去掉,再求剩下的两个子树中直径的中点。 代码如下: #include <stdio.h>#include <string.h>#include <algorithm>#include <map>#

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl