我们为什么要使用NodeJS

科普文一则,说说我对NodeJS(一种服务端JavaScript实现)的一些认识,以及我为什么会向后端工程师推荐NodeJS.

"Node.js 是服务器端的 JavaScript 运行环境,它具有无阻塞(non-blocking)事件驱动(event-driven)等的特色,Node.js 采用 V8 引擎,同样,Node.js 实现了类似 Apache 和 nginx 的web服务,让你可以通过它来搭建基于 JavaScript 的 Web App。"

上周末参与了CNodeJS社区的第一次北京聚会,现场气氛非常的好.而作为一名前端开发,我在后面的讨论环节讲了下我对NodeJS的看法,主要回答的问题是"我为什么会向后端工程师推荐NodeJS".这其实是去年年底大团队技术总结的话题之一,包含在我之前发过的PPT:团队年终技术Review中.因为之前没有准备,当天仓促上阵,也不知道说清楚了没,不如就在这里再详细展开记录下.

我想不仅仅是NodeJS,当我们要引入任何一种新技术前都必须要搞清楚几个问题: 
        1.我们遇到了什么问题? 
        2.这项新技术解决什么问题,是否契合我们遇到的问题? 
        3.我们遇到问题的多种解决方案中,当前这项新技术的优势体现在哪儿? 
        4.使用新技术,带来哪些新问题,严重么,我们能否解决掉?

我们的问题:Server端阻塞 
        NodeJS被设计用来解决服务端阻塞问题.通过一段简单的代码解释何为阻塞:

Js代码  

  1. //根据ID,在数据库中Persons表中查出Name
  2. var name = db.query("select name from persons where id=1");
  3. //进程等待数据查询完毕,然后使用查询结果.
  4. output("name")

这段代码的问题是在上面两个语句之间,在整个数据查询的过程中,当前程序进程往往只是在等待结果的返回.这就造成了进程的阻塞.对于高并发,I/O密集行的网络应用中,一方面进程很长时间处于等待状态,一方面为了应付新的请求不断的增加新的进程.这样的浪费会导致系统支持QPS远远小于后端数据服务能够支撑的QPS,成为系统的瓶颈.而且这样的系统也特别容易被慢链接攻击(客户端故意不接收或减缓接收数据,加长进程等待时间). 
如何解决阻塞问题 
        解决这个问题的办法是,建立一种事件机制,发起查询请求之后,立即将进程交出,当数据返回后触发事件,再继续处理数据:

Js代码  

  1. //定义如何后续数据处理函数
  2. function onDataLoad(name){
  3. output("name");
  4. }
  5. //发起数据请求,同时指定数据返回后的回调函数
  6. db.query("select name from persons where id=1",onDataLoad);

我们看到按照这个思路解决阻塞问题,首先我们要提供一套高效的异步事件调度机制.而主要用于处理浏览器端的各种交互事件的JavaScript.相对于其他语言,至少有两个关键点特别适合完成这个任务. 
为什么JS适合解决阻塞问题 
        首先JavaScript是一种函数式编程语言,函数编程语言最重要的数学基础是λ演算(lambda calculus) -- 即函数可以接受函数当作输入(参数)和输出(返回值). 
        函数可以作为其他函数的参数输入的这个特性,使得为事件指定回调函数变得很容易.特别是JavaScript还支持匿名函数.通过匿名函数的辅助,之前的代码可以进行简写如下.

Js代码  

  1. db.query("select name from persons where id=1",function(name){
  2. output(name);
  3. });

还有一个关键问题是,异步回调的运行上下文保持(称状态保持),我看一段代码来说明何为状态保持.

Js代码  

  1. //传统同步写法:将查询和结果打印抽象为一个方法
  2. function main(){
  3. var id = "1";
  4. var name = db.query("select name from persons where id=" + id);
  5. output("person id:" + id + ", name:" + name);
  6. }
  7. main();

前面的写法在传统的阻塞是编程中非常常见,但接下来进行异步改写时会遇到一些困扰.

