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。不管什么时候,这个全局变量用起来都很方便。如果不这么做,在不同的目录中运行你的程序时很可能会出现难以诊断的错误。