Node.js开发指南 -02

文件系统 fs

fs 模块是文件操作的封装,它提供了文件的读取、写入、更名、删除、遍历目录、链接等 POSIX 文件系统操作。与其他模块不同的是, fs 模块中所有的操作都提供了异步的和同步的两个版本,例如读取文件内容的函数有异步fs.readFile() 和同步的fs.readFileSync() 。

fs.readFile

fs.readFile(filename,[encoding],[callback(err,data)])是最简单的读取文件的函数。它接受一个必选参数 filename ,表示要读取的文件名。
第二个参数 encoding是可选的,表示文件的字符编码。
callback 是回调函数,用于接收文件的内容。
如果不指定 encoding ,则 callback 就是第二个参数。
回调函数提供两个参数 err 和 data , err 表示有没有错误发生, data 是文件内容。
如果指定了 encoding , data 是一个解析后的字符串,否则 data 将会是以 Buffer 形式表示的二进制数据。
例如以下程序,我们从 content.txt 中读取数据,但不指定编码:

var fs = require('fs');
fs.readFile('content.txt', function(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});

假设 content.txt 中的内容是 UTF-8 编码的 Text 文本文件示例,运行结果如下:
<Buffer 54 65 78 74 20 e6 96 87 e6 9c ac e6 96 87 e4 bb b6 e7 a4 ba e4 be 8b>
这个程序以二进制的模式读取了文件的内容, data 的值是 Buffer 对象。
如果我们给fs.readFile 的 encoding 指定编码:

var fs = require('fs');
fs.readFile('content.txt', 'utf-8', function(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});

那么运行结果则是:
Text 文本文件示例
log.console('err)和log.error(‘err’)的区别
在这里插入图片描述
当读取文件出现错误时, err 将会是 Error 对象。如果 content.txt 不存在,运行前面的代码则会出现以下结果:
{ [Error: ENOENT, no such file or directory 'content.txt'] errno: 34, code: 'ENOENT',path: 'content.txt' }
Node.js 的异步编程接口习惯是以函数的最后一个参数为回调函数,通常一个函数只有一个回调函数。回调函数是实际参数中第一个是 err ,其余的参数是其他返回的内容。如果没有发生错误, err 的值会是 null 或undefined 。如果有错误发生, err 通常是 Error 对象的实例

fs.readFileSync

fs.readFileSync(filename, [encoding])是 fs.readFile 同步的版本。它接受的参数和 fs.readFile 相同,而读取到的文件内容会以函数返回值的形式返回。如果有错误发生, fs 将会抛出异常,你需要使用== try 和 catch 捕捉并处理异常。
与同步 I/O 函数不同,Node.js 中
异步函数大多没有返回值==

fs.open

fs.open(path, flags, [mode], [callback(err, fd)])是 POSIX open 函数的封装,与 C 语言标准库中的 fopen 函数类似。它接受两个必选参数, path 为文件的路径,
flags 可以是以下值。
 r :以读取模式打开文件。
 r+ :以读写模式打开文件。
 w :以写入模式打开文件,如果文件不存在则创建。
 w+ :以读写模式打开文件,如果文件不存在则创建。
 a :以追加模式打开文件,如果文件不存在则创建。
 a+ :以读取追加模式打开文件,如果文件不存在则创建。
mode 参数用于创建文件时给文件指定权限,默认是 0666。回调函数将会传递一个文件描述符 fd。

① 文件权限指的是 POSIX 操作系统中对文件读取和访问权限的规范,通常用一个八进制数来表示。例如 0754 表
示文件所有者的权限是 7 (读、写、执行),同组的用户权限是 5 (读、执行),其他用户的权限是 4 (读),
写成字符表示就是 -rwxr-xr--。
② 文件描述符是一个非负整数,表示操作系统内核为当前进程所维护的打开文件的记录表索引。

fs.read

fs.read(fd, buffer, offset, length, position, [callback(err, bytesRead,buffer)])是 POSIX read 函数的封装,相比 fs.readFile 提供了更底层的接口。 fs.read的功能是从指定的文件描述符 fd 中读取数据并写入 buffer 指向的缓冲区对象。
offset 是buffer 的写入偏移量。
length 是要从文件中读取的字节数。
position 是文件读取的起始位置,如果 position 的值为 null ,则会从当前文件指针的位置读取。
回调函数传递bytesRead 和 buffer ,分别表示读取的字节数和缓冲区对象。
以下是一个使用 fs.open 和 fs.read 的示例。

var fs = require('fs');
fs.open('content.txt', 'r', function(err, fd) {
if (err) {
console.error(err);
return;
}
var buf = new Buffer(8);
fs.read(fd, buf, 0, 8, null, function(err, bytesRead, buffer) {
if (err) {
console.error(err);
return;
}
console.log('bytesRead: ' + bytesRead);
console.log(buffer);
})
});

运行结果则是:
bytesRead: 8
<Buffer 54 65 78 74 20 e6 96 87>
一般来说,除非必要,否则不要使用这种方式读取文件,因为它要求你手动管理缓冲区
和文件指针,尤其是在你不知道文件大小的时候,这将会是一件很麻烦的事情

在这里插入图片描述
在这里插入图片描述

HTTP 服务器与客户端

Node.js 标准库提供了 http 模块,其中封装了一个高效的 HTTP 服务器和一个简易的HTTP 客户端。 http.Server 是一个基于事件的 HTTP 服务器,它的核心由 Node.js 下层 C++部分实现,而接口由 JavaScript 封装,兼顾了高性能与简易性。 http.request 则是一个HTTP 客户端工具,用于向 HTTP 服务器发起请求,例如实现 Pingback
(① Pingback 是博客系统中用来通知文章被他人引用的一种手段,例如 WordPress 会自动解析文章中的链接,发送
Pingback 以告知链接被引用。) 或者内容抓取。

使用 http 实现一个服务器:

//app.js
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>Hello World</p>');
}).listen(3000);
console.log("HTTP server is listening at port 3000.");

