第五章 使用Node.js进行Web开发 - Node.js开发指南

返回介绍

第五章 使用Node.js进行Web开发

发布于 2017-06-20 字数75344 浏览 995 评论 0

阅读到这一章为止,你已经学习了许多知识,但还缺乏实战性的内容。本章,我们打算从零开始用 Node.js 实现一个微博系统,功能包括路由控制、页面模板、数据库访问、用户注册、登录、用户会话等内容。

我们会介绍 Express 框架、MVC 设计模式、ejs 模板引擎以及 MongoDB 数据库的操作。通过实战演练,你将会了解到网站开发的基本方法。本章涉及的代码较多,所有的代码均可以在 http://www.byvoid.com/project/node 找到,但你最好还是亲自输入这些代码。现在就让我们开始一起动手来实现一个微博网站吧。

5.1 准备工作

在开始动手之前,我们首先要大致知道 Node.js 实现网站的工作原理。Node.js 和 PHP、Perl、ASP、JSP 一样,目的都是实现动态网页,也就是说由服务器动态生成 HTML 页面。

之所以要这么做,是因为静态 HTML 的可扩展性非常有限,无法与用户有效交互。同时如果有大量相似的内容,例如产品介绍页面,那么1000个产品就要1000个静态的 HTML 页面,维护这1000个页面简直是一场灾难,因此动态生成 HTML 页面的技术应运而生。

最早实现动态网页的方法是使用Perl (是C++,任何语言都可以,Perl只是最常见的。)和 CGI。在 Perl 程序中输出 HTML 内容,由 HTTP服务器调用 Perl 程序,将结果返回给客户端。这种方式在互联网刚刚兴起的 20 世纪 90 年代非常流行,几乎所有的动态网页都是这么做的。但问题在于如果 HTML 内容比较多,维护非常不方便。

大概在 2000 年左右,以 ASP、PHP、JSP 的为代表的以模板为基础的语言出现了,这种语言的使用方法与 CGI 相反,是在以 HTML 为主的模板中插入程序代码(例如 ASP 的 <% %> 和 PHP 的 <?php ?> 标签,在这些标签内添加处理代码。)。这种方式在2002年前后非常流行,但它的问题是页面和程序逻辑紧密耦合,任何一个网站规模变大以后,都会遇到结构混乱,难以处理的问题。

为了解决这种问题,以 MVC 架构为基础的平台逐渐兴起,著名的 Ruby on Rails、Django、Zend Framework 都是基于 MVC 架构的。MVC (Model-View-Controller,模型-视图-控制器)是一种软件的设计模式,它最早是由 20 世纪 70 年代的 Smalltalk 语言提出的,即把一个复杂的软件工程分解为三个层面:模型、视图和控制器。

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

我们称 PHP、ASP、JSP 为“模板为中心的架构”,表 5-1 是两种Web开发架构的一个对比。

表5-1 Web 开发架构对比
特性 模板为中心架构 MVC 架构
页面产生方式 执行并替换标签中的语句 由模板引擎生成 HTML 页面
路径解析 对应到文件系统 由控制器定义
数据访问 通过 SQL语句查询或访问文件系统 对象关系模型
架构中心 脚本语言是静态 HTTP 服务器的扩展 静态 HTTP 服务器是脚本语言的补充
适用范围 小规模网站 大规模网站
学习难度 容易 较难

注释:例如 http://example.com/hello/world.php 对应服务器上的 /hello/world.php 这个文件。当然这不是绝对的,现在很多PHP开发框架都是只提供单个入口,利用服务器的 Rewrite 支持实现了路径的自由控制。我们一般情况下指的是原生的(或默认的)支持。

这两种架构都出自原始的 CGI,但不同之处是前者走了一条粗放扩张的发展路线,由于易学易用,在几年前应用较广,而随着互联网规模的扩大,后者优势逐渐体现,目前已经成为主流。

Node.js 本质上和 Perl 或 C++ 一样,都可以作为 CGI 扩展被调用,但它还可以跳过 HTTP服务器,因为它本身就是。传统的架构中 HTTP 服务器的角色会由 Apache、Nginx、IIS 之类的软件来担任,而 Node.js 不需要(或者说不是必要的,因为你也可以把 Node.js 的服务器当作 Apache 或 Nginx 后端。)。Node.js 提供了 http 模块,它是由 C++ 实现的,性能可靠,可以直接应用到生产环境。图5-1 是一个简单的架构示意图。

第五章 使用Node.js进行Web开发

Node.js 和其他的语言相比的另一个显著区别,在于它的原始封装程度较低。例如 PHP 中你可以访问 $_REQUEST 获取客户端的 POST 或 GET 请求,通常不需要直接处理 HTTP 协议,比如我们需要知道 HTTP 成功响应时要返回一个 200 状态码,而不需要手动完成“返回 200 状态码”这项工作。但这不带表你可以轻易地切换到非 HTTP 协议,因为代码仍然是与 HTTP 协议耦合的。

这些语言要求由 HTTP 服务器来调用,因此你需要设置一个 HTTP 服务器来处理客户端的请求,HTTP 服务器通过 CGI 或其他方式调用脚本语言解释器,将运行的结果传递回HTTP 服务器,最终再把内容返回给客户端。而在 Node.js 中,很多工作需要你自己来做(并不是都要自己动手,因为有第三方框架的帮助)。

5.1.1 使用 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'];

在 3.5.1 节中使用了类似下面的方法(用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);

这种差别可能会让你大吃一惊,PHP 的实现要比Node.js容易得多。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、会话的管理。当你用这种方式建成一个网站的时候,你就几乎已经做好了一个完整的框架了。

5.1.2 Express 框架

npm 提供了大量的第三方模块,其中不乏许多 Web 框架,我们没有必要重复发明轮子,因而选择使用 Express 作为开发框架,因为它是目前最稳定、使用最广泛,而且 Node.js 官方推荐的唯一一个 Web 开发框架。

