模板引擎 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 <filename> 命令,就可以自动完成测试了。

mocha test.js
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 在线阅读 http://interview.poetries.top[http://interview.poetr...
    程序员poetry阅读 114,315评论 24 450
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • “现代人的崩溃是一种默不作声的崩溃。看起来很正常,会说笑、会打闹、会社交,表面平静,实际上心里的糟心事已经积累到一...
    木卯丁阅读 147评论 0 1
  • 文/磊少 01. 从去年开始,很多行业大v都在提一个观点,微商的未来是直营,代理制度终将被淘汰。但是一年过去了,做...
    我是磊少阅读 990评论 10 8