这段代码中, http.createServer 创建了一个 http.Server 的实例,将一个函数作为 HTTP 请求处理函数。这个函数接受两个参数,分别是请求对象( req )和响应对象( res )。在函数体内, res 显式地写回了响应代码 200 (表示请求成功),指定响应头为’Content-Type’: ‘text/html’ ,然后写入响应体 ‘

Node.js

’ ,通过 res.end
结束并发送。最后该实例还调用了 listen 函数, 启动服务器并监听 3000 端口。

http.Server 的事件

http.Server 是一个基于事件的 HTTP 服务器,所有的请求都被封装为独立的事件,开发者只需要对它的事件编写响应函数即可实现 HTTP 服务器的所有功能。它继承自EventEmitter ,提供了以下几个事件。

 request :当客户端请求到来时,该事件被触发,提供两个参数 req 和 res ,分别是http.ServerRequest 和 http.ServerResponse 的实例,表示请求和响应信息。

 connection :当 TCP 连接建立时,该事件被触发,提供一个参数 socket ,为net.Socket 的实例。 connection 事件的粒度要大于 request ,因为客户端在Keep-Alive 模式下可能会在同一个连接内发送多次请求。

 close :当服务器关闭时,该事件被触发。注意不是在用户连接断开时。除此之外还有 checkContinue 、 upgrade 、 clientError 事件,通常我们不需要关心,只有在实现复杂的 HTTP 服务器的时候才会用到。

在这些事件中,最常用的就是 request 了,因此 http 提供了一个捷径:
http.createServer([requestListener]),功能是创建一个 HTTP 服务器并将requestListener 作为 request 事件的监听函数,这也是我们前面例子中使用的方法。
事实上它显式的实现方法是:

//httpserver.js
var http = require('http');
var server = new http.Server();
server.on('request', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>Hello World</p>');
});
server.listen(3000);
console.log("HTTP server is listening at port 3000.");

http.ServerRequest

http.ServerRequest 是 HTTP 请求的信息,是后端开发者最关注的内容。它一般由http.Server 的 request 事件发送,作为第一个参数传递,通常简称 request 或 req 。
ServerRequest 提供一些属性,表 4-2 中列出了这些属性。
HTTP 请求一般可以分为两部分:请求头(Request Header)和请求体(Requset Body)。
以上内容由于长度较短都可以在请求头解析完成后立即读取。而请求体可能相对较长,
需要一定的时间传输,因此 http.ServerRequest 提供了以下3个事件用于控制请求体
传输。

 data :当请求体数据到来时,该事件被触发。该事件提供一个参数 chunk ,表示接
收到的数据。如果该事件没有被监听,那么请求体将会被抛弃。该事件可能会被调
用多次。
 end :当请求体数据传输完成时,该事件被触发,此后将不会再有数据到来。
 close : 用户当前请求结束时,该事件被触发。不同于 end ,如果用户强制终止了传输,也还是调用 close 。
表4-2
在这里插入图片描述

获取 GET 请求内容

我们如何接受客户端的表单请求呢?由于 GET 请求直接被嵌入在路径中,URL是完整的请求路径,包括了 ? 后面的部分,因此你可以手动解析后面的内容作为 GET请求的参数。Node.js 的== url 模块中的 parse 函数==提供了这个功能,例如:

//httpserverrequestget.js
var http = require('http');
var url = require('url');
var util = require('util');
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(util.inspect(url.parse(req.url, true)));
}).listen(3000);

在浏览器中访问 http://127.0.0.1:3000/user?name=byvoid&email=byvoid@byvoid.com,我
们可以看到浏览器返回的结果:

{
 search: '?name=byvoid&email=byvoid@byvoid.com',
query: { name: 'byvoid', email: 'byvoid@byvoid.com' },
pathname: '/user',
path: '/user?name=byvoid&email=byvoid@byvoid.com',
href: '/user?name=byvoid&email=byvoid@byvoid.com'
 }

通过 url.parse,原始的 path 被解析为一个对象,其中== query 就是我们所谓的 GET请求的内容==,而路径则是 pathname

获取 POST 请求内容

HTTP 协议 1.1 版本提供了8种标准的请求方法,其中最常见的就是 GET 和 POST。相比GET 请求把所有的内容编码到访问路径中,POST 请求的内容全部都在请求体中。http.ServerRequest 并没有一个属性内容为请求体,原因是等待请求体传输可能是一件耗时的工作,譬如上传文件。而很多时候我们可能并不需要理会请求体的内容,恶意的 POST
请求会大大消耗服务器的资源。所以 Node.js 默认是不会解析请求体的
当你需要的时候,需要手动来做。让我们看看实现方法:

//httpserverrequestpost.js
var http = require('http');
var querystring = require('querystring');
var util = require('util');
http.createServer(function(req, res) {
var post = '';
req.on('data', function(chunk) {
post += chunk;
});
req.on('end', function() {
post = querystring.parse(post);
res.end(util.inspect(post));
});
}).listen(3000);

上面代码并没有在请求响应函数中向客户端返回信息,而是定义了一个 post 变量,用于在闭包中暂存请求体的信息。通过 req 的 data 事件监听函数,每当接受到请求体的数据,就累加到 post 变量中。
在 end 事件触发后,通过querystring.parse 将 post 解析为真正的 POST 请求格式,然后向客户端返回。

不要在真正的生产应用中使用上面这种简单的方法来获取 POST 请
求,因为它有严重的效率问题和安全问题,这只是一个帮助你理解的示例。

http.ServerResponse