Js代码  

  1. //异步写法:
  2. function main(){
  3. var id = "1";
  4. db.query("select name from persons where id=" + id,function(name){
  5. output("person id:" + id + ", name:" + name);//n秒后数据返回后执行回调
  6. });
  7. }
  8. main();

细心的同学可以注意到,当等待了n秒数据查询结果返回后执行回调时.回调函数中却仍然使用了main函数的局部变量"id",而"id"已经在n秒前走出了其作用域,这是为什么呢?熟悉JavaScript的同学会淡然告诉您:"这是闭包(closures)~". 
        其实在复杂的应用中,我们一定会遇到这类场景.即在函数运行时需要访问函数定义时的上下文数据(注意:一定要区分函数定义时和函数运行时这样的字眼和其代表的意义,不然很快就会糊涂).而在异步编程中,函数的定义和运行又分处不同的时间段,那么保持上下文的问题变得更加突出了. 
        在这个例子中,db.query作为一个公共的数据库查询方法,把"id"这个业务数据传入给db.query,交由其保存是不太合适的.但聪明的同学们可以抽象一下,让db.query再支持一个需要保持状态的数据对象传入,当数据查询完毕后可以把这些状态数据原封不动的回传.如下:

Js代码  

  1. function main(){
  2. var id = "1";
  3. var currentState = new Object();
  4. currentState.person_id = id;
  5. db.query("select name from persons where id=" + id, function(name,state){
  6. output("person id:" + state.person_id + ", name:" + name);
  7. },currentState);//注意currentState是db.query的第三个参数
  8. }
  9. main();

记住这种重要的思路,我们再看看是否还能进一步的抽象?可以的,不过接下的动作之前,我们先要了解在JavaScript中一个函数也是一个对象.一个函数实例fn除了具备可函数体的定义之外,仍然可以在这个函数对象实例之上扩展属性,如fn.a=1;受到这个启发我们尝试把需要保持的状态直接绑定到函数实例上.

Js代码  

  1. function main(){
  2. var id = "1";
  3. var currentState = new Object();
  4. currentState.person_id = id;
  5. function onDataLoad(name){
  6. output("person id:" + onDataLoad.state.person_id + ", name:" + name);
  7. }
  8. onDataLoad.state = currentState ;//为函数指定state属性,用于保持状态
  9. db.query("select name from persons where id=" + id, onDataLoad);
  10. }

我们做了什么?生成了currentState对象,然后在函数onDataLoad定义时,将currentState绑定给onDataLoad这个函数实例.那么在onDataLoad运行时,就可以拿到定义时的state对象了.而闭包就是内置了这个过程而已.

在每个函数运行时,都有一个运行时对象称为Execution context,它包含如下variable object(VO,变量对象),scope chain(作用域链)和thisValue三部分.详见ECMA-262 JavaScript. The Core 
         
        其中变量对象VO,包含了所有局部变量的引用.对于main函数,局部变量"id"存储在VO.id内.看起来用VO来代替我们的currentSate最合适了.但main函数还可能嵌套在其他函数之内,所以我们需要ScopeChain,它是一个包含当前运行函数VO和其所有父函数scope的数组. 
        所以在这个例子中,在onDataLoad函数定义时,就为默认为其绑定了一个[[scope]]属性指向其父函数的ExecutionContext的ScopeChain.而当函数onDataLoad执行时,就可以通过[[scope]]属性来访问父函数的VO对象来找到id,如果父函数的VO中没有id这个属性,就再继续向上查找其祖先的VO对象,直到找到id这个属性或到达最外层返回undefined.也正是因为这个引用,造成VO的引用计数不为0,在走出作用域时,才不会被垃圾回收. 
        很多人觉得闭包很难理解,其实我们只要能明确需要区分函数定义和函数运行这两个时机,记住闭包让函数在运行时能够访问到函数定义时的所处作用域内的所有变量.或者说函数定义时能访问到什么变量,那么在函数运行时通过相同的变量名一样能访问到.

