使用node.js进行服务器端JavaScript编程(1)


随着 Web 2.0 概念和 Ajax 技术的流行,JavaScript 作为 Ajax 应用开发中必不可少的一部分,已经得到了广泛的流行。开发人员也开始逐步的熟悉和掌握 JavaScript,并积累了相关的开发经验。虽然 JavaScript 目前主要用在 Web 应用中,以浏览器作为运行平台,但是已经有相关的尝试把 JavaScript 迁移到服务器端,这其中包括 Aptana 的 Jaxer 等。这种做法与 Google GWT 是异曲同工的。Google GWT 允许开发人员使用 Java 语言来编写 Web 前端代码。这两种做法的目的都是为了复用开发人员已经掌握的知识和积累的经验。在这点上,node.js 类似于 Jaxer。


简单的来说,node.js 是一个允许开发人员使用 JavaScript 语言编写服务器端代码的框架。也就是说编写的 JavaScript 代码可以直接运行在本地机器上,而不仅限于浏览器。从实现的角度来说,Jaxer 和 node.js 都使用了已有的 JavaScript 执行引擎。Jaxer 用的是 Mozilla Firefox 中使用的 JavaScript 引擎,而 node.js 用的则是 Google Chrome 中用的 V8 引擎。


node.js 入门


node.js 可以运行在 Linux、Windows 和 Macintosh 等主流的操作系统上。在 Windows 平台上运行 node.js 的话,需要 Cygwin 或是 MinGW 的支持。下面以常用的 Windows 平台为例来说明。首先需要安装 Cygwin。安装的时候需要选择 gcc-g++ 、make、openssl 和 python 等包。gcc 的版本必须是最新的。接着从 参考资料 中给出的地址下载 node.js 0.4.0 版本的源代码。下载解压之后,依次在 Cygwin 中运行 ./configure 、make 和 make install 等命令进行编译和安装。安装完成之后,直接运行 node 命令就可以启动 node.js 提供的命令行。在命令行中可以直接输入 JavaScript 代码并运行。也可以通过 node server.js 的方式来运行一个 JavaScript 文件 server.js 。


代码清单 1 中给出了一个简单的“Hello World”程序的示例。通过 node helloworld.js 来运行该 JavaScript 文件之后,会在控制台输出“Hello World”。


清单 1. 使用 node.js 的“Hello World”程序


  1. process.stdout.write("Hello World"); 

代码清单 1 中的 process 表示的是当前运行的 node.js 进程,其属性 stdout 表示的是进程的标准输出流。通过 write() 方法向给流中写入一个字符串。从 代码清单 1 可以看到,使用 JavaScript 就可以访问标准输出流等本地系统上的资源。这从一个侧面反映出来了 node.js 的强大。


在 node.js 可以运行的 JavaScript 代码中,可以使用一些全局的对象:包括 代码清单 1 中用到的 process 、下面会介绍的用来加载模块的 require() 方法、表示当前正在执行的 JavaScript 文件名的 __filename 、表示当前正在执行的 JavaScript 文件的目录的__dirname 和与浏览器中相似的用来执行定时任务的 setTimeout() 和 setInterval() 方法等。


在介绍了 node.js 的基本知识之后,下面介绍 node.js 的模块化结构。


模块化结构


node.js 使用了 CommonJS 定义的模块系统。不同的功能组件被划分成不同的模块。应用可以根据自己的需要来选择使用合适的模块。每个模块都会暴露一些公共的方法或属性。模块使用者直接使用这些方法或属性即可,不需要关系模块内部的实现细节。除了系统预置的多个模块之外,应用开发团队也可以利用这个机制来将应用拆分成多个模块,以提高代码的可复用性。


使用模块


在 node.js 中使用一个模块的方式是非常简单的。使用某个模块之前需要首先声明对它的依赖。在 JavaScript 代码中可以直接使用全局函数 require() 来加载一个模块。如 require("http") 可以加载系统预置的 http 模块。而 require("./myModule.js") 用来加载与当前 JavaScript 文件同一目录下的 myModule.js 模块。如果使用 require() 的路径以“/”开头的话,则认为是模块 JavaScript 文件在操作系统上的绝对路径。如果不是这两种情况的话,node.js 就会尝试在当前 JavaScript 文件的父目录及其祖先目录下的 node_modules目录下查找。比如目录 /usr/home/my.js 中调用了 require("other.js") 的话,node.js 会依次尝试查找下列文件:/usr/home/node_modules/other.js 、/usr/node_modules/other.js 和 /node_modules/other.js 。


require() 方法的返回值是该模块所暴露出来的公开 JavaScript 对象,包含了可供使用的方法和属性。代码清单 2 给出了模块的基本使用方式。


清单 2. 模块的基本使用方式


  1. var greetings = require("./greetings.js");   
  2. var msg = greetings.sayHello("Alex", "zh_CN");   
  3. process.stdout.write(msg); 

如 代码清单 2 所示,一般是直接把 require() 方法的返回值赋值给一个变量,在 JavaScript 代码中直接使用此变量即可。greetings.js 模块暴露了一个 sayHello() 方法,当前 JavaScript 代码直接使用了该方法。


开发自己的模块


开发自己的模块的基本工作是在模块对应的 JavaScript 文件中编写模块相关的代码。这其中封装了模块的内部处理逻辑。一般来说,一个模块通常会暴露一些公开的方法或属性给其使用者。模块的内部代码需要把这些方法或属性给暴露出来。代码清单 3 给出了 代码清单 2 中所使用的 greetings.js 文件的内容。


