开始接触Node.js

对于存在的事物,你会问为什么?      
对于不存在的事物,我会问为什么不?
                                                                                         ——萧伯纳

为什么用Node.js

如果一张图片上能拼写出一千个单词,那么一千张图片能拼写出多少个单词呢?如果是无穷无尽这样的图片呢?

我先通过一个拼字游戏[[1]](说明)(如图1.1)来介绍Node.js。这是一个实时在线的没有结束的拼字游戏,这个游戏使用的技术和本书要讲解的技术是一致的。当我第一次看到这个游戏的时候就有了想去了解其技术实现的冲动,所以希望你也能试玩下这个游戏。令人难以置信的一点是,这个游戏是作为Node Knockout大赛的参赛作品,其制作用时只有48个小时。

Joynet的工程副总裁坎特里尔·布莱恩(Bryan Cantrill)曾经说过当你用Node.js去做一些东西的时候,你可能会有这样的感觉:真的是这样的么?是不是需要更加复杂一些。我在跟你分享这本书的内容的时候也是这样的情绪。通过对这本书的学习你会发现用Node.js去做些东西是很有乐趣的。

Node.js是一个服务端的JavaScript平台,由一个极简的核心库和丰富的生态系统组成。它基于谷歌的V8内核之上运行,因为谷歌出色的工程师使V8这个Javascript引擎速度非常的快。同时JavaScript这门语言在客户端也十分流行,但是它作为Node.js的标准语言其实是出于工程方面考虑的。具体细节会在本章的后续介绍中详细说明。

Node.js官方的介绍是这样的

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,他可以很方便的创建快速、灵活的网络应用。
Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。
Node.js 在分布式部署的数据密集型实时应用方面表现完美。

对于新手来说这似乎难以理解,这只是简单总结了Node.js一些核心的优点,仍然还有很多细节需要进一步说明。

通常当人们听说用Javascript作为目标语言时都会感到震惊,那是因为在程序员的社区中普遍认为Javascript不像C、C++或者Java那样是一个地道的编程语言。JavaScript最早是作为浏览器的解释型语言出现的,其名字中的Java实际上是为了能有个好兆头而取自当时愈发火热的Java语言。

JavaScript从最初很低的起点开始发展到现在已经被所有主流浏览器支持,同时也包括移动端的浏览器。它不仅是一个流行的语言,同时因为它在市面上有着大量的工具和库可供使用,使它也成为一个真正强大的工程工具。JavaScript在服务端也提供了像其他技术能做到的诸如持续集成、部署、关系型数据库链接、面向业务架构等支持。

在与谷歌V8引擎的结合下,现在Node.js的速度已经相当快。事实上它比Ruby、Python等其他脚本语言速度要快上几倍。在benchmark提供的测试结果中,同等程度的代码其各项测试平均水平,JavaScript V8引擎的速度是Python3的13倍,是Ruby的8倍。这个结果对于一个动态语言来说是难以置信的,不小一部分是因为V8内核的优化,像编译成机器语言预执行。

Benchmarking(标杆分析法)

Benchmarking是一个复杂的话题,同时这些数据也是不能让人完全信服的。这里用Benchmarking提供的数据只是为了消除人们对JavaScript语言本身很慢的误解。

官方介绍中有提到事件驱动和非阻塞I/O,传统的程序都是通过同步方式工作的,当一句代码在执行时,系统会等待其返回结果并作出处理,之后才会执行后续的代码。因此有时会出现很长的等待时间,比如在网络环境下读写数据库的操作。

像Java和C#这种语言可以通过开辟新线程的方式来解决这种问题。一个线程可以看作是一个用来处理任务的轻量级的进程。在多线程编程中如果存在多个线程同时请求同一个资源,这将是一件很难处理的事。举个例子简单说明这种业务场景:比如有一个计数器,现在两个线程同时操作这个计数器,一个线程增加计数器数字,而另一个则相反减少计数器数字。

