nodejs学习笔记

前段时间学习了一下Node.js的相关知识,现在简单的做了一个整理。附件中的demo例子如果你已经安装好node.js的环境,

可以直接运行。

 

一、Node.js是什么?

 

"Node.js 是服务器端的 JavaScript 运行环境,它具有无阻塞(non-blocking)和事件驱动(event-driven)等的特色,Node.js 采用 V8 引擎,

同样,Node.js 实现了类似 Apache 和 nginx 的web服务,让你可以通过它来搭建基于 JavaScript 的 Web App。"

 

Node.js不是JS应用、而是JS运行平台

 

Node.js采用C++语言编写而成,是一个Javascript的运行环境。既然不是Javascript应用,为何叫.js呢?因为Node.js是一个Javascript的

运行环境。提到Javascript,大家首先想到的是日常使用的浏览器,现代浏览器包含了各种组件,包括渲染引擎、Javascript引擎等,其中Javascript

引擎负责解释执行网页中的Javascript代码。作为Web前端最重要的语言之一,Javascript一直是前端工程师的专利。不过,Node.js是一个后端的

Javascript运行环境(支持的系统包括linux、Windows),这意味着你可以编写系统级或者服务器端的Javascript代码,交给Node.js来解释执行,

简单的命令类似于:

 

#node helloworld.js

 

Node.js采用了Google Chrome浏览器的V8引擎,性能很好,同时还提供了很多系统级的API,如文件操作、网络编程等。浏览器端的Javascript

代码在运行时会受到各种安全性的限制,对客户系统的操作有限。相比之下,Node.js则是一个全面的后台运行时,为Javascript提供了其他语言能够

实现的许多功能。

 

二、Node.js的安装与配置

 

