模板引擎 easyTpl 的实现

[TOC]

模板引擎 easyTpl 的实现

概述

项目中经常需要使用js模板去渲染字符串,像 handlebars 这样的模板引擎过于庞大,其实自己完全可以实现一个简单的模板引擎,下面就让我们一起来实现一下吧。

在做之前我们需要知道如何去使用,比如下面的方式:

var data = {
    name: 'Chen Danwei',
    addr: 'Shenzhen'
};

var tpl = 'Hello, my name is {{name}}. I am in {{addr}}';
var str = easyTpl(tpl, data);
console.log(str); 

输出:

'Hello, my name is Chen Danwei. I am in Shenzhen.'

easyTpl 函数需要接收模板字符串和数据两个参数,返回替换变量后的字符串。

如何实现呢?

(1)第一步,尝试写正则表达式,匹配 {{variable}}{{variable.varable}} 形式的字符串,其中 variable 需要满足变量的命名规范。

代码1:

var reg = /{{[a-zA-Z$_][a-zA-Z$_0-9\.]*}}/ig;
var strs = [
    'hello{{__}}',             
    "hello {{}}",              
    'hello {name}',            
    'hello {{name.age}}',      
    'hello {{{good}}',         
    'hello {{123ok dd}}',      
    'hello {{ {{dd}}{{ok.dd}}' 
];

strs.forEach(function(str) {
    console.log(str.match(reg));
});

输出:

{{__}}

null

null

{{name.age}}

{{good}}

null

{{dd}}, {{ok.dd}}

注:上面的测试代码做为我们单元测试的原型,后续单元测试会用到。

(2)第二步,数组遍历,进行字符串替换。

代码2:

function easyTpl(tpl, data) {
    var reg = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
    return tpl.replace(reg, function(raw, key, index, str) {
        return data[key] || raw;     
    });
}

var strs = [
    'hello{{__}}',             
    "hello {{}}",             
    'hello {name}',         
    'hello {{friend.name}}',    
    'hello {{{age}}',      
    'hello {{123ok dd}}',    
    'hello {{sex}} {{sex}} {{sex}} {{name}}'
];

var data = {
    name: 'Chen Danwei',
    age: 25,
    sex: '男',
    friend: {
        name: 'xiaoming'
    }
};

strs.forEach(function(str) {
    console.log(easyTpl(str, data));
});

输出:

'hello{{__}}'

'hello {{}}'

'hello {name}'

'hello {{friend.name}}'

'hello {25'

'hello {{123ok dd}}'

'hello 男 男 男 Chen Danwei'

是不是很简单,上面的核心代码 easyTpl 函数仅仅3行代码就能基本满足上面代码1例子中的需求。但是,如果是下面代码3的情况就有问题了。

代码3:

var data = {
    name: 'Chen Danwei',
    dog: {
        color: 'yellow',
        age: 3
    }
};

var tpl = 'Hello, my name is {{name}}. I have a {{dog.age}} year old {{dog.color}} dog.';

console.log(easyTpl(tpl, data));

  // 应输出:'Hello, my name is Chen Danwei. I have a 3 year old yellow dog.'
// 实际输出:'Hello, my name is Chen Danwei. I have a {{dog.age}} year old {{dog.color}} dog.'

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

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

我们可以把模板变量以 . 号进行字符串分割,使用循环访问对应变量的值,如下所示:

代码4:

function easyTpl(tpl, data) {
    var reg = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
    return tpl.replace(reg, function(raw, key, index, str) {
        var paths = key.split('.'),
            lookup = data;
        while(paths.length > 0) {
            lookup = lookup[paths.shift()];
        }
        return lookup || raw;
    });
}

console.log(easyTpl(tpl, data));

输出:

'Hello, my name is Chen Danwei. I have a 3 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 env
        context[name] = definition();
    }
})('easyTpl', function() {
    return function(tpl, data) {
        var reg = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
        return tpl.replace(reg, function(raw, key, index, str) {
            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 env
    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'] 中的 context 就是 (function(name, definition, context){})('easyTpl', function() {...}, this); 中的 this,也就是 window。所以该部分代码的写法为 CMDAMD 规范下模块定义的方式。

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

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

单元测试

mocha 是一个简单、灵活有趣的 Javascript 测试框架,用于 Node.js 和浏览器上的 Javascript 应用测试。

chai 是一个 BDD/TDD 模式的断言库,可以在 Node.js 和浏览器环境运行,可以搞笑的和任何 Javascript 测试框架搭配使用。

npm install -g mocha
npm install chai

test.js 代码如下:

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');
        });

    });
});

接着执行 mocha 命令,就可以自动完成测试了。

mocha test.js

你可能感兴趣的:(模板引擎 easyTpl 的实现)