窗外阳光明媚,深蓝的天,淡淡的云,还有北归的雁。
我们已经学习掌握了这么多的IT前沿技术元素,那么把他们系统地组织在一起,可以形成什么?学习和理解这一章中的例程,需要用到前面学到的所有知识。
六. Node.js与 客户端模板语言
用JavaScript编写服务器程序有一个很大的好处,那就是因为网页客户端程序也使用JavaScript,所以服务器和客户端可以相互重用部分数据处理的逻辑。比如说,第三章中曾使用过的模板引擎nTenjin,我们将它引入到网页之中,这样的话我们的网页客户端也具有了模板的处理能力。
<script type="text/javascript" src="/static/js/nTenjin.js"></script>
在第三章中,我们已经可以在服务端处理网页模板并实现类似PHP的动态网页功能。为什么这里还要让网页客户端来处理模板呢?那是因为,在现代高性能服务器架构中,我们希望前端服务端所做的逻辑处理越少越好。一些面向客户端的计算,包括一些图片处理、文字处理、样式处理等,我们希望由客户端来完成。面向大数据的计算则交给云端完成。甚至于我们希望,前端服务端只提供高速的动态数据呈现服务。下图就是我们本章要实现的高性能服务器架构:
在第三章的例程5中,服务端JavaScript逻辑解析nTenjin模板,并根据传入的数据生成HTML动态网页。我们要让解析模板和生成网页的计算在网页客户端中完成,服务器需要告诉网页,模板是什么,给模板传入的数据是什么。下面我们搭建一个RESTful-API服务器为网页客户端的Ajax请求提供模板和数据。
6.1 RESTful-API Server提供模板和数据
我们在4.2节的例程6的基础上,稍作修改,设计两个RESTful-API。请看代码的详细注释。
例程7:
demo7.js如下:
/**
* Date: 12-3-25
* 演示程序7
* 说明: 使用客户端模板nTenjin输出动态轻博客内容,RESTful-API Server提供模板和数据。
*
* 请求1:http://127.0.0.1:8080/api/feedlist/username
* 响应1:["这是第一篇文章", "这是第二篇文章", "这是第3篇文章"]
*
* 请求2:http://127.0.0.1:8080/api/template
* 响应2:{"模板名":{"string":"模板内容"},"模板夹名":{"模板名":{"string":"模板内容"},"模板名":{"string":"模板内容"}}}
*/
var http = require("http");
var route = require("./demo7lib/route");
var routemap = require("./demo7lib/routemap");
var template = require("./demo7lib/template.js");
var i = 1;
template.loadTemplate();
http.createServer(
function (request, response) {
response.writeHead(200, {
"Content-Type":"application/json; charset=UTF-8"
});
queryobj = {};
route(routemap, request.url, request, response, queryobj);
i++;
console.log("服务器访问被访问次数: i = " + i);
response.end();
}).listen(8080);
console.log("服务器开启");
定义两个API的路由表,routemap.js如下:
/**
* Date: 12-3-25
* 说明: RESTful-API web请求url路由表
*/
var requestHandles = require("./requestHandles");
var routemap = {
"get":{
"/api/feedlist/:id":requestHandles.feedlist,
"/api/template":requestHandles.template,
},
"post":{
},
"put":{
},
"del":{
}
};
module.exports = routemap;
在requestHandles.js中,实现两个API的处理器:
/**
* Date: 12-3-25
* 说明: requestHandles
*/
var requestHandles = {
};
var feedlist = ["这是第一篇文章", "这是第二篇文章", "这是第3篇文章"];
requestHandles.feedlist = function (request, response, pathObject, queryobj) {
response.write(JSON.stringify(feedlist));
};
var template = require("./template.js");
requestHandles.template = function (request, response, pathObject, queryobj) {
response.write(JSON.stringify(template.html));
};
module.exports = requestHandles;
上面的requestHandles.template处理器中,template.html就是服务器要输出的模板库。它由template对象的loadTemplate方法生成。我们将模板文件放在工程的子文件夹“/demo7lib/client_template/”中,下面的loadTemplate方法可以将它们读取出来,安装层级关系,生成数据对象。支持子文件夹,支持实时更新模板。虽然有详细的注释,理解下面这段代码,还是需要用到回调函数,异步回调函数,递归调用,闭包性等这些知识。template.js如下:
/**
* Date: 12-3-25
* 说明: 预读客户端模板(服务器版)
*/
var fs = require('fs');
var template = {html:{} };
template.loadTemplate = function (localpath, html) {
if (localpath == null) {
localpath = './demo7lib/client_template/';
}
if (html == null) {
html = this.html;
}
var files = fs.readdirSync(localpath);
for (index in files) {//迭代文件夹里的所有文件
var file = files[index];
var filepath = localpath + '/' + file;
var stat = fs.lstatSync(filepath);
var key = file.split('.')[0];
if (!stat.isDirectory()) {//如果不是文件夹,这读取文件内容,并监视文件变化
html[key] = {};
html[key].string = fs.readFileSync(filepath, encoding = 'utf8');//读取文件
watchChange(key, html, filepath);
function watchChange(wacthkey, wacthhtml, wacthfilepath) {//内部函数,监视文件变化。
var watcher = fs.watch(wacthfilepath, function (curr, prev) {//文件发生变化的匿名回调函数
watcher.close();//关闭监视器
var success = false;
var i = 1;
while (success == false) {//重新读取文件,多次读取直到成功
try {
wacthhtml[wacthkey].string = fs.readFileSync(wacthfilepath, encoding = 'utf8');
success = true;
}
catch (err) {
success = false;
}
i++;
}
console.log('模板已更新');
watchChange(wacthkey, wacthhtml, wacthfilepath);//异步递归调用,继续监视文件变化
})
}
}
else {//如果是文件夹
var innerHtml = {};
html[key] = innerHtml;
this.loadTemplate(filepath, innerHtml)//递归调用,继续读取子文件夹文件。
}
}
};
module.exports = template;
放在工程的子文件夹“/demo7lib/client_template/”中的模板文件,采用UTF8编码。文件扩展名不限。其中一个模板feedlist.html的部分内容如下:
<div class="feed-list" id="feed-list">
<?js for (var i = 0, l = it.length; i < l; i++) { ?>
<!-- 省去很多内容 -->
<div class="feed-txt-full rich-content">
<div class="feed-txt-summary">
<p>#{ it[i] }</p>
</div>
</div>
<!-- 省去很多内容 -->
</div>
<?js } ?>
</div>
模板在形式上,和我们第三章例程5的模板没有什么区别。
6.2 网页客户端解析模板和数据
运行我们上一章配置好的Nginx反向代理服务器,让Nginx将80端口上的API请求发送给监听8080端口的Node.js RESTful-API服务器处理。在网页所包含的js代码中,我们通过Ajax请求来获得API服务器所提供的模板和数据。下面的代码将编译模板,根据数据生成动态html编码,并插入到网页的相应位置
。在网页中引入客户端js代码:
<script type="text/javascript" src="/static/js/jquery.min.js"></script>
<script type="text/javascript" src="/static/js/index.js"></script>
<script type="text/javascript" src="/static/js/nTenjin.js"></script>
<script type="text/javascript" src="/static/js/template.js"></script>
其中index.js如下:
/**
* Date: 12-3-25
* 说明: 初始化数据结构和网页事件
* 根据通过RESTful-API取得的数据,生成动态网页
*/
var data = {};//数据结构根节点
data.htmlElments = {};//动态html元素,数据容器
initialize();
function initialize() {
$(document).ready(function () {//匿名回调函数,网页初始化完成后调用
template.loadTemplate(function () {//匿名回调函数,loadTemplate()完成后调用
renderfeedlist();//生成动态html编码
});
}
);
}
function renderfeedlist() {//通过Ajax请求获得数据,根据数据生成动态html编码
$.getJSON("/api/feedlist/user",//通过Ajax请求获得数据
function (result) {
if (result != null) {
var feedlistdata = result;
template.html['feedlist'].data = feedlistdata;
var feedlist = template.html['feedlist'].template.render(feedlistdata);//根据数据feedlistdata生成动态html编码
data.htmlElments['feedlist'] = feedlist;
$("#feed-list-holder").html(feedlist);//使用jquery id选择器找到动态html编码的插入位置,并插入
}
});
}
template.js如下:
/**
* Date: 12-3-25
* 说明:通过RESTful-API预读客户端模板(客户端版)
*/
var tenjin = nTenjin;
var template = {html:{} };//对象html是数据结构
data.template = template;//挂到数据结构根节点上
template.loadTemplate = function (callback) {
$.getJSON("/api/template",//发送Ajax请求,获得模板
function (result) {
if (result != null) {
template.html = result;
}
convertTemplate(template.html);//不在生成网页时编译html模板,提高性能
callback();//异步调用回调函数,顺序执行异步逻辑
function convertTemplate(html) {//内部函数,编译html模板,提高性能
for (key in html) {
if (html[key].string != null) {
html[key].template = new tenjin.Template();
html[key].template.convert(template.html[key].string);//编译html模板, 生成JavaScript函数
}
else {
convertTemplate(html[key]);//递归调用,处理子文件夹模板
}
}
}
}
);
}
例程7的运行结果如下图所示。js数组feedlist,对应着每一条记录。看上去,和例程5的效果一样,然而所不同的是,每一条记录是根据Ajax请求的数据,由客户端模板动态生成,并更新在网页上的。理想情况下,一个网页从来不需要刷新,所有的显示和操作都由Ajax动态完成,这叫做单页面web程序。
题外讨论:《JavaScript的编码规范》 有人说JavaScript不面向对象,JavaScript不适合做大型工程。早在第一章,我们就分析过JavaScript的对象模型。它不是传统的面向对象,而是一种面向实例的对象模型。它和JSON数据格式一样,都是最优秀的工程实践。每一个大型项目的开发和维护都会伴有熵变的过程,复杂度会一直增加,直到维护成本大于开发成本。我们总结了几条JavaScript的实践经验,希望可以帮助大家在开发JavaScript项目是更轻松一些。当然,不会像《Google_C++编码规范》一样,规定变量必须采用匈牙利命名法。 1.数据结构和处理逻辑尽量分离,要有清晰的边界。 不要让处理逻辑污染了数据结构,不要让数据结构混乱了处理逻辑。就像上面template.js中template.html就是存放模板的数据结构,而template.loadTemplate则是获取模板、编译模板的处理逻辑。 2.为数据结构定义一个根节点,所有对象都可以通过这个根找到。 就像上面index.js中的window.data,就是网页应用的数据结构根节点,一些我们定义的对象比如data.template就挂接到数据结构根节点上。jquery的著名的$符,就是一个根对象。如果定义jquery插件,都是$符的子节点。 3.不使用继承,尽量适宜对象组合,并且将所有的对象都按照层次关系挂到根节点上去。 虽然JavaScript可以通过prototype来实现对象的继承,但我们并不推荐这么做。因为世界上并没有那种关系一定要用继承关系来表达,包括46条染色体的自由组合。 4.数据结构用名词命名,逻辑处理尽量用动词命名。 就像requestHandles.js中的处理器,可以叫handle,也可以叫handler。因为它是处理逻辑,所有我们还是选择命名为handle。 5.局部变量和内部函数的定义,要离使用的地方尽可能的近。 在JavaScript里你不可能访问到一个定义在BIOS芯片里的变量。所以在template.js的内部函数convertTemplate(),紧接在它的调用位置之后定义。 6.局部变量要根据闭包性原理考虑它的作用范围。包括不同对象和函数中的空间作用范围和由于回调递归带来的异步时间作用范围。 我们思考这样一个问题,在服务器template.js中,根据闭包性原理,在watchChange()的匿名函数里是可以访问到变量key和filepath的,为什么还要用还要用函数参数传递进去呢?那是因为,在watchChange事件发生,异步回调匿名函数时,key和filepath的值早已改变,不再是我们注册回调函数时的那些变量了。 7.对于递归,要写明注释,即使你的代码和散文一样的优美。 8.对于超过15行的大函数,尽量写明注释。不是写怎样逻辑实现,注明函数的特征输入输出,和边际输入输出更有效。 |
借用TeamCola的广告词:早在19世纪的维多利亚女王时代,王尔德就说过:“我不想谋生,我想生活。”而到了21世纪的网络时代,更多有想法和闯劲的年轻人则宣布:“我不是在上班,而是在和搭档们为梦想而工作。”
本文目录:《一个周末掌握IT前沿技术之node.js篇》 文本粗陋,欢迎斧正!欢迎投稿!原创文章,转载请链接。联系邮箱:(it.Web.technical#gmail.com) |