Express ( http://expressjs.com/ ) 除了为 http 模块提供了更高层的接口外,还实现了许多功能,其中包括:

  • 路由控制;
  • 模板解析支持;
  • 动态视图;
  • 用户会话;
  • CSRF 保护;
  • 静态文件服务;
  • 错误控制器;
  • 访问日志;
  • 缓存;
  • 插件支持。

需要指出的是,Express 不是一个无所不包的全能框架,像 Rails 或 Django 那样实现了模板引擎甚至 ORM (Object Relation Model,对象关系模型)。它只是一个轻量级的 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 的数据了。

5.2 快速开始

在上一小节我们已经介绍了 Web 开发的典型架构,我们选择了用 Express 作为开发框架来开发一个网站,从现在开始我们就要真正动手实践了。

5.2.1 安装 Express

首先我们要安装 Express。如果一个包是某个工程依赖,那么我们需要在工程的目录下使用本地模式安装这个包,如果要通过命令行调用这个包中的命令,则需要用全局模式安装(关于本地模式和全局模式,参见 3.3.4节),因此按理说我们使用本地模式安装 Express 即可。

但是Express 像很多框架一样都提供了 Quick Start(快速开始)工具,这个工具的功能通常是建立一个网站最小的基础框架,在此基础上完成开发。当然你可以完全自己动手,但我还是推荐使用这个工具更快速地建立网站。为了使用这个工具,我们需要用全局模式安装Express,因为只有这样我们才能在命令行中使用它。运行以下命令:

$ npm install -g express

等待数秒后安装完成,我们就可以在命令行下通过 express 命令快速创建一个项目了。在这之前先使用 express –help 查看帮助信息:

Usage: express [options] [path]
Options:
    -s, --sessions              add session support
    -t, --template <engine>     add template <engine> support (jade|ejs). default=jade
    -c, --css <engine>          add stylesheet <engine> support (stylus). default=plain css
    -v, --version               output framework version
    -h, --help                  output help information

Express 在初始化一个项目的时候需要指定模板引擎,默认支持Jade和ejs,为了降低学习难度我们推荐使用 ejs ,同时暂时不添加 CSS 引擎和会话支持。

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

5.2.2 建立工程

通过以下命令建立网站基本结构:

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 文件,内容是:

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

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

5.2.3 启动服务器

用 Express 实现的网站实际上就是一个 Node.js 程序,因此可以直接运行。我们运行 nodeapp.js,看到 Express server listening on port 3000 in development mode。

接下来,打开浏览器,输入地址 http://localhost:3000,你就可以看到一个简单的 Welcometo Express 页面了。如果你能看到如图5-2 所示的页面,那么说明你的设定正确无误。

第五章 使用Node.js进行Web开发

图5-2 Express 初始欢迎页面

要关闭服务器的话,在终端中按 Ctrl + C。注意,如果你对代码做了修改,要想看到修改后的效果必须重启服务器,也就是说你需要关闭服务器并再次运行才会有效果。如果觉得有些麻烦,可以使用 supervisor 实现监视代码修改和自动重启,具体使用方法参见 3.1.3 节。

注意命令行中显示服务器运行在开发模式下(development mode),因此不要在生产环境中部署它。我们会在 6.3 节中介绍如何在真实的生产环境下部署 Node.js 服务器。

5.2.4 工程的结构

现在让我们回过头来看看 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 的例子,这个文件长了不少,不过并不复杂。下面来分析一下这段代码。

首先我们导入了 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 方法(如PUT、DELETE等HTTP方法,浏览器是不支持的。)。router 是项目的路由支持。static 提供了静态文件支持。errorHandler 是错误控制器。

app.get(‘/’, routes.index); 是一个路由控制器,用户如果访问“ / ”路径,则由 routes.index 来控制。

最后服务器通过 app.listen(3000); 启动,监听3000端口。

2. 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>

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

5.3 路由控制

在上一节,我们已经讲过了如何使用 Express 建立一个基本工程,这个工程只包含一些基础架构,没有任何实际内容。从这一小节开始,我们将会讲述 Express 的基本使用方法,在前面例子的基础上逐步完善这个工程。

5.3.1 工作原理

当通过浏览器访问 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;
}

由 Express 创建的网站架构如图5-3 所示。

第五章 使用Node.js进行Web开发

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

5.3.2 创建路由规则

当我们在浏览器中访问譬如 http://localhost:3000/abc 这样不存在的页面时,服务器会在响应头中返回 404 Not Found 错误,浏览器显示如图5-4 所示。

第五章 使用Node.js进行Web开发

图5-4 访问不存在的页面时浏览器看到的结果

这是因为 /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,可以看到类似于图5-5 的页面,刷新页面可以看到时间发生变化,因为你看到的内容是动态生成的结果。

第五章 使用Node.js进行Web开发

图5-5 访问 /hello 时显示的内容

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

5.3.3 路径匹配

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

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

修改以后重启 app.js,访问 http://localhost:3000/user/byvoid,可以看到页面显示了以下内容:

user: byvoid

路径规则 /user/:username 会被自动编译为正则表达式,类似于 \/user\/([^\/]+)\/?这样的形式。路径参数可以在响应函数中通过 req.params 的属性访问。

路径规则同样支持 JavaScript 正则表达式,例如 app.get(\/user\/([^\/]+)\/?,callback)。这样的好处在于可以定义更加复杂的路径规则,而不同之处是匹配的参数是匿名的,因此需要通过 req.params[0]、req.params[1] 这样的形式访问。

5.3.4 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 设计模式,这4种方法通常分别用于实现以下功能。

  • GET:获取
  • POST:新增
  • PUT:更新
  • DELETE:删除

这是因为这4种方法有不同的特点,按照定义,它们的特点如表 5-2 所示。

所谓安全是指没有副作用,即请求不会对资源产生变动,连续访问多次所获得的结果不受访问者的影响。而幂等指的是重复请求多次与一次请求的效果是一样的,比如获取和更新操作是幂等的,这与新增不同。删除也是幂等的,即重复删除一个资源,和删除一次是一样的。

表5-2 REST风格HTTP 请求的特点
请求方式 安全 幂等
GET
POST
PUT
DELETE

Express 对每种 HTTP 请求方法都设计了不同的路由绑定函数,例如前面例子全部是app.get,表示为该路径绑定了 GET 请求,向这个路径发起其他方式的请求不会被响应。表 5-3 是 Express 支持的所有 HTTP 请求的绑定函数。

表5-3 Express 支持的 HTTP 请求的绑定函数
请求方式 绑定函数
GET app.get(path, callback)
POST app.post(path, callback)
PUT app.put(path, callback)
DELETE app.delete(path, callback)
PATCH app.patch(path, callback)
TRACE app.trace(path, callback)
CONNECT app.connect(path, callback)
OPTIONS app.options(path, callback)
所有方法 app.all(path, callback)

