学习 NodeJ

更新时间:2023-09-28 21:31:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

NodeJS基础

什么是NodeJS

JS是脚本语言,脚本语言都需要一个解析器才能运行。对于写在HTML页面里的JS,浏览器充当了解析器的角色。而对于需要独立运行的JS,NodeJS就是一个解析器。

每一种解析器都是一个运行环境,不但允许JS定义各种数据结构,进行各种计算,还允许JS使用运行环境提供的内置对象和方法做一些事情。例如运行在浏览器中的JS的用途是操作DOM,浏览器就提供了document之类的内置对象。而运行在NodeJS中的JS的用途是操作磁盘文件或搭建HTTP服务器,NodeJS就相应提供了fs、http等内置对象。

有啥用处

尽管存在一听说可以直接运行JS文件就觉得很酷的同学,但大多数同学在接触新东西时首先关心的是有啥用处,以及能带来啥价值。

NodeJS的作者说,他创造NodeJS的目的是为了实现高性能Web服务器,他首先看重的是事件机制和异步IO模型的优越性,而不是JS。但是 他需要选择一种编程语言实现他的想法,这种编程语言不能自带IO功能,并且需要能良好支持事件机制。JS没有自带IO功能,天生就用于处理浏览器中的 DOM事件,并且拥有一大群程序员,因此就成为了天然的选择。

如他所愿,NodeJS在服务端活跃起来,出现了大批基于NodeJS的Web服务。而另一方面,NodeJS让前端众如获神器,终于可以让自己的能力覆盖范围跳出浏览器窗口,更大批的前端工具如雨后春笋。 因此,对于前端而言,虽然不是人人都要拿NodeJS写一个服务器程序,但简单可至使用命令交互模式调试JS代码片段,复杂可至编写工具提升工作效率。 NodeJS生态圈正欣欣向荣。

如何安装

安装程序

NodeJS提供了一些安装程序,都可以在nodejs.org这里下载并安装。

Windows系统下,选择和系统版本匹配的.msi后缀的安装文件。Mac OS X系统下,选择.pkg后缀的安装文件。 编译安装

Linux系统下没有现成的安装程序可用,虽然一些发行版可以使用apt-get之类的方式安装,但不一定能安装到最新版。因此Linux系统下一般使用以下方式编译方式安装NodeJS。 1. 确保系统下g++版本在4.6以上,python版本在2.6以上。

2. 从nodejs.org下载tar.gz后缀的NodeJS最新版源代码包并解压到某个位置。

3. 进入解压到的目录,使用以下命令编译和安装。

?

1 $ ./configure 2 $ make 3 $ sudo make install

如何运行

打开终端,键入node进入命令交互模式,可以输入一条代码语句后立即执行并显示结果,例如: ?

1 $ node

2 > console.log('Hello World!'); 3 Hello World!

如果要运行一大段代码的话,可以先写一个JS文件再运行。例如有以下hello.js。 ? 1 function hello() { 2 console.log('Hello World!'); 3 } 4 hello();

写好后在终端下键入node hello.js运行,结果如下: ? 1 $ node hello.js 2 Hello World! 权限问题

在Linux系统下,使用NodeJS监听80或443端口提供HTTP(S)服务时需要root权限,有两种方式可以做到。

一种方式是使用sudo命令运行NodeJS。例如通过以下命令运行的server.js中有权限使用80和443端口。一般推荐这种方式,可以保证仅为有需要的JS脚本提供root权限。 ? 1 $ sudo node server.js

另一种方式是使用chmod +s命令让NodeJS总是以root权限运行,具体做法如下。因为这种方式让任何JS脚本都有了root权限,不太安全,因此在需要很考虑安全的系统下不推荐使用。 ?

1 $ sudo chown root /usr/local/bin/node 2 $ sudo chmod +s /usr/local/bin/node

模块

编写稍大一点的程序时一般都会将代码模块化。在NodeJS中,一般将代码合理拆分到不同的JS文件中,每一个文件就是一个模块,而文件路径就是模块名。

在编写每个模块时,都有require、exports、module三个预先定义好的变量可供使用。 require

require函数用于在当前模块中加载和使用别的模块,传入一个模块名,返回一个模块导出对象。模块名可使用相对路径(以./开头),或者是绝对路径(以/或C:之类的盘符开头)。另外,模块名中的.js扩展名可以省略。以下是一个例子。

? 1 var foo1 = require('./foo'); 2 var foo2 = require('./foo.js'); 3 var foo3 = require('/home/user/foo'); 4 var foo4 = require('/home/user/foo.js'); 5 6 // foo1至foo4中保存的是同一个模块的导出对象。

另外,可以使用以下方式加载和使用一个JSON文件,模块名中.json扩展名不可省略。 ? 1 var data = require('./data.json'); exports

exports对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过require函数使用当前模块时得到的就是当前模块的exports对象。以下例子中导出了一个公有方法。 ?

1 exports.hello = function () { 2 console.log('Hello World!'); 3 }; module