http.ServerResponse 是返回给客户端的信息,决定了用户最终能看到的结果。它也是由 http.Server 的 request 事件发送的,作为第二个参数传递,一般简称为response 或 res 。http.ServerResponse 有三个重要的成员函数,用于返回响应头、响应内容以及结束请求。

 response.writeHead(statusCode, [headers]) :向请求的客户端发送响应头。statusCode 是 HTTP 状态码,如 200 (请求成功)、404 (未找到)等。 headers是一个类似关联数组的对象,表示响应头的每个属性。该函数在一个请求内最多只能调用一次,如果不调用,则会自动生成一个响应头。
 response.write(data, [encoding]) :向请求的客户端发送响应内容。 data 是一个 Buffer 或字符串,表示要发送的内容。如果 data 是字符串,那么需要指定encoding 来说明它的编码方式,默认是 utf-8 。在 response.end 调用之前,
response.write 可以被多次调用。
 response.end([data], [encoding]) :结束响应,告知客户端所有发送已经完成。当所有要返回的内容发送完毕的时候,该函数 必须 被调用一次。它接受两个可选参数,意义和 response.write 相同。如果不调用该函数,客户端将永远处于等待状态。

HTTP 客户端

http 模块提供了两个函数 http.request 和 http.get ,功能是作为客户端向 HTTP服务器发起请求。

http.request(options, callback)发起 HTTP 请求。接受两个参数, option 是一个类似关联数组的对象,表示请求的参数, callback 是请求的回调函数。

option常用的参数如下所示。
 host :请求网站的域名或 IP 地址。
 port :请求网站的端口,默认 80。
 method :请求方法,默认是 GET。
 path :请求的相对于根的路径,默认是“ / ”。 QueryString 应该包含在其中。
例如 /search?query=byvoid 。
 headers :一个关联数组对象,为请求头的内容。
callback 传递一个参数,为 http.ClientResponse 的实例。
http.request 返回一个 http.ClientRequest 的实例。

下面是一个通过 http.request 发送 POST 请求的代码:

//httprequest.js
var http = require('http');
var querystring = require('querystring');
var contents = querystring.stringify({
name: 'byvoid',
email: 'byvoid@byvoid.com',
address: 'Zijing 2#, Tsinghua University',
});
var options = {
host: 'www.byvoid.com',
path: '/application/node/post.php',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length' : contents.length
}
};
var req = http.request(options, function(res) {
res.setEncoding('utf8');
res.on('data', function (data) {
console.log(data);
});
});
req.write(contents);
req.end();

运行后结果如下:

array(3) {
["name"]=>
string(6) "byvoid"
["email"]=>
string(17) "byvoid@byvoid.com"
["address"]=>
string(30) "Zijing 2#, Tsinghua University"
}

不要忘了通过 req.end() 结束请求,否则服务器将不会收到信息。

 http.get(options, callback) http 模块还提供了一个更加简便的方法用于处理GET请求: http.get 。它是 http.request 的简化版,唯一的区别在于 http.get自动将请求方法设为了 GET 请求,同时不需要手动调用 req.end() 。

//httpget.js
var http = require('http');
http.get({host: 'www.byvoid.com'}, function(res) {
res.setEncoding('utf8');
res.on('data', function (data) {
console.log(data);
});
});

http.ClientRequest

http.ClientRequest 是由 http.request 或 http.get 返回产生的对象,表示一个已经产生而且正在进行中的 HTTP 请求。它提供一个 response 事件,即 http.request或 http.get 第二个参数指定的回调函数的绑定对象。我们也可以显式地绑定这个事件的监听函数:

//httpresponse.js
var http = require('http');
var req = http.get({host: 'www.byvoid.com'});
req.on('response', function(res) {
res.setEncoding('utf8');
res.on('data', function (data) {
console.log(data);
});
});

http.ClientRequest 像 http.ServerResponse 一样也提供了 write 和 end 函数,用于向服务器发送请求体,通常用于 POST、PUT 等操作。所有写结束以后必须调用 end函数以通知服务器,否则请求无效。 http.ClientRequest 还提供了以下函数。

 request.abort() :终止正在发送的请求。
 request.setTimeout(timeout, [callback]) :设置请求超时时间, timeout 为毫秒数。当请求超时以后, callback 将会被调用。
此外还有 request.setNoDelay([noDelay]) 、 request.setSocketKeepAlive
([enable] , [initialDelay]) 等函数,具体内容请参见 Node.js 文档。

http.ClientResponse

http.ClientResponse 与 http.ServerRequest 相似,提供了三个事件 data 、 end和 close ,分别在数据到达、传输结束和连接结束时触发,其中 data 事件传递一个参数chunk ,表示接收到的数据。http.ClientResponse 也提供了一些属性,用于表示请求的结果状态,参见表 4-3。
在这里插入图片描述
http.ClientResponse 还提供了以下几个特殊的函数。

 response.setEncoding([encoding]) :设置默认的编码,当 data 事件被触发
时,数据将会以 encoding 编码。默认值是 null ,即不编码,以 Buffer 的形式存
储。常用编码为 utf8。
 response.pause() :暂停接收数据和发送事件,方便实现下载功能。
 response.resume() :从暂停的状态中恢复。

使用Node.js进行Web开发

