nodejs入门实践

本文探讨Node.js的异步编程模型,包括非阻塞IO、事件回调等概念,并通过示例代码演示如何创建一个简易Web服务器及路由处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

nodejs在13年被炒得挺火,我也是在14年初第一次听说它。最初认识这个平台(当时以为是一门编程语言)的时候,发现它和javascript是同源的,第一感觉就是和浏览器上跑的js的类库不同(一个最能刺激我的联想就是android系统 和javaweb两套编程平台,语言一样类库不同,现在看来也不能来类比。我认为nodejs可以看成是一个拥有独立解释器的平台,它是基于谷歌浏览器的V8引擎, 运行时建立的平台。也许有很多人和我一样,一年前还在为学习web开发而学习如何使用JQuery,Extjs等等JS库,但这仅仅是"用",是Javascript的用户,而并不是Javascript开发者。当我接触到nodejs时候,我发现要想拿下它,就要对Javascript动真格的了。

我先来谈谈我对nodejs的异步编程、非阻塞IO、事件回调等等概念的理解。

node是单线程的,这时官方给出的node的特性(具体细节我也不清楚为什么要是单线程),可是我们都知道,Java,C++等许多高级语言都是多线程的,node的性能肯定不如他们好啊,那我们需要梳理一下性能的决定因素(我做web开发仅仅1年,我提出我自己的理解但不一定全面)。通常来说一个系统的响应速度和系统访问数据库或文件系统等待的时间有关,还和一次请求的业务逻辑重量级有关(比如有耗时算法或运算则会影响性能)。那么针对这两方面,一个是IO操作,一个是CPU操作。

有了上面两个影响因素,我们再来看看node是如何执行程序的,先来看一个例子:

 fs.readFile("./view/index.html","binary",function(error,data){
       console.log("file was loaded!!\n"+data)
 }
 console.log("hello world")

我先不仔细说node里面语法细节,如果想知道更多node的语法知识,可以去这个网站看看http://www.runoob.com/nodejs/nodejs-tutorial.html  ,或者买本参考书边敲边查 。

这个代码的作用是读取一个文件,文件读取完后取出文件内容打印。代码最后打了一个hello world的log,如果index.html比较大的时候,这段代码的结果是 先打印hello world,然后打印文件内容。

那么Java代码呢?

 BufferedReader buffere = new BufferedReader(new FileReader("./view/index.html"));
 String len = "";
 while((len=buffere.readLine())!=null){
   System.out.println(len);
 }
 System.out.println("hello world!");

这段代码是我盲打的,没有提示不知道有没有小毛病,大家自行改动就好哈!

结果呢?先打印了文件内容,然后输出 hello world。而且大家会明显觉得 这个代码运行到结束好像卡顿了一下一样(如果文件很大的话)。很明显,Java读取文件的时候和输出hello world的时候是处于同一个线程,所以会出现卡段一小下然后再顺序输出的结果。

那么问题就来了,node是单线程的啊!读取文件的步骤和hello world输出也是在一个线程上的,为什么它的输出顺序不一样呢?(这不是个巧合,除非你的代码运行速度还不如文件操作快)。

这也就引出了node的一个特性了(其实有编程经验的人看到node里面读文件有个回调函数可能也会有点感觉),node的这种文件操作是异步的非IO阻塞的,也就是说代码在执行IO操作的时候并不会被阻塞,而是继续执行后面的程序,像不像Ajax呢,用户体验强了很多。

读取文件大家要是没有感觉的话,那就换成数据库,数据库读取速度很慢的时候,Java程序就会被阻塞在读数据库的地方,而node提供了很多环境可以不受阻塞影响继续执行,如果后面还有IO阻塞操作,就不用排队等了,异步操作似的这些需要IO操作的地方能同时执行。

需要注意node中的readFile这些函数中均使用了回调方法,并不能说使用了回调方法后程序就异步化了(实际上我们自己写的回调函数中包含耗时操作,比如循环10W次,程序不会像刚才那样异步执行),而要理解为node的异步机制是通过回调方法实现的。实际上类似 readFile 这类JS规范之外的由运行环境提供的特殊函数做的事情是创建一个平行线程后立即返回,让JS主进程可以接着执行后续代码,并在收到平行进程的通知后再执行回调函数。而为了监听事件,node平台好像也采用了轮训的机制,我们的代码好像处于一个巨大的while(true)中,时时刻刻检查事件是否触发完毕。

但是node的短板在于处理CPU密集型业务的时候很吃力,比如大量的运算(上面提到的循环10W次)node一样吃不消,那么对于Java来说 他可以是多线程执行的,相比之下node虽然号称异步,但是也仅仅针对IO阻塞的时候将阻塞后要执行的事情通过时间机制延后了,但是程序整体的执行还是要看这个程序中大计算的时间,因此运行速度就捉急了。。。

说了这么多也是我学了四天node后对node的一个答题认知,所以观点上也许存在很多不恰当的地方,还请大家指正。

那么下面来点代码(node安装和npm安装我就不想说了,网上有很多资料),node做web开发不像Java,他是自托管的,不需要一个专门的tomcat等等中间件,自己就能做。那么我们来创建一个 helloworld服务器

var http = require("http"); 
http.createServer(function(request,response){
    response.writeHead(200;{"Content-Type":"text/html"});
    response.write("hello world!");
    response.end();
}).listen(1428);

乍一看这个代码,第一感觉就是和平时编程的思路有些不一样,不太习惯,但是使用过事件机制的人应该会比较亲切。

这个代码比较简单,第一行可以看做是Java中的导包,第二行是在创建一个服务,创建完成后执行相应。但是从编码上我们也能看出来有很多东西都是我们自定义的(content-type),比较麻烦,这几天我也在找nodejs的框架类库等等,express这款mvc框架应该是node界比较主流的基于http协议的mvc框架,当然还有一些我记不太清楚的框架,还有些底层的框架比如node封装的socket.io,可以实现即时通信等等。这些都是我后续将要学习的对象。

那么我们来做一个简单的东西,就是给出一个确切的请求,服务器返回一个页面或则资源给我们,请求rest也能得到相应的页面,比如lcoalhost:1234/index 和localhost:1234:/view/index.html都能得到主页面。做这个事情之前我们要先梳理一下思路,首先我们先定义一下我们的项目结构。

我使用webstorm作为开发的IDE,其他的工具也不错linux的vim或者notepad++等等就不介绍了。

153914_cl3S_2320871.png

resources一般来存放资源文件,view就是视图层的html或者模板文件(比如jade),routes叫路由,我开始并不是很喜欢这么叫,因为习惯了java中的controller一个词,也叫作请求控制器,用来分发请求的,在这里作用一样。当然我的习惯是再写一个service层,不过我看别人的demo里面都没有service包,就把业务层代码放在routes中了。我的命名结构其实不合理,还有很多结构比如引用第三方的node_module,package.json等(效果和maven的pom.xml差不多),不过我对那些结构的使用还不熟练,先做个简单的例子玩玩看。

现在开始编写我们的代码一般我们都以index.js这个文件作为项目的入口,而且 require导入的时候,不写具体模块名字则默认导入 名为 index.js的包(前提是nidex.js真的存在在根目录)。这个index.js通常要做的事儿就是把我们需要的模块聚集在一起(至少我目前觉得是这样)。这样一来如果我们把createServer这样的操作直接放在index中似乎index的”入口特性“就不是那么明显了,特别是代码业务都写到这里面似乎不是很明智,我们需要的是让我们的代码看起来像乐高积木一样可插拔,模块化,这也是node模块存在的意义。

那么我们可以将程序这样划分:创建服务的代码我们认为是程序的开始,于是createServer这样的代码我们可以将它设置到一个名叫app的模块中(application顾名思义 应用业务的开始),那么具体的请求我们返回给他什么样的页面,或者返回错误信息,这样的操作我们放到一个handler中,也可以理解为这是业务逻辑,这个模块我们起名叫requesthandler;入口和处理都有了,还需要一个请求分发器,就好比有招聘的人(handler),有应聘者(很多request),这些招聘者需要一个招聘平台获得职务,这就是router要做的事情,router好比第三方,来了请求,检查一下请求,然后”按劳分配“或”介绍“到各个岗位,这个检查是必要的,总不能让一个医生去应聘律师岗位吧。

那么思路有了就可以开始编写了。首先可以写app.js这个模块最好写,只要开一个server就行了:

var http = require("http");
var url = require("url");
exports.run = function(handler,route){
    http.createServer(function(request,response){
        var pathname = url.parse(request.url).pathname;
        route(handler,request,response,pathname);
    }).listen(1234);
}

url模块是node的api,可以提供我们识别请求的地址,从而方便router将其分发到不同的handler,代码里的route变量,其实是一个route方法,node里面的回调方法是通过传递一个函数的指针在某个方法中回调函数指针来达到监听的目的(Java也一样你写一个匿名函数,这个函数的具体实现在运行期间被确定,同时监听器绑定了匿名对象,在事件触发时执行你的方法)。handler也是一个函数指针,不过在这里表现不出来(注意这里的handler route都是自定义的局部变量名,你可以任意定义,但只要在这个函数域一致,顺序呢能和前面调用的位置对上号就行)。

url模块可以方便我们解析请求的url字符串,比如 localhost:8080/test/index.html ,通过url的parse后这里得到的pathname值为 /test/index.html,

listen不多解释,就是指定端口,大家最好用1024以上的端口,不要超过4位就行了。

exports.run 这是什么呢?第一反应是run是一个函数,因为它接受了一个匿名函数。那么exports是什么?他是node里面的一个对象,他表示当前这个模块的导出对象,exports是个对象,所以他能调用run,那么run是什么?run是你自定义的,你要是开心,你可以这么写:

 function run(){....}
 exports.run = run;

典型的函数指针赋值。这个代码作用就是 让这个模块对外界的模块提供了一个方法 叫 run().对比Java就是有一个public void run(){....} 一样。只要你导入了这个模块,就可以用模块对象调用导出对象,没有被exports所调用的函数都是不能对外服务的,类比java的private。

我们把request和response交给路由,但是具体的业务逻辑不是路由做的,路由只管分发,至于响应什么,回复什么,是handler来做,由此可知request和response在route中还是要被向下传递的。下面写一个router:

 exports.route = function(handler,request,response,path){
    /**
     * 判断请求是否是带 .**的资源
     */
    if(path.indexOf(".")>=0){
        console.log("in suffix")
        if(path.indexOf(".jpg")>=0||path.indexOf(".jpeg")>=0||path.indexOf(".png")>=0||path.indexOf(".bmp")>=0)
            handler["image"](request,response,path);
        else handler["html"](request,response,path);
    }else{
        console.log("in req")
        handler["req"](request,response,path);
    }
}

头大了?一点点看其实就是些业务逻辑。这里我代码写的也不好因为对node的api不像java一样熟悉所以用的一些方法也许是绕远路的(学了4天知道的也就那点,惭愧哈!)。

path是刚才传上来的url路径,indexOf(".")是要干嘛?当然是为了看看请求是index.html还是rest的index。这里我判断一下有没有后缀,有后缀和没后缀在handler下的处理方式是不一样的,以为涉及到读取文件时加不加后缀。如果有后缀,则看看后缀是html文本还是图片类型(这里也就是模拟一下,做不到面面俱到),不同资源类型的处理方法也不一样(前面提到的不能给医生安排律师的工作,道理就在这)。如果是没有后缀则当成rest请求,handler处理的时候凑齐资源路径再读取。这里handler有一个很奇怪的用法:

 handler["html"](request,response,path);

这也是我没见过的,有个概念说一下 叫关联数组,大概意思就是一个数组不光可以通过数字来定位,也可以通过字符串(也就是数组内容)来定位,这一点在很多语言中我都没见过,Java里面List能做到前者,map能做到后者,但是没有合二为一的,为什么要在这用这个特性?handler是一个handler模块对象,js里面对json这个结构运用比较活,我的理解是js把模块对象也看做为一个"json",他的导出成员(函数)是可以通过模块对象的指针来定位的(说的比较拗口,你可以想一下数组 int a[10];  a就好比是那个handler ,handler["name"]就是a[0],就是handler的具体内容),由于js是弱类型脚本,不检查变量类型,所以我们一路上传递的handler在这里这样使用他也不会关心你用的是一个方法还是一个变量(handler中可以不光exports函数,可以到处一些变量什么的变量和函数类型肯定不一样的对吧!)。

好了,路由终于”艰难“的理解完了。下面看看handler的细节。

var fs = require("fs");
exports.html = function(request,response,path){
    console.log("请求名:"+path);
    fs.readFile("."+path,"binary",function(error,data){
        console.log("suffix path :"+path);
        if(error){
            fs.readFile("./view/404page.html","binary",function(error,data){
                response.writeHead(404,{"Content-Type":"text/html;charset=utf-8"});
                response.write(data,"binary");
                response.end();
            });
        }else{
            response.writeHead(200,{"Content-Type":"text/html;charset=utf-8"});
            response.write(data,"binary");
            response.end();
        }
    });
}
exports.image = function(request,response,path){
    console.log("请求名:"+path);
    fs.readFile("."+path,"binary",function(error,data){
        console.log("suffix path :"+path);
        if(error){
            fs.readFile("./view/404page.html","binary",function(error,data){
                response.writeHead(404,{"Content-Type":"text/html;charset=utf-8"});
                response.write(data,"binary");
                response.end();
            });
        }else{
            response.writeHead(200,{"Content-Type":"image/jpeg"});
            response.write(data,"binary");
            response.end();
        }
    });
}

exports.req = function(request,response,path){
    console.log("请求名:"+path);
    response.writeHead(200,{"Content-Type":"text/html;charset=utf-8"});
    var realpath = "."+path;
    if(path.indexOf("/view/")<0){
        realpath = "./view"+path;
        console.log("不包含view路径")
    }
    fs.readFile(realpath+".html","binary",function(error,data){
        console.log("path: "+realpath)
        if(error){
            fs.readFile("./view/404page.html","binary",function(error,data){
                response.write(data.toString(),"binary");
                response.end();
            });
        }else{
            response.write(data,"binary");
            response.end();
        }
    });
}

    篇幅比较长,我挑一点说。因为路由可以给我们传递请求响应对象,还有path信息,我们可以通过path去服务器中查找用户需要什么文件,找到了就write给他,找不到就给一个我们默认的404page的界面信息。视图层都放在view里面了,所以我判断了一下如果请求是带资源后缀”.“的,就直接去访问;如果不带的话就自动加上再去访问;对于图片我们的content-type是不一样的,所以我选择把它分离出来(你也可以写一起,因为在我看来解析url并分发给业务 这是分发起应该做的事儿,所以分析url的业务我放在了路由中,大家也可以在handler中做,这样可以少些一些方法,但是编写handler的步骤就相对复杂)。

readFile在正文前面就提到了,是个非阻塞式方法,我们在回调中写响应信息,注意end方法也一定要写在回调中,否则这个请求在读取文件的时候就end了程序就bug了。

 小demo就算完成了,访问以下这些界面看看:

localhost:1234

localhost:1234/view/index.html

lcaolhost:1234/index

localhost:1234/resources/test.png

nodejs的确是一个处理大并发和IO操作的利器,和php,Java一样,他也有一套开发模板,我见过宣传的最多的是angularjs+nodejs+mongodb,全是js呢!! 全栈工程师的福音啊!

node在github上有很多开源项目,上至网站,下至单片机,设计领域挺广泛,甚至还看见一个nodejs基于linux的操作系统。

未来一段时间除了项目要做,我会投入一点时间咋node上,时不时换个脑子也好。而且作为一门相对比较新的技术,以及函数式编程的理念,我认为它的发展前景比较宽广。

下面把test.png这个图片奉献给大家,也是我的头像。

163512_Ri53_2320871.png

,哈哈!node学起来是有一些吃力,比较考验程序员,但是难得东西学起来才有价值呢!希望各位编程愉快(话说每次看到这只猫我的心情都会好很多!!!!果然每种生物都有 逗比 出现啊!)

转载于:https://my.oschina.net/rpgmakervx/blog/515937

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值