关于状态保持是本文的重点,在我看到的多数NodeJS的介绍文章,并没有详解这里,我们只是知道了要解决阻塞问题,但是JavaScript解决阻塞问题的优势在哪里,作为一个前端开发,我想有必要详细解释一下. 
        
        其实说到状态保持还有一个类似的场景,比如用户从A页面提交表单到B页面,如果提交数据校验不通过,则需要返回A页面,同时保持用户在A页面填写的内容并提示用户修改不对的地方.从提交到返回显示这也是一个包含网络交互的异步过程.传统网页,用户的状态通过请求传递到服务端,交由后端状态保持(类似交给db.query的currentSate).而使用Ajax的网页,因为并未离开原页面,那么服务端只要负责校验用户提交的数据是否正确即可,发送错误,返回错误处相关信息即可,这就是所谓前端状态保持.可以看到这个场景里边服务端做的事情变少了,变纯粹了.正如我们的例子中db.query不再存储转发第三个state参数,变得更轻量. 
        
        我们看到通过JavaScript函数式语言特性,匿名函数支持和闭包很漂亮的解决了同步编程到异步编程转化过程中遇到的一系列最重要的问题.但JavaScript是否就是最好的?这就要回答我们引用新技术时需要考虑的最后一个问题了 
使用NodeJS是否带来额外的困扰,如何解决 
        性能真的是最好么?不用比较我们也可以得到结论NodeJS,做无阻塞编程性能较难做到极致.何为极致,处理一个请求需要占用多少内存,多少cpu资源,多少带宽,如果有浪费就不是极致.阻塞式编程浪费了大量进程资源只是在等待,导致大量内存和cpu的浪费.NodeJs好很多,但也正是因为一些闭包等JS内建机制也会导致资源的浪费,看下面的代码

Js代码  

  1. function main(){
  2. var id = "1";
  3. var str = "..."; //这里存储一个2M的字符串
  4. db.query("select name from persons where id=" + id,function(name){
  5. output("person id:" + id + ", name:" + name);//n秒后数据返回后执行回调
  6. });
  7. }
  8. main();

直到数据查询完成,变量str所使用的2M内存不会被释放,而str保持下去可能并没有意义.前面已经解释过闭包的原理,闭包并没有智能到只包起来今后可能被访问到的对象.即使不了解闭包的原理,也可以通过一段简单脚本验证这点:

Js代码  

  1. function main(){
  2. var id = "1";
  3. var str = "..."; //这里存储一个2M的字符串
  4. window.setTimeout(function(){
  5. debugger;    //我们在这里设置断点
  6. },10000)
  7. }
  8. main();

我们在回调函数当中只设置一个断点,并不指明我们要访问哪个变量.然后我们在控制台监视一下,id和str都是可以拿到的.(此处结论不严谨,各种新浏览器已经就此做了相关优化,详见评论2楼,特别是2楼给出的详细测试报告的连接) 
        所以我来不负责任的预测一下,性能极端苛刻的场景,无阻塞是未来,但无阻塞发展下去,或者有更轻量的脚本引擎产生(lua?),或者V8JS引擎可能要调整可以disable闭包,或者我们可以通过给JS开发静态编译器在代码发布前优化我们的代码. 
        
        我之前谈到过JS静态编译器:"如果给JS代码发布正式使用前增加一个编译步骤,我们能做些什么",动态语言的实时编译系统只完成了静态语言编译中的将代码转化为字节码的过程,而静态语言编译器的额外工作,如接口校验,全局性能优化等待.所以JS也需要一个静态的编译器来完成这些功能,Google利用ClouserComplier提供了系列编译指令,让JS更好的实现OO编程,我来利用静态编译器解决一些JS做细粒度模块化引入的性能方面的问题.而老赵最近的项目JSCEX,则也是利用JS发布前的编译环节重点解决异步编程的代码复杂度问题. 
        我们习惯于阻塞式编程的写法,切换到异步模式编程,往往对于太多多层次的callback嵌套弄得不知所措.所以老赵开发的JS静态编译器,借鉴F#的Computation Expressions,让大家遵守一些小的约定后,能够仍然保持同步编程的写法,写完的代码通过JSCEX编译为异步回调式的代码再交给JS引擎执行. 
        如果这个项目足够好用,那就也解决了一个使用NodeJS这种新技术,却加大编程复杂度这个额外引入的困扰.甚至可以沿着这个思路,在静态编译阶段优化内存使用.