清单 3. greetings.js 模块的内容


  1. var languages = {   
  2.     "zh_CN" : "你好,",   
  3.     "en" : "Hello, "  
  4.  };        
  5.  exports.sayHello = function(name, language) {   
  6.     return languages[language] || languages["en"] + name;   
  7.  }; 

如 代码清单 3 所示,exports 对象的内容就是模块的使用者调用 require() 方法的返回值中所包含的内容。模块通过这种方式来声明其所暴露出来的公开方法和属性。在模块中定义的变量,如 languages ,是只对模块内部的代码可见的。


如果一个模块所包含的内容比较多,也可以用文件夹的方式来组织。可以在文件夹的根目录下面创建一个 package.json 文件,其内容中包含了模块的名称和入口 JavaScript 文件的路径。如果没有提供这个 package.json 文件的话,node.js 会默认在文件夹中查找index.js 文件作为模块的启动 JavaScript 文件。


在介绍完 node.js 的模块化结构之后,下面介绍其事件驱动机制。


事件驱动


开发过 Web 应用的人都熟悉浏览器中的事件处理机制。当对某个 DOM 元素上的某类事件感兴趣的时候,只需要在该 DOM 元素上面注册一个事件监听器即可。如 ele.addEventListener("click", function() {}) 就添加了一个对 click 事件的监听器。当事件发生的时候,事件监听器的 JavaScript 方法就会被调用。事件的处理方法是异步执行的。这种异步执行的方式非常适合于开发高性能并发网络应用。实际上,目前的高性能并发应用开发一般有两种做法:第一种是使用多线程的机制,另外一种就是采用基于事件驱动的方式。多线程的问题在于应用开发起来难度较高,很容易出现线程饥饿或是死锁等问题,对开发人员提出了更高的要求。而事件驱动的方式则更加灵活,很容易为 Web 开发人员所理解和使用,也不存在线程死锁等问题。依托于性能强大的 Google V8 引擎和先进的事件 I/O 架构,node.js 可以成为创建高性能服务器端应用的良好基础。


基于 node.js 开发应用与开发 Web 应用有相似的编程模型。很多模块都会暴露出一些事件,使用这些模块的代码通过注册事件监听器的方式来添加相应的处理逻辑。代码清单 4 中给出了一个简单的 HTTP 代理服务器的实现代码。


清单 4. HTTP 代理服务器


  1. var http = require("http");   
  2. var url = require("url");   
  3.  
  4. http.createServer(function (req, res) {   
  5.    var urlObj = url.parse(req.url, true); // 获取被代理的 URL   
  6.    var urlToProxy = urlObj.query.url;   
  7.    if (!urlToProxy) {   
  8.        res.statusCode = 400;   
  9.        res.end("URL 是必须的。");   
  10.    }   
  11.    else {   
  12.        console.log("处理代理请求:" + urlToProxy);   
  13.        var parsedUrl = url.parse(urlToProxy);   
  14.        var opt = {   
  15.            host : parsedUrl.hostname,   
  16.            port : parsedUrl.port || 80,   
  17.            path : (parsedUrl.pathname || "") + (parsedUrl.search || "")   
  18.                + (parsedUrl.hash || "")   
  19.        };   
  20.        http.get(opt, function(pres) { // 请求被代理 URL 的内容  
  21.            res.statusCode = pres.statusCode;   
  22.            var headers = pres.headers;   
  23.            for (var key in headers) {   
  24.                res.setHeader(key, headers[key]);   
  25.            }   
  26.            pres.on("data"function(chunk) {   
  27.                res.write(chunk); // 写回数据  
  28.            });   
  29.            pres.on("end"function() {   
  30.                res.end();   
  31.            });   
  32.        });   
  33.    }   
  34. }).listen(8088, "127.0.0.1");   
  35.  
  36. console.log("代理服务器已经在 8088 端口启动。"); 

整个代理服务器的实现比较简单。首先通过 http 模块中的 createServer() 方法用来创建一个 HTTP 服务器,再通过 listen() 方法就可以让该 HTTP 服务器在特定端口监听。在 createServer() 方法中传入的参数是 HTTP 请求的响应方法。实际上,每个 HTTP 请求都是对应于 HTTP 服务器上的一个 request 事件。代码清单 4 中的 HTTP 服务器创建部分实际上等价于 代码清单 5 中给出的实现方式。


清单 5. 使用事件机制的 HTTP 服务器创建方式


  1. var server = http.createServer();   
  2. server.on("request"function(req, res) {   
  3. }); 

在请求的处理方法里面,通过 http.get() 方法来获取被代理 URL 的内容。这里同样采用了基于事件的处理方式。pres.on("data", function(chunk) {}) 在 pres 的 data 事件上添加了一个处理方法。该方法的作用是当获取到被代理 URL 的内容的时候,就把获取到的内容写回到原始 HTTP 请求的响应中。对于 end 事件的处理也是同样的。在使用 node.js 进行开发的时候,会经常遇到这种使用事件处理方法和回调方法的场景。


在介绍了 node.js 的事件驱动机制之后,下面介绍一些常用的模块。





标签:

友情链接
轻松育儿世界奇观
苏ICP备16066217号-2