1.初识Express
Express 网站上是这样介绍 Express 的: “精简的、灵活的 Node.js Web 程序框架,为构建单页、多页及混合的 Web 程序提供了一系列健壮的功能特性。 ”这究竟是什么意思呢?下面我们来逐一解读一下。
精简
这是 Express 最吸引人的特性之一。框架开发者经常会忘掉“少即是多”这一基本原则。Express 的哲学是在你的想法和服务器之间充当薄薄的一层。这并不意味着它不够健壮,或者没有足够的有用特性,而是尽量少干预你,让你充分表达自己的思想,同时提供一些有用的东西。
灵活
Express 哲学中的另一个关键点是可扩展。Express 提供了一个非常精简的框架,你可以根据自己的需要添加 Express 功能中的不同部分,替换掉不能满足需要的部分。这种做法很新鲜。很多框架把什么都给你了,一行代码还没写,你拥有的就已经是一个臃肿、神秘而复杂的项目了。通常,你的第一项任务就是把不需要的功能砍掉,或者替换掉不能满足需求的功能。Express 则采取了截然不同的方式,让你在需要时才去添加东西。
Web程序框架
这里需要琢磨一下语义了。什么是 Web 程序?这意味着 Express 就不能做出网站或者网页了吗?不,网站是 Web 程序,网页也是 Web 程序。但 Web 程序的含义不止这些,它还可以向其他 Web 程序提供功能(还有别的) 。一般而言, “程序”是具有功能的,它不止是内容的静态集合(尽管这也是非常简单的 Web 程序) 。尽管现在“程序” (在你的设备本地运行的东西)和“网页” (通过网络为你的设备服务的东西)之间有明显的界限,但这种界限渐渐变得模糊了,这要感谢 PhoneGap 这样的项目,同时也要感谢微软允许 HTML5 像本地应用程序一样在桌面上运行。不难想象,几年之内程序和网站之间的界限将不复存在。
单页Web程序
单页 Web 程序是比较新颖的想法。不像之前的网站,用户每次访问不同的页面都要发起网络请求,单页 Web 程序把整个网站(或很大一部分)都下载到客户端浏览器上。经过初始下载后,用户访问不同页面的速度更快了,因为几乎不需要或者只要很少的服务端通信。单页程序的开发可以使用 Angular 或 Ember 等流行框架,Express 跟它们都
配合得很好。
多页和混合的Web程序
多页 Web 程序是更传统的方式。网站上的每个页面都是通过向服务器发起单独的请求得到的。这种方式确实比较传统,但这并不意味着它没有优点,或者说单页程序更好。只是现在有更多选择了,你可以决定哪些内容应该作为单页程序提供,哪些应该通过不同的请求提供。 “混合”说的就是同时使用这两种方式的网站。
如果你还是很困惑 Express 究竟是什么,不用担心。有时候只管把某些东西拿来用就好了,不用先理解它是什么,本书将教你如何用 Express 开发 Web 程序。
2.用Node实现的简单Web服务器
如果你之前曾经做过静态的 HTML 网站,或者有 PHP 或 ASP 背景,可能习惯用 Web 服务器(比如 Apache 或 IIS)提供静态文件服务,以便使用浏览器通过网络查看这些文件。比如说,如果你创建了一个名为 about.html 的文件,并把它放到了恰当的目录下,然后就可以访问 http://localhost/about.html 查看这个文件。根据 Web 服务器的配置,你甚至可以省略 .html,但 URL 和文件名之间的关系很清晰:Web 服务器知道文件在机器的哪个地方,
并能把它返回给浏览器。
从 localhost 的名字就能看出来,它指的是你所在的机器。这是 IPv4 回环地址 127.0.0.1 或者 IPv6 回环地址 ::1 的常用别名。你应该更常见到 127.0.0.1,不过本书中用的是 localhost。如果你用的是远程的机器(比如通过 SSH 访问的) ,记得浏览 localhost 时访问的不是你眼前的那台机器。
Node 所提供的范式跟传统的 Web 服务器不同:你写的程序就是 Web 服务器。Node 只是给你提供了一个构建 Web 服务器的框架。你可能会说“但我不想写 Web 服务器” 。这是很自然的反应:你想写一个程序,而不是Web 服务器。然而在 Node 里编写 Web 服务器非常简单(甚至只需要几行代码) ,并且你因此取得了对程序的控制权,这是非常值得的。那么我们开始吧。如果你已经安装了 Node,也已经熟悉了终端,现在一切都准备好了。
2.1 Hello World
我发现正规的编程入门范例总是输出毫无创意的“Hello World”消息。但打破这样的传统似乎是不敬之举,所以我们也从这里开始吧,然后再去做一些更有趣的事情。用你喜欢的编辑器创建一个 helloWorld.js 文件:
var http = require('http');
http.createServer(function(req,res){
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello world!');
}).listen(3000);
console.log('Server started on localhost:3000; press Ctrl-C to terminate....');
确保是和 helloWorld.js 在同一个目录下,输入 node hello World.js。然后打开浏览器访问http://localhost:3000,你的第一个 Web 服务器就建成啦!这个服务器并没有返回 HTML,而只是向你的浏览器传递了一条普通的文本消息“Hello world!” 。如果你想要尝试发送HTML,可以试验一下:只要把 text/plain 换成 text/html,再把 'Hello world!' 换成一个包含有效 HTML 的字符串就行了。在这里就不演示了,因为我要尽量避免在 JavaScript里写 HTML。
2.2 事件驱动编程
Node 的核心理念是事件驱动编程。这对程序员来说,意味着你必须知道有哪些事件,以及如何响应这些事件。很多人接触事件驱动编程是从用户界面开始的:用户点击了什么,然后你处理“点击事件” 。这个类比很好,因为程序员不能控制用户什么时间点击或者是
否会点击,所以事件驱动编程真的很直观。在服务器上响应事件这种概念性的跳跃可能会
比较难,但原理是一样的。在前面那个例子中,事件是隐含的:HTTP 请求就是要处理的事件。http.createServer 方
法将函数作为一个参数,每次有 HTTP 请求发送过来就会调用那个函数。我们这个简单的程序只是把内容类型设为普通文本,并发送字符串“Hello world!” 。
2.3 路由
路由是指向客户端提供它所发出的请求内容的机制。对基于 Web 的客户端 / 服务器端程序而言,客户端在 URL 中指明它想要的内容,具体来说就是路径和查询字符串(第 6 章会详细讲解 URL 的组成部分) 。
我们扩展一下“Hello world!”那个例子,做些更有意思的事情。做一个有首页、关于页面和未找到页面的极其简单的网站。目前我们还像之前那个例子一样,不提供 HTML,只提供普通文本:
var http = require('http');
http.createServer(function(req,res){
// 规范化 url,去掉查询字符串、可选的反斜杠,并把它变成小写
var path = req.url.replace(/\/?(?:\?.*)?$/, '').toLowerCase();
switch(path) {
case '':
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Homepage');
break;
case '/about':
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('About');
break;
default:
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
break;
}
}).listen(3000);
console.log('Server started on localhost:3000; press Ctrl-C to terminate....');
运行这段代码,你会发现现在你可以访问首页 (http://localhost: 3000)和关于页面(http://localhost:3000/about) 。所有查询字符串都会被忽略(所以 http://localhost:3000/?foo=bar 也是返回首页) ,并且其他所有 URL(http://localhost:3000/foo)返回的都是未找到页面。
2.4 静态资源服务
现在我们有了一些可用的简单路由,接下来我们提供一些真正的 HTML 和 logo 图片。因为这些内容不会变化,所以它们都被称为“静态资源” (相对于股票之类的内容,你每次刷新页面,股价都会变化) 。
用 Node 提供静态资源只适用于初期的小型项目,对于比较大的项目,你应该会想用 Nginx 或 CDN 之类的代理服务器来提供静态资源。对此,第 16 章会有更多介绍。
如果你用过 Apache 或 IIS,可能习惯于只是创建一个 HTML 文件,访问它,然后让它自动发送到客户端。Node 不是那样的:我们必须打开文件,读取其中的内容,然后将这些内容发送给浏览器。所以我们要在项目里创建一个名为 public 的目录(在下一章中,你就会明白我们为什么不管它叫 static) 。在这个目录下创建文件 home.html、about.html、notfound.html,子目录 img,以及一个名为 img/logo.jpg 的图片。以上这些工作就由你自己来完成了:既然你在阅读这本书,那么你应该知道怎么编写 HTML 文件和找张图片。在你的 HTML 文件中这样引用 logo:
<img href="/img/logo.jpg" alt="logo">
接下来修改 helloWorld.js:
var http = require('http'),
fs = require('fs');
function serveStaticFile(res, path, contentType, responseCode) {
if(!responseCode) responseCode = 200;
fs.readFile(__dirname + path, function(err,data) {
if(err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('500 - Internal Error');
} else {
res.writeHead(responseCode,
{ 'Content-Type': contentType });
res.end(data);
}
});
}
http.createServer(function(req,res){
// 规范化 url,去掉查询字符串、可选的反斜杠,并把它变成小写
var path = req.url.replace(/\/?(?:\?.*)?$/, '')
.toLowerCase();
switch(path) {
case '':
serveStaticFile(res, '/public/home.html', 'text/html');
break;
case '/about':
serveStaticFile(res, '/public/about.html', 'text/html');
break;
case '/img/logo.jpg':
serveStaticFile(res, '/public/img/logo.jpg',
'image/jpeg');
break;
default:
serveStaticFile(res, '/public/404.html', 'text/html',
404);
break;
}
}).listen(3000);
console.log('Server started on localhost:3000; press Ctrl-C to terminate....');
这 个 例 子 中, 我 们 的 路 由 是 非 常 缺 乏 想 象 力 的。 如 果 你 访 问 http://localhost:3000/about,就返回 public/about.html 文件。你可以随意修改路由,也可以随意修改文件。比如说,如果你一周里的每一天都要换一个关于页
面,你可能会有 public/about_mon.html、public/about_tue.html 等之类的页面,在你的路由中定义好逻辑,从而在用户访问 http://localhost:3000/about 时能提供恰当的页面。注意,我们创建了一个辅助函数 serveStaticFile,它完成了大部分工作。fs.readFile 是读取文件的异步方法。这个函数有同步版本,fs.readFileSync,但这种异步思考问题的方式,你接触得越早越好。这个函数不复杂:它调用 fs.readFile 读取指定文件中的内容。fs.readFile 读取完文件后执行回调函数,如果文件不存在,或者读取文件时遇到许可权限方面的问题,会设定 err 变量,并且会返回一个 HTTP 500 的状态码表明服务器错误。如果文件读取成功,文件会带着特定的响应码和内容类型发给客户端。
*__dirname 会被解析为正在执行的脚本所在的目录。所以如果你的脚本放在/home/sites/app.js 中,则 __dirname 会被解析为 /home/sites。不管什么时候,这个全局变量用起来都很方便。如果不这么做,在不同的目录中运行你的程序时很可能会出现难以诊断的错误。*