Node.js 和 PHP、Perl、ASP、JSP 一样,目的都是实现动态网页,也就是说由服务器动态生成 HTML 页面
之所以要这么做,是因为静态 HTML 的可扩展性非常有限,无法与用户有效交互。同时如果有大量相似的内容,例如产品介绍页面,那么1000个产品就要1000个静态的 HTML 页面,维护这1000个页面简直是一场灾难,因此动态生成 HTML 页面的技术应运而生。
最早实现动态网页的方法是使用Perl 和 CGI。在 Perl 程序中输出 HTML 内容,由 HTTP服务器调用 Perl 程序,将结果返回给客户端。这种方式在互联网刚刚兴起的 20 世纪 90 年代非常流行,几乎所有的动态网页都是这么做的。但问题在于如果 HTML 内容比较多,维护非常不方便。
大概在 2000 年左右,以 ASP、PHP、JSP 的为代表的以模板为基础的语言出现了,这种语言的使用方法与 CGI 相反,是在以 HTML 为主的模板中插入程序代码。
这种方式在2002年前后非常流行,但它的问题是页面和程序逻辑紧密耦合,任何一个网站规模变大以后,都会遇到结构混乱,难以处理的问题。
为了解决这种问题,以 MVC 架构为基础的平台逐渐兴起,著名的 Ruby on Rails、Django、Zend Framework 都是基于 MVC 架构的。
MVC (Model-View-Controller,模型视图控制器)是一种软件的设计模式,它最早是由 20 世纪 70 年代的 Smalltalk 语言提出的,即把一个复杂的软件工程分解为三个层面:模型、视图和控制器。

 模型是对象及其数据结构的实现,通常包含数据库操作。
 视图表示用户界面,在网站中通常就是 HTML 的组织结构。
 控制器用于处理用户请求和数据流、复杂模型,将输出传递给视图。

在这里插入图片描述
Node.js 提供了 http 模块,它是由 C++ 实现的,性能可靠,可以直接应用到生产环境。
图5-1 是一个简单的架构示意图。
在这里插入图片描述
Node.js 和其他的语言相比的另一个显著区别,在于它的原始封装程度较低。很多工作需要你自己来做(并
不是都要自己动手,因为有第三方框架的帮助)。

使用 http 模块

Node.js 由于不需要另外的 HTTP 服务器,因此减少了一层抽象,给性能带来不少提升,
但同时也因此而提高了开发难度。举例来说,我们要实现一个 POST 数据的表单,例如:

<form method="post" action="http://localhost:3000/">
<input type="text" name="title" />
<textarea name="text"></textarea>
<input type="submit" />
</form>

这个表单包含两个字段: title 和 text ,提交时以 POST 的方式将请求发送给http://localhost:3000/。假设我们要实现的功能是将这两个字段的东西原封不动地返回给用户,PHP 只需写两行代码,储存为 index.php 放在网站根目录下即可:

echo $_POST['title'];
echo $_POST['text'];

(用 http 模块):

var http = require('http');
var querystring = require('querystring');
var server = http.createServer(function(req, res) {
var post = '';
req.on('data', function(chunk) {
post += chunk;
});
req.on('end', function() {
post = querystring.parse(post);
res.write(post.title);
res.write(post.text);
res.end();
});
}).listen(3000);

Node.js 完成这样一个简单任务竟然如此复杂:你需要先创建一个 http 的实例,在其请求处理函数中手动编写
req 对象的事件监听器。当客户端数据到达时,将 POST 数据暂存在闭包的变量中,直到 end事件触发,解析 POST 请求,处理后返回客户端。其实这个比较是不公平的,PHP 之所以显得简单并不是因为它没有做这些事,而是因为
PHP 已经将这些工作完全封装好了,只提供了一个高层的接口,而 Node.js 的 http 模块提供的是底层的接口,尽管使用起来复杂,却可以让我们对 HTTP 协议的理解更加清晰。
但是,我们并不是为了理解 HTTP 协议才来使用 Node.js 的,作为 Web 应用开发者,我们不需要知道实现的细节,更不想与这些细节纠缠从而降低开发效率。难道 Node.js 的抽象如此之差,把不该有的细节都暴露给了开发者吗?
实际上,Node.js 虽然提供了 http 模块,却不是让你直接用这个模块进行 Web 开发的。http 模块仅仅是一个 HTTP 服务器内核的封装,你可以用它做任何 HTTP 服务器能做的事情,不仅仅是做一个网站,甚至实现一个 HTTP 代理服务器都行。你如果想用它直接开发网站,那么就必须手动实现所有的东西了,小到一个 POST 请求,大到 Cookie、会话的管理。
当你用这种方式建成一个网站的时候,你就几乎已经做好了一个完整的框架了。

Express框架

Express ( http://expressjs.com/ ) 除了为 http 模块提供了更高层的接口外,还实现了
许多功能,其中包括:
 路由控制;
 模板解析支持;
 动态视图;
 用户会话;
 CSRF 保护;
 静态文件服务;
 错误控制器;
 访问日志;
 缓存;
 插件支持。
它只是一个轻量级的 Web 框架,多数功能只是对 HTTP 协议中常用操作的封装,更多的功能需要插件或者整合其他模块来完成。
下面用 Express 重新实现前面的例子:

var express = require('express');
var app = express.createServer();
app.use(express.bodyParser());
app.all('/', function(req, res) {
res.send(req.body.title + req.body.text);
});
app.listen(3000);

可以看到,我们不需要手动编写 req 的事件监听器了,只需加载 express.bodyParser()就能直接通过 req.body 获取 POST 的数据了。

建立工程

通过以下命令建立网站基本结构:
express -t ejs microblog
当前目录下出现了子目录 microblog,并且产生了一些文件:
create : microblog
create : microblog/package.json
create : microblog/app.js
create : microblog/public
create : microblog/public/javascripts
create : microblog/public/images
create : microblog/public/stylesheets
create : microblog/public/stylesheets/style.css
create : microblog/routes
create : microblog/routes/index.js
create : microblog/views
create : microblog/views/layout.ejs
create : microblog/views/index.ejs
dont forget to install dependencies:
$ cd microblog && npm install
在这里插入图片描述
它还提示我们要进入其中运行 npm install, 我们依照指示,结果如下:

ejs@0.6.1 ./node_modules/ejs
express@2.5.8 ./node_modules/express
-- qs@0.4.2
-- mime@1.2.4
-- mkdirp@0.3.0
-- connect@1.8.5

它自动安装了依赖 ejs 和 express。这是为什么呢?检查目录中的 package.json 文件,内
容是:

【① ejs (Embedded JavaScript) 是一个标签替换引擎,其语法与 ASP、PHP 相似,易于学习,目前被广泛应用。Express默认提供的引擎是 jade,它颠覆了传统的模板引擎,制定了一套完整的语法用来生成 HTML 的每个标签结构,功能强大但不易学习。】

{
"name": "microblog"
, "version": "0.0.1"
, "private": true
, "dependencies": {
"express": "2.5.8"
, "ejs": ">= 0.0.1"
}
}

其中 dependencies 属性中有 express 和 ejs 。无参数的 npm install 的功能就是检查当前目录下的 package.json,并自动安装所有指定的依赖。

工程的结构

现在让我们回过头来看看 Express 都生成了哪些文件。除了 package.json,它只产生了两个 JavaScript 文件 app.js 和 routes/index.js。模板引擎 ejs 也有两个文件 index.ejs 和layout.ejs,此外还有样式表 style.css。下面来详细看看这几个文件。

  1. app.js
    app.js 是工程的入口,我们先看看其中有什么内容:
/**
* Module dependencies.
*/
var express = require('express')
, routes = require('./routes');
var app = module.exports = express.createServer();
// Configuration
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
app.use(express.errorHandler());
});
// Routes
app.get('/', routes.index);
app.listen(3000);
console.log("Express server listening on port %d in %s mode", app.address().port,
app.settings.env);