JavaScript通过另一种方式解决了这种问题,那就是只允许有一个线程。当进行耗时的I/O操作时,比如读数据库,程序不会等待,相应的它会立刻执行这行代码后续的代码。当I/O操作返回结果时,它会触发一个回调函数来处理返回结果。这种操作方式看起来是不太现实的,不用担心在这本书会在后续相信说明,因为这种操作方式我们会反复用到。Node.js的简洁、快速、事件驱动编程模型等特性很适合当代这种异步的Web应用程序。

优点和缺点

Node.js不是万能的,它也存在一系列的问题,计算机程序可以被大致分为两类:CPU密集型和I/O密集型,对计算机密集型问题的处理能力取决于计算机时钟周期的数量,质数计算 [[2]]() 就是一个例子。Node.js本身的设计不是为了解决CPU密集型的问题。在Node.js正式发布之前,Ryan Dahl曾提出:

现在有一个非常苛刻的需求,任何一个计算的请求都必须在5毫秒内返回,如果你超过了这个时间限制。那么这个请求就会被中断。如果你提及了一个相当蠢的而且还说阻塞的JavaScript服务端代码,用它来计算前 10000 个质数的时候就会被中断。

我们需要一个不允许开发者去写出很蠢的代码这样一个开发环境,Ruby、Python、C++、PHP这些语言对于做web开发的人来说都过于自由和灵活了。

对于I/O密集型问题的处理能力取决于I/O的吞吐量,比如硬盘、内存和网络带宽、甚至数据缓存都可以产生影响。许多问题都是I/O密集型的,比如说C10K[[3]](),在这一方面Node.js是十分合适的,它能作为一个web服务端能处理十万或者更多个客户端同时链接。一些其他的技术平台是没有这样的容纳能力的,它们需要各样的补或者变通的方法来处理这种问题。Node.js能够胜任这种业务是因为它为有专门为并发场景而设计的异步非阻塞机制。

希望通过我的说明,你能对Node.js有进一步了解的兴趣,是时候开始进入正题了。

这本书将要构建一个实时的股票交易引擎,它将在浏览器端实时更新价格。出于好奇,你可能会问“是不是应该用C来构建一个股票市场的交易引擎来提升运算速度?” 是的,如果我们真的要构建一个应用于实际的交易引擎,我们肯定会用C

这本书的主要目的是说明讲解一些技术内容而不是构建一个投入使用的项目。对于股票交易这种特殊的实时应用,对软件和硬件的要求是很特殊的,因为它要满足微秒级的计数。除此之外也有很多不需要微秒级精准的应用不如Twitter、Facebook、eBay。这就是Node.js所擅长的,你将会通过这本书了解如何去构建这些应用。

从零开始

首先安装Node.js。创建一个Web项目,并完成一个表单页。最后简单介绍下如何连接数据库。

安装

我们可以通过源码来安装Node.js,不过这里我们用更加简单的方法:通过包管理工具。

访问https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager,根据你当前的操作系统选择合适的版本,目前支持的操作系统有:Gentoo、Debian、Unbunt、openSUSE、SLE(SUSE Linux Enterprises)、Fedroa、 RHEL/CentOS 等其他的Linux系统、Mac OS X和Windows。

安装完成后,在命令行工具中输入node进入REPL(read-eval-print-loop),输入如下代码测试安装是否成功。

console.log('Hello world');

会得到如下输出:

Hello world
undefined

如果你的操作结果和上述一致,说明你成功完成了第一个Node.js程序。之所以Hello world后面会有一个undefined是因为console总是会打印出返回类型。现在你已经完成的最基础的Hello world教学了。我向你保证这将是这本书中第一个也是唯一一个无聊的教学例子。好了,直入主题,我们将创建一个验证模块来验证用户名和密码是否匹配,这中间我们会用到基于云的NoSQL技术(可能撞破头你都不明白这是什么,不用担心,我马上就会说明)。

什么是基于云的NoSQL技术,你为什么一定要知道这个东西?现在对于云服务技术的宣传已经过于夸张。对于我们来说,之所以要用它是因为我们这个程序的规模,用云服务更加合适。当然你也可以通过轻点几下鼠标就能创建一个虚拟服务器。

