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

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

    5.1 准备工作

    在开始动手之前,我们首先要大致知道Node.js实现网站的工作原理。Node.js和PHP、Perl、ASP、JSP一样,目的都是实现动态网页,也就是说由服务器动态生成HTML页面。之所以要这么做,是因为静态HTML的可扩展性非常有限,无法与用户有效交互。同时如果有大量相似的内容,例如产品介绍页面,那么1000个产品就要1000个静态的HTML页面,维护这1000个页面简直是一场灾难,因此动态生成HTML页面的技术应运而生。

    最早实现动态网页的方法是使用Perl【1】和CGI。在Perl程序中输出HTML内容,由HTTP服务器调用Perl程序,将结果返回给客户端。这种方式在互联网刚刚兴起的20世纪90年代非常流行,几乎所有的动态网页都是这么做的。但问题在于如果HTML内容比较多,维护非常不方便。大概在2000年左右,以ASP、PHP、JSP的为代表的以模板为基础的语言出现了,这种语言的使用方法与CGI相反,是在以HTML为主的模板中插入程序代码【2】。这种方式在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开发架构对比

    使用Node.js进行Web开发 - 图1


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

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

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

    使用Node.js进行Web开发 - 图2

    图5-1 Node.js与PHP架构的对比

    Node.js和其他的语言相比的另一个显著区别,在于它的原始封装程度较低。例如PHP中你可以访问$_REQUEST获取客户端的POST或GET请求,通常不需要直接处理HTTP协议【4】。这些语言要求由HTTP服务器来调用,因此你需要设置一个HTTP服务器来处理客户端的请求,HTTP服务器通过CGI或其他方式调用脚本语言解释器,将运行的结果传递回HTTP服务器,最终再把内容返回给客户端。而在Node.js中,很多工作需要你自己来做(并不是都要自己动手,因为有第三方框架的帮助)。

    5.1.1 使用http模块

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

    使用Node.js进行Web开发 - 图3

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

    使用Node.js进行Web开发 - 图4

    在3.5.1节中使用了类似下面的方法(用http模块):

    使用Node.js进行Web开发 - 图5

    这种差别可能会让你大吃一惊,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重新实现前面的例子:

    使用Node.js进行Web开发 - 图6

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

    5.2 快速开始

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

    5.2.1 安装Express

    首先我们要安装Express。如果一个包是某个工程依赖,那么我们需要在工程的目录下使用本地模式安装这个包,如果要通过命令行调用这个包中的命令,则需要用全局模式安装(关于本地模式和全局模式,参见3.3.4节),因此按理说我们使用本地模式安装Express即可。但是Express像很多框架一样都提供了Quick Start(快速开始)工具,这个工具的功能通常是建立一个网站最小的基础框架,在此基础上完成开发。当然你可以完全自己动手,但我还是推荐使用这个工具更快速地建立网站。为了使用这个工具,我们需要用全局模式安装Express,因为只有这样我们才能在命令行中使用它。运行以下命令:

    使用Node.js进行Web开发 - 图7

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

    使用Node.js进行Web开发 - 图8

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

    5.2.2 建立工程

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

    使用Node.js进行Web开发 - 图9

    当前目录下出现了子目录microblog,并且产生了一些文件:

    使用Node.js进行Web开发 - 图10

    它还提示我们要进入其中运行npm install,我们依照指示,结果如下:

    使用Node.js进行Web开发 - 图11

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

    使用Node.js进行Web开发 - 图12

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

    5.2.3 启动服务器

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

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

    使用Node.js进行Web开发 - 图13

    图5-2 Express初始欢迎页面

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

    使用Node.js进行Web开发 - 图14

    注意命令行中显示服务器运行在开发模式下(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是工程的入口,我们先看看其中有什么内容:

    使用Node.js进行Web开发 - 图15

    对比上一节使用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方法【6】。router是项目的路由支持。static提供了静态文件支持。errorHandler是错误控制器。

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

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

    2.routes/index.js

    routes/index.js是路由文件,相当于控制器,用于组织展示的内容:

    使用Node.js进行Web开发 - 图16

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

    3.index.ejs

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

    使用Node.js进行Web开发 - 图17

    它的基础是HTML语言,其中包含了形如使用Node.js进行Web开发 - 图18的标签,功能是显示引用的变量,即res.render函数第二个参数传入的对象的属性。

    4.layout.ejs

    模板文件不是孤立展示的,默认情况下所有的模板都继承自layout.ejs,即使用Node.js进行Web开发 - 图19部分才是独特的内容,其他部分是共有的,可以看作是页面框架。

    使用Node.js进行Web开发 - 图20

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

    5.3 路由控制

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

    5.3.1 工作原理

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

    访问http://localhost:3000,浏览器会向服务器发送以下请求:

    使用Node.js进行Web开发 - 图21

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

    使用Node.js进行Web开发 - 图22

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

    使用Node.js进行Web开发 - 图23

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

    使用Node.js进行Web开发 - 图24

    图5-3 Express网站的架构

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

    5.3.2 创建路由规则

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

    使用Node.js进行Web开发 - 图25

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

    这是因为/abc是一个不存在的路由规则,而且它也不是一个public目录下的文件,所以Express返回了404Not Found的错误。

    接下来我们会讲述如何创建路由规则。

    假设我们要创建一个地址为/hello的页面,内容是当前的服务器时间,让我们看看具体做法。打开app.js,在已有的路由规则app.get('/', routes.index)后面添加一行:

    使用Node.js进行Web开发 - 图26

    修改routes/index.js,增加hello函数:

    使用Node.js进行Web开发 - 图27

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

    使用Node.js进行Web开发 - 图28

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

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

    5.3.3 路径匹配

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

    使用Node.js进行Web开发 - 图29

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

    使用Node.js进行Web开发 - 图30

    路径规则/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请求的特点

    使用Node.js进行Web开发 - 图31

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

    表5-3 Express支持的HTTP请求的绑定函数

    使用Node.js进行Web开发 - 图32


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

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

    5.3.5 控制权转移

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

    使用Node.js进行Web开发 - 图33

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

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

    使用Node.js进行Web开发 - 图34

    当访问被匹配到的路径时,如http://localhost:3000/user/carbo,会发现终端中打印了all methods captured,而且浏览器中显示了user: carbo。这说明请求先被第一条路由规则捕获,完成console.log使用next()转移控制权,又被第二条规则捕获,向浏览器返回了信息。

    这是一个非常有用的工具,可以让我们轻易地实现中间件,而且还能提高代码的复用程度。例如我们针对一个用户查询信息和修改信息的操作,分别对应了GET和PUT操作,而两者共有的一个步骤是检查用户名是否合法,因此可以通过next()方法实现:

    使用Node.js进行Web开发 - 图35

    上面例子中,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开发 - 图36

    图5-6 模板引擎在MVC架构中的位置

    5.4.2 使用模板引擎

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

    我们在app.js中通过以下两个语句设置了模板引擎和页面模板的位置:

    使用Node.js进行Web开发 - 图37

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

    使用Node.js进行Web开发 - 图38

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

    使用Node.js进行Web开发 - 图39

    上面代码其中有两处使用Node.js进行Web开发 - 图40,用于模板变量显示,它们在模板翻译时会被替换成Express,因为res.render传递了{title: 'Express' }。

    ejs的标签系统非常简单,它只有以下3种标签。

    使用Node.js进行Web开发 - 图41:JavaScript代码。
    使用Node.js进行Web开发 - 图42:显示替换过HTML特殊字符的内容。
    使用Node.js进行Web开发 - 图43:显示原始HTML内容。

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

    5.4.3 页面布局

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

    使用Node.js进行Web开发 - 图44

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

    使用Node.js进行Web开发 - 图47

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

    使用Node.js进行Web开发 - 图48

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

    5.4.4 片段视图

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

    使用Node.js进行Web开发 - 图49

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

    使用Node.js进行Web开发 - 图50

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

    使用Node.js进行Web开发 - 图51

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

    使用Node.js进行Web开发 - 图52

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

    5.4.5 视图助手

    Express提供了一种叫做视图助手的工具,它的功能是允许在视图中访问一个全局的函数或对象,不用每次调用视图解析的时候单独传入。前面提到的partial就是一个视图助手。

    视图助手有两类,分别是静态视图助手和动态视图助手。这两者的差别在于,静态视图助手可以是任何类型的对象,包括接受任意参数的函数,但访问到的对象必须是与用户请求无关的,而动态视图助手只能是一个函数,这个函数不能接受参数,但可以访问req和res对象。

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

    使用Node.js进行Web开发 - 图53

    对应的视图helper、ejs的内容如下:

    使用Node.js进行Web开发 - 图54

    访问http://localhost:3000/helper可以看到如图5-7所示的内容。

    使用Node.js进行Web开发 - 图55

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

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

    5.5 建立微博网站

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

    5.5.1 功能分析

    开发中的一个大忌就是没有想清楚要做什么就开始动手,因此我们准备在动手实践之前先规划一下网站的功能,即使是出于学习目的也不例外。首先,微博应该以用户为中心,因此需要有用户的注册和登录功能。微博网站最核心的功能是信息的发表,这个功能涉及许多方面,包括数据库访问、前端显示等。一个完整的微博系统应该支持信息的评论、转发、圈点用户等功能,但出于演示目的,我们不能一一实现所有功能,只是实现一个微博社交网站的雏形。

    5.5.2 路由规划

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

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

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

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

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

    使用Node.js进行Web开发 - 图56

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

    使用Node.js进行Web开发 - 图57

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

    5.5.3 界面设计

    我们在开发网站的时候必须时刻意识到网站是为用户开发的,因而用户界面是非常重要的。一种普遍的观点是后端的开发者不必太多关注前端用户体验,因为这是前端程序员和设计师要做的事情。但实际上为了设计一个优雅的界面,后端程序员也不得不介入功能实现,因为很多时候前端和后端无法完全划分,仅仅靠前端开发者是无法设计出优美而又可用的界面的。【7】

    作为后端开发者,你可能和我一样都不太擅长设计,不过没关系,我们可以利用已有的优秀设计。如果你认同Twitter的简洁风格,那么Twitter Bootstrap是最好的选择。Twitter Bootstrap是由Twitter的设计师和工程师发起的开源项目,它提供了一套与Twitter风格一致的简洁、优雅的Web UI,包含了完全由HTML、CSS、JavaScript实现的用户交互工具。不管你是资深的前端工程师,还是专业的后端开发者,你都可以轻松地使用Twitter Bootstrap制作出优美的界面。图5-8是Twitter Bootstrap部件的介绍页面。

    使用Node.js进行Web开发 - 图58

    图5-8 Twitter Bootstrap

    5.5.4 使用Bootstrap

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

    使用Node.js进行Web开发 - 图59

    其中所有的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:

    使用Node.js进行Web开发 - 图60

    使用Node.js进行Web开发 - 图61

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

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

    使用Node.js进行Web开发 - 图62

    首页的效果如图5-9所示。

    使用Node.js进行Web开发 - 图63

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

    2.MongoDB

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

    使用Node.js进行Web开发 - 图64

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

    3.连接数据库

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

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

    使用Node.js进行Web开发 - 图65

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

    使用Node.js进行Web开发 - 图66

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

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

    使用Node.js进行Web开发 - 图67

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

    5.6.2 会话支持

    在完成用户注册和登录功能之前,我们需要先了解会话的概念。会话是一种持久的网络协议,用于完成服务器和客户端之间的一些交互行为。会话是一个比连接粒度更大的概念,一次会话可能包含多次连接,每次连接都被认为是会话的一次操作。在网络应用开发中,有必要实现会话以帮助用户交互。例如网上购物的场景,用户浏览了多个页面,购买了一些物品,这些请求在多次连接中完成。许多应用层网络协议都是由会话支持的,如FTP、Telnet等,而HTTP协议是无状态的,本身不支持会话,因此在没有额外手段的帮助下,前面场景中服务器不知道用户购买了什么。

    为了在无状态的HTTP协议之上实现会话,Cookie诞生了。Cookie是一些存储在客户端的信息,每次连接的时候由浏览器向服务器递交,服务器也向浏览器发起存储Cookie的请求,依靠这样的手段服务器可以识别客户端。我们通常意义上的HTTP会话功能就是这样实现的。具体来说,浏览器首次向服务器发起请求时,服务器生成一个唯一标识符并发送给客户端浏览器,浏览器将这个唯一标识符存储在Cookie中,以后每次再发起请求,客户端浏览器都会向服务器传送这个唯一标识符,服务器通过这个唯一标识符来识别用户。

    对于开发者来说,我们无须关心浏览器端的存储,需要关注的仅仅是如何通过这个唯一标识符来识别用户。很多服务端脚本语言都有会话功能,如PHP,把每个唯一标识符存储到文件中。Express也提供了会话中间件,默认情况下是把用户信息存储在内存中,但我们既然已经有了MongoDB,不妨把会话信息存储在数据库中,便于持久维护。为了使用这一功能,我们首先要获得一个叫做connect-mongo的模块,在package.json中添加一行代码:

    使用Node.js进行Web开发 - 图68

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

    使用Node.js进行Web开发 - 图69

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

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

    5.6.3 注册和登入

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

    1.注册页面

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

    使用Node.js进行Web开发 - 图70

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

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

    使用Node.js进行Web开发 - 图71

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

    使用Node.js进行Web开发 - 图72

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

    使用Node.js进行Web开发 - 图73

    图5-10 注册页面的效果

    2.注册响应

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

    使用Node.js进行Web开发 - 图74

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

    □req.body就是POST请求信息解析过后的对象,例如我们要访问用户传递的password域的值,只需访问req.body['password']即可。
    □req.flash是Express提供的一个奇妙的工具,通过它保存的变量只会在用户当前和下一次的请求中被访问,之后会被清除,通过它我们可以很方便地实现页面的通知和错误信息显示功能。
    □res.redirect是重定向功能,通过它会向用户返回一个303See 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的文件,内容如下:

    使用Node.js进行Web开发 - 图75

    使用Node.js进行Web开发 - 图76

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

    4.视图交互

    现在几乎已经万事俱备,只差视图的支持了。为了实现不同登录状态下页面呈现不同内容的功能,我们需要创建动态视图助手,通过它我们才能在视图中访问会话中的用户数据。同时为了显示错误和成功的信息,也要在动态视图助手中增加响应的函数。

    打开app.js,添加以下代码:

    使用Node.js进行Web开发 - 图77

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

    使用Node.js进行Web开发 - 图78

    上面功能是为已登入用户和未登入用户显示不同的信息。在container中,使用Node.js进行Web开发 - 图79之前加入:

    使用Node.js进行Web开发 - 图80

    它的功能是页面通知。

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

    使用Node.js进行Web开发 - 图81

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

    使用Node.js进行Web开发 - 图82

    图5-12 注册成功

    5.登入和登出

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

    使用Node.js进行Web开发 - 图83

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

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

    使用Node.js进行Web开发 - 图84

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

    使用Node.js进行Web开发 - 图85

    图5-13 用户登入

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

    5.6.4 页面权限控制

    在前面我们已经实现了用户登入,并且在页面中通过不同的内容反映出了用户已登入和未登入的状态。现在我们还有一个工作要做,就是为页面设置访问权限。例如,登出功能应该只对已登入的用户开放,注册和登入页面则应该阻止已登入的用户访问。如何实现这一点呢?最简单的方法是在每个页面的路由响应函数内检查用户是否已经登录,但这会带来很多重复的代码,违反了DRY【10】原则。因此,我们利用路由中间件来实现这个功能。

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

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

    使用Node.js进行Web开发 - 图86

    使用Node.js进行Web开发 - 图87

    使用Node.js进行Web开发 - 图88

    使用Node.js进行Web开发 - 图89

    5.7 发表微博

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

    5.7.1 微博模型

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

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

    使用Node.js进行Web开发 - 图90

    使用Node.js进行Web开发 - 图91

    使用Node.js进行Web开发 - 图92

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

    5.7.2 发表微博

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

    使用Node.js进行Web开发 - 图93

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

    5.7.3 用户页面

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

    使用Node.js进行Web开发 - 图94

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

    使用Node.js进行Web开发 - 图95

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

    使用Node.js进行Web开发 - 图96

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

    使用Node.js进行Web开发 - 图97

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

    使用Node.js进行Web开发 - 图98

    图5-14 用户页面

    5.7.4 首页

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

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

    使用Node.js进行Web开发 - 图99

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

    使用Node.js进行Web开发 - 图100

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

    使用Node.js进行Web开发 - 图101

    图5-15 登入之前的首页

    使用Node.js进行Web开发 - 图102

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

    注 释

    【1】是C++,任何语言都可以,Perl只是最常见的。

    【2】例如ASP的使用Node.js进行Web开发 - 图103和PHP的使用Node.js进行Web开发 - 图104标签,在这些标签内添加处理代码。

    【3】或者说不是必要的,因为你也可以把Node.js的服务器当作Apache或Nginx后端。

    【4】比如我们需要知道HTTP成功响应时要返回一个200状态码,而不需要手动完成“返回200状态码”这项工作。但这不带表你可以轻易地切换到非HTTP协议,因为代码仍然是与HTTP协议耦合的。

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

    【6】如PUT、DELETE等HTTP方法,浏览器是不支持的。

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

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

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

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