概述
序
html在前端一直被认为是最简单的,但又容易被忽略,在单页面开发中,通常被当作字符串保存在变量中,把它单纯作为一层渲染层来使用,但是,他拥有XML的结构,还拥有保存数据的功能。如果把相关的数据放在html上,而不是单独的在js中另外创建一个数据结构去存储,会大大减少js的代码量。
我非常追崇用最原始的html去构建页面,这样子可以构建最直接,最符合用户直觉的页面,而且是与框架无关的。然而使用纯html来构建复杂的单页面的模板就会非常的力不从心,根据数据和html模板来生成html的模板引擎技术就应运而出了,市面也有很多的优秀的前端html模板引擎,使用方便但不容易扩展。因此需要开发一个符合自己开发习惯的html模板来配合日常开发的使用。主要遵从一下两点
- html模板引擎生成的html必须要标准的html,且不应该在元素上增加可有可无的属性,这一点非常重要,特别在调试页面的时候,能准确的定位问题并去修改,这样能减少一些不确定性;
- 应该以满足功能为先,追求性能为次。有时候为了某个功能需要消耗很大的性能,但是功能如果是必不可少的,也应该先实现,后期再进行优化处理。
需求
由数据和html模板来生成符合要求的html,这要求模板引擎拥有简单的数据分析功能。我们这里的数据一般都是切换页面时候,上一个页面传过来的或者是后端传回来的数据,功能主要有:
- 最基础的数据替换;
- 拥有数据过滤功能,即数据转换;
- 简单的判断语句和简单的逻辑判断;
- 简单的循环语句;
- 上面的情况杂糅在一起,比如多重循环混合多重判断。
实现思路
-
数据替换可以很容易用正则表达式来替换,如下代码
// str代表html模板,obj代表数据, // 比如obj = { a: 1, c: { b: 2 } }, str = <div>{{a}}{{c.b}}</div>, 也可以支持多级 // 也要支持简单的变量 obj = { a: 1, c: { b: 2, a: 3 }, s: "b" } str = <div>{{c.[s]}}</div> function getProperty(obj, str) { str = str.replace(/[(.*?)]/g, function (item, match) { return getProperty(obj, match); }) var tempArray = str.split("."); var property = obj[tempArray[0]], i = 1, len = tempArray.length; while (len - i >= 1) { property = property[tempArray[i]]; i++; } return property; }
-
数据过滤,如下代码
// 存在app.filter = {objToStr: function (obj) { return Object.keys(obj).join(";"); }} // 有obj = { a: {a: 1, b: 2, c: 3 } }, str = <div>{{a | objToStr}}</div> function getProperty(obj, str, target) { str = str.replace(/[(.*?)]/g, function (item, match) { return getProperty(obj, match, target); }) var strArray = str.split("|").map(function (item) { return item.replace(/s+/g, ""); }) var tempArray = strArray[0].split("."); var property = obj[tempArray[0]], i = 1, len = tempArray.length; while (len - i >= 1) { property = property[tempArray[i]]; i++; } var filterStr = strArray[1]; if (filterStr) { var app = target._getApp(); if (filterStr in app.filter) property = app.filter[filterStr](property); } return property; } 注:后续支持 {{a | objToString:xxx,bbb,ccc}} objToFilter : function (obj, a1, a2, a3) { } // a1: xxx, a2: bbb, a3: ccc
-
简单的逻辑判断
var compare = { "==": function (left, right) { return left == right; }, "!=": function (left, right) { return left != right; }, ">=": function (left, right) { return left >= right; }, "<=": function (left, right) { return left <= right; }, "<": function (left, right) { return left < right; }, ">": function (left, right) { return left > right; } };
首先将str根据情况分成若干个情况的简单语句,然后判断哪个情况符合并将该语句渲染出来,
比如 obj = { a: 1 } str = {{if a == 1}}a == 1{{else}}a != 1{{/if}}
将html分成a == 1和a != 1,然后根据上面的逻辑判断选择渲染
具体代码实现略。 -
简单的循环语句
与逻辑判断类似,将循环内的语句提取出来,然后分别根据不同的索引进行渲染,最后将它们拼接起来
比如 obj = { a: [ {a: 2, c: 3}, { a: 3, c: 4 }], c: 2 }, str = {{each a}}a: {{a}}; c: {{c}}{{/each}}
它会分成两组分别为{index: 1, a: 2, c: 3}, {index: 2, a: 3, c: 4 }渲染两次a: {{a}}; c: {{c}}并把
拼接起来。这里存在一个问题,如果要引用循环外的{ c: 2 }, 却被里面的c覆盖了。因此引入了第二种
str = {{each item as a}}a: {{item.a}}; c: {{c}}{{/each}}
它的渲染对象分别为 { item: { a: 2, c: 3, index: 1}, c: 2 }, { item: { a: 3, c: 4, index: 2}, c: 2 }还有一种情况,数组内的元素不是对象。比如 obj = { a: [3, 2 ] };
里面会把它转化为 { a: [ { index: 1, content: 3}, { index: 2, content: 3}]}具体实现代码略。
-
上诉的4种情况都可以用简单的正则表达式来进行替换,选取符合规格的数据进行填充和拼接。对于多重循环,比如
{{each item as a}}{{each obj as item.b}}obj.c{{/each}}{{/each}}
或者更多循环,仅仅通过正则表达式,无法精确的匹配具有对应的多层{{each}}{{/each}}, 只能通过字符串匹配,将他们一层一层的剥离开来,所幸的是他们都是一一对应的。
多重判断也能通过这样的逻辑进行匹配,最终把它们还原相应的字符串对于多重循环和多重判断混合在一起,我们优先处理清除循环的语句,然后清除判断语句,最后来处理其它的。这是因为每个循环语句,自定义了一个作用域,如果先处理判断语句,将会赋值失败。最后的代码如下
function render(str, obj, target) { var that = target || this; obj = obj || that.data; if (!isEach) str = filterEach(str, obj, that); // 消除each相关语句 if (!isIf) str = filterIf(str, obj, that); // 消除if相关语句 var pattern = /{?{{s*[a-zA-Z_$[]][w$[]]*(.[a-zA-Z_$[]][w$[]]*)*s*(|s*[a-zA-Z_$[]][w$[]]*)*s*}}}?/g; var newStr = str.replace(pattern, function (match) { if (ELSE.test(match)) return match; var isdecode = match.indexOf("{{{") > -1; // 是否要转义 var len = isdecode ? 3 : 2; return getProperty(obj, match.slice(len, -len).replace(/^s+|s+$/g, ""), that, !isdecode); }); return newStr; }
其它应用
上面讲解了html模板引擎原理,可以通过renderHTML(str, obj)来进行渲染,同时如果使用以下方法也会间接调用这个方法:
- innerHTML(dom, str, feeback, obj);
- insertAdjacentHTML(dom, str, pos, feeback, obj);
- outerHTML(dom, str, feeback, obj);
上面的obj如果未传,会默认是Page实例对象的data, feeback代表将html插入到页面后的回调方法,这是一异步过程,需要考虑到下面一篇的组件介绍。html是一种数据的结构,同时也应该能是一种渲染的表达,它不应该被特定的框架所特有,将最原始的html直接放在浏览器就能显示。因此获得页面的html,让它可以被各个页面都能无障碍使用,给Page的原型方法添加一个getHTML方法,他将返回一个HTML,这个html是最原始的html,不带任何模板痕迹的。
如何给页面传数据,只有通过app.render(pagename, isReplace, option); 这里的option就是传给新页面的数据,这里的pagename可以是个url,或通过页面配置匹配到对应的pagename,并将url的附加信息提取出来,复制到新的页面里当参数pagename是url且带有数据,option也带有相同key的数据的时候,option会覆盖url上的数据如果url在页面配置中找不到,会跳到404页面或500页面。
-
url支持如下的传数据方式
比如是这样的一个页面配置 { name: “second”, url: “/second”, js: “/public/second/second.js”, title: “次页” }
通过调用 app.render("/second?a=4&c=5") 等同于 app.render(“second”, false, { a: 4, c: 5 }); -
比如一个页面配置{ name: “second”, url: “/detail/:id”, js: “/public/second/second.js”, title: “次页” };
通过调用 app.render("/detail/123")相当于 app.render(“second”, false, { id: 123 });
如果同时存在上面两种情况,后一种会覆盖前一种
另外的在单页面中,a链接元素会跳转到外链,可能会导致回不到该页面上,因此通过改变它的事件,统一将它导航到单页面的配置页面中,如果不存在,将会跳转到404或者500页面,代码如下:
window.addEventListener("click", function (ev) {
var a = Component.getNodeName(ev.target, "A"); // 获取是否是a链接或者是它的子节点
if (a) {
var url = a.getAttribute("href");
if (url && url.indexOf("http") === -1) {
ev.preventDefault(); // 阻止默认行为,阻止冒泡
ev.stopPropagation();
that.render(url, a.target === "_self"); // 跳转页面
}
}
}, true);
案例地址
下篇准备
在介绍组件Component之前,将代码做一下调整,将Page大部分处理转到HtmlProto上,然后Page对象继承HtmlProto, HtmlProto代表着存在html的对象,所有带html渲染的对象都是继承HtmlProto对象,在这里只有Component和Page,因为它们有很多共同之处。
由于Page针对的是业务逻辑,必要的ajax操作只能在Page对象中使用,HtmlProto拥有最简单的获取页面的功能,Component对象也只继承最简单的获取页面的功能。
总结
主要介绍了html模板引擎的实现原理和应用,并在案例中说明简单的用法。还介绍了切换页面的数据传递。
推广
底层框架开源地址:https://gitee.com/string-for-100w/string
演示网站: https://www.renxuan.tech/
最后
以上就是含糊毛巾为你收集整理的8.html模板引擎以及页面数据来源的全部内容,希望文章能够帮你解决8.html模板引擎以及页面数据来源所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复