一些项目因需求不同,如需SEO或小项目,使用angular、react或vue就大材小用了。可以通过webpack、gulp这些构建工具,也能快速完成html页面开发,并且也能使用less/sass/styus等样式预编译功能,以及将js、html分模块、分组件进行开发。
webpack生成企业站静态页面已经讲了三篇,本该结篇,但这几天又想到以前项目上使用过MVP模式开发,一部分通过后台渲染,一部分前端渲染;这样能减轻后端压力,也能提升开发速度,让前端在写静态页面时候就能完成页面数据渲染工作。有需求就有革新的动力,所以这篇将对之前功能再度升级改造。
当然,这种方法把数据获取到客户端通过art-template模板引擎对数据进行渲染,实现是没问题;但是完全前端进行渲染,这样对SEO优化不太友好,若是不考虑此问题不大;若将head标签和导航等公共部分在服务端渲染,其他内容通过ajax请求在客户端渲染,也能满足SEO优化需求和减轻后端压力。
另外art-template存在在webpack编译环境 和 浏览器的生产环境,两个环境语法标签会存在冲突,在webpack构建生成静态页面时候语法标签就会被解析掉,所以要重点注意一点,虽然这是可以通过art-templte配置来解决。
1.项目环境搭建 地址:Webpack生成企业站静态页面 - 项目搭建-CSDN博客
2. 组件化 地址:Webpack生成企业站静态页面 - 组件化-CSDN博客
3. 增加数据处理能力 地址:Webpack生成企业站静态页面 - 增强数据处理能力-CSDN博客
该篇是和前三篇连贯讲解的,所以从后面开始看的,可能地方不太清楚或运行后报错等,出现这些问题可以翻看下前几篇,这里就不再从零开始讲解项目如何搭建和完善的了。
一、配置修改
首先修改一下webpack.config.js,对外增加暴露art-template插件,在客户端浏览器中运行时,也能使用art-template。
1.1 暴露art-template
这里大家注意的时,添加的是template-web.js,直接用require引用art-template会报错。
入口entry位置添加artTemplate,生成artTemplate.js到dist/js目录中。如下图:
在module的rules中,之前对外暴露jquery位置后面,再增加一个暴露art-template的loader。如下图:
webapck.config.js代码如下:
const { resolve } = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const artTemplate = require('art-template');
const { entrys, staticHtmls, watchHtmls } = require('./static.config');
const htmlData = require('./html.data.json');
// 指定art-template模板路径
artTemplate.defaults.root = resolve(__dirname, './src');
// 指定模板名称
artTemplate.defaults.extname = '.html';
module.exports = {
entry: {
artTemplate: ['art-template/lib/template-web.js'],
jquery: ['jquery'],
...entrys()
},
output: {
filename: 'js/[name].bundle.js', // 对入口名称进行命名,
clean: true // 在生成文件之前清空 output 目录
},
module: {
rules: [
// MiniCssExtractPlugin.loader 提取CSS为独立文件
// css-loader 会对 @import 和 url() 进行处理,就像 js 解析 import/require() 一样。
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
// webpack 将 Less 编译为 CSS 的 loader。
{
test: /\.less$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
},
// 匹配图片资源,指定构建后存储位置和命名
{
test: /\.(png|jpg|jpeg|svg|gif)$/i,
type: 'asset/resource',
generator: {
filename: 'images/[name][ext]'
}
},
// 匹配html页面,并提取内部资源文件
{
test: /\.html$/i,
loader: "html-loader",
exclude: /node_modules/,
options: {
minimize: false, // 不压缩html内容
preprocessor: (content, loaderContext) => {
// 正则达式
let regEx = /<datalist>\s*{\s*[\s\S]*?\s*}\s*<\/datalist>/gi,
// 区域html页面中json数据
htmlResult = content.match(regEx),
// 公共部分json数据
returnData = {...htmlData},
// 临时存储html中读取到的json数据
data = {};
// 判断htmlResult如果不为空,并且数组(match匹配到数据返回为数组格式)则转换字符串内容为json数据
if(null!=htmlResult&&Array.isArray(htmlResult)){
try {
// 去除datalist标签
htmlResult = htmlResult.map(item => {
item = item.replace('<datalist>', '');
item = item.replace('</datalist>', '');
return JSON.parse(item);
});
// 合并数据
htmlResult.forEach(item => {
Object.assign(data, item);
});
// 合并到htmlData数据集中
Object.assign(returnData, data);
} catch (error) {
console.error('html result:', error);
}
// 清除页面中数据
content = content.replace(regEx, '');
}
return artTemplate.compile(content)(returnData);
}
}
},
// 将jquery暴露给全局对象(self、window 和 global)
{
test: require.resolve('jquery'),
loader: "expose-loader",
options: {
exposes: ["$", "jQuery"]
}
},
// 对外暴露art-template
{
test: require.resolve('art-template/lib/template-web.js'),
loader: "expose-loader",
options: {
exposes: ["artTemplate"]
}
}
]
},
plugins: [
...staticHtmls(),
// 提取css文件
new MiniCssExtractPlugin({
filename: 'css/[name]_bundle.css', //输出文件
}),
],
resolve: {
alias: {
// 样式路径目录别名
$css: resolve(__dirname, 'src/css')
}
},
mode: 'production',
devServer: {
static: {
// 指定服务运行目录
directory: resolve(__dirname, 'dist')
},
watchFiles: [...watchHtmls()],
compress: true, //gzip压缩
port: 3000, //指定端口号
open: true, //服务启动后自动在浏览器打开
hot: true //开启热更
}
}
1.2 html引入art-template
要想在客户端浏览器中能调用art-template,还需要做最后一步,前篇中为了方便后期维护,将页面配置信息单独放在static.config.js文件中,需要在这把artTemplate放到chunks中,如下图:
这样再执行webpack命令,artTemplate.bundle.js文件就成自动注入到所有html文件中。
static.config.js文件代码如下:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 引用JS文件
const scripts = ["common", "index", "list", "article", "contact", "message"];
// 需要生成的静态页面
const htmls = [
{name: "index.html", chunks: ['index']},
{name: "list.html", chunks: ['list']},
{name: "article.html", chunks: ['article']},
{name: "contact.html", chunks: ['contact']},
{name: "message.html", chunks: ['message']}
];
/**
* 静态页面配置
*/
module.exports = {
// 重组js入口文件
entrys: () => {
let params = {};
scripts.forEach(name => {
params[name] = resolve(__dirname, `./src/js/${name}.js`);
})
return params;
},
// 重组静态页面文件
staticHtmls: () => htmls.map(item => {
return new HtmlWebpackPlugin({
inject: 'body',
template: resolve(__dirname, `./src/${item.name}`),
filename: item.name,
chunks: ['jquery', 'artTemplate', 'common', ...item.chunks],
minify: false
});
}),
// 监听热更html页面
watchHtmls: () => htmls.map(item => resolve(__dirname, `./src/${item.name}`))
}
二、创建mock数据
第二步,使用mock来模拟后台接口,这里使用梦学谷的mock,地址:Easy Mock。这个平台使用太简单了,就不教大家怎么使用了。现在通过这个模拟我们需要的接口,如下图:
/inviteList 接口返回数据代码如下:
{
"code": 200,
"data": [{
"title": "2015-03-17诚招",
"datetime": "2014-10-2",
"url": "javascript:;"
},
{
"title": "2014-03-12招聘",
"datetime": "2014-03-12",
"url": "javascript:;"
},
{
"title": "2013-03-30诚招学生暑期工",
"datetime": "2014-10-2",
"url": "javascript:;"
}
],
"message": "sccuess"
}
/bottomList 接口返回数据代码如下:
{
"code": 200,
"data": [{
"title": "新春有礼——望玉岛别墅标间推出亲民价,餐饮",
"datetime": "2014-10-2",
"url": "javascript:;"
},
{
"title": "新春有礼——望玉岛别墅标间推出亲民价,餐饮",
"datetime": "2014-10-2",
"url": "javascript:;"
},
{
"title": "新春有礼——望玉岛别墅标间推出亲民价,餐饮",
"datetime": "2014-10-2",
"url": "javascript:;"
},
{
"title": "新春有礼——望玉岛别墅标间推出亲民价,餐饮",
"datetime": "2014-10-2",
"url": "javascript:;"
}
],
"message": "success"
}
/firstList 接口返回数据代码如下:
{
"code": 200,
"data": [{
"title": "望玉岛标题",
"description": " 望玉岛度假村一日游活动方案:活动主题,放飞心情,走进自然,活动地;望玉岛度假村,行程特点;领略望玉岛... ",
"thumb": "images/index_09.jpg",
"url": "javascript:;"
},
{
"title": "望玉岛标题",
"description": " 望玉岛度假村一日游活动方案:活动主题,放飞心情,走进自然,活动地;望玉岛度假村,行程特点;领略望玉岛... ",
"thumb": "images/index_10.jpg",
"url": "javascript:;"
},
{
"title": "望玉岛标题",
"description": " 望玉岛度假村一日游活动方案:活动主题,放飞心情,走进自然,活动地;望玉岛度假村,行程特点;领略望玉岛... ",
"thumb": "images/index_11.jpg",
"url": "javascript:;"
}
],
"message": "success"
}
由于这里是项目演示而模拟的数据,所以好个区域是使用同一份接口数据,就不一一造模拟数据了。
三、ajax封装
这里主要是演示如果通过ajax请求获取数据,再将数据渲染到页面,所以就简单封装并模拟。如果有朋友希望使用axios或其他相关请求工具,可以自行研究。
由于ajax请求所有页面都需要使用到,所以这里将封装内容放到之前创建的common.js文件中,代码如下:
import '$css/common.less';
(function(){
// ajax请求地址
const ajaxBaseUrl = "https://mock.mengxuegu.com/mock/${mock的Project ID}/webpack_ajax_pro";
// 配置art-template语法模板
artTemplate.defaults.rules[1].test = /{%([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*%}/;
/**
* ajax 请求
* @param {*} url
* @param {*} params
* @param {*} type
*/
function ajaxRequest(url, params, type){
return new Promise(function(resolve, reject){
$.ajax({
url: ajaxBaseUrl + url,
type: type,
data: params,
success: function(res){
resolve(res);
},
error: function(e){
reject(e);
}
});
});
}
/**
* 封装Get请求
* @param {*} id
* @param {*} url
* @param {*} params
*/
$.fn.requestGetRender = function(id, url, params){
let that = this;
ajaxRequest(url, params, 'get').then(function(res){
let html = artTemplate(id, res);
$(that).html(html);
}).catch(function(msg){
console.error(msg);
});
}
/**
* 封装POST请求
* @param {*} id
* @param {*} url
* @param {*} params
*/
$.fn.requestPostRender = function(id, url, params){
let that = this;
ajaxRequest(url, params, 'post').then(function(res){
let html = artTemplate(id, res);
$(that).html(html);
}).catch(function(msg){
console.error(msg);
});
}
})();
代码里需要注意的是art-template引擎的语法规则修改,因为<% %>和{{ }}在webpack中默认使用,如果客户端代码中不修改语法规则,webpack构建时含有<% 和 {{ 的语法内容都会被匹配到和清空,这样到客户端时就获取不到任何模板内容了。
客户端修改为开始标签为{% 和结束标签为%},代码如下:
// 配置art-template语法模板
artTemplate.defaults.rules[1].test = /{%([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*%}/;
另外ajax这里就不细讲了,如不了解它的,可以去看下相关文档。
art-template在expose-loader中对外暴露名称已修改为artTemplate,所以在成功获取到数据后,可以直接使用artTemplate来渲染数据。通过artTemplate渲染后得到新的html内容,直接通过jquery将其追加到DOM中。
另外有些人可能不清楚$.fn是什么,不了解jquery朋友可以查看下相关手册,了解其用法和作用。
四、语法标签修改
以上工作做完后,我们可以修改index.html中数据了。将之前不管使用{{set inviteList = ... }},还是<datalist>标签引用的JSON数据,把需要通过接口获取的数据可以全部删除掉。第二步将客户端浏览器要使用到的模板信息,独立出来在template中创建indexTpl.html,单独存放。如下图:
indexTpl.html模板代码如下:
<script type="text/html" id="tpl_top">
{%each data as item%}
<dl class="b_list">
<dt>
<a href="#" target="_blank">
<img src="{%item.thumb%}" alt="09" />
</a>
</dt>
<dd>
<h2><a href="{%item.url%}" target="_blank">{%item.title%}</a></h2>
<span>
{%item.description%}
<a href="{%item.url%}" target="_blank">[详细情况]</a>
</span>
</dd>
</dl>
{%/each%}
</script>
<script type="text/html" id="tpl_bottom">
{%each data as item %}
<li>
<span>{% item.datetime %}</span>
<a href="{% item.url %}" target="_blank">{% item.title %}</a>
</li>
{%/each%}
</script>
对页面代码分析可知,几个区域主要分为这两种模板数据结构,所以结构一样的可以共用一个模板。另外模板里的语法开始和结束标签也修改为common.js中修改的语法规则,如果未修改客户端浏览器运行时可能渲染为空。
现在index.html文件中代码如下:
<!-- 修改当前页面对应标题 -->
{{$data.navIndexName = "首页"}}
{{extend './template/layout.html'}}
<datalist>
{
"title": "首页"
}
</datalist>
{{block 'content'}}
<!-- mainer_wrapper -->
<div id="mainer_wrapper">
<!-- main -->
<div class="main-container">
<div class="clear"></div>
<!-- 广告 START -->
<div class="box_gg">
<a href="#" target="_blank">
<img src="images/index_08.jpg" alt="banner" />
</a>
</div>
<!-- 广告 END -->
<!-- box_content 最新活动 START -->
<div class="box_content wd490 fl">
<div class="title"><a href="#" class="more">MORE >></a>最新活动</div>
<div class="content">
<div id="newsBox"></div>
<div class="clear"></div>
</div>
</div>
<!-- /box_content 最新活动 END -->
<!-- box_content 旅游项目 START -->
<div class="box_content wd490 fl mg_l15">
<div class="title"><a href="#" class="more">MORE >></a>旅游项目</div>
<div class="content">
<div id="travelBox"></div>
<div class="clear"></div>
</div>
</div>
<!-- /box_content 旅游项目 END -->
<!-- box_content 旅游咨询 START -->
<div class="box_content fl" style="width:360px;">
<div class="title"><a href="#" class="more">MORE >></a>旅游咨询</div>
<div class="content">
<ul class="box_list" id="consultBox"></ul>
</div>
</div>
<!-- /box_content 旅游咨询 END -->
<!-- box_content 贵宾服务 START -->
<div class="box_content fl mg_l15" style="width:360px;">
<div class="title"><a href="#" class="more">MORE >></a>贵宾服务</div>
<div class="content">
<ul class="box_list" id="serviceBox"></ul>
</div>
</div>
<!-- /box_content 贵宾服务 END -->
<!-- box_content 招聘信息 START -->
<div class="box_content fl mg_l15" style="width:240px;">
<div class="title"><a href="#" class="more">MORE >></a>招聘信息</div>
<div class="content">
<ul class="box_list" id="invitBox"></ul>
</div>
</div>
<!-- /box_content 招聘信息 END -->
<div class="clear"></div>
</div>
<!-- /main -->
</div>
<!-- /mainer_wrapper -->
{{include './template/indexTpl.html'}}
{{/block}}
这里将独立出去的art-template渲染需要的模板文件,再通过include引入进来。其他不需要的数据也清理干净,页面显示更为整洁了。
此时执行npx webpack serve命令,发现页面数据为空,这是因为还未写调用接口数据的代码,如下图:
html代码除了将多余json数据、模板代码作了处理,还需要添加相关ID,这在后面客户端渲染页面数据会有用的。如下图:
五、页面渲染
由于是首页数据渲染,之前也创建了index.html的入口文件,所以调用接口的功能就写在index.js文件中,代码如下:
import '$css/index.less';
$(function(){
// 最新活动
$('#newsBox').requestGetRender('tpl_top', '/firstList', {});
// 旅游项目
$('#travelBox').requestGetRender('tpl_top', '/firstList', {});
// 旅游咨询
$('#consultBox').requestGetRender('tpl_bottom', '/bottomList', {});
// 贵宾服务
$('#serviceBox').requestGetRender('tpl_bottom', '/bottomList', {});
// 招聘信息
$('#invitBox').requestGetRender('tpl_bottom', '/inviteList', {});
});
如果devServer打开了hot功能,在index.js中添加上面代码后,页面数据已显示出来了。如下图:
这里写的比较简单,也是按个人思路整理的,希望对大家有帮助。不管用什么方法,只要能满足项目需求,达到预期效果,用什么方式实现并不重要。