NoSQL这个技术相对较新(注意编辑这段的时候是2012年),所以很难有一个全面的定义来具体说明。它可以被看做是一个用来存储大数据量的非结构化或者半结构化数据的数据库技术。像谷歌、亚马逊、Facebook这种大公司,因为要存储用户会产生的大量数据,所以它们大范围的使用NoSQL技术。

在这本书中我们采用MongoDB作为NoSQL数据库,稍后会在第三章详细讲解MongoDB。MongoDB是一个成熟、灵活的面向文档的数据库,在很多企业环境都在使用MongoDB(可以参考foursquare and craigslist)。文档型数据库是指用弱结构化的文档(比如 XML、JSON)来代替传统的数据行。MongoDB允许ad hoc query [4] 所以它保留了SQL的灵活性。这里选择MongoLab作为这个股票监测程序的数据库服务平台,因为MongoLab有提供免费的MongoDB服务。

基本准备

首先在浏览器中打开MongoLab并注册一个账号,然后点击 Create New 创建一个数据库,选择亚马逊EC2作为服务商,选择免费版,填写数据库名称、连接用户名和密码。

现在创建一个Web项目,Node.js本身提供了一个内置的最基本的HTTP服务。在这之上是一个叫做Connect的中间件框架它提供了数据库连接、cookies、session、日志、压缩等功能(详情可以访问Connect)。在Connect之上就是Express,Express提供了路由、模板(采用Jade作为模板引擎)和视图渲染等功能。这本书的项目主要使用Express来搭建。

图-1.2 注册一个MongoLab账号

图-1.2 注册一个MongoLab账号

通过如下命令安装Express

sudo npm install -g [email protected]

通过 -g 参数来指定这个包能够在全局中调用。 @2.5.8 指定了Express的版本,这本书中全部都采用此版本。

全局安装和局部安装

Node.js官方博客是建议使用全局安装的,指南上说明如果你想在命令行中使用该工具包则应该全局安装,如果你想在某个项目中使用该工具包则应该局部安装。对于Epress这种包,则应该全局安装,并且在需要的项目里局部安装。

在命令行中键入如下命令,这里采用默认的参数(即为项目名)。

 express authentication

你会看到如下输出:

create : authentication
create : authentication/package.json
M
dont forget to install dependencies:
$ cd authentication && npm install

$ cd authentication && npm installnpm install 会根据package.json配置文件中指定的依赖下载相应的包。package.json是一个文本文件,通过JSON(JavaScript Object Notation)格式指定包依赖,将package.json安装如下进行修改:

/*    chapter01/authentication/package.json (excerpt)     */

{
    "name": "authentication"
    , "version": "0.0.1"
    , "private": true
    , "dependencies": {
        "express": "2.5.8" 
      , "jade": "0.26.1"
      , "mongoose": "2.6.5"
    } 
}
避免依赖陷阱

