8.html模板引擎以及页面数据来源

本文介绍了HTML模板引擎的实现原理,包括数据替换、数据过滤、逻辑判断和循环语句等功能,并探讨了在前端开发中如何使用模板引擎简化代码,提高效率。此外,还讨论了页面间数据传递的方式以及模板引擎在实际项目中的应用。
摘要由CSDN通过智能技术生成

html在前端一直被认为是最简单的,但又容易被忽略,在单页面开发中,通常被当作字符串保存在变量中,把它单纯作为一层渲染层来使用,但是,他拥有XML的结构,还拥有保存数据的功能。如果把相关的数据放在html上,而不是单独的在js中另外创建一个数据结构去存储,会大大减少js的代码量。

我非常追崇用最原始的html去构建页面,这样子可以构建最直接,最符合用户直觉的页面,而且是与框架无关的。然而使用纯html来构建复杂的单页面的模板就会非常的力不从心,根据数据和html模板来生成html的模板引擎技术就应运而出了,市面也有很多的优秀的前端html模板引擎,使用方便但不容易扩展。因此需要开发一个符合自己开发习惯的html模板来配合日常开发的使用。主要遵从一下两点

  • html模板引擎生成的html必须要标准的html,且不应该在元素上增加可有可无的属性,这一点非常重要,特别在调试页面的时候,能准确的定位问题并去修改,这样能减少一些不确定性;
  • 应该以满足功能为先,追求性能为次。有时候为了某个功能需要消耗很大的性能,但是功能如果是必不可少的,也应该先实现,后期再进行优化处理。

需求

由数据和html模板来生成符合要求的html,这要求模板引擎拥有简单的数据分析功能。我们这里的数据一般都是切换页面时候,上一个页面传过来的或者是后端传回来的数据,功能主要有:

  1. 最基础的数据替换;
  2. 拥有数据过滤功能,即数据转换;
  3. 简单的判断语句和简单的逻辑判断;
  4. 简单的循环语句;
  5. 上面的情况杂糅在一起,比如多重循环混合多重判断。

实现思路

  1. 数据替换可以很容易用正则表达式来替换,如下代码

    // 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;
    }
    
  2. 数据过滤,如下代码

    // 存在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
    
  3. 简单的逻辑判断

    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
    ,然后根据上面的逻辑判断选择渲染
    具体代码实现略。

  4. 简单的循环语句

    与逻辑判断类似,将循环内的语句提取出来,然后分别根据不同的索引进行渲染,最后将它们拼接起来
    比如 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}]}

    具体实现代码略。

  5. 上诉的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)来进行渲染,同时如果使用以下方法也会间接调用这个方法:

  1. innerHTML(dom, str, feeback, obj);
  2. insertAdjacentHTML(dom, str, pos, feeback, obj);
  3. 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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值