我将在0.10.13的版本上介绍Node.js的安装和配置。(从nodejs的官网http://nodejs.org/#download可以查看到最新的二进制版本和源代码)

 

学习的时候是在windows环境下,因此下面只介绍Windows平台下的Node.js安装

 

在Windows(xp、Windows7)平台下,我介绍一种普通的安装Node.js的方法

 

可以从这里(http://nodejs.org/dist/v0.10.13/node-v0.10.13-x86.msi)直接下载到Node.js编译好的msi文件。然后双击即可在程序的引导下完成安装。

 

在命令行中直接运行:

 

命令行将打印出:

 

该引导步骤会将node.exe文件安装到C:\Program Files\nodejs\目录下,并将该目录添加进PATH环境变量。

 

其他操作系统下的安装配置方式请参见深入浅出Node.js(二):Node.js&NPM的安装与配置http://www.infoq.com/cn/articles/nodejs-npm-install-config

 

接下来,马上开始我们第一个Node.js应用:“Hello World”。

 

打开你最喜欢的编辑器,创建一个helloworld.js文件。我们要做就是向STDOUT输出“Hello World”,如下是实现该功能的代码:

 

console.log("Hello World");

 

保存该文件,并通过Node.js来执行:

 

 

环境安装正常的话,就会在终端输出Hello World 。

 

 

 

三、Node.js模块的实现

 

在使用Node.js的模块之前,我们首先了解一下Node.js的require机制。

 

简单模块定义和使用

 

在Node.js中,定义一个模块十分方便。我们以计算圆形的面积和周长两个方法为例,来表现Node.js中模块的定义方式。

 

将这个文件存为module.js,并新建一个module_test.js文件,并写入以下代码:

 

可以看到模块调用也十分方便,只需要require需要调用的文件即可。

 

在require了这个文件之后,定义在exports对象上的方法便可以随意调用。Node.js将模块的定义和调用都封装得极其简单方便,从API对用户

友好这一个角度来说,Node.js的模块机制是非常优秀的。

 

模块载入策略

 

Node.js的模块分为两类,一类为原生(核心)模块,一类为文件模块。原生模块在Node.js源代码编译的时候编译进了二进制执行文件,加载

的速度最快。另一类文件模块是动态加载的,加载速度比原生模块慢。但是Node.js对原生模块和文件模块都进行了缓存,于是在第二次require

时,是不会有重复开销的。其中原生模块都被定义在lib这个目录下面,文件模块则不定性。

 

 

由于通过命令行加载启动的文件几乎都为文件模块。我们从Node.js如何加载文件模块开始谈起。加载文件模块的工作,主要由原生模块module

来实现和完成,该原生模块在启动时已经被加载,进程直接调用到runMain静态方法。

 

 

_load静态方法在分析文件名之后执行

 

 

并根据文件路径缓存当前模块对象,该模块实例对象则根据文件名加载。

 

 

实际上在文件模块中,又分为3类模块。这三类文件模块以后缀来区分,Node.js会根据后缀名来决定加载方法。

  • .js。通过fs模块同步读取js文件并编译执行。
  • .node。通过C/C++进行编写的Addon。通过dlopen方法进行加载。
  • .json。读取文件,调用JSON.parse解析加载。

这里我们将详细描述js后缀的编译过程。Node.js在编译js文件的过程中实际完成的步骤有对js文件内容进行头尾包装。以app.js为例,

包装之后的app.js将会变成以下形式:

 

 

这段代码会通过vm原生模块的runInThisContext方法执行(类似eval,只是具有明确上下文,不污染全局),返回为一个具体的function对象。

最后传入module对象的exports,require方法,module,文件名,目录名作为实参并执行。

 

这就是为什么require并没有定义在app.js 文件中,但是这个方法却存在的原因。从Node.js的API文档中可以看到还有__filename、__dirname、

module、exports几个没有定义但是却存在的变量。其中__filename和__dirname在查找文件路径的过程中分析得到后传入的。module变量是这

个模块对象自身,exports是在module的构造函数中初始化的一个空对象({},而不是null)。

 

在这个主文件中,可以通过require方法去引入其余的模块。而其实这个require方法实际调用的就是load方法。

 

load方法在载入、编译、缓存了module后,返回module的exports对象。这就是circle.js文件中只有定义在exports对象上的方法才能被外部调用的原因。

 

以上所描述的模块载入机制均定义在lib/module.js中。

 

require方法中的文件查找策略较为复杂,具体的相关原理知识我会在以后的学习笔记中具体阐述。

 

 

四、Node.js的事件机制

 

  Node.js在其Github代码仓库(https://github.com/joyent/node)上有着一句短短的介绍:Evented I/O for V8JavaScript。这句近似广告语的句子却道尽了

Node.js自身的特色所在:基于V8引擎实现的事件驱动IO。

 

事件机制的实现

 

Node.js中大部分的模块,都继承自Event模块(http://nodejs.org/docs/latest/api/events.html )。Event模块(events.EventEmitter)是一个简单的事件监听器

模式的实现。具有addListener/on,once,removeListener,removeAllListeners,emit等基本的事件监听模式的方法实现。它与前端DOM树上的事件并不相同,

因为它不存在冒泡,逐层捕获等属于DOM的事件行为,也没有preventDefault()、stopPropagation()、 stopImmediatePropagation() 等处理事件传递的方法。

 

从另一个角度来看,事件侦听器模式也是一种事件钩子(hook)的机制,利用事件钩子导出内部数据或状态给外部调用者。Node.js中的很多对象,大多具有黑盒

的特点,功能点较少,如果不通过事件钩子的形式,对象运行期间的中间值或内部状态,是我们无法获取到的。这种通过事件钩子的方式,可以使编程者不用关注

组件是如何启动和执行的,只需关注在需要的事件点上即可。

以一个创建http服务器为例:

 

 

在这段HTTP request的代码中,程序员只需要将视线放在onRequest这些业务事件点即可,至于内部的流程如何,无需过于关注。

 

事件机制的进阶应用

 

继承event.EventEmitter

 

实现一个继承了EventEmitter类是十分简单的,以下是Node.js中流对象继承EventEmitter的例子:

 

 

Node.js在工具模块中封装了继承的方法,所以此处可以很便利地调用。程序员可以通过这样的方式轻松继承EventEmitter对象,利用事件机制,可以帮助

你解决一些问题。

 

多事件之间协作

 

在略微大一点的应用中,数据与Web服务器之间的分离是必然的,如新浪微博、Facebook、Twitter等。这样的优势在于数据源统一,并且可以为相同数据源

制定各种丰富的客户端程序。以Web应用为例,在渲染一张页面的时候,通常需要从多个数据源拉取数据,并最终渲染至客户端。Node.js在这种场景中可以

很自然很方便的同时并行发起对多个数据源的请求。

 

Node.js通过异步机制使请求之间无阻塞,达到并行请求的目的,有效的调用下层资源。

 

 

五、Node.js的异步I/O实现

 

 

异步I/O的必要性

 

有的语言为了设计得使应用程序调用方便,将程序设计为同步I/O的模型。这意味着程序中的后续任务都需要等待I/O的完成。在等待I/O完成的过程中,程序

无法充分利用CPU。为了充分利用CPU,和使I/O可以并行,目前有两种方式可以达到目的:

  • 多线程单进程
    多线程的设计之处就是为了在共享的程序空间中,实现并行处理任务,从而达到充分利用CPU的效果。多线程的缺点在于执行时上下文交换的开销较大,

和状态同步(锁)的问题。同样它也使得程序的编写和调用复杂化。

  • 单线程多进程
    为了避免多线程造成的使用不便问题,有的语言选择了单线程保持调用简单化,采用启动多进程的方式来达到充分利用CPU和提升总体的并行处理能力。

它的缺点在于业务逻辑复杂时(涉及多个I/O调用),因为业务逻辑不能分布到多个进程之间,事务处理时长要远远大于多线程模式。

理想的异步I/O模型

 

理想的异步I/O应该是应用程序发起异步调用,而不需要进行轮询,进而处理下一个任务,只需在I/O完成后通过信号或是回调将数据传递给应用程序即可。

 

Node.js的异步I/O模型

 

很多同学在遇见Node.js后必然产生过对回调函数究竟如何被调用产生过好奇。在文件I/O这一块与普通的业务逻辑的回调函数不同在于它不是由我们自己的

代码所触发,而是系统调用结束后,由系统触发的。下面我们以最简单的fs.open方法来作为例子,探索Node.js与底层之间是如何执行异步I/O调用和回调

函数究竟是如何被调用执行的。

 

 

fs.open的作用是根据指定路径和参数,去打开一个文件,从而得到一个文件描述符,是后续所有I/O操作的初始操作。

 

在JavaScript层面上调用的fs.open方法最终都透过node_file.cc调用到了libuv中的uv_fs_open方法,这里libuv作为封装层,分别写了两个平台下的

代码实现,编译之后,只会存在一种实现被调用。

 

请求对象

 

在uv_fs_open的调用过程中,Node.js创建了一个FSReqWrap请求对象。从JavaScript传入的参数和当前方法都被封装在这个请求对象中,其中回调

函数则被设置在这个对象的oncomplete_sym属性上。

 

 

对象包装完毕后,调用QueueUserWorkItem方法将这个FSReqWrap对象推入线程池中等待执行。

 

 

QueueUserWorkItem接受三个参数,第一个是要执行的方法,第二个是方法的上下文,第三个是执行的标志。当线程池中有可用线程的时候调用

uv_fs_thread_proc方法执行。该方法会根据传入的类型调用相应的底层函数,以uv_fs_open为例,实际会调用到fs__open方法。调用完毕之后,

会将获取的结果设置在req->result上。然后调用PostQueuedCompletionStatus通知我们的IOCP对象操作已经完成。

 

 

PostQueuedCompletionStatus方法的作用是向创建的IOCP上相关的线程通信,线程根据执行状况和传入的参数判定退出。

至此,由JavaScript层面发起的异步调用第一阶段就此结束。

 

事件循环

 

在调用uv_fs_open方法的过程中实际上应用到了事件循环。以在Windows平台下的实现中,启动Node.js时,便创建了一个基于IOCP的事件循环loop

,并一直处于执行状态。

 

 

每次循环中,它会调用IOCP相关的GetQueuedCompletionStatus方法检查是否线程池中有执行完的请求,如果存在,poll操作会将请求对象加入到loop

的pending_reqs_tail属性上。 另一边这个循环也会不断检查loop对象上的pending_reqs_tail引用,如果有可用的请求对象,就取出请求对象的result

属性作为结果传递给oncomplete_sym执行,以此达到调用JavaScript中传入的回调函数的目的。 至此,整个异步I/O的流程完成结束。其流程如下:

 

事件循环和请求对象构成了Node.js的异步I/O模型的两个基本元素,这也是典型的消费者生产者场景。在Windows下通过IOCP的

GetQueuedCompletionStatus、PostQueuedCompletionStatus、QueueUserWorkItem方法与事件循环实。对于*nix平台下,这个流程的不同之处

在与实现这些功能的方法是由libeio和libev提供。

 

六、Node.js从到理论到实战

 

上面阐述了一些理论知识,下面我们开始一个完整的基于Node.js的web应用,这个例子是一个针对入门者的很好的文档上的,大家都可以用来作为入门学习。

 

用例如下:

  • 用户可以通过浏览器使用我们的应用。
  • 当用户请求http://domain/start时,可以看到一个欢迎页面,页面上有一个文件上传的表单。
  • 用户可以选择一个图片并提交表单,随后文件将被上传到http://domain/upload,该页面完成上传后会把图片显示在页面上。

应用不同模块分析

 

我们来分解一下这个应用,为了实现上文的用例,我们需要实现哪些部分呢?

  • 我们需要提供Web页面,因此需要一个HTTP服务器
  • 对于不同的请求,根据请求的URL,我们的服务器需要给予不同的响应,因此我们需要一个路由,用于把请求对应到请求处理程序(request handler)
  • 当请求被服务器接收并通过路由传递之后,需要可以对其进行处理,因此我们需要最终的请求处理程序
  • 路由还应该能处理POST数据,并且把数据封装成更友好的格式传递给请求处理入程序,因此需要请求数据处理功能
  • 我们不仅仅要处理URL对应的请求,还要把内容显示出来,这意味着我们需要一些视图逻辑供请求处理程序使用,以便将内容发送给用户的浏览器
  • 最后,用户需要上传图片,所以我们需要上传处理功能来处理这方面的细节

我们从server.js开始,

 

 

接下来是 router.js —— 我们不再需要传递postData了,这次要传递request对象:

 

 

最后,要实现我们的用例:允许用户上传图片,并将该图片在浏览器中显示出来。

 

这里我们要用到的外部模块是FelixGeisendörfer开发的node-formidable模块。它对解析上传的文件数据做了很好的抽象。

 

使用该模块,首先需要安装该模块。Node.js有它自己的包管理器,叫NPM。它可以让安装Node.js的外部模块变得非常方便。通过如下一条命令就可以完成该模块的安装:

 

 

如果终端输出如下内容:

 

 

就说明模块已经安装成功了。

 

现在我们就可以用formidable模块了——使用外部模块与内部模块类似,用require语句将其引入即可:

 

var formidable = require("formidable");

 

现在,request对象就可以在我们的upload请求处理程序中使用了。node-formidable会处理将上传的文件保存到本地/tmp目录中,而我们需要做的是

确保该文件保存成/tmp/sophie.jpg。 这里假设只允许上传jpg图片。

 

接下来,我们把处理文件上传以及重命名的操作放到一起,如下requestHandlers.js所示:

 

 

好了,重启服务器,我们应用所有的功能就可以用了。

 

在浏览器中输入http://localhost:8888/start

选择一张本地图片,将其上传到服务器,然后浏览器就会显示该图片。

 

如果不能正常显示图片,服务端报的错误类似如下的截图一样:

 

 

追根原因是fs.rename 问题,临时文件写成功了,但是就是无法移动...

是由于磁盘分区导致的,我建的工程在F盘,符合这个条件 C: -> F:

 

解决这个需要以下2步:

(1)改变formidable的默认零时文件夹路径,保证和目标目录处于同一个磁盘分区(这样就不会存在跨区访问的问题)

formidable.IncomingForm.UPLOAD_DIR = your_path   (我的demo中所建的目录名称为tmp)

 

(2)将fs.renameSync(files.upload.path,"/tmp/sophie.jpg");中的"/tmp/sophie.jpg"改为 "./tmp/sophie.jpg"。

 

友情提示:在Windows上尽量不要操作C盘,权限问题很麻烦的

 

 

好了,写到这儿,也累了。Node.js还有很多知识需要我们去深入学习,

 

如何操作数据库、如何进行单元测试、Node.js的异步IO原理、Connect模块等等,

 

最后,学习Node.js的一些学习网址

 

http://nodejs.org/   node.js官网,上面有介绍、安装包下载、API文档供开发者下载

https://github.com/joyent/node/wiki Node.js社区的wiki

http://www.nodecloud.org/

http://www.nodebeginner.org/index-zh-cn.html 入门者学习文档

 

除此之外,网上上也有许多牛人写的有关Node.js的好文章,总之学习Node.js的资料还是相当多的,各位后端开发的亲们,有兴趣的

话,都可以使用Node.js做一些有意思的东西。

转自:天雷兄的邮件分享

你可能感兴趣的:(nodejs学习笔记)