如何用javascript實現一個模板引擎

模板引擎簡介

模板引擎是html渲染必不可少的工具,前端開發的同窗經歷了從最原始的字符串拼接、用數組push/join,發展到封裝簡單的string.format函數,再到功能更增強大的模板引擎,能夠在模板中內嵌處理邏輯。javascript

傳統的頁面開發語言技術asp.net,php,jsp都內置了模板引擎,javascrip常見的模板引擎有jquery的tmpl插件,underscore的template函數,ejs、jade、artTemplate等,以ejs爲例,模板語法以下:php

<% if (user) { %>
    <h2><%= user.name %></h2>
<% } %>
複製代碼

模板中能夠直接訪問綁定的數據源變量,而且能夠支持任意的javascript代碼片斷,如if,for循環等css

原理

咱們從需求出發,想想把上面的模板字符串中的變量替換成數據源中的變量值,並支持嵌入的if等邏輯,須要怎樣實現?能支持內嵌js代碼執行,那確定是要使用eval來編譯的,只要把模板當作兩部分,一部分是在百分號之內的,使用eval執行;另外一部分是在百分號之外的,原樣輸出; 最後順序拼接成一個完整的字符串就能夠了。html

正則表達式分割字符串

首先要把模板,按照%分隔進行拆分紅多個代碼片斷,並按順序存入到數組中 分割模板字符串能夠用String對象的split方法,不少人不知道split方法是能夠支持正則表達式的,由於咱們的分隔符有兩個<%和%>,因此要用正則表達式,能夠同時匹配這兩個字符,代碼就只有一行:前端

var arr=str.split(/(\<%=?|%\>)/gm);
複製代碼

拆分後的代碼片斷生成一個數組,咱們遍歷一下這個數組,將代碼片斷分紅3種類型java

  1. javascript代碼
  2. html片斷(原樣輸出的字符)
  3. 用<%=%>輸出的變量值

只要按順序拼接生成的字符串代碼,最終編譯執行字符串代碼就能夠了node

編譯字符串的方法

1. eval

js最多見的編譯字符串的方法固然是eval,能夠直接在當前函數做用域將一個字符串編譯成可執行代碼,有一條代碼規範叫作don't be eval,這是由於使用eval是用代碼生成代碼,會讓代碼更加難以看懂,而且會有一些性能損耗。jquery

2. new Function

與eval功能類似,不一樣的是他會將傳入的字符串代碼編輯後生成一個function,並且能夠生成函數的參數,更加方便調用,封裝性隔離性更好。調用形式:new Function(args,functionBody),爲了更好的可讀性,咱們選擇new Functiongit

3. setTimeout/setInterval

setTimeout/setInterval 的第一個參數,能夠傳入一個函數,這是最常規的用法,也能夠傳入一個字符串,使用字符串的時候自動調用eval執行,這個用法不太常見。github

做用域問題

模板中確定須要一些動態變量,就是所謂的數據源,模板編譯後是生成獨立函數的,做用域是隔離的,而數據源是在業務代碼中,有獨立的做用域,不能直接訪問,那麼怎麼能把兩個不一樣的做用域鏈接起來呢,with,只有with能作到,將指定的對象插入到當前做用域鏈的頂端。

完整代碼實現

function render(tpl,data){
    tpl=tpl.replace(/(\r|\n)/ig,"");
    var arr=tpl.split(/(\<%=?|%\>)/gm);//拆分模板
    var funcBody=["with(this){\r\nvar result=[];"];
    var item,codeType;
    codeType=0;
    for(var i=0;i<arr.length;i++){
        item=arr[i];
        //將代碼片斷分爲3類
        if(item=="<%"){
           codeType=1;
           continue;
        }else if(item=="<%="){
           codeType=2;
           continue;
        }else if(item=="%>"){
            codeType=0;
            continue;
        } 
        //爲3類代碼片斷生成最終可被eval的函數體
        if(codeType==0){ //字符
            funcBody.push("result.push(\"");
            funcBody.push(item);
            funcBody.push("\");\r\n");
        }else if(codeType==1){ //代碼
            funcBody.push(item);
            funcBody.push("\r\n");
        }else if (codeType==2){ //代碼輸出
            funcBody.push("result.push(");
            funcBody.push(item);
            funcBody.push(");\r\n");
        }
    }
    funcBody.push("return result.join('')\r\n}");
    var template_func=new Function(["renderData"],funcBody.join(""));
    return template_func.apply(data,[data]);
 }
複製代碼

測試一下

var html=render("<% list.forEach(function (item,idx){ %>"
+" <div><%=idx+1+'、'+item%></div>"
+" <%})%>",
{list:["javascript","css","node.js"]})
document.write(html)
複製代碼

完美運行

咱們來看一下最終編譯後生成的函數是什麼樣子的,以下圖:

因爲生成的函數是用apply調用的,template_inner.apply(data,[data]),因此函數內部的this指向傳入的數據源(data變量),所以能夠在模板中直接使用傳入的數據源對象

模板引擎與ES6模板字符串對比

ES6新增了模板字符串功能,不一樣於普通字符串的單引號和雙引號,模板字符用`符號定義,在模板字符中直接能夠經過${變量名}訪問當前做用域內的變量並直接輸出該變量的值,而且在js文件中定義大段的html字符串時,通常是把html片斷粘帖進來,包含不少換行符,而模板字符串能夠直接兼容換行符,使用起來很是方便。上文中的模板,用ES6的模板字符串來實現,代碼也很是精簡,以下:

var user={name:"windy"};
var str=`<h2>${user.name}</h2>`
複製代碼

ES6模板字符串與普通的模板引擎相比,最大區別在於開發流程的不一樣,業務邏輯是在js中實現的,模板只實現純淨的變量替換功能,代碼與邏輯分離,比較規範易用,可維護也較好,而普通的模板引擎不只實現了變量替換,還能夠內嵌js邏輯代碼,更加靈活和強大。

最後,推薦一下我的的開源項目,內置了模板引擎的node.js web開發框架,webcontext:

github.com/windyfancy/…

相關文章
相關標籤/搜索