通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象。例如模块导出对象默认是一个普通对象,如果想改成一个函数的话,可以使用以下方式。 ? 1 module.exports = function () {

2 console.log('Hello World!'); 3 };

以上代码中,模块默认导出对象被替换为一个函数。 模块初始化

一个模块中的JS代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对象。之后,缓存起来的导出对象被重复利用。 主模块

通过命令行参数传递给NodeJS以启动程序的模块被称为主模块。主模块负责调度组成整个程序的其它模块完成工作。例如通过以下命令启动程序时,main.js就是主模块。 ? 1 $ node main.js 完整示例 例如有以下目录。 ? 1 - /home/user/hello/ 2 - util/ 3 counter.js 4 main.js

其中counter.js内容如下: ? 1 var i = 0; 2 3 function count() { 4 return ++i; 5 } 6 7 exports.count = count;

该模块内部定义了一个私有变量i,并在exports对象导出了一个公有方法count。 主模块main.js内容如下:

? 1 var counter1 = require('./util/counter'); 2 var counter2 = require('./util/counter');

3 4 console.log(counter1.count()); 5 console.log(counter2.count()); 6 console.log(counter2.count()); 运行该程序的结果如下: ? 1 $ node main.js 2 1 3 2 4 3

可以看到,counter.js并没有因为被require了两次而初始化两次。

二进制模块

虽然一般我们使用JS编写模块,但NodeJS也支持使用C/C++编写二进制模块。编译好的二进制模块除了文件扩展名是.node外,和JS模块的使用方式相同。虽然二进制模块能使用操作系统提供的所有功能,拥有无限的潜能,但对于前端同学而言编写过于困难,并且难以跨平台使用,因此不在本教程的覆盖范围内。

小结

本章介绍了有关NodeJS的基本概念和使用方法,总结起来有以下知识点:

NodeJS是一个JS脚本解析器,任何操作系统下安装NodeJS本质上做的事情都是把NodeJS执行程序复制到一个目录,然后保证这个目录在系统PATH环境变量下,以便终端下可以使用node命令。

? 终端下直接输入node命令可进入命令交互模式,很适合用来测试一些JS代码片段,比如正则表

达式。

? NodeJS使用CMD模块系统,主模块作为程序入口点,所有模块在执行过程中只初始化一次。 ? 除非JS模块不能满足需求,否则不要轻易使用二进制模块,否则你的用户会叫苦连天。

?

代码的组织和部署

有经验的C程序员在编写一个新程序时首先从make文件写起。同样的,使用NodeJS编写程序前,为了有个良好的开端,首先需要准备好代码的目录结构和部署方式,就如同修房子要先搭脚手架。本章将介绍与之相关的各种知识。

模块路径解析规则

我们已经知道,require函数支持斜杠(/)或盘符(C:)开头的绝对路径,也支持./开头的相对路径。但这两种路径在模块之间建立了强耦合关系,一旦某个模块文件的存放位置需要变更,使用该模块的其

