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 V8 JavaScript。这句近似广告语的句子却道尽了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对象:
 
    

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

这里我们要用到的外部模块是Felix Geisendö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

(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  入门者学习文档

除此之外,InfoQ上也有许多有关Node.js的好文章,总之学习Node.js的资料还是相当多的,各位后端开发的亲们,有兴趣的
话,都可以使用Node.js做一些有意思的东西。


你可能感兴趣的:(入门,demo,实践,Node.js原理)