PATCH 方式是 IETF RFC 5789 ( http://tools.ietf.org/html/rfc5789 ) 新增的 HTTP 方法,功能定义是部分更新某个资源。

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

5.3.5 控制权转移

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方法如果接受了参数,即代表发生了错误。使用这种方法可以把错误检查分段化,降低代码耦合度。

5.4 模板引擎

上一节我们介绍了 Express 的路由控制方法,它是网站架构最核心的部分,即MVC架构中的控制器。在这一小节里,我们会讲述模板引擎的使用和集成,也就是视图。视图决定了用户最终能看到什么,因此也是最重要部分,这里我们以 ejs 为例介绍模板引擎的使用方法。

5.4.1 什么是模板引擎

模板引擎(Template Engine)是一个从页面模板根据一定的规则生成 HTML 的工具。它的发轫可以追溯到 1996 年 PHP 2.0 的诞生。

PHP 原本是 Personal Home Page Tools(个人主页工具)的简称,用于取代 Perl 和 CGI 的组合,其功能是让代码嵌入在 HTML 中执行,以产生动态的页面,因此 PHP 堪称是最早的模板引擎的雏形。随后的 ASP、JSP 都沿用了这个模式,即建立一个 HTML 页面模板,插入可执行的代码,运行时动态生成 HTML。

按照这种模式,整个网站就由一个个的页面模板组成,所有的逻辑都嵌入在模板中。这种模式大大降低了动态网页开发的门槛,因此一开始很受欢迎,但随着规模的扩大它会遇到许多问题,下面列举几个主要的。

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

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

模板引擎的功能是将页面模板和要显示的数据结合起来生成 HTML 页面。它既可以运行在服务器端又可以运行在客户端,大多数时候它都在服务器端直接被解析为 HTML,解析完成后再传输给客户端,因此客户端甚至无法判断页面是否是模板引擎生成的。

有时候模板引擎也可以运行在客户端,即浏览器中,典型的代表就是 XSLT,它以 XML 为输入,在客户端生成 HTML 页面。但是由于浏览器兼容性问题,XSLT 并不是很流行。目前的主流还是由服务器运行模板引擎。

在 MVC 架构中,模板引擎包含在服务器端。控制器得到用户请求后,从模型获取数据,调用模板引擎。模板引擎以数据和页面模板为输入,生成 HTML 页面,然后返回给控制器,由控制器交回客户端。图5-6 是模板引擎在 MVC 架构中的示意图。

第五章 使用Node.js进行Web开发

5.4.2 使用模板引擎

基于 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 内容。

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

5.4.3 页面布局

上面的例子介绍了页面模板的翻译,但我们看到的不止这两行,原因是 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 页面的<head>部分以及页眉页脚中的大量内容是重复的,因此我们可以把它们放在 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 作为页面布局。

5.4.4 片段视图

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’。

5.4.5 视图助手

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 可以看到如图5-7 所示的内容。

第五章 使用Node.js进行Web开发

图5-7 使用视图助手的页面

视图助手的本质其实就是给所有视图注册了全局变量,因此无需每次在调用模板引擎时传递数据对象。当我们在后面使用 session 时会发现它是非常有用的。

5.5 建立微博网站

在前面的几节中,我们已经对 Express 进行了基本的介绍,现在让我们动手开始创建一个微博网站吧。

5.5.1 功能分析

开发中的一个大忌就是没有想清楚要做什么就开始动手,因此我们准备在动手实践之前先规划一下网站的功能,即使是出于学习目的也不例外。首先,微博应该以用户为中心,因此需要有用户的注册和登录功能。

微博网站最核心的功能是信息的发表,这个功能涉及许多方面,包括数据库访问、前端显示等。一个完整的微博系统应该支持信息的评论、转发、圈点用户等功能,但出于演示目的,我们不能一一实现所有功能,只是实现一个微博社交网站的雏形。

5.5.2 路由规划

在完成功能设计以后,下一个要做的事情就是路由规划了。路由规划,或者说控制器规划是整个网站的骨架部分,因为它处于整个架构的枢纽位置,相当于各个接口之间的粘合剂,所以应该优先考虑。

根据功能设计,我们把路由按照以下方案规划。

  • /:首页
  • /u/[user]:用户的主页
  • /post:发表信息
  • /reg:用户注册
  • /login:用户登录
  • /logout:用户登出

以上页面还可以根据用户状态细分。发表信息以及用户登出页面必须是已登录用户才能操作的功能,而用户注册和用户登入所面向的对象必须是未登入的用户。首页和用户主页则针对已登入和未登入的用户显示不同的内容。

打开 app.js,把 Routes 部分修改为:

app.get('/', routes.index);
app.get('/u/:user', routes.user);
app.post('/post', routes.post);
app.get('/reg', routes.reg);
app.post('/reg', routes.doReg);
app.get('/login', routes.login);
app.post('/login', routes.doLogin);
app.get('/logout', routes.logout);

其中 /post、/login 和 /reg 由于要接受表单信息,因此使用 app.post 注册路由。/login和 /reg 还要显示用户注册时要填写的表单,所以要以 app.get 注册。同时在 routes/index.js中添加相应的函数:

exports.index = function(req, res) {
 res.render('index', { title: 'Express' });
};
exports.user = function(req, res) {
};
exports.post = function(req, res) {
};
exports.reg = function(req, res) {
};
exports.doReg = function(req, res) {
};
exports.login = function(req, res) {
};
exports.doLogin = function(req, res) {
};
exports.logout = function(req, res) {
};

我们将在5.6节介绍会话(session),说明如何管理用户的状态。

5.5.3 界面设计

我们在开发网站的时候必须时刻意识到网站是为用户开发的,因而用户界面是非常重要的。一种普遍的观点是后端的开发者不必太多关注前端用户体验,因为这是前端程序员和设计师要做的事情。

但实际上为了设计一个优雅的界面,后端程序员也不得不介入功能实现,因为很多时候前端和后端无法完全划分,仅仅靠前端开发者是无法设计出优美而又可用的界面的。

我并不是鼓励后端开发者越俎代疱,只是建议后端开发者略微了解前端的技术,以便于在大型工程中更好地合作。同时当没有前端开发者与你合作的时候,也可以设计出不至于太难看的页面。

作为后端开发者,你可能和我一样都不太擅长设计,不过没关系,我们可以利用已有的优秀设计。如果你认同 Twitter 的简洁风格,那么 Twitter Bootstrap 是最好的选择。

Twitter Bootstrap 是由 Twitter 的设计师和工程师发起的开源项目,它提供了一套与 Twitter 风格一致的简洁、优雅的 Web UI,包含了完全由 HTML、CSS、JavaScript 实现的用户交互工具。不管你是资深的前端工程师,还是专业的后端开发者,你都可以轻松地使用Twitter Bootstrap 制作出优美的界面。图5-8 是Twitter Bootstrap 部件的介绍页面。

第五章 使用Node.js进行Web开发

图5-8 Twitter Bootstrap

5.5.4 使用 Bootstrap

现在我们就用 Bootstrap 开始设计我们的界面。从http://twitter.github.com/bootstrap/下载bootstrap.zip,解压后可以看到以下文件:

css/bootstrap-responsive.css
css/bootstrap-responsive.min.css
css/bootstrap.css
css/bootstrap.min.css
img/glyphicons-halflings-white.png
img/glyphicons-halflings.png
js/bootstrap.js
js/bootstrap.min.js

其中所有的 JavaScript 和 CSS 文件都提供了开发版和产品版,前者是原始的代码,后者经过压缩,文件名中带有 min。将 img 目录复制到工程 public 目录下,将 bootstrap.css、bootstrap-responsive.css 复制到 public/stylesheets 中,将 bootstrap.js 复制到 public/javascripts 目录中,然后从http://jquery.com/下载一份最新版的 jquery.js 也放入 public/javascripts 目录中。

接下来,修改 views/layout.ejs:

<!DOCTYPE html>
<html>
<head>
    <title><%= title %> - Microblog</title>
    <link rel='stylesheet' href='/stylesheets/bootstrap.css' />
    <style type="text/css">
    body {
        padding-top: 60px;
        padding-bottom: 40px;
    }
    </style>
    <link href="stylesheets/bootstrap-responsive.css" rel="stylesheet">
</head>
<body>
    <div class="navbar navbar-fixed-top">
        <div class="navbar-inner">
            <div class="container">
                <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </a>
                <a class="brand" href="/">Microblog</a>
                <div class="nav-collapse">
                    <ul class="nav">
                        <li class="active"><a href="/">首页</a></li>
                        <li><a href="/login">登入</a></li>
                        <li><a href="/reg">注册</a></li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
    <div id="container" class="container">
        <%- body %>
        <hr />
        <footer>
            <p><a target="_blank" class="external-link" href="http://www.byvoid.com/" target="_blank">BYVoid</a> 2012</p>
        </footer>
    </div>
    <script src="/javascripts/jquery.js"></script>
    <script src="/javascripts/bootstrap.js"></script>
</body>
</html>

上面代码是使用 Bootstrap部件实现的一个简单页面框架,整个页面分为顶部工具栏、正文和页脚三部分,其中正文和页脚包含在名为 container 的 div 标签中。

最后我们设计首页,修改 views/index.ejs:

<div class="hero-unit">
    <h1>欢迎来到 Microblog</h1>
    <p>Microblog 是一个基于 Node.js 的微博系统。</p>
    <p>
    <a class="btn btn-primary btn-large" href="/login">登录</a>
    <a class="btn btn-large" href="/reg">立即注册</a>
    </p>
</div>
<div class="row">
    <div class="span4">
        <h2>Carbo 说</h2>
        <p>东风破早梅 向暖一枝开 冰雪无人见 春从天上来</p>
    </div>
    <div class="span4">
        <h2>BYVoid 说</h2>
        <p>
        Open Chinese Convert(OpenCC)是一个开源的中文简繁转换项目,
        致力于制作高质量的基于统计预料的简繁转换词库。
        还提供函数库(libopencc)、命令行简繁转换工具、人工校对工具、词典生成程序、
        在线转换服务及图形用户界面。</p>
    </div>
    <div class="span4">
        <h2>佛振 说</h2>
        <p>中州韵输入法引擎 / Rime Input Method Engine 取意历史上通行的中州韵,
        愿写就一部汇集音韵学智慧的输入法经典之作。
        项目网站设在 http://code.google.com/p/rimeime/
        创造应用价值是一方面,更要坚持对好技术的追求,希望能写出灵动而易于扩展的代码,
        使其成为一款个性十足的开源输入法。</p>
    </div>
</div>

第五章 使用Node.js进行Web开发

图5-9 使用 Bootstrap 实现的首页

怎么样?即使不懂设计也做出了优雅的界面,使用 Bootstrap 可以大大简化前端设计工作。

5.6 用户注册和登录

在上一节我们使用 Bootstrap 创建了网站的基本框架。在这一节我们要实现用户会话的功能,包括用户注册和登录状态的维护。为了实现这些功能,我们需要引入会话机制来记录用户状态,还要访问数据库来保存和读取用户信息。现在就让我们从数据库开始。

5.6.1 访问数据库

我们选用 MongoDB 作为网站的数据库系统,它是一个开源的 NoSQL 数据库,相比MySQL 那样的关系型数据库,它更为轻巧、灵活,非常适合在数据规模很大、事务性不强的场合下使用。

1. NoSQL

什么是 NoSQL 呢?为了解释清楚,首先让我们来介绍几个概念。在传统的数据库中,数据库的格式是由表(table)、行(row)、字段(field)组成的。表有固定的结构,规定了每行有哪些字段,在创建时被定义,之后修改很困难。

行的格式是相同的,由若干个固定的字段组成。每个表可能有若干个字段作为索引(index),这其中有的是主键(primary key),用于约束表中的数据,还有唯一键(unique key),确保字段中不存放重复数据。表和表之间
可能还有相互的约束,称为外键(foreign key)。对数据库的每次查询都要以行为单位,复杂的查询包括嵌套查询、连接查询和交叉表查询。

拥有这些功能的数据库被称为关系型数据库,关系型数据库通常使用一种叫做 SQL(Structured Query Language)的查询语言作为接口,因此又称为 SQL 数据库。典型的 SQL 数据库有 MySQL、Oracle、Microsoft SQL Server、PostgreSQL、SQLite,等等。

NoSQL 是 1998 年被提出的,它曾经是一个轻量、开源、不提供SQL功能的关系数据库。但现在 NoSQL 被认为是 Not Only SQL 的简称,主要指非关系型、分布式、不提供 ACID 的数据库系统。

ACID 是数据库系统中事务(transaction)所必须具备的四个特性,即原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。

正如它的名称所暗示的,NoSQL 设计初衷并不是为了取代 SQL 数据库的,而是作为一个补充,它和 SQL 数据库有着各自不同的适应领域。NoSQL 不像 SQL 数据库一样都有着统一的架构和接口,不同的 NoSQL 数据库系统从里到外可能完全不同。

2. MongoDB

MongoDB 是一个对象数据库,它没有表、行等概念,也没有固定的模式和结构,所有的数据以文档的形式存储。所谓文档就是一个关联数组式的对象,它的内部由属性组成,一个属性对应的值可能是一个数、字符串、日期、数组,甚至是一个嵌套的文档。下面是一个MongoDB 文档的示例:

{
    "_id" : ObjectId( "4f7fe8432b4a1077a7c551e8" ),
    "uid" : 2004,
    "username" : "byvoid",
    "net9" : {
        "nickname" : "BYVoid",
        "surname" : "Kuo",
        "givenname" : "Carbo",
        "fullname" : "Carbo Kuo",
        "emails" : [ "byvoid@byvoid.com", "byvoid.kcp@gmail.com" ],
        "website" : "http://www.byvoid.com",
        "address" : "Zijing 2#, Tsinghua University"
    }
}

上面文档中 uid 是一个整数属性,username 是字符串属性,_id 是文档对象的标识符,格式为特定的 ObjectId。net9 是一个嵌套的文档,其内部结构与一般文档无异。从格式来看文档好像 JSON,没错,MongoDB 的数据格式就是 JSON ,因此与 JavaScript 的亲和性很强。在 Mongodb 中对数据的操作都是以文档为单位的,当然我们也可以修改文档的部分属性。对于查询操作,我们只需要指定文档的任何一个属性,就可在数据库中将满足条件的所有文档筛选出来。为了加快查询,MongoDB 也对文档实现了索引,这一点和 SQL 数据库一样。

准确地说,MongoDB 的数据格式是 BSON (Binary JSON),它是 JSON 的一个扩展。

3. 连接数据库

现在,让我们来看看如何连接数据库吧。首先确保已在本地安装好了 MongoDB,如果没有,请去http://www.mongodb.org/ 查看如何安装。

为了在 Node.js 中使用 MongoDB,我们需要获取一个模块。打开工程目录中的 package.json,在 dependencies 属性中添加一行代码:

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

然后运行 npm install 更新依赖的模块。接下来在工程的目录中创建 settings.js 文件,这个文件用于保存数据库的连接信息。我们将用到的数据库命名为 microblog,数据库服务器在本地,因此Settings.js文件的内容如下:

module.exports = {
    cookieSecret: 'microblogbyvoid',
    db: 'microblog',
    host: 'localhost',
};

其中,db 是数据库的名称,host 是数据库的地址。cookieSecret 用于 Cookie 加密与数据库无关,我们留作后用。

接下来在 models 子目录中创建 db.js,内容是:

var settings = require('../settings');
var Db = require('mongodb').Db;
var Connection = require('mongodb').Connection;
var Server = require('mongodb').Server;
module.exports = new Db(settings.db, new Server(settings.host, Connection.DEFAULT_PORT, {}));

以上代码通过 module.exports 输出了创建的数据库连接,在后面的小节中我们会用到这个模块。由于模块只会被加载一次,以后我们在其他文件中使用时均为这一个实例。

5.6.2 会话支持

在完成用户注册和登录功能之前,我们需要先了解会话的概念。会话是一种持久的网络协议,用于完成服务器和客户端之间的一些交互行为。会话是一个比连接粒度更大的概念,一次会话可能包含多次连接,每次连接都被认为是会话的一次操作。

在网络应用开发中,有必要实现会话以帮助用户交互。例如网上购物的场景,用户浏览了多个页面,购买了一些物
品,这些请求在多次连接中完成。许多应用层网络协议都是由会话支持的,如 FTP、Telnet 等,而 HTTP 协议是无状态的,本身不支持会话,因此在没有额外手段的帮助下,前面场景中服务器不知道用户购买了什么。

为了在无状态的 HTTP 协议之上实现会话,Cookie 诞生了。Cookie 是一些存储在客户端的信息,每次连接的时候由浏览器向服务器递交,服务器也向浏览器发起存储 Cookie 的请求,依靠这样的手段服务器可以识别客户端。我们通常意义上的 HTTP 会话功能就是这样实现的。

具体来说,浏览器首次向服务器发起请求时,服务器生成一个唯一标识符并发送给客户端浏览器,浏览器将这个唯一标识符存储在 Cookie 中,以后每次再发起请求,客户端
浏览器都会向服务器传送这个唯一标识符,服务器通过这个唯一标识符来识别用户。

对于开发者来说,我们无须关心浏览器端的存储,需要关注的仅仅是如何通过这个唯一标识符来识别用户。很多服务端脚本语言都有会话功能,如 PHP,把每个唯一标识符存储到文件中。

Express 也提供了会话中间件,默认情况下是把用户信息存储在内存中,但我们既然已经有了 MongoDB,不妨把会话信息存储在数据库中,便于持久维护。为了使用这一功能,我们首先要获得一个叫做 connect-mongo 的模块,在 package.json 中添加一行代码:

{
    "name": "microblog",
    "version": "0.0.1",
    "private": true,
    "dependencies": {
        "express": "2.5.8",
        "ejs": ">= 0.0.1",
        "connect-mongo": ">= 0.1.7",
        "mongodb": ">= 0.9.9"
    }
}

运行 npm install 获得模块。然后打开 app.js,添加以下内容:

var MongoStore = require('connect-mongo');
var settings = require('../settings');
app.configure(function(){
    app.set('views', __dirname + '/views');
    app.set('view engine', 'ejs');
    app.use(express.bodyParser());
    app.use(express.methodOverride());
    app.use(express.cookieParser());
    app.use(express.session({
        secret: settings.cookieSecret,
        store: new MongoStore({
            db: settings.db
        })
    }));
    app.use(app.router);
    app.use(express.static(__dirname + '/public'));
});

其中 express.cookieParser() 是 Cookie 解析的中间件。express.session() 则提供会话支持,设置它的 store 参数为 MongoStore 实例,把会话信息存储到数据库中,以避免丢失。

在后面的小节中,我们可以通过 req.session 获取当前用户的会话对象,以维护用户相关的信息。

5.6.3 注册和登入

我们已经准备好了数据库访问和会话存储的相关信息,接下来开始实现网站的第一个功能,用户注册和登入。

1. 注册页面

首先来设计用户注册页面的表单,创建 views/reg.ejs 文件,内容是:

<form class="form-horizontal" method="post">
    <fieldset>
        <legend>用户注册</legend>
        <div class="control-group">
            <label class="control-label" for="username">用户名</label>
            <div class="controls">
                <input type="text" class="input-xlarge" id="username" name="username">
                <p class="help-block">你的账户名称,用于登录和显示。</p>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="password">口令</label>
            <div class="controls">
                <input type="password" class="input-xlarge" id="password" name="password">
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="password-repeat">重复输入口令</label>
            <div class="controls">
                <input type="password" class="input-xlarge" id="password-repeat"
                name="password-repeat">
            </div>
        </div>
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">注册</button>
        </div>
    </fieldset>
</form>

这个表单中有3个输入单元,分别是 username、password 和 password-repeat。表单的请求方法是 POST,将会发送到相同的路径下。

到目前为止我们所有的路由规则还都写在了 app.js 中,随着规模扩大其维护难度不断提高,因此我们需要把所有的路由规则分离出去。修改 app.js 的 app.configure 部分,用app.use(express.router(routes)) 代替 app.use(app.router):

app.configure(function(){
app.set('views', --dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(express.session({
    secret: settings.cookieSecret,
    store: new MongoStore({
        db: settings.db
    })
}));
app.use(express.router(routes));
    app.use(express.static(--dirname + '/public'));
});

接下来打开 routes/index.js,把内容改为:

module.exports = function(app) {
    app.get('/', function(req, res) {
        res.render('index', {
            title: '首页'
        });
    });
    app.get('/reg', function(req, res) {
        res.render('reg', {
            title: '用户注册',
        });
    });
};

现在运行 app.js,在浏览器中打开 http://localhost:3000/reg,可以看到如图5-10所示的页面。

第五章 使用Node.js进行Web开发

图5-10 注册页面的效果

2. 注册响应

上面这个页面十分简洁优雅,看了以后是不是有立即注册的冲动呢?当然,现在点击注册是没有效果的,因为我们还没有实现 POST 请求发送后的功能,下面就来实现。在 routes/index.js 中添加 /reg 的 POST 响应函数:

app.post('/reg', function(req, res) {
    //检验用户两次输入的口令是否一致
    if (req.body['password-repeat'] != req.body['password']) {
        req.flash('error', '两次输入的口令不一致');
        return res.redirect('/reg');
    }
    //生成口令的散列值
    var md5 = crypto.createHash('md5');
    var password = md5.update(req.body.password).digest('base64');
    var newUser = new User({
        name: req.body.username,
        password: password,
    });
    //检查用户名是否已经存在
    User.get(newUser.name, function(err, user) {
        if (user)
            err = 'Username already exists.';
        if (err) {
            req.flash('error', err);
            return res.redirect('/reg');
        }
        //如果不存在则新增用户
        newUser.save(function(err) {
            if (err) {
                req.flash('error', err);
                return res.redirect('/reg');
            }
            req.session.user = newUser;
            req.flash('success', '注册成功');
            res.redirect('/');
        });
    });
});

这段代码用到了一些新的东西,我们一一说明。

  • req.body 就是 POST 请求信息解析过后的对象,例如我们要访问用户传递的password 域的值,只需访问 req.body[‘password’] 即可。
  • req.flash 是 Express 提供的一个奇妙的工具,通过它保存的变量只会在用户当前和下一次的请求中被访问,之后会被清除,通过它我们可以很方便地实现页面的通知和错误信息显示功能。
  • res.redirect 是重定向功能,通过它会向用户返回一个 303 See Other 状态,通知浏览器转向相应页面。
  • crypto 是 Node.js 的一个核心模块,功能是加密并生成各种散列,使用它之前首先要声明 var crypto = require(‘crypto’)。我们代码中使用它计算了密码的散列值。
  • User 是我们设计的用户对象,在后面我们会详细介绍,这里先假设它的接口都是可用的,使用前需要通过 var User = require(‘../models/user.js’) 引用。
  • User.get 的功能是通过用户名获取已知用户,在这里我们判断用户名是否已经存在。User.save 可以将用户对象的修改写入数据库。
  • 通过 req.session.user = newUser 向会话对象写入了当前用户的信息,在后面我们会通过它判断用户是否已经登录。

3. 用户模型

在前面的代码中,我们直接使用了 User 对象。User 是一个描述数据的对象,即 MVC架构中的模型。前面我们使用了许多视图和控制器,这是第一次接触到模型。与视图和控制器不同,模型是真正与数据打交道的工具,没有模型,网站就只是一个外壳,不能发挥真实的作用,因此它是框架中最根本的部分。现在就让我们来实现 User 模型吧。

在 models 目录中创建 user.js 的文件,内容如下:

var mongodb = require('./db');
function User(user) {
    this.name = user.name;
    this.password = user.password;
};
module.exports = User;
User.prototype.save = function save(callback) {
    // 存入 Mongodb 的文档
    var user = {
    name: this.name,
    password: this.password,
    };
    mongodb.open(function(err, db) {
        if (err) {
        return callback(err);
        }
        // 读取 users 集合
        db.collection('users', function(err, collection) {
            if (err) {
                mongodb.close();
                return callback(err);
            }
            // 为 name 属性添加索引
            collection.ensureIndex('name', {unique: true});
            // 写入 user 文档
            collection.insert(user, {safe: true}, function(err, user) {
                mongodb.close();
                callback(err, user);
            });
        });
    });
};
User.get = function get(username, callback) {
    mongodb.open(function(err, db) {
        if (err) {
            return callback(err);
        }
        // 读取 users 集合
        db.collection('users', function(err, collection) {
            if (err) {
                mongodb.close();
                return callback(err);
            }
            // 查找 name 属性为 username 的文档
            collection.findOne({name: username}, function(err, doc) {
                mongodb.close();
                if (doc) {
                    // 封装文档为 User 对象
                    var user = new User(doc);
                    callback(err, user);
                } else {
                    callback(err, null);
                }
            });
        });
    });
};

以上代码实现了两个接口,User.prototype.save 和 User.get,前者是对象实例的方法,用于将用户对象的数据保存到数据库中,后者是对象构造函数的方法,用于从数据库中查找指定的用户。

4. 视图交互

现在几乎已经万事俱备,只差视图的支持了。为了实现不同登录状态下页面呈现不同内容的功能,我们需要创建动态视图助手,通过它我们才能在视图中访问会话中的用户数据。

同时为了显示错误和成功的信息,也要在动态视图助手中增加响应的函数。打开 app.js,添加以下代码:

app.dynamicHelpers({
    user: function(req, res) {
        return req.session.user;
    },
    error: function(req, res) {
        var err = req.flash('error');
        if (err.length)
            return err;
        else
            return null;
    },
    success: function(req, res) {
        var succ = req.flash('success');
        if (succ.length)
            return succ;
        else
            return null;
    },
});

接下来,修改 layout.ejs中的导航栏部分:

<ul class="nav">
    <li class="active"><a href="/">首页</a></li>
    <% if (!user) { %>
        <li><a href="/login">登入</a></li>
        <li><a href="/reg">注册</a></li>
    <% } else { %>
        <li><a href="/logout">登出</a></li>
    <% } %>
</ul>

上面功能是为已登入用户和未登入用户显示不同的信息。在 container 中,<%- body %>之前加入:

<% if (success) { %>
    <div class="alert alert-success">
        <%= success %>
    </div>
<% } %>
<% if (error) { %>
    <div class="alert alert-error">
        <%= error %>
    </div>
<% } %>

它的功能是页面通知。现在看看最终的效果吧,图5-11和图5-12分别是注册时遇到错误和注册成功以后的画面。

第五章 使用Node.js进行Web开发

图5-11 两次输入的密码不一致

第五章 使用Node.js进行Web开发

图5-12 注册成功

5. 登入和登出

当我们完成用户注册的功能以后,再实现用户登入和登出就相当容易了。把下面的代码加到 routes/index.js 中:

app.get('/login', function(req, res) {
    res.render('login', {
        title: '用户登入',
    });
});
app.post('/login', function(req, res) {
    //生成口令的散列值
    var md5 = crypto.createHash('md5');
    var password = md5.update(req.body.password).digest('base64');
    User.get(req.body.username, function(err, user) {
        if (!user) {
            req.flash('error', '用户不存在');
            return res.redirect('/login');
        }
        if (user.password != password) {
            req.flash('error', '用户口令错误');
            return res.redirect('/login');
        }
        req.session.user = user;
        req.flash('success', '登入成功');
        res.redirect('/');
    });
});
app.get('/logout', function(req, res) {
    req.session.user = null;
    req.flash('success', '登出成功');
    res.redirect('/');
});

在这里你可以清晰地看出登入和登出仅仅是 req.session.user 变量的标记,非常简单。但这会不会有安全性问题呢?不会的,因为这个变量只有服务端才能访问到,只要不是黑客攻破了整个服务器,无法从外部改动。

最后我们创建 views/login.ejs,内容如下:

<form class="form-horizontal" method="post">
    <fieldset>
        <legend>用户登入</legend>
        <div class="control-group">
            <label class="control-label" for="username">用户名</label>
            <div class="controls">
                <input type="text" class="input-xlarge" id="username" name="username">
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="password">口令</label>
            <div class="controls">
                <input type="password" class="input-xlarge" id="password" name="password">
            </div>
        </div>
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">登入</button>
        </div>
    </fieldset>
</form>

在浏览器中访问http://localhost:3000/login,你将会看到如图5-13 所示的页面。

第五章 使用Node.js进行Web开发

图5-13 用户登入

至此用户注册和登录的功能就完全实现了。

5.6.4 页面权限控制

在前面我们已经实现了用户登入,并且在页面中通过不同的内容反映出了用户已登入和未登入的状态。现在我们还有一个工作要做,就是为页面设置访问权限。例如,登出功能应该只对已登入的用户开放,注册和登入页面则应该阻止已登入的用户访问。如何实现这一点呢?

最简单的方法是在每个页面的路由响应函数内检查用户是否已经登录,但这会带来很多重复的代码,违反了 DRY 原则。因此,我们利用路由中间件来实现这个功能。

DRY (Don’t Repeat Yourself) 是软件工程设计的一个基本原则,又称“一次且仅一次”(Once And Only Once),指的是开发中应该避免相同意义的代码重复出现。

5.3.5 节介绍了同一路径绑定多个响应函数的方法,通过调用 next() 转移控制权,这种方法叫做路由中间件。我们可以把用户登入状态检查放到路由中间件中,在每个路径前增加路由中间件,即可实现页面权限控制。

最终的 routes/index.js 内容如下:

var crypto = require('crypto');
var User = require('../models/user.js');
    module.exports = function(app) {
    app.get('/', function(req, res) {
        res.render('index', {
            title: '首页'
        });
    });
    app.get('/reg', checkNotLogin);
    app.get('/reg', function(req, res) {
        res.render('reg', {
            title: '用户注册',
        });
    });
    app.post('/reg', checkNotLogin);
    app.post('/reg', function(req, res) {
        //检验用户两次输入的口令是否一致
        if (req.body['password-repeat'] != req.body['password']) {
            req.flash('error', '两次输入的口令不一致');
            return res.redirect('/reg');
        }
        //生成口令的散列值
        var md5 = crypto.createHash('md5');
        var password = md5.update(req.body.password).digest('base64');
        var newUser = new User({
            name: req.body.username,
            password: password,
        });
        //检查用户名是否已经存在
        User.get(newUser.name, function(err, user) {
            if (user)
            err = 'Username already exists.';
            if (err) {
                req.flash('error', err);
                return res.redirect('/reg');
            }
            //如果不存在则新增用户
            newUser.save(function(err) {
                if (err) {
                req.flash('error', err);
                return res.redirect('/reg');
                }
                req.session.user = newUser;
                req.flash('success', '注册成功');
                res.redirect('/');
            });
        });
    });
    app.get('/login', checkNotLogin);
    app.get('/login', function(req, res) {
        res.render('login', {
        title: '用户登入',
        });
    });
    app.post('/login', checkNotLogin);
    app.post('/login', function(req, res) {
        //生成口令的散列值
        var md5 = crypto.createHash('md5');
        var password = md5.update(req.body.password).digest('base64');
        User.get(req.body.username, function(err, user) {
            if (!user) {
                req.flash('error', '用户不存在');
                return res.redirect('/login');
            }
            if (user.password != password) {
                req.flash('error', '用户口令错误');
                return res.redirect('/login');
            }
            req.session.user = user;
            req.flash('success', '登入成功');
            res.redirect('/');
        });
    });
    app.get('/logout', checkLogin);
    app.get('/logout', function(req, res) {
        req.session.user = null;
        req.flash('success', '登出成功');
        res.redirect('/');
    });
};
function checkLogin(req, res, next) {
    if (!req.session.user) {
        req.flash('error', '未登入');
        return res.redirect('/login');
    }
    next();
}
function checkNotLogin(req, res, next) {
    if (req.session.user) {
        req.flash('error', '已登入');
        return res.redirect('/');
    }
    next();
}

5.7 发表微博

现在网站已经具备了用户注册、登入、页面权限控制的功能,这些功能为网站最核心的部分——发表微博做好了准备。在这个小节里,我们将会实现发表微博的功能,完成整个网站的设计。

5.7.1 微博模型

现在让我们从模型开始设计。仿照用户模型,将微博模型命名为 Post 对象,它拥有与User 相似的接口,分别是 Post.get 和 Post.prototype.save。Post.get 的功能是从数据库中获取微博,可以按指定用户获取,也可以获取全部的内容。Post.prototype.save是 Post 对象实例的方法,用于将对象的变动保存到数据库。

创建 models/post.js,写入以下内容:

var mongodb = require('./db');
function Post(username, post, time) {
    this.user = username;
    this.post = post;
    if (time) {
        this.time = time;
    } else {
        this.time = new Date();
    }
};
module.exports = Post;
Post.prototype.save = function save(callback) {
    // 存入 Mongodb 的文档
    var post = {
        user: this.user,
        post: this.post,
        time: this.time,
    };
    mongodb.open(function(err, db) {
        if (err) {
            return callback(err);
        }
        // 读取 posts 集合
        db.collection('posts', function(err, collection) {
            if (err) {
                mongodb.close();
                return callback(err);
            }
            // 为 user 属性添加索引
            collection.ensureIndex('user');
            // 写入 post 文档
            collection.insert(post, {safe: true}, function(err, post) {
                mongodb.close();
                callback(err, post);
            });
        });
    });
};
Post.get = function get(username, callback) {
    mongodb.open(function(err, db) {
        if (err) {
            return callback(err);
        }
        // 读取 posts 集合
        db.collection('posts', function(err, collection) {
            if (err) {
                mongodb.close();
                return callback(err);
            }
            // 查找 user 属性为 username 的文档,如果 username 是 null 则匹配全部
            var query = {};
            if (username) {
                query.user = username;
            }
            collection.find(query).sort({time: -1}).toArray(function(err, docs) {
                mongodb.close();
                if (err) {
                    callback(err, null);
                }
                // 封装 posts 为 Post 对象
                var posts = [];
                docs.forEach(function(doc, index) {
                    var post = new Post(doc.user, doc.post, doc.time);
                    posts.push(post);
                });
                callback(null, posts);
            });
        });
    });
};

在后面我们会通过控制器调用这个模块。

5.7.2 发表微博

我们曾经约定通过 POST 方法访问 /post 以发表微博,现在让我们来实现这个控制器。在 routes/index.js 中添加下面的代码:

app.post('/post', checkLogin);
app.post('/post', function(req, res) {
    var currentUser = req.session.user;
    var post = new Post(currentUser.name, req.body.post);
    post.save(function(err) {
        if (err) {
            req.flash('error', err);
            return res.redirect('/');
        }
        req.flash('success', '发表成功');
        res.redirect('/u/' + currentUser.name);
    });
});

这段代码通过 req.session.user 获取当前用户信息,从 req.body.post 获取用户发表的内容,建立 Post 对象,调用 save() 方法存储信息,最后将用户重定向到用户页面。

5.7.3 用户页面

用户页面的功能是展示用户发表的所有内容,在routes/index.js中加入以下代码:

app.get('/u/:user', function(req, res) {
    User.get(req.params.user, function(err, user) {
        if (!user) {
            req.flash('error', '用户不存在');
            return res.redirect('/');
        }
        Post.get(user.name, function(err, posts) {
            if (err) {
                req.flash('error', err);
                return res.redirect('/');
            }
            res.render('user', {
                title: user.name,
                posts: posts,
            });
        });
    });
});

它的功能是首先检查用户是否存在,如果存在则从数据库中获取该用户的微博,最后通过 posts 属性传递给 user 视图。views/user.ejs 的内容如下:

<% if (user) { %>
    <%- partial('say') %>
<% } %>
<%- partial('posts') %>

根据 DRY 原则,我们把重复用到的部分都提取出来,分别放入 say.ejs 和 posts.ejs。say.ejs的功能是显示一个发表微博的表单,它的内容如下:

<form method="post" action="/post" class="well form-inline center" style="text-align:center;">
    <input type="text" class="span8" name="post">
    <button type="submit" class="btn btn-success"><i class="icon-comment icon-white">
    </i> 发言</button>
</form>

posts.ejs 的目的是按照行列显示传入的 posts 的所有内容:

<% posts.forEach(function(post, index) {
    if (index % 3 == 0) { %>
        <div class="row">
    <%} %>
    <div class="span4">
        <h2><a href="/u/<%= post.user %>"><%= post.user %></a> 说</h2>
        <p><small><%= post.time %></small></p>
        <p><%= post.post %></p>
    </div>
    <% if (index % 3 == 2) { %>
        </div><!-- end row -->
    <% } %>
<%}) %>
<% if (posts.length % 3 != 0) { %>
    </div><!-- end row -->
<%} %>

完成上述工作后,重启服务器。在用户的页面上发表几个微博,然后可以看到用户页面的效果如图5-14 所示。

第五章 使用Node.js进行Web开发

图5-14 用户页面

5.7.4 首页

最后一步是实现首页的内容。我们计划在首页显示所有用户发表的微博,按时间从新到旧的顺序。

在 routes/index.js 中添加下面代码:

app.get('/', function(req, res) {
    Post.get(null, function(err, posts) {
        if (err) {
            posts = [];
        }
        res.render('index', {
            title: '首页',
            posts: posts,
        });
    });
});

它的功能是读取所有用户的微博,传递给页面 posts 属性。接下来修改首页的模板index.ejs:

<% if (!user) { %>
    <div class="hero-unit">
        <h1>欢迎来到 Microblog</h1>
        <p>Microblog 是一个基于 Node.js 的微博系统。</p>
        <p>
        <a class="btn btn-primary btn-large" href="/login">登录</a>
        <a class="btn btn-large" href="/reg">立即注册</a>
        </p>
    </div>
<% } else { %>
    <%- partial('say') %>
<% } %>
<%- partial('posts') %>

下面看看首页的效果吧,图5-15和图5-16是用户登入之前和登入以后看到的首页效果。

第五章 使用Node.js进行Web开发

图5-15 登入之前的首页

第五章 使用Node.js进行Web开发

图5-16 登入以后的首页

5.7.5 下一步

到此为止,微博网站的基本功能就完成了。这个网站仅仅是微博的一个雏形,距离真正的微博还有很大的距离。例如,我们没有对注册信息进行完整的验证,如用户名的规则,密码的长短等。

为了防止恶意注册还应该带有验证码和邮件认证的功能,甚至还应该支持OAuth。我们对发帖没有进行任何限制,尽管注入 HTML 是不可能的,但至少还应该对长度有限制。

首页和用户页面的显示都是没有数量限制的,当微博很多以后这个页面可能会很长,应该实现分页的功能。作为社交工具,最重要的用户关注、转帖、评论、圈点用户这些功能都没有实现。

除了功能上的不足,这个网站还有潜在的性能问题,例如每次查询数据库都没有限制取得的数量,还应该对一些访问频繁的页面增加缓存机制。另外,我们一直是以开发模式在运行着这个网站,没有讨论如何把它真正部署起来,我们会在下一章详细讨论。

如果你对这个用 Node.js 实现的微博网站有兴趣,请访问https://github.com/BYVoid/microblog,这里有 Microblog 示例中的完整代码,而且在其基础上还做了进一步的改进,也欢迎你为它“添砖加瓦”。

5.8 参考资料

  • “Node.js简单介绍并实现一个简单的Web MVC框架”: http://club.cnodejs.org/topic/4f16442ccae1f4aa27001135。
  • “A HTTP Proxy Server in 20 Lines of node.js Code”: http://www.catonmat.net/http-proxyin-nodejs/。
  • “Node.js Recommended Third-party Modules”: http://nodejs.org/api/appendix_1.html。
  • Express Guide: http://expressjs.com/guide.html。
  • EJS:Embedded JavaScript: http://embeddedjs.com/。
  • Jade: http://jade-lang.com/。
  • JSON-P: http://www.json-p.org/。
  • Connect: http://www.senchalabs.org/connect/。
  • “深入浅出REST”: http://www.infoq.com/cn/articles/rest-introduction。
  • “HTTP Verbs: 谈POST、PUT 和 PATCH 的应用”: http://ihower.tw/blog/archives/6483。
  • Template engine (Web): http://en.wikipedia.org/wiki/Template_engine_(Web)。
  • Bootstrap: http://twitter.github.com/bootstrap/。
  • MongoDB Manual: http://www.mongodb.org/display/DOCS/Manual。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

目前还没有任何评论,快来抢沙发吧!