3 setTimeout(function () { 4 try {

5 callback(null, fn());

6 } catch (err) { 7 callback(err); 8 } 9 }, 0); 10 } 11 12 async(null, function (err, data) { 13 if (err) { 14 console.log('Error: %s', err.message); 15 } else {

16 // Do something. 17 } 18 }); 19 20 -- Console ------------------------------ 21 Error: object is not a function

可以看到,异常再次被捕获住了。在NodeJS中,几乎所有异步API都按照以上方式设计,回调函数中第一个参数都是err。因此我们在编写自己的异步函数时,也可以按照这种方式来处理异常,与NodeJS的设计风格保持一致。

有了异常处理方式后,我们接着可以想一想一般我们是怎么写代码的。基本上,我们的代码都是做一些事情,然后调用一个函数,然后再做一些事情,然后再调用一个函数,如此循环。如果我们写的是同步代码,只需要在代码入口点写一个try语句就能捕获所有冒泡上来的异常,示例如下。 ?

1 function main() { 2 // Do something. 3 syncA();

4 // Do something. 5 syncB();

6 // Do something. 7 syncC(); 8 } 9 10 try { 11 main(); 12 } catch (err) { 13 // Deal with exception. 14 }

但是,如果我们写的是异步代码,就只有呵呵了。由于每次异步函数调用都会打断代码执行路径,只能通过回调函数来传递异常,于是我们就需要在每个回调函数里判断是否有异常发生,于是只用三次异步函数调用,就会产生下边这种代码。

?

1 function main(callback) { 2 // Do something.

3 asyncA(function (err, data) { 4 if (err) {

5 callback(err); 6 } else {

7 // Do something

8 asyncB(function (err, data) { 9 if (err) { 10 callback(err); 11 } else { 12 // Do something 13 asyncC(function (err, data) { 14 if (err) { 15 callback(err); 16 } else { 17 // Do something 18 callback(null); 19 } 20 }); 21 } 22 }); 23 } 24 }); 25 } 26 27 main(function (err) { 28 if (err) {

29 // Deal with exception. 30 } 31 });

可以看到,回调函数已经让代码变得复杂了,而异步方式下对异常的处理更加剧了代码的复杂度。如果NodeJS的最大卖点最后变成这个样子,那就没人愿意用NodeJS了,因此接下来会介绍NodeJS提供的一些解决方案。

域(Domain)

官方文档: http://nodejs.org/api/domain.html

NodeJS提供了domain模块,可以简化异步代码的异常处理。在介绍该模块之前,我们需要首先理解“域”的概念。简单的讲,一个域就是一个JS运行环境,在一个运行环境中,如果一个异常没有被捕获,将作为一个全局异常被抛出。NodeJS通过process对象提供了捕获全局异常的方法,示例代码如下 ?

1 process.on('uncaughtException', function (err) { 2 console.log('Error: %s', err.message); 3 }); 4

5 setTimeout(function (fn) { 6 fn(); 7 }); 8

9 -- Console ------------------------------ 10 Error: undefined is not a function

虽然全局异常有个地方可以捕获了,但是对于大多数异常,我们希望尽早捕获,并根据结果决定代码的执行路径。我们用以下HTTP服务器代码作为例子: ?

1 function async(request, callback) { 2 // Do something.

3 asyncA(request, function (err, data) { 4 if (err) {

5 callback(err); 6 } else {

7 // Do something

8 asyncB(request, function (err, data) { 9 if (err) { 10 callback(err); 11 } else { 12 // Do something 13 asyncC(request, function (err, data) { 14 if (err) {

15 callback(err); 16 } else { 17 // Do something 18 callback(null, data); 19 } 20 }); 21 } 22 }); 23 } 24 });

25 } 26 27 http.createServer(function (request, response) { 28 async(request, function (err, data) { 29 if (err) { 30 response.writeHead(500); 31 response.end(); 32 } else { 33 response.writeHead(200); 34 response.end(data); 35 } 36 }); 37 });

以上代码将请求对象交给异步函数处理后,再根据处理结果返回响应。这里采用了使用回调函数传递异常的方案,因此async函数内部如果再多几个异步函数调用的话,代码就变成上边这副鬼样子了。为了让代码好看点,我们可以在每处理一个请求时,使用domain模块创建一个子域(JS子运行环境)。在子域内运行的代码可以随意抛出异常,而这些异常可以通过子域对象的error事件统一捕获。于是以上代码可以做如下改造:

?

1 function async(request, callback) { 2 // Do something.

3 asyncA(request, function (data) { 4 // Do something

5 asyncB(request, function (data) { 6 // Do something

7 asyncC(request, function (data) { 8 // Do something 9 callback(data); 10 }); 11 }); 12 });

13 } 14 15 http.createServer(function (request, response) { 16 var d = domain.create(); 17 18 d.on('error', function () { 19 response.writeHead(500); 20 response.end(); 21 }); 22

23 d.run(function () {

24 async(request, function (data) { 25 response.writeHead(200); 26 response.end(data); 27 }); 28 }); 29 });

可以看到,我们使用.create方法创建了一个子域对象,并通过.run方法进入需要在子域中运行的代码的入口点。而位于子域中的异步函数回调函数由于不再需要捕获异常,代码一下子瘦身很多。 陷阱

无论是通过process对象的uncaughtException事件捕获到全局异常,还是通过子域对象的error事件捕获到了子域异常,在NodeJS官方文档里都强烈建议处理完异常后立即重启程序,而不是让程序继续运行。按照官方文档的说法,发生异常后的程序处于一个不确定的运行状态,如果不立即退出的话,程序可能会发生严重内存泄漏,也可能表现得很奇怪。

但这里需要澄清一些事实。JS本身的throw..try..catch异常处理机制并不会导致内存泄漏,也不会让程序 的执行结果出乎意料,但NodeJS并不是存粹的JS。NodeJS里大量的API内部是用C/C++实现的,因此NodeJS程序的运行过程中,代码执 行路径穿梭于JS引擎内部和外部,而JS的异常抛出机制可能会打断正常的代码执行流程,导致C/C++部分的代码表现异常,进而导致内存泄漏等问题。

因此,使用uncaughtException或domain捕获异常,代码执行路径里涉及到了C/C++部分的代码时,如果不能确定是否会导致内存泄漏等问题,最好在处理完异常后重启程序比较妥当。而使用try语句捕获异常时一般捕获到的都是JS本身的异常,不用担心上诉问题。

小结

本章介绍了JS异步编程相关的知识,总结起来有以下几点:

不掌握异步编程就不算学会NodeJS。

? 异步编程依托于回调来实现,而使用回调不一定就是异步编程。

? 异步编程下的函数间数据传递、数组遍历和异常处理与同步编程有很大差别。 ? 使用domain模块简化异步代码的异常处理,并小心陷阱。

?

大示例

学习讲究的是学以致用和融会贯通。至此我们已经分别介绍了NodeJS的很多知识点,本章作为最后一章,将完整地介绍一个使用NodeJS开发Web服务器的示例。

需求

我们要开发的是一个简单的静态文件合并服务器,该服务器需要支持类似以下格式的JS或CSS文件合并请求。

本文来源:https://www.bwwdw.com/article/j8jd.html

Top