knockoutJS学习笔记01:从拼接字符串到编写模板引擎
开篇
关于knockout的文章,园里已经有很多大神写过了,而且都写得很好。其实knockout学习起来还是很容易的,看看官网的demo和园里的文章,练习练习就可以上手了(仅限使用,不包含研究源码)。之所以想写这个系列,主要是想记录自己的学习和应用过程,也希望能给初学者一点帮助。
既然是学习过程就一步一步来,从最开始的解决方案,到优化过程,到最后的实现方案。有了思考和对比,才会更加明白这个东西有什么好处,为什么使用它、什么情况要使用它。ok, 官网学习链接为☺:knockoutJS
准备例子
过程是这样的:前台发送ajax请求,后台返回json字符串,前台生成html,插入到dom。这个过程我们再熟悉不过了,接下来我们就用多种方式完成这个例子。
先用jquery简单写一个发送请求的方法,如下:
1 2 3 4 5 6 7 8 9 10 11 | window.Tester = { callback: function (fn) { $.ajax({ url: "../Handlers/GetCourse.ashx" , success: function (data) { data = $.parseJSON(data); fn(data); } }); } } |
后台对应的实体对象,如下:
1 2 3 4 5 6 7 8 9 | 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如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | < 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元素,然后越写越多,越写越乱...,写到自己都看不太懂了,最后干脆挥挥手留给别人去看。我们都不希望这样做,有代码洁癖的朋友,看到这些应该会发狂。
我们来看一下实现上面的效果,用拼接字符串是怎么样的,代码如下:
1 2 3 4 5 6 7 8 9 | 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。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | < 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 > |
1 2 3 4 5 6 7 8 9 10 11 12 | 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 (说明:正则水平一般,卡了好久...,厉害的朋友在回复写出更好的!)。实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 | 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 就是用来实现对象冒充的。
解决了这两个问题,实现起来就轻松多了,如下:
1 2 3 | 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 不能加'',否则就作为普通字符串进行拼接了。字符串拼接太麻烦了,在网上看到一种很好的做法,通过数组实现,代码如下:
1 2 3 4 5 6 7 8 9 | 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 对象,最后生成字符串返回。这里我们简单封装一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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);
}
...
结合上面的,封装一个最终版,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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 相同的功能。先定义模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <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> |
模板定义好后,执行代码就只有一行了!如下:
1 2 3 | Tester.callback( function (data) { $( "#course" ).html(template( "#courseTmpl" ,data)); }); |
通过使用模板引擎,我只需要定义好模板,传递数据,渲染工作就由模板引擎自动完成了。
这里还有一个小知识点,script的type属性设置为:text/tmpl,这个属性是浏览器不认识的。如果script的type是浏览器支持的(如text/javascript),就会当做脚本执行或通过src属性请求下载脚本再执行,如果是浏览器不支持的,就会忽略。所以这里可以用来存储数据,大多数模板也都是定义在这个地方。
四、总结
上面的模板引擎很简单,只有30行左右,但它其实已经可以解决一些简单的问题了。实际它还有许多问题没考虑,书写起来还是比较复杂的,也不可能针对多变的需求都适用,所以还是建议用于简单的应用或学习。很好的是,它让我们明白了整个解决思路和模板运行的过程。
实际上现成的模板引擎已经很多了,接下来一篇就将介绍其中一个。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?