開篇
關于knockout的文章,園里已經有很多大神寫過了,而且都寫得很好。其實knockout學習起來還是很容易的,看看官網的demo和園里的文章,練習練習就可以上手了(僅限使用,不包含研究源碼)。之所以想寫這個系列,主要是想記錄自己的學習和應用過程,也希望能給初學者一點幫助。
既然是學習過程就一步一步來,從最開始的解決方案,到優化過程,到最后的實現方案。有了思考和對比,才會更加明白這個東西有什么好處,為什么使用它、什么情況要使用它。ok, 官網學習鏈接為?:knockoutJS
準備例子
過程是這樣的:前臺發送ajax請求,后臺返回json字符串,前臺生成html,插入到dom。這個過程我們再熟悉不過了,接下來我們就用多種方式完成這個例子。
先用jquery簡單寫一個發送請求的方法,如下:
window.Tester = {callback: function(fn) {$.ajax({url: "../Handlers/GetCourse.ashx",success: function(data) {data = $.parseJSON(data);fn(data);}});}
}
后臺對應的實體對象,如下:
public class CourseInfo{public string CourseID { get; set; }public string IconPath { get; set; }public string CourseName { get; set; }public string TeacherName { get; set; }public string CreatedDate { get; set; }public int StudyNumber { get; set; }}
html如下:
<ul id="course"><li><a href="/Default.aspx?courseID=001"><div class="course-img"><img src="../Image/1.jpg" /></div><div class="course-info"><div class="names"><span>jquery源碼解析</span><span class="fr">李老師</span></div><div class="pros"><span>2015-08-08</span><span class="fr">100人學習</span></div></div></a></li></ul>
界面效果:
一、拼接字符串
相信很多人開始都用過拼接字符串來生成dom元素,然后越寫越多,越寫越亂...,寫到自己都看不太懂了,最后干脆揮揮手留給別人去看。我們都不希望這樣做,有代碼潔癖的朋友,看到這些應該會發狂。
我們來看一下實現上面的效果,用拼接字符串是怎么樣的,代碼如下:
Tester.callback(function(data) {for (var i = 0; i < data.length; i++) {var courseImg = "<div class='course-img'><img src='" + data[i].IconPath + "' alt='" + data[i].CourseName + "'/></div>";var names = "<div class='names'><span>" + data[i].CourseName + "</span><span class='fr'>" + data[i].TeacherName + "</span></div>";var pros = "<div class='pros'><span>" + data[i].CreatedDate + "</span><span class='fr'>" + data[i].StudyNumber + "人學習</span></div>";var item = "<li><a target='_blank' href='Default.aspx?courseID=" + data[i].CourseID + "'>" + courseImg + "<div class='course-info'>" + names + pros + "</div></a></li>";$("#course").append(item);}});
可以很快得出下面幾點:1.拼接寫起來很麻煩? 2.不能給人清晰的dom結構 3.到處都是字符串修改起來很麻煩。實際項目中,我們應該盡量避免這種情況。
二、clone dom
為了解決上面的缺點,我們可以把html模板先寫好,并隱藏。等到需要時,再clone一份,生成html。代碼如下:
<div id="tmp" class="noen"><ul><li id="tmpItem"><a><div class="course-img"><img/></div><div class="course-info"><div class="names"><span></span><span class="fr"></span></div><div class="pros"><span></span><span class="fr"></span></div></div></a></li></ul></div>
Tester.callback(function(data) {for (var i = 0; i < data.length; i++) {var item = $("#tmpItem").clone();item.find("a").attr("href", "Default.aspx?CourseID=" + data[i].CourseID);item.find(".course-img>img").attr({ "src": data[i].IconPath, "alt": data[i].CourseName });item.find(".names>span:eq(0)").text(data[i].CourseName);item.find(".names>span:eq(1)").text(data[i].TeacherName);item.find(".pros>span:eq(0)").text(data[i].CreatedDate);item.find(".pros>span:eq(1)").text(data[i].StudyNumber + "人學習");$("#course").append(item);}});
? 看起來比拼接字符串好多了。這里我們提到了“模板”的概念,但它還不是真正意義上的模板,所謂模板應該是:基礎內容準備好了,就差數據,只要把數據傳遞過來,就可以生成完整內容。可以看到,我們上面還是自己去解析數據,然后生成內容,而不是自動化的過程。如果可以這樣生成html就最好了:var html = template("#tmpID",data); tmpID 表示模板的id,data 是數據,這樣生成html,不用自己去for遍歷。沒錯,這就是大多數模板引擎的實現思路。
三、模板引擎
關于js模板引擎有很多,我也會在下一篇文章單獨介紹。不過在這里我不想馬上就用現成的,我們自己先實現試試看!
3.1 基礎版
首先我們需要找到字符串中真實數據的位置,這通常是通過“占位符”來實現的,例如:${ $};然后再將占位符替換為真實的數據。查找占位符可以用正則表達式實現,替換占位符用字符串操作即可。
例如字符串:my name is ${name$}, i am ${year$} years old。 數據為:{name : "tom", year : 18}。我們希望生成最后的結果是: my name is tom, i am 18 years old。
先編寫匹配占位符的正則表達式:/\${((?:.(?!\$}))*.)?\$}/g (說明:正則水平一般,卡了好久...,厲害的朋友在回復寫出更好的!)。實現代碼如下:
var reg = /\${((?:.(?!\$}))*.)?\$}/g; var str = "my name is ${name$}, i am ${year$} years old";var data = {name : "tom",year : 18}var match;while (match = reg.exec(str)) {str = str.replace(match[0], data[match[1]]);}console.log(str);//my name is tom, i am 18 years old
簡單解釋一下:核心是exec方法,它返回的是一個數組,包括匹配到字符串的值,和其位置等。match[0] 是占位符;match[1] 是占位為內的內容(如name)。這樣通過一個循環,就可以將所有匹配找到。
?3.2 改進版
上面例子實在太簡單了,看一個稍微復雜點的結構。字符串是:my name is ${name$}, i am ${info.age$} years old。數據為:{name: "tom", info: {age:18}}。按上面的做法就不能得到正確的結果了,因為匹配后 match[1] 為 “info.age”,而 data["info.age"] 顯然不能獲取到18。如果可以在字符串里寫js呢,例如:this.name或this.info.age,運行時this由我們傳遞并執行,這樣問題就解決了。這里有兩個問題:1. 如何在字符串里寫js代碼?? 2.this 如何動態決定?
要在字符串里寫代碼執行,Function 就可以實現。Function接收字符串類型的參數,前面的是函數的參數,最后一個是函數的執行體。例如:var fn = new Function("arg1","arg2","return arg1 + arg2;"); fn 就是一個函數,接收兩個參數。可以執行得到結果:console.log(fn(1,2)); //3。那么 this 如何由我們動態決定呢?答案就是:對象冒充。js 的 call, apply 就是用來實現對象冒充的。
解決了這兩個問題,實現起來就輕松多了,如下:
var code = "return 'my name is ' + this.name + ', i am ' + this.info.age + ' years old';";var fn = new Function(code).apply(data);console.log(fn);
這里我們創建一個函數,函數執行體就是code,this指向了data對象。注意,這里 this.name 不能加'',否則就作為普通字符串進行拼接了。字符串拼接太麻煩了,在網上看到一種很好的做法,通過數組實現,代碼如下:
var code = "var result = [];"code += "result.push('my name is ');";code += "result.push(this.name);";code += "result.push(' i am ');";code += "result.push(this.info.age);";code += "result.push(' years old');"; code += "return result.join('');"; var fn = new Function(code).apply(data);console.log(fn());
? 同樣,數據部分不能加''。這種方式很巧妙,fn 執行時,會從 var? result = []; 開始執行,this 就是 data 對象,最后生成字符串返回。這里我們簡單封裝一下:
var str = "my name is ${this.name$}, i am ${this.info.age$} years old";var data = {name: "tom",info: {age:18}}function template(html, data) {if (!html) {return;}var reg = /\${((?:.(?!\$}))*.)?\$}/g;var cursor = 0;var code = "var result = [];\n";var match;while (match = reg.exec(html)) {code += "result.push('" + html.substring(cursor, match.index) + "');\n";code += "result.push(" + match[1] + ");\n";cursor = match.index + match[0].length;}code += "result.push('" + html.substring(cursor) + "');\n";code += "return result.join('')"; //console.log(code);return new Function(code.replace(/\n/g,"")).apply(data);}console.log(template(str, data));
3.3 最終版
許多時候后臺返回的是json數組字符串,這時需用使用邏輯判斷和循環來處理。這里需要一個正則:/(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g 用來匹配判斷循環關鍵字。需要注意的是,當遇到這些關鍵字的時候,就不能push到數組里了,而應該是作為程序的一部分執行,例如:
var result = [];
for(var i=0;i<10;i++){
result.push(this.name);
}
...
結合上面的,封裝一個最終版,如下:
function template(id, data) {if (!id) {throw new Error("模板id不能為空!");}var jTmpl = $(id);if(jTmpl.length <= 0){throw new Error("找不到id為:"+id+"的模板");}var html = jTmpl.html();if(!html){return html;}html = html.replace(/\"/g,"\\\"");var reg = /\${((?:.(?!\$}))*.)?\$}/g; var logicReg = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;var cursor = 0;var code = "var result = [];\n"; var match;var key; while (match = reg.exec(html)) {code += "result.push('" + html.substring(cursor, match.index) + "');\n"; code += match[1].match(logicReg) ? match[1] : "result.push(" + match[1] + ");"; code += "\n";cursor = match.index + match[0].length;}code += "result.push('" + html.substring(cursor) + "');\n";code += "return result.join('')"; //console.log(code.replace(/\n/g, ""));return new Function(code.replace(/\n/g, "")).apply(data);}
? 我們試著用這個模板完成上面拼接字符串和clone dom 相同的功能。先定義模板:
<script type="text/tmpl" id="courseTmpl">${for(var i=0,length=this.length;i<length;i++){$}<li> <a href="Default.aspx?courseID=${this[i].CourseID$}"><div class="course-img"><img src="${this[i].IconPath$}" alt="${this[i].CourseName$}"/></div><div class="course-info"><div class="names"><span>${this[i].TeacherName$}</span><span class="fr">${this[i].CourseName$}</span></div><div class="pros"><span>${this[i].CreatedDate$}</span><span class="fr">${this[i].StudyNumber$}人學習</span></div></div></a></li>${}$}
</script>
? 模板定義好后,執行代碼就只有一行了!如下:
Tester.callback(function(data) {$("#course").html(template("#courseTmpl",data));});
通過使用模板引擎,我只需要定義好模板,傳遞數據,渲染工作就由模板引擎自動完成了。
這里還有一個小知識點,script的type屬性設置為:text/tmpl,這個屬性是瀏覽器不認識的。如果script的type是瀏覽器支持的(如text/javascript),就會當做腳本執行或通過src屬性請求下載腳本再執行,如果是瀏覽器不支持的,就會忽略。所以這里可以用來存儲數據,大多數模板也都是定義在這個地方。
四、總結
上面的模板引擎很簡單,只有30行左右,但它其實已經可以解決一些簡單的問題了。實際它還有許多問題沒考慮,書寫起來還是比較復雜的,也不可能針對多變的需求都適用,所以還是建議用于簡單的應用或學習。很好的是,它讓我們明白了整個解決思路和模板運行的過程。
實際上現成的模板引擎已經很多了,接下來一篇就將介紹其中一個。