從手寫(xiě)一個(gè)模版引擎說(shuō)起

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

本文首先用傳統(tǒng)的方法實(shí)現(xiàn)一個(gè)模版函數(shù);在此基礎(chǔ)上封裝成可在不同環(huán)境(瀏覽器環(huán)境亡蓉、node環(huán)境)晕翠、不同規(guī)范(CMD、AMD)下使用的組件;之后演示了如何把組件上傳到npm庫(kù)(可通過(guò)npm install easyTpl直接安裝)淋肾;上傳到bower庫(kù)(可通過(guò)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) 第一步,先嘗試寫(xiě)正則表達(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));
});

上面的測(cè)試代碼也是我們單元測(cè)試的原型叹坦,后續(xù)單元測(cè)試會(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}}"

是不是很簡(jiǎn)單募书,上面的核心代碼easyTpl函數(shù)區(qū)區(qū)3行就能能基本滿足上面代碼1例子里的需求绪囱。

但是,如果是下面代碼4的情況就有問(wèn)題了

代碼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)無(wú)法滿足需求鬼吵。因?yàn)樵诒闅v到{{dog.age}}時(shí)會(huì)執(zhí)行替換,data[key]data["dog.age"]篮赢,而這種寫(xiě)法顯然無(wú)法得到 age的值齿椅。

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

我們可以把模版變量以.號(hào)進(jìn)行字符串分割荷逞,使用循環(huán)訪問(wè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"

完美解決問(wèn)題种远,可以把該函數(shù)放到項(xiàng)目的通用庫(kù)里涩澡,在簡(jiǎn)單場(chǎng)景下可以很方便的使用。當(dāng)然正如這個(gè)模版引擎功能還很弱坠敷,如果在復(fù)雜的場(chǎng)景下(判斷妙同、遍歷)使用還需進(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。所以該部分代碼的寫(xiě)法為CMD缘缚、AMD規(guī)范下模塊定義的方式勾笆。

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

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

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

單元測(cè)試

mocha 是一個(gè)簡(jiǎn)單、靈活有趣的 JavaScript 測(cè)試框架该园,用于 Node.js 和瀏覽器上的 JavaScript 應(yīng)用測(cè)試。
Chai是一個(gè)BDD/TDD模式的斷言庫(kù)帅韧,可以再node和瀏覽器環(huán)境運(yùn)行里初,可以高效的和任何js測(cè)試框架搭配使用。

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)系作者
  • 序言:七十年代末忽舟,一起剝皮案震驚了整個(gè)濱河市双妨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌叮阅,老刑警劉巖刁品,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異浩姥,居然都是意外死亡挑随,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)勒叠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)兜挨,“玉大人,你說(shuō)我怎么就攤上這事眯分“杌悖” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵弊决,是天一觀的道長(zhǎng)噪舀。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么与倡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任界逛,我火速辦了婚禮,結(jié)果婚禮上蒸走,老公的妹妹穿的比我還像新娘仇奶。我一直安慰自己,他們只是感情好比驻,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布该溯。 她就那樣靜靜地躺著,像睡著了一般别惦。 火紅的嫁衣襯著肌膚如雪狈茉。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天掸掸,我揣著相機(jī)與錄音氯庆,去河邊找鬼。 笑死扰付,一個(gè)胖子當(dāng)著我的面吹牛堤撵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播羽莺,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼实昨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了盐固?” 一聲冷哼從身側(cè)響起荒给,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刁卜,沒(méi)想到半個(gè)月后志电,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛔趴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年挑辆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夺脾。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡之拨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咧叭,到底是詐尸還是另有隱情蚀乔,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布菲茬,位于F島的核電站吉挣,受9級(jí)特大地震影響派撕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜睬魂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一终吼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧氯哮,春花似錦际跪、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至肠虽,卻和暖如春幔戏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背税课。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工闲延, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人韩玩。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓垒玲,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親找颓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子侍匙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法叮雳,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法妇汗,異常的語(yǔ)法帘不,線程的語(yǔ)...
    子非魚(yú)_t_閱讀 31,659評(píng)論 18 399
  • Node.js是目前非常火熱的技術(shù)杨箭,但是它的誕生經(jīng)歷卻很奇特寞焙。 眾所周知,在Netscape設(shè)計(jì)出JavaScri...
    w_zhuan閱讀 3,617評(píng)論 2 41
  • @轉(zhuǎn)自GitHub 介紹js的基本數(shù)據(jù)類型互婿。Undefined捣郊、Null、Boolean慈参、Number呛牲、Strin...
    YT_Zou閱讀 1,158評(píng)論 0 0
  • 最近為了應(yīng)對(duì)客戶的需求,需要對(duì)群聊中根據(jù)用戶的設(shè)置進(jìn)行消息免打擾驮配。抽空又仔細(xì)看了一下iOS push的格式娘扩,并且使...
    誰(shuí)動(dòng)了我的芝麻糖閱讀 9,104評(píng)論 0 8
  • 自26歲始便給自己訂下的目標(biāo):堅(jiān)持看書(shū)琐旁,努力修煉平和涮阔、寬容與豁達(dá)的心態(tài),并努力提高自身修養(yǎng)灰殴。讓自己有一個(gè)豐盈而美好...
    小沫沫沫沫閱讀 490評(píng)論 0 0