從手寫一個(gè)模版引擎說起

概述:項(xiàng)目中經(jīng)常需要使用js模版去渲染字符串,handlebars這樣的模版引擎又過于龐大,其實(shí)自己可以完全可以實(shí)現(xiàn)一個(gè)簡單的模版引擎,寥寥十幾行代碼而已。

本文首先用傳統(tǒng)的方法實(shí)現(xiàn)一個(gè)模版函數(shù);在此基礎(chǔ)上封裝成可在不同環(huán)境(瀏覽器環(huán)境、node環(huán)境)、不同規(guī)范(CMD、AMD)下使用的組件;之后演示了如何把組件上傳到npm庫(可通過npm install easyTpl直接安裝);上傳到bower庫(可通過bower install easyTpl)下載。

模版引擎easyTpl的實(shí)現(xiàn)

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

代碼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函數(shù)需要接收模版字符串和數(shù)據(jù)兩個(gè)參數(shù),返回替換變量后的字符串。

如何實(shí)現(xiàn)呢?

(1) 第一步,先嘗試寫正則表達(dá)式,匹配{{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));
});

上面的測試代碼也是我們單元測試的原型,后續(xù)單元測試會(huì)用到。

(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函數(shù)區(qū)區(qū)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); 
 // 應(yīng)輸出: Hello, my name is ruoyu. I have a 2 year old yellow dog.
// 實(shí)際輸出: Hello, my name is  hunger. I have a {{dog.age}} year old  {{dog.color}} dog.

此時(shí),代碼3里的easyTpl函數(shù)已經(jīng)無法滿足需求。因?yàn)樵诒闅v到{{dog.age}}時(shí)會(huì)執(zhí)行替換,data[key]data["dog.age"],而這種寫法顯然無法得到 age的值。

如何對(duì)多層嵌套的JSON對(duì)象進(jìn)行解析呢?

我們可以把模版變量以.號(hào)進(jìn)行字符串分割,使用循環(huán)訪問對(duì)應(yīng)變量的值。如代碼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"

完美解決問題,可以把該函數(shù)放到項(xiàng)目的通用庫里,在簡單場景下可以很方便的使用。當(dāng)然正如這個(gè)模版引擎功能還很弱,如果在復(fù)雜的場景下(判斷、遍歷)使用還需進(jìn)一步完善。

代碼封裝

下面的例子演示了如何封裝代碼,讓我們的代碼模塊化,并可以在各個(gè)端方便使用。


(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);

對(duì)上面的代碼分段講解:


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

最外層是一個(gè)立即執(zhí)行函數(shù),用于封裝和隔離作用域,傳遞3個(gè)參數(shù)進(jìn)去。第一個(gè)參數(shù)是模塊名稱,第二個(gè)參數(shù)是模塊的具體實(shí)現(xiàn)方式,第三個(gè)參數(shù)是模塊當(dāng)前所處的作用域(在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();
    }

如果當(dāng)前模塊運(yùn)行在node環(huán)境下,則遵循CommonJS規(guī)范,必然存在module.exports這個(gè)全局變量。上面的代碼相當(dāng)于

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

如果當(dāng)前模塊運(yùn)行在遵循AMD(如RequireJS)和CMD(如SeaJS) 規(guī)范的框架下,則分別存在window.define.amdwindow.define.cmd這兩個(gè)變量,而代碼context['define']中的content就是(function (name, definition, context) {})('easyTpl', function () {...}, this);中的this,也就是window。所以該部分代碼的寫法為CMD、AMD規(guī)范下模塊定義的方式。

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

如果當(dāng)前模塊運(yùn)行在普通的瀏覽器端,則執(zhí)行context[name] = definition();,即window['easyTpl'] = definition();

各環(huán)境demo演示地址:

單元測試

mocha 是一個(gè)簡單、靈活有趣的 JavaScript 測試框架,用于 Node.js 和瀏覽器上的 JavaScript 應(yīng)用測試。
Chai是一個(gè)BDD/TDD模式的斷言庫,可以再node和瀏覽器環(huán)境運(yùn)行,可以高效的和任何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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,637評(píng)論 18 399
  • Node.js是目前非?;馃岬募夹g(shù),但是它的誕生經(jīng)歷卻很奇特。 眾所周知,在Netscape設(shè)計(jì)出JavaScri...
    w_zhuan閱讀 3,722評(píng)論 2 41
  • @轉(zhuǎn)自GitHub 介紹js的基本數(shù)據(jù)類型。Undefined、Null、Boolean、Number、Strin...
    YT_Zou閱讀 1,312評(píng)論 0 0
  • 最近為了應(yīng)對(duì)客戶的需求,需要對(duì)群聊中根據(jù)用戶的設(shè)置進(jìn)行消息免打擾。抽空又仔細(xì)看了一下iOS push的格式,并且使...
    誰動(dòng)了我的芝麻糖閱讀 9,404評(píng)論 0 8
  • 自26歲始便給自己訂下的目標(biāo):堅(jiān)持看書,努力修煉平和、寬容與豁達(dá)的心態(tài),并努力提高自身修養(yǎng)。讓自己有一個(gè)豐盈而美好...
    小沫沫沫沫閱讀 539評(píng)論 0 0

友情鏈接更多精彩內(nèi)容