首先我们导入了 Express 模块,前面已经通过 npm 安装到了本地,在这里可以直接通过require 获取。 routes 是一个文件夹形式的本地模块,即 ./routes/index.js ,它的功能是为指定路径组织返回内容,相当于 MVC 架构中的控制器。通过 express.createServer()函数创建了一个应用的实例,后面的所有操作都是针对于这个实例进行的。
接下来是三个 app.configure 函数,分别指定了通用、开发和产品环境下的参数。
第一个 app.configure 直接接受了一个回调函数,后两个则只能在开发和产品环境中调用。
app.set 是 Express 的参数设置工具,接受一个键(key)和一个值(value),可用的参数如下所示。

 basepath :基础地址,通常用于 res.redirect() 跳转。
 views :视图文件的目录,存放模板文件。
 view engine :视图模板引擎。
 view options :全局视图参数对象。
 view cache :启用视图缓存。
 case sensitive routes :路径区分大小写。
 strict routing :严格路径,启用后不会忽略路径末尾的“ / ”。
 jsonp callback :开启透明的 JSONP 支持。
Express 依赖于 connect,提供了大量的中间件,可以通过 app.use 启用。 app.configure
中启用了5个中间件: bodyParser 、 methodOverride 、 router 、 static 以及 errorHandler 。
bodyParser 的功能是解析客户端请求,通常是通过 POST 发送的内容。
methodOverride
用于支持定制的 HTTP 方法。
router是项目的路由支持。 static 提供了静态文件支持。
errorHandler 是错误控制器。
app.get(’/’, routes.index); 是一个路由控制器,用户如果访问“ / ”路径,则由 routes.index 来控制。
最后服务器通过 app.listen(3000); 启动,监听3000端口。

  1. routes/index.js
    routes/index.js 是路由文件,相当于控制器,用于组织展示的内容:
/*
* GET home page.
*/
exports.index = function(req, res) {
res.render('index', { title: 'Express' });
};

app.js 中通过 app.get(’/’, routes.index); 将“ / ”路径映射到 exports.index函数下。
其中只有一个语句 res.render(‘index’, { title: ‘Express’ }) ,功能是调用模板解析引擎,翻译名为 index 的模板,并传入一个对象作为参数,这个对象只有一个属性,即 title: ‘Express’ 。

3. index.ejs

index.ejs 是模板文件,即 routes/index.js 中调用的模板,内容是:

<h1><%= title %></h1>
<p>Welcome to <%= title %></p>

它的基础是 HTML 语言,其中包含了形如 <%= title %> 的标签,功能是显示引用的
变量,即 res.render 函数第二个参数传入的对象的属性。

4. layout.ejs

模板文件不是孤立展示的,默认情况下所有的模板都继承自 layout.ejs,即 <%- body %>
部分才是独特的内容,其他部分是共有的,可以看作是页面框架。

<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<%- body %>
</body>
</html>

以上就是一个基本的工程结构,十分简单,功能划分却非常清楚。我们会在后面的小节
中基于这个工程继续完善,直到实现一个功能完整的网站。

路由控制

工作原理

当通过浏览器访问 app.js 建立的服务器时,会看到一个简单的页面,实际上它已经完成了许多透明的工作,现在就让我们来解释一下它的工作机制,以帮助理解网站的整体架构。
访问 http://localhost:3000,浏览器会向服务器发送以下请求:

GET / HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.142
Safari/535.19
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh;q=0.8,en-US;q=0.6,en;q=0.4
Accept-Charset: UTF-8,*;q=0.5

其中第一行是请求的方法、路径和 HTTP 协议版本,后面若干行是 HTTP 请求头。 app 会解析请求的路径,调用相应的逻辑。app.js 中有一行内容是 app.get(’/’, routes.index) ,它的作用是规定路径为“ / ”的 GET 请求由 routes.index 函数处理。 routes.index 通过 res.render(‘index’, { title: ‘Express’ }) 调用视图模板 index ,传递 title
变量。最终视图模板生成 HTML 页面,返回给浏览器,返回的内容是:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 202
Connection: keep-alive
<!DOCTYPE html>
<html>
<head>
<title>Express</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1>Express</h1>
<p>Welcome to Express</p>
</body>
</html>

