从手写一个模版引擎说起

概述:项目中经常需要使用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

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,581评论 18 399
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 3,607评论 2 41
  • @转自GitHub 介绍js的基本数据类型。Undefined、Null、Boolean、Number、Strin...
    YT_Zou阅读 1,143评论 0 0
  • 最近为了应对客户的需求,需要对群聊中根据用户的设置进行消息免打扰。抽空又仔细看了一下iOS push的格式,并且使...
    谁动了我的芝麻糖阅读 9,043评论 0 8
  • 自26岁始便给自己订下的目标:坚持看书,努力修炼平和、宽容与豁达的心态,并努力提高自身修养。让自己有一个丰盈而美好...
    小沫沫沫沫阅读 483评论 0 0