NodeJS还要解决什么问题 
        说了这么多,无阻塞编程要做的还远不止这些.首先需要一个高效的JS引擎,高效的事件池和线程池.另外几乎所有和NodeJS交互的传统模块如文件系统,数据访问,HTTP解析,DNS解析都是阻塞式的,都需要额外改造. 
        正是NodeJS作者极其团队,认清问题问题以及JS解决这些问题方面的优势.基于高效的V8 JavaScript引擎,贡献了大量的智慧和精力解决上述大部分问题后才有NodeJS横空出世. 
        当前Node社区如此火热,千余开源的NodeJS模块,活跃在WebFramework,WebSocket,RPC,模板引擎,数据抓取服务,图形图像几乎所有工程领域.

后记 
        本文主要的信息来自nodejs作者在JSConf09,JSConf10上的分享. 
        而作为前端开发,着重讲了函数式编程,闭包对于无阻塞开发的重要意义.我期待这篇文章能够给前端和后端同学都带来收获.

同样作为前端开发,不得不再插几句,说说服务端JS能够解决的另一个问题: 
        当前的Web开发前后端使用不同的语言,很多相同的业务逻辑要前后端分别用不同语言重复实现.比如越来越多重度依赖JS的胖客户端应用,当客户浏览器禁用JavaScript时,则需要使用服务端语言将主业务流程再实现一次(这即是所谓的"渐进增强"). 
        当我们拥有了服务端JavaScript语言,我们自然就会想到能否利用NodeJS做到"一次开发,渐进增强".解决掉这个为小量用户,浪费大量时间的恼人的问题.我们先要解决问题,这是使用NodeJS的最大动力.基于之前的统计,因为各种原因浏览器不支持JS的用户大概接近1%,至少淘宝绝对不会主动放弃这部分用户.至于在服务端也使用JS是否能够替掉LAMP架构,抑或NodeJS会对常见MVC架构带来何种冲击,V/C这些层是否能在前后端任意流动这些问题都是NodeJS解决问题后带来的额外话题. 
        "一次开发,渐进增强"这方面的实践,YAHOO仍然是先驱,早在一年多前开始YAHOO通过nodejs-yui3项目做了很多卓越的贡献,而淘宝自主开发的前端框架Kissy也有服务端运行的相关尝试,详见我的同事拔赤的分享.而接下来的几个月我也将在这方面做一些尝试,有一定积累后我将再写一篇文章更好的分析这个问题..

JS在诞生之时就不仅仅是浏览器端工具,如今JS能再一次回到服务端展示拳脚,感谢V8,感谢NodeJS作者,团队和社区的诸多贡献者,祝Node好运,JS好运.

原文:http://limu.iteye.com/blog/1013223

时间: 2024-10-23 13:34:05

我们为什么要使用NodeJS的相关文章

Node.js 使用angularjs取得Nodejs http服务端返回的JSON数组示例

server.js代码: // 内置http模块,提供了http服务器和客户端功能(path模块也是内置模块,而mime是附加模块) var http=require("http"); // 创建服务器,创建HTTP服务器要调用http.createServer()函数,它只有一个参数,是个回调函数,服务器每次收到http请求后都会调用这个回调函数.服务器每收到一条http请求,都会用新的request和response对象触发请求函数. var server=http.createS

Nodejs + MongoDb

一.搭建开发环境 进入 http://nodejs.org 下载开发环境 http://Expressjs.com 下载安装Express  npm install -g express 继续安装ejs:npm install ejs 如果要想运行Node.js程序,则现在只能够使用“node app.js”,而这样的运行方式,如果在app.js文件修改之后往往需要重新启动才可以加载新的内容,这对于开发是非常不方便的, 为此,可以使用一个supervisor组件包,它可以动态的加载修改之后的开发

nodejs应用在linux服务器中的部署

