美文网首页饥人谷技术博客
从手写一个模版引擎说起

从手写一个模版引擎说起

作者: 饥人谷_若愚 | 来源:发表于2015-12-15 17:52 被阅读683次

    概述:项目中经常需要使用js模版去渲染字符串,handlebars这样的模版引擎又过于庞大,其实自己可以完全可以实现一个简单的模版引擎,寥寥十几行代码而已。

    本文首先用传统的方法实现一个模版函数;在此基础上封装成可在不同环境(浏览器环境、node环境)、不同规范(CMD、AMD)下使用的组件;之后演示了如何把组件上传到npm库(可通过npm install easyTpl直接安装);上传到bower库(可通过bower install easyTpl)下载。

    模版引擎easyTpl的实现

    在做之前需要先思考如何去用,比如下面的方式:

    代码1:

    var data = {
        name: 'ruoyu',
        addr: 'Hunger Valley'
    };
    var tpl = 'Hello, my name is  {{name}}. I am in {{addr}}.';
    var str = easyTpl(tpl, data);
    console.log(str);  // 输出: Hello, my name is ruoyu. I am in Hunger Valley.
    

    上面的例子需要输出Hello, my name is ruoyu. I am in Hunger Valley.
    因此,easyTpl函数需要接收模版字符串和数据两个参数,返回替换变量后的字符串。

    如何实现呢?

    (1) 第一步,先尝试写正则表达式,匹配{{variable}}{{variable.variable}}形式的字符串,其中variable满足变量的命名格式。
    代码2:

    var reg = /{{[a-zA-Z$_][a-zA-Z$_0-9\.]*}}/ig;
    var strs =  [
        ''hello{{__}}',  //{{__}}
        "hello {{}}", //null
        'hello {name}',    //null
        'hello {{name.age}}',  //{{name.age}}
        'hello {{{good}}',   //{{good}}
        'hello {{123ok dd}}',  //null
        'hello {{ {{dd}}{{ok.dd}}'  //{{dd}}, {{ok.dd}}
      ]
    
    strs.forEach(function(str){
      console.log(str.match(reg));
    });
    

    上面的测试代码也是我们单元测试的原型,后续单元测试会用到。

    (2) 第二步, 遍历字符串,做替换

    代码3:

    
    function easyTpl(tpl, data){
        var re = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
        return tpl.replace(re, function(raw, key, offset, string){
          return data[key]||raw;
        }); 
    }
    
    var strs =  [
        'hello{{__}}',
        'hello {name}',
        'hello {{friend.name}}',
        'hello {{{age}}',
        'hello {{123ok dd}}',
        'hello {{sex}} {{sex}} {{sex}} {{friend.name}}'
    ];
    
    var data = {
      name: 'hunger',
      age: 28,
      sex: '男',
      friend: {
        name: 'xiaoming'
      }
      
    };
    strs.forEach(function(str){
      console.log(easyTpl(str, data));
    });
    

    输出:

    "hello{{__}}"
    
    "hello {name}"
    
    "hello {{friend.name}}"
    
    "hello {28"
    
    "hello {{123ok dd}}"
    
    "hello 男 男 男 {{friend.name}}"
    

    是不是很简单,上面的核心代码easyTpl函数区区3行就能能基本满足上面代码1例子里的需求。

    但是,如果是下面代码4的情况就有问题了

    代码4:

    var data = {
        name: 'ruoyu',
        dog: {
            color: 'yellow',
            age: 2
        }
    };
    var tpl = 'Hello, my name is  {{name}}. I have a {{dog.age}} year old  {{dog.color}} dog.';
    var str = easyTpl(tpl, data);
    console.log(str); 
     // 应输出: Hello, my name is ruoyu. I have a 2 year old yellow dog.
    // 实际输出: Hello, my name is  hunger. I have a {{dog.age}} year old  {{dog.color}} dog.
    

    此时,代码3里的easyTpl函数已经无法满足需求。因为在遍历到{{dog.age}}时会执行替换,data[key]data["dog.age"],而这种写法显然无法得到 age的值。

    如何对多层嵌套的JSON对象进行解析呢?

    我们可以把模版变量以.号进行字符串分割,使用循环访问对应变量的值。如代码4所示。

    代码4:

    function easyTpl2(tpl, data){
        var re = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
        return tpl.replace(re, function(raw, key, offset, string){
          var paths = key.split('.'),
              lookup = data;
          while(paths.length>0){
            lookup = lookup[paths.shift()];
          }
          return lookup||raw;
        }); 
    }
    console.log(easyTpl2(strs[6], data));
    //输出 "Hello, my name is  hunger. I have a 2 year old  yellow dog"
    

    完美解决问题,可以把该函数放到项目的通用库里,在简单场景下可以很方便的使用。当然正如这个模版引擎功能还很弱,如果在复杂的场景下(判断、遍历)使用还需进一步完善。

    代码封装

    下面的例子演示了如何封装代码,让我们的代码模块化,并可以在各个端方便使用。

    
    (function (name, definition, context) {
        if (typeof module != 'undefined' && module.exports) {
            // in node env
            module.exports = definition();
        } else if (typeof context['define'] == 'function' && (context['define']['amd'] || context['define']['cmd'])  ) {
            //in requirejs seajs env
            define(definition);
        } else {
            //in client evn
            context[name] = definition();
        }
    })('easyTpl', function () {
        return function (tpl, data){
            var re = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
            return tpl.replace(re, function(raw, key, offset, string){
              var paths = key.split('.'),
                  lookup = data;
              while(paths.length>0){
                lookup = lookup[paths.shift()];
              }
              return lookup||raw;
            }); 
        }
    }, this);
    

    对上面的代码分段讲解:

    
    (function (name, definition, context) {})('easyTpl', function () {...}, this);
    

    最外层是一个立即执行函数,用于封装和隔离作用域,传递3个参数进去。第一个参数是模块名称,第二个参数是模块的具体实现方式,第三个参数是模块当前所处的作用域(在node端和在浏览器端是不同的)。

        if (typeof module != 'undefined' && module.exports) {
            // in node env
            module.exports = definition();
        } else if (typeof context['define'] == 'function' && (context['define']['amd'] || context['define']['cmd'])  ) {
            //in requirejs seajs env
            define(definition);
        } else {
            //in client evn
            context[name] = definition();
        }
    

    如果当前模块运行在node环境下,则遵循CommonJS规范,必然存在module.exports这个全局变量。上面的代码相当于

    var definition = function(){
      return function(tpl, data){...};
    }
    module.exports = definition();
    

    如果当前模块运行在遵循AMD(如RequireJS)和CMD(如SeaJS) 规范的框架下,则分别存在window.define.amdwindow.define.cmd这两个变量,而代码context['define']中的content就是(function (name, definition, context) {})('easyTpl', function () {...}, this);中的this,也就是window。所以该部分代码的写法为CMD、AMD规范下模块定义的方式。

    define(function(){
       return function(tpl, data){...};
    });
    

    如果当前模块运行在普通的浏览器端,则执行context[name] = definition();,即window['easyTpl'] = definition();

    各环境demo演示地址:

    单元测试

    mocha 是一个简单、灵活有趣的 JavaScript 测试框架,用于 Node.js 和浏览器上的 JavaScript 应用测试。
    Chai是一个BDD/TDD模式的断言库,可以再node和浏览器环境运行,可以高效的和任何js测试框架搭配使用。

    npm install -g mocha
    npm install chai
    
    var assert = require('chai').assert,
        easyTpl = require('../lib/easyTpl');
    
    
    var units = [
        [
            {
                name: 'ruoyu',
                addr: 'Hunger Valley'
            },
            'I\'m {{name}}. I live in {{addr}}.',
            'I\'m ruoyu. I live in Hunger Valley.'
        ],
        [
            {
                name: 'ruoyu',
                dog: {
                    color: 'yellow',
                    age: 2
                }
            },
            'My name is {{name}}. I have a {{dog.age}} year old {{dog.color}} dog.',
            'My name is ruoyu. I have a 2 year old yellow dog.'
        ],
        [
            {
                name: 'ruoyu',
                dog: {
                    color: 'yellow',
                    age: 2,
                    friend: {
                        name: 'hui'
                    }
                }
            },
            'My name is {{name}}. I have a {{dog.age}} year old {{dog.color}} dog. His friend is {{dog.friend.name}}.',
            'My name is ruoyu. I have a 2 year old yellow dog. His friend is hui.'
        ]
    ]
    
    describe('easyTpl', function () {
        it('should replace patten correctly', function () {
            units.forEach(function(testData, idx){
                assert.equal(easyTpl(testData[1], testData[0]), testData[2],  'test ' + idx + ' failed');
            });
    
        });
    });
    

    提交到NPM Bower

    参考
    参考

    Github 地址: https://github.com/jirengu/easytpl

    相关文章

      网友评论

        本文标题:从手写一个模版引擎说起

        本文链接:https://www.haomeiwen.com/subject/lueuhttx.html