浏览器在接收到内容以后,经过分析发现要获取 /stylesheets/style.css,因此会再次向服务器发起请求。app.js 中并没有一个路由规则指派到 /stylesheets/style.css,但 app通过app.use(express.static(__dirname + ‘/public’)) 配置了静态文件服务器,因此/stylesheets/style.css 会定向到 app.js 所在目录的子目录中的文件 public/stylesheets/style.css,
向客户端返回以下信息:

HTTP/1.1 200 OK
X-Powered-By: Express
Date: Mon, 02 Apr 2012 15:56:55 GMT
Cache-Control: public, max-age=0
Last-Modified: Mon, 12 Mar 2012 12:49:50 GMT
ETag: "110-1331556590000"
Content-Type: text/css; charset=UTF-8
Accept-Ranges: bytes
Content-Length: 110
Connection: keep-alive
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

在这里插入图片描述
这是一个典型的 MVC 架构,浏览器发起请求,由路由控制器接受,根据不同的路径定向到不同的控制器。控制器处理用户的具体请求,可能会访问数据库中的对象,即模型部分。控制器还要访问模板引擎,生成视图的 HTML,最后再由控制器返回给浏览器,完成一次请求。

创建路由规则

当我们在浏览器中访问譬如 http://localhost:3000/abc 这样不存在的页面时,服务器会在响应头中返回 404 Not Found 错误
这是因为 /abc 是一个不存在的路由规则,而且它也不是一个 public 目录下的文件,所以Express返回了404 Not Found的错误。接下来我们会讲述如何创建路由规则。
假设我们要创建一个地址为 /hello 的页面,内容是当前的服务器时间,让我们看看具体做法。
打开 app.js,在已有的路由规则 app.get(’/’, routes.index) 后面添加一行:
app.get('/hello', routes.hello);
修改 routes/index.js,增加 hello 函数:

/*
* GET home page.
*/
exports.index = function(req, res) {
res.render('index', { title: 'Express' });
};
exports.hello = function(req, res) {
res.send('The time is ' + new Date().toString());
};

重启 app.js,在浏览器中访问 http://localhost:3000/hello , 刷新页面可以看到时间发生变化,因为你看到的内容是动态生成的结果。

服务器在开始监听之前,设置好了所有的路由规则,当请求到达时直接分配到响应函数。app.get 是路由规则创建函数,它接受两个参数,第一个参数是请求的路径,第二个参数是一个回调函数,该路由规则被触发时调用回调函数,其参数表传递两个参数,分别是 req和 res ,表示请求信息和响应信息。

路径匹配

上面的例子是为固定的路径设置路由规则,Express 还支持更高级的路径匹配模式。例如我们想要展示一个用户的个人页面,路径为 /user/[username],可以用下面的方法定义路由规则:

app.get('/user/:username', function(req, res) {
res.send('user: ' + req.params.username);
});

路径规则 /user/:username 会被自动编译为正则表达式,类似于 /user/([^/]+)/?这样的形式。路径参数可以在响应函数中通过 req.params 的属性访问。路径规则同样支持 JavaScript 正则表达式,例如 app.get(/user/([^/]+)/?,callback) 。这样的好处在于可以定义更加复杂的路径规则,而不同之处是匹配的参数是匿名的,因此需要通过 req.params[0] 、 req.params[1] 这样的形式访问。

REST 风格的路由规则

Express 支持 REST 风格的请求方式,在介绍之前我们先说明一下什么是 REST。REST 的意思是 表征状态转移(Representational State Transfer),它是一种基于 HTTP 协议的网络应用的接口风格,充分利用 HTTP 的方法实现统一风格接口的服务。HTTP 协议定义了以下8种标准的方法。

 GET:请求获取指定资源。
 HEAD:请求指定资源的响应头。
 POST:向指定资源提交数据。
 PUT:请求服务器存储一个资源。
 DELETE:请求服务器删除指定资源。
 TRACE:回显服务器收到的请求,主要用于测试或诊断。
 CONNECT:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。
 OPTIONS:返回服务器支持的HTTP请求方法。

其中我们经常用到的是 GET、POST、PUT 和 DELETE 方法。根据 REST 设计模式,这

种方法通常分别用于实现以下功能。
 GET:获取
 POST:新增
 PUT:更新
 DELETE:删除

这是因为这4种方法有不同的特点,按照定义,它们的特点如表 5-2 所示。
所谓安全是指没有副作用,即请求不会对资源产生变动,连续访问多次所获得的结果不受访问者的影响。
而幂等指的是重复请求多次与一次请求的效果是一样的,比如获取和更新操作是幂等的,这与新增不同。删除也是幂等的,即重复删除一个资源,和删除一次是一样的。
在这里插入图片描述
Express 对每种 HTTP 请求方法都设计了不同的路由绑定函数,例如前面例子全部是app.get ,表示为该路径绑定了 GET 请求,向这个路径发起其他方式的请求不会被响应。

在这里插入图片描述
例如我们要绑定某个路径的 POST 请求,则可以用 app.post(path, callback) 的方法。需要注意的是 app.all 函数,它支持把所有的请求方式绑定到同一个响应函数,是一个非常灵活的函数,在后面我们可以看到许多功能都可以通过它来实现。

控制权转移

Express 支持同一路径绑定多个路由响应函数,例如:

app.all('/user/:username', function(req, res) {
res.send('all methods captured');
});
app.get('/user/:username', function(req, res) {
res.send('user: ' + req.params.username);
});

但当你访问任何被这两条同样的规则匹配到的路径时,会发现请求总是被前一条路由规则捕获,==后面的规则会被忽略。==原因是 Express 在处理路由规则时,会优先匹配先定义的路由规则,因此后面相同的规则被屏蔽。

Express 提供了路由控制权转移的方法,即回调函数的第三个参数 next ,通过调用next() ,会将路由控制权转移给后面的规则,例如:

