从手写一个模版引擎说起

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

  • requirejs demo
  • seajs demo
  • simple demo
  • node 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

你可能感兴趣的:(从手写一个模版引擎说起)