在配置依赖包时可以通过 `*` 来代替具体版本号,这样会从版本库中获取最新版本安装到本地,始终采用最新版本的工具包听起来是一件和极客很酷的事,但是要知道你的项目并不一定能100%兼容下一个版本,所以我建议在配置依赖包使要指明具体版本,你可以查看[这篇文章](http://blog.nodejs.org/2012/02/27/managing-node-js-dependencies-with-shrinkwrap/)<sup>[5]</sup>去了解更加稳定健壮的解决方案。

在命令行键入cd authentication 命令进入authentication根目录,然后执行 npm install 命令,项目所依赖的包就会自动下载下来。然后键入 node app,在浏览器中输入 http://localhost:3000 你会看到如下信息:

“Welcome to Express.”

是不是很简单。

创建一个简单的表单

接下来要创建一个基本的表单用来向服务端传递数据。通常会选择Apache这种Web服务器作为站点文件的存放容器。Node.js是崇尚极简主义的,它本身自己可以作为Web服务启动,所以你可以先不用配置这样的Web服务器。如果你想要从硬盘读文件的话,你需要对代码做一些修改,关闭程序进程,打开 app.js 文件,在文件顶部引用fs(用来读文件)依赖包,同时添加 /form 路由:

/*chapter01/authentication/app.js (excerpt)*/

var express = require('express') 
, routes = require('./routes') 
, fs = require('fs');

...

// Routes
app.get('/', routes.index);

app.get('/form', function(req, res) {        ①
    fs.readFile('./form.html', function(error, content) {    ②
        if (error) {
            res.writeHead(500);
            res.end();
        }
        else {
            res.writeHead(200, { 'Content-Type': 'text/html' });
            res.end(content, 'utf-8');
        }
    }); 
});

这里有两点需要说明:

① 用来处理 /form 路由,当有用户访问 http://localhost:3000/form 时,Express会执行对应的代码来处理这个请求。

② 这段代码会读取本地的 form.html 文件并返回给浏览器,如果发生错误则会返回状态码 500。

回调函数

对于从其他语言转JavaScript的朋友来说,第一次接触回调函数这个概念稍微有些难以理解,我们通过如下例子来说明。
setTimeout(function() {
    console.log('first or second');
}, 500);
console.log('we will see');
执行这段代码,第一段代码设置了一个500ms的定时器,JavaScript会立刻执行后续代码,而不是等待返回结果。这就是为什么 `we will see`会在`first or second`之前打印出来。
在Node.js中一切都是异步方式设计的,所以只有你自己的代码可能阻塞进程。在这个示例程序中第一行代码也可以是一个从云数据库读写数据的操作,这同样不会阻塞代码的执行。这个范式第一次接触会有些迷惑,但是一旦你熟悉它的用法你会发现这真是一个优雅的解决方案。

我们创建一个非常简单的表单页面。

<!-- chapter01/authentication/form.html -->

<form action="/signup" method="post">
    <div>
        <label>Username:</label>
        <input type="text" name="username"/><br/>
    </div>
    <div>
        <label>Password:</label>
        <input type="password" name="password"/>
    </div>
    <div><input type="submit" value="Sign Up"/></div>
</form>

访问 http://localhost:3000/form,你会看到如 图-1.3 所示界面。

图-1.3 表单页面

图-1.3 表单页面

现在先不要关注页面的样式,我们会在后续的章节统一处理。

当用户点击 Sign Up 按钮时,表单会将数据传输到 /signup,现在通过Express添加一个方法来处理表单数据,复制以下代码到app.js。

/*    chapter01/authentication/app.js (excerpt)    */

app.post('/signup', function(req, res) {
  var username = req.body.username;
  var password = req.body.password;
  User.addUser(username, password, function(err, user) {
      if (err) throw err;
    res.redirect('/form');
  });
});

第一行代码和我们之前定义的 app.get 代码很相似,只是这里定义为 app.post。 接下来的两行从请求对象中获取到用户名和密码,接下来 User.addUser(username, password, function(err, user) { 这段代码看起来有些神秘,这其中的User来自哪里?我们稍后将会创建一个users module,在这其中会定义User。Node.js提供了一个优秀的模块系统,允许编程人员去封装隔离他们的代码。对于组织一个复杂的项目,代码分离是一个核心的编程原则。庆幸的是Node.js可以很方便的定义模块来组织特定功能的代码。回到上述代码的说明,当用户添加完成后,跳转返回表单页面。

实际开发情况

在实际开发中,Express提供了很多有用的抽象,也就是说我们不必像现在这种方式从硬盘读取文件。同时也要注意Node.js默认是不支持"hot swapping"(指只有重启Node.js服务,在上次启动后修改的代码才会有效)的。每次修改都重启服务是一件繁琐的事情,所以这里建议安装node-supervisor来监测文件改变,自动重启服务。

数据库

在创建users模块之前,需要先创建一个数据库模块。首先创建一个lib目录,在该目录下新建一个db.js文件,然后拷贝如下代码:

/*     chapter01/authentication/lib/db.js    */

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

module.exports.mongoose = mongoose;
module.exports.Schema = Schema;

// Connect to cloud database
var username = "user"
var password = "password";
var address = ' @dbh42.mongolab.com:27427/nockmarket'; connect();

// Connect to mongo
function connect() {
    var url = 'mongodb://' + username + ':' + password + address;
    mongoose.connect(url);
}
function disconnect() {    
    mongoose.disconnect();
}

如此简答的代码就能连接基于云的NoSQL服务是相当惊人的,不过这里你还需要根据你实际申请的情况修正用户名、密码和连接地址。比如 var username = "user" 修改为 var username = bob927。你也可以在MongoLab追加新的用户,如图-1.4。

图-1.4 MongoLab用户标签页

图-1.4 MongoLab用户标签页

你可以从 图-1.5 中找到数据库连接字符串示例。

图-1.5 MongoLab连接字符串

图-1.5 MongoLab连接字符串

我们采用Mongoose来连接Node.js和MongoDB。 module.export是Node.js的语法,一般用于将模块内定义的变量和方法暴露给外部供其他模块调用。

接下来说明Mongoose驱动中的表结构对象(Schema object),简单的说表结构对象就是 一种定义数据集结构的方式。在这个例子中我们的表结构中包括用户名(字符串类型)和密码(字符串类型)。除此之外我们还定义了两个函数用于建立/关闭与数据库的连接。

接下来就可以定义users模块了,在项目根目录下创建一个models文件夹,在该文件夹下新建一个User.js文件并拷贝如下代码:

/*  chapter01/authentication/models/User.js (excerpt)   */
var db = require('../lib/db');

var UserSchema = new db.Schema({
    username : {type: String, unique: true}
  , password : String
});

var MyUser = db.mongoose.model('User', UserSchema);

// Exports
module.exports.addUser = addUser;

// Add user to database
function addUser(username, password, callback) {
  var instance = new MyUser();
  instance.username = username;
  instance.password = password;
  instance.save(function (err) {
    if (err) {  callback(err);  }
    else {  callback(null, instance); }
  });
}

首先导入之前定义的数据库模块db.js,然后定义了一个简单的表结构UserSchema。接下来实例化一个user,把用户名和密码传递给这个user,然后调用save方法将其存入数据库。如果发生错误,会在回调函数中返回错误信息,如果执行正常就返回这个user对象。

密码安全

在这个例子中,我们直接将密码明文存入到数据库中是存在安全隐患的,正常情况下密码都是需要加密的。目前存在很多种加密技术对密码进行加密,使其能更好的抵挡词典式攻击(dictionary attack)。

现在可以在Express中使用user模块了,在app.js文件里添加如下代码。

/*   chapter01/authentication/app.js (excerpt)   */
var express = require('express')
, routes = require('./routes')
, fs = require('fs')
, User = require('./models/User.js');

现在重启服务,然后提交表单数据,查看MongoLab,应该和 图-1.6 类似。

图-1.6

图-1.6 MongoDB中新添加的用户数据
注意users集合是自动创建的,你现在已经成功建设了一个基于云端NoSQL的应用。如果你的下一个项目是Facebook或者Twitter这种大用户量的应用,那么你在做架构的时候一定要留有足够的拓展空间。

目录结构

建立一个专门的目录去存一个单独的文件看起来似乎有些过于繁琐,当项目增长变大的时候,出于更好的理解和组织项目,将文件按照一定的逻辑分开存放是很必要的。简单的说我会将数据库模型放在models目录下,业务逻辑和帮助方法会存放在lib目录下。

总结

在这章中,主要讲解了如下内容:

  1. Node.js的设计理念,以及它和其他编程环境的区别。
  2. 安装Node.js
  3. 在控制台通过REPL执行一段程序代码。
  4. 创建一个基本的Express应用。
  5. 基本的包管理。
  6. 模块化管理代码,以及exports方法。
  7. 通过表单提交数据。
  8. 通过MongoLab云服务将数据存储到MongoDB。