app.all('/user/:username', function(req, res, next) {
console.log('all methods captured');
next();
});
app.get('/user/:username', function(req, res) {
res.send('user: ' + req.params.username);
});

当访问被匹配到的路径时,如 http://localhost:3000/user/carbo,会发现终端中打印了 allmethods captured ,而且浏览器中显示了 user: carbo 。这说明请求先被第一条路由规则捕获,完成 console.log 使用 next() 转移控制权,又被第二条规则捕获,向浏览器返回了信息。
这是一个非常有用的工具,可以让我们轻易地实现中间件,而且还能提高代码的复用程度。

例如我们针对一个用户查询信息和修改信息的操作,分别对应了 GET 和 PUT 操作,而两者共有的一个步骤是检查用户名是否合法,因此可以通过 next() 方法实现:

var users = {
'byvoid': {
name: 'Carbo',
website: 'http://www.byvoid.com'
}
};
app.all('/user/:username', function(req, res, next) {
// 检查用户是否存在
if (users[req.params.username]) {
next();
} else {
next(new Error(req.params.username + ' does not exist.'));
}
});
app.get('/user/:username', function(req, res) {
// 用户一定存在,直接展示
res.send(JSON.stringify(users[req.params.username]));
});
app.put('/user/:username', function(req, res) {
// 修改用户信息
res.send('Done');
});

app.all 定义的这个路由规则实际上起到了中间件的作用,把相似请求的相同部分提取出来,有利于代码维护其他 next 方法如果接受了参数,即代表发生了错误。使用这种方法可以把错误检查分段化,降低代码耦合度。

模板引擎

什么是模板引擎

模板引擎(Template Engine)是一个从页面模板根据一定的规则生成 HTML 的工具。其功能是让代码嵌入在 HTML 中执行,以产生动态的页面,因此 PHP 堪称是最早的模板引擎的雏形。随后的 ASP、JSP 都沿用了这个模式,即建立个 HTML 页面模板,插入可执行的代码,运行时动态生成 HTML。按照这种模式,整个网站就由一个个的页面模板组成,所有的逻辑都嵌入在模板中。这种模式大大降低了动态网页开发的门槛,因此一开始很受欢迎,但随着规模的扩大它会遇到许多问题,下面列举几个主要的。

 页面功能逻辑与页面布局样式耦合,网站规模变大以后逐渐难以维护。
 语法复杂,对于非技术的网页设计者来说门槛较高,难以学习。
 功能过于全面,页面设计者可以在页面上编程,不利于功能划分,也使模板解析效
率降低。

这些问题制约了早期模板引擎的发展,直到 MVC 开发模式普及,模板引擎才开始遍地开花。现代的模板引擎是 MVC 的一部分,在功能划分上它严格属于视图部分,因此功能以生成 HTML 页面为核心,不会引入过多的编程语言的功能。相较于一门编程语言,它通常学习起来相当容易。

模板引擎的功能是将页面模板和要显示的数据结合起来生成 HTML 页面。它既可以运行在服务器端又可以运行在客户端,大多数时候它都在服务器端直接被解析为 HTML,解析完成后再传输给客户端,因此客户端甚至无法判断页面是否是模板引擎生成的。有时候模板引擎也可以运行在客户端,即浏览器中,典型的代表就是 XSLT,它以 XML 为输入,在客户端生成 HTML 页面。但是由于浏览器兼容性问题,XSLT 并不是很流行。目前的主流还是由服务器运行模板引擎。在 MVC 架构中,模板引擎包含在服务器端。控制器得到用户请求后,从模型获取数据,调用模板引擎。模板引擎以数据和页面模板为输入,生成 HTML 页面,然后返回给控制器,由控制器交回客户端。是模板引擎在 MVC 架构中的示意图。
在这里插入图片描述

使用模板引擎

基于 JavaScript 的模板引擎有许多种实现,我们推荐使用 ejs (Embedded JavaScript),因为它十分简单,而且与 Express 集成良好。由于它是标准 JavaScript 实现的,因此它不仅可以运行在服务器端,还可以运行在浏览器中。我们这一章的示例是在服务器端运行 ejs,这样减少了对浏览器的依赖,而且更符合传统架构的习惯。
我们在 app.js 中通过以下两个语句设置了模板引擎和页面模板的位置:

app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');

表明要使用的模板引擎是 ejs,页面模板在 views 子目录下。在 routes/index.js 的exports.index 函数中通过如下语句调用模板引擎:

res.render('index', { title: 'Express' });

res.render 的功能是调用模板引擎,并将其产生的页面直接返回给客户端。它接受两个参数,第一个是模板的名称,即 views 目录下的模板文件名,不包含文件的扩展名;第二个参数是传递给模板的数据,用于模板翻译。index.ejs 内容如下:

<h1><%= title %></h1>
<p>Welcome to <%= title %></p>

上面代码其中有两处 <%= title %> ,用于模板变量显示,它们在模板翻译时会被替换成 Express,因为 res.render 传递了 { title: ‘Express’ } 。
ejs 的标签系统非常简单,它只有以下3种标签。

 <% code %> :JavaScript 代码。
 <%= code %> :显示替换过 HTML 特殊字符的内容。
 <%- code %> :显示原始 HTML 内容。

我们可以用它们实现页面模板系统能实现的任何内容。

页面布局

上面的例子介绍了页面模板的翻译,但我们看到的不止这两行,原因是 Express 还自动套用了 layout.ejs,它的内容是:

<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<%- body %>
</body>
</html>

layout.ejs 是一个页面布局模板,它描述了整个页面的框架结构,默认情况下每个单独的页面都继承自这个框架,替换掉 <%- body %> 部分。这个功能通常非常有用,因为一般为了保持整个网站的一致风格,HTML 页面的 部分以及页眉页脚中的大量内容是重复的,因此我们可以把它们放在 layout.ejs 中。当然,这个功能并不是强制的,如果想关闭它,可以在 app.js 的中 app.configure 中添加以下内容,这样页面布局功能就被关闭了。