1.(可选)添加用户: addgroup wmui添加用户组useradd -d /home/wmui -s /bin/bash -m wmui创建wmui用户passwd wmui设置密码,如果忘记密码,也可用此命令重置密码usermod -a -G wmui wmui 添加用户到组visudo 设置sudo权限然后会跳转到下面页面 root ALL=(ALL:ALL) ALL下面添加wmui ALL=(ALL) NOPASSWD: ALLctrl+x保存退出接下来打开一个新的窗口,测试是否登

centos 6.5安装NodeJS

centos 6.5安装NodeJS 下载 可以在本地下载node.js最新版,然后通过ftp工具上传到服务器,或者直接在服务器终端使用wget命令下载(我当时下载的是node-v7.5.0-linux-x86版本,其他版本请查看上面链接然后替换即可): $ wget http://nodejs.org/dist/latest/node-v7.5.0-linux-x86.tar.gz 解压 进入服务器终端,找到上传或者下载的安装包,解压 $ tar -zvxf node-v7.5.0-linux

NodeJS之queryString

前面的话 无论是前端还是后端,经常出现的应用场景是URL中参数的处理.nodeJS的queryString模块提供了一些处理 query strings 的工具.本文将详细介绍nodeJS中的queryString var querystring = require('querystring'); /* { unescapeBuffer: [Function], unescape: [Function: qsUnescape], escape: [Function], encode: [Func

进阶之初探nodeJS

一.前言 在"初探nodeJS"随笔中,我们对于node有了一个大致地了解,并在最后也通过一个示例,了解了如何快速地开启一个简单的服务器. 今儿,再次看了该篇随笔,发现该随笔理论知识稍多,适合初级入门node,固萌生一个想法--想在该篇随笔中,通过一步步编写一个稍大一点的node示例,让我们在整体上更加全面地了解node. so,该篇随笔是建立在"初探nodeJS"之上的,固取名为"进阶之初探nodeJS". 好了,侃了这多,那么我们即将实现一个

引用nodejs的url模块实现url路由功能

我们在本地创建服务器之后需要写不同的后缀名来访问同一个站点的不同页面,如果不实现路由功能.则每次访问localhost:3000 不论后面写什么  比如localhost:3000/index.localhost:3000/detail...最终都会访问同一个地址:而用url路由则可以实现访问不同的页面:在实现url路由功能之前,首先需要引入url模块:const url = require('url');  nodejs 为我们提供了url模块,并且提供了url模块的一些方法: 最终要的方法是

nodejs入门 多人聊天视频

Node.js 是运行在服务端的 JavaScript,如果你熟悉Javascript,那么你将会很容易的学会Node.js. 下边是nodejs的安装和多人聊天系统, windows环境下 1.安装nodejs nodejs的官网 https://nodejs.org/en/ 下载nodejs最新版本一步步安装就好. 没有什么可说的. 2.安装完成以后查看当前nodejs的版本和测试nodejs时候成功 查看nodejs版本. cmd命令行窗口 ---> node -v 测试nodejs 是否

NodeJs编写小爬虫

一,爬虫及Robots协议 爬虫,是一种自动获取网页内容的程序.是搜索引擎的重要组成部分,因此搜索引擎优化很大程度上就是针对爬虫而做出的优化. robots.txt是一个文本文件,robots是一个协议,而不是一个命令.robots.txt是爬虫要查看的第一个文件.robots.txt文件告诉爬虫在服务器上什么文件是可以被查看的,搜索机器人就会按照该文件中的内容来确定访问的范围. 如上图,我们可以在网站中直接访问robots.txt文件查看网站禁止访问和允许访问的文件. 二,使用NodeJs爬去

ActiveMQ + NodeJS + Stomp 极简入门

前提 安装ActiveMQ和Nodejs 测试步骤 1.执行bin\win32\activemq.bat启动MQ服务 2. 打开http://localhost:8161/admin/topics.jsp 用户名和密码都是 admin 3. 下载Stomp npm install stomp-client 4. js的测试代码 var Stomp = require('stomp-client'); var destination = '/topic/myTopic'; var client =