app.set('view options', {
layout: false
});

另一种情况是,一个网站可能需要不止一种页面布局,例如网站分前台展示和后台管理系统,两者的页面结构有很大的区别,一套页面布局不能满足需求。这时我们可以在页面模板翻译时指定页面布局,即设置 layout 属性,例如:

function(req, res) {
res.render('userlist', {
title: '用户列表后台管理系统',
layout: 'admin'
});
};

这段代码会在翻译 userlist 页面模板时套用 admin.ejs 作为页面布局。

片段视图

Express 的视图系统还支持片段视图 (partials),它就是一个页面的片段,通常是重复的内容,用于迭代显示。通过它你可以将相对独立的页面块分割出去,而且可以避免显式地使用 for 循环。让我们看一个例子,在 app.js 中新增以下内容:

app.get('/list', function(req, res) {
res.render('list', {
title: 'List',
items: [1991, 'byvoid', 'express', 'Node.js']
});
});

在 views 目录下新建 list.ejs,内容是:

<ul><%- partial('listitem', items) %></ul>

同时新建 listitem.ejs,内容是:

<li><%= listitem %></li>

访问 http://localhost:3000/list,可以在源代码中看到以下内容:

<!DOCTYPE html>
<html>
<head>
<title>List</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<ul><li>1991</li><li>byvoid</li><li>express</li><li>Node.js</li></ul>
</body>
</html>

partial 是一个可以在视图中使用函数,它接受两个参数,第一个是片段视图的名称,第二个可以是一个对象或一个数组,如果是一个对象,那么片段视图中上下文变量引用的就是这个对象;如果是一个数组,那么其中每个元素依次被迭代应用到片段视图。片段视图中上下文变量名就是视图文件名,例如上面的 ‘listitem’ 。

视图助手

Express 提供了一种叫做视图助手的工具,它的功能是允许在视图中访问一个全局的函数或对象,不用每次调用视图解析的时候单独传入。前面提到的 partial 就是一个视图助手。视图助手有两类,分别是静态视图助手和动态视图助手。这两者的差别在于,静态视图助手可以是任何类型的对象,包括接受任意参数的函数,但访问到的对象必须是与用户请求无关的,而动态视图助手只能是一个函数,这个函数不能接受参数,但可以访问 req 和 res 对象。

静态视图助手可以通过 app.helpers() 函数注册,它接受一个对象,对象的每个属性名称为视图助手的名称,属性值对应视图助手的值。动态视图助手则通过 app.dynamicHelpers()注册,方法与静态视图助手相同,但每个属性的值必须为一个函数,该函数提供 req 和 res ,
参见下面这个示例:

var util = require('util');
app.helpers({
inspect: function(obj) {
return util.inspect(obj, true);
}
});
app.dynamicHelpers({
headers: function(req, res) {
return req.headers;
}
});
app.get('/helper', function(req, res) {
res.render('helper', {
title: 'Helpers'
});
});

对应的视图helper、ejs的内容如下:
<%=inspect(headers)%>
访问 http://localhost:3000/helper 可以看到如图内容。
在这里插入图片描述
视图助手的本质其实就是给所有视图注册了全局变量,因此无需每次在调用模板引擎时传递数据对象。当我们在后面使用 session 时会发现它是非常有用的。

P102建立微博网站

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于微信小程序的家政服务预约系统采用PHP语言和微信小程序技术,数据库采用Mysql,运行软件为微信开发者工具。本系统实现了管理员和客户、员工三个角色的功能。管理员的功能为客户管理、员工管理、家政服务管理、服务预约管理、员工风采管理、客户需求管理、接单管理等。客户的功能为查看家政服务进行预约和发布自己的需求以及管理预约信息和接单信息等。员工可以查看预约信息和进行接单。本系统实现了网上预约家政服务的流程化管理,可以帮助工作人员的管理工作和帮助客户查询家政服务的相关信息,改变了客户找家政服务的方式,提高了预约家政服务的效率。 本系统是针对网上预约家政服务开发的工作管理系统,包括到所有的工作内容。可以使网上预约家政服务的工作合理化和流程化。本系统包括手机端设计和电脑端设计,有界面和数据库。本系统的使用角色分为管理员和客户、员工三个身份。管理员可以管理系统里的所有信息。员工可以发布服务信息和查询客户的需求进行接单。客户可以发布需求和预约家政服务以及管理预约信息、接单信息。 本功能可以实现家政服务信息的查询和删除,管理员添加家政服务信息功能填写正确的信息就可以实现家政服务信息的添加,点击家政服务信息管理功能可以看到基于微信小程序的家政服务预约系统里所有家政服务的信息,在添加家政服务信息的界面里需要填写标题信息,当信息填写不正确就会造成家政服务信息添加失败。员工风采信息可以使客户更好的了解员工。员工风采信息管理的流程为,管理员点击员工风采信息管理功能,查看员工风采信息,点击员工风采信息添加功能,输入员工风采信息然后点击提交按钮就可以完成员工风采信息的添加。客户需求信息关系着客户的家政服务预约,管理员可以查询和修改客户需求信息,还可以查看客户需求的添加时间。接单信息属于本系统里的核心数据,管理员可以对接单的信息进行查询。本功能设计的目的可以使家政服务进行及时的安排。管理员可以查询员工信息,可以进行修改删除。 客户可以查看自己的预约和修改自己的资料并发布需求以及管理接单信息等。 在首页里可以看到管理员添加和管理的信息,客户可以在首页里进行家政服务的预约和公司介绍信息的了解。 员工可以查询客户需求进行接单以及管理家政服务信息和留言信息、收藏信息等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值