當前位置:
首頁 > 知識 > Node入门(二)

Node入门(二)

接上文:

我们创建了服务器,并且向创建它的方法传递了一个函数。无论何时我们的服务器收到一个请求,这个函数就会被调用。

我们不知道这件事情什么时候会发生,但是我们现在有了一个处理请求的地方:它就是我们传递过去的那个函数。至于它是被预先定义的函数还是匿名函数,就无关紧要了。

这个就是传说中的回调。我们给某个方法传递了一个函数,这个方法在有相应事件发生时调用这个函数来进行回调。

至少对我来说,需要一些工夫才能弄懂它。你如果还是不太确定的话就再去读读Felix的博客文章。

让我们再来琢磨琢磨这个新概念。我们怎么证明,在创建完服务器之后,即使没有HTTP请求进来,我们的回调函数也没有被调用的情况下,我们的代码还继续有效呢?我们试试这个:

注意:在onRequest(我们的回调函数)触发的地方,我用console.log输出了一段文本。在HTTP服务器开始工作之后,也输出一段文本。

当我们与往常一样,运行它node server.js时,它会马上在命令行上输出“Server has started.”。当我们向服务器发出请求(在浏览器访问http://localhost:8888/),“Request received.”这条消息就会在命令行中出现。

这就是事件驱动的异步服务器端JavaScript和它的回调啦!

请注意,当我们在服务器访问网页时,我们的服务器可能会输出两次“Request received.”。

时尝试读取

服务器是如何处理请求的

好的,接下来我们简单分析一下我们服务器代码中剩下的部分,也就是我们的回调函数onRequest()的主体部分。

当回调启动,我们的onRequest()函数被触发的时候,有两个参数被传入:request和response。

它们是对象,你可以使用它们的方法来处理HTTP请求的细节,并且响应请求(比如向发出请求的浏览器发回一些东西)。

所以我们的代码就是:当收到请求时,使用response.writeHead()函数发送一个HTTP状态200和HTTP头的内容类型(content-type),使用response.write()函数在HTTP相应主体中发送文本“Hello World"。

最后,我们调用response.end()完成响应。

目前来说,我们对请求的细节并不在意,所以我们没有使用request对象。

服务端的模块放在哪里

OK,就像我保证过的那样,我们现在可以回到我们如何组织应用这个问题上了。我们现在在server.js文件中有一个非常基础的HTTP服务器代码,而且我提到通常我们会有一个叫index.js的文件去调用应用的其他模块(比如server.js中的HTTP服务器模块)来引导和启动应用。

我们现在就来谈谈怎么把server.js变成一个真正的Node.js模块,使它可以被我们(还没动工)的index.js主文件使用。

也许你已经注意到,我们已经在代码中使用了模块了。像这样:

Node.js中自带了一个叫做“http”的模块,我们在我们的代码中请求它并把返回值赋给一个本地变量。

这把我们的本地变量变成了一个拥有所有http模块所提供的公共方法的对象。

给这种本地变量起一个和模块名称一样的名字是一种惯例,但是你也可以按照自己的喜好来:

很好,怎么使用Node.js内部模块已经很清楚了。我们怎么创建自己的模块,又怎么使用它呢?

等我们把server.js变成一个真正的模块,你就能搞明白了。

事实上,我们不用做太多的修改。把某段代码变成模块意味着我们需要把我们希望提供其功能的部分 导出到请求这个模块的脚本。

目前,我们的HTTP服务器需要导出的功能非常简单,因为请求服务器模块的脚本仅仅是需要启动服务器而已。

我们把我们的服务器脚本放到一个叫做start的函数里,然后我们会导出这个函数。

这样,我们现在就可以创建我们的主文件index.js并在其中启动我们的HTTP了,虽然服务器的代码还在server.js中。

创建index.js文件并写入以下内容:

正如你所看到的,我们可以像使用任何其他的内置模块一样使用server模块:请求这个文件并把它指向一个变量,其中已导出的函数就可以被我们使用了。

好了。我们现在就可以从我们的主要脚本启动我们的的应用了,而它还是老样子:

非常好,我们现在可以把我们的应用的不同部分放入不同的文件里,并且通过生成模块的方式把它们连接到一起了。

我们仍然只拥有整个应用的最初部分:我们可以接收HTTP请求。但是我们得做点什么——对于不同的URL请求,服务器应该有不同的反应。

对于一个非常简单的应用来说,你可以直接在回调函数onRequest()中做这件事情。不过就像我说过的,我们应该加入一些抽象的元素,让我们的例子变得更有趣一点儿。

处理不同的HTTP请求在我们的代码中是一个不同的部分,叫做“路由选择”——那么,我们接下来就创造一个叫做路由的模块吧。

如何来进行请求的“路由”

我们要为路由提供请求的URL和其他需要的GET及POST参数,随后路由需要根据这些数据来执行相应的代码(这里“代码”对应整个应用的第三部分:一系列在接收到请求时真正工作的处理程序)。

因此,我们需要查看HTTP请求,从中提取出请求的URL以及GET/POST参数。这一功能应当属于路由还是服务器(甚至作为一个模块自身的功能)确实值得探讨,但这里暂定其为我们的HTTP服务器的功能。

我们需要的所有数据都会包含在request对象中,该对象作为onRequest()回调函数的第一个参数传递。但是为了解析这些数据,我们需要额外的Node.JS模块,它们分别是url和querystring模块。

当然我们也可以用querystring模块来解析POST请求体中的参数,稍后会有演示。

现在我们来给onRequest()函数加上一些逻辑,用来找出浏览器请求的URL路径:

好了,我们的应用现在可以通过请求的URL路径来区别不同请求了——这使我们得以使用路由(还未完成)来将请求以URL路径为基准映射到处理程序上。

在我们所要构建的应用中,这意味着来自/start和/upload的请求可以使用不同的代码来处理。稍后我们将看到这些内容是如何整合到一起的。

现在我们可以来编写路由了,建立一个名为router.js的文件,添加以下内容:

如你所见,这段代码什么也没干,不过对于现在来说这是应该的。在添加更多的逻辑以前,我们先来看看如何把路由和服务器整合起来。

我们的服务器应当知道路由的存在并加以有效利用。我们当然可以通过硬编码的方式将这一依赖项绑定到服务器上,但是其它语言的编程经验告诉我们这会是一件非常痛苦的事,因此我们将使用依赖注入的方式较松散地添加路由模块(你可以读读Martin Fowlers关于依赖注入的大作来作为背景知识)。

首先,我们来扩展一下服务器的start()函数,以便将路由函数作为参数传递过去:

同时,我们会相应扩展index.js,使得路由函数可以被注入到服务器中:

在这里,我们传递的函数依旧什么也没做。

如果现在启动应用(node index.js,始终记得这个命令行),随后请求一个URL,你将会看到应用输出相应的信息,这表明我们的HTTP服务器已经在使用路由模块了,并会将请求的路径传递给路由:

行为驱动执行

请允许我再次脱离主题,在这里谈一谈函数式编程。

将函数作为参数传递并不仅仅出于技术上的考量。对软件设计来说,这其实是个哲学问题。想想这样的场景:在index文件中,我们可以将router对象传递进去,服务器随后可以调用这个对象的route函数。

就像这样,我们传递一个东西,然后服务器利用这个东西来完成一些事。嗨那个叫路由的东西,能帮我把这个路由一下吗?

但是服务器其实不需要这样的东西。它只需要把事情做完就行,其实为了把事情做完,你根本不需要东西,你需要的是动作。也就是说,你不需要名词,你需要动词。

理解了这个概念里最核心、最基本的思想转换后,我自然而然地理解了函数编程。

我是在读了Steve Yegge的大作名词王国中的死刑之后理解了函数编程。你也去读一读这本书吧,真的。这是曾给予我阅读的快乐的关于软件的书籍之一。

路由给真正的请求处理程序

回到正题,现在我们的HTTP服务器和请求路由模块已经如我们的期望,可以相互交流了,就像一对亲密无间的兄弟。

当然这还远远不够,路由,顾名思义,是指我们要针对不同的URL有不同的处理方式。例如处理/start的“业务逻辑”就应该和处理/upload的不同。

在现在的实现下,路由过程会在路由模块中“结束”,并且路由模块并不是真正针对请求“采取行动”的模块,否则当我们的应用程序变得更为复杂时,将无法很好地扩展。

我们暂时把作为路由目标的函数称为请求处理程序。现在我们不要急着来开发路由模块,因为如果请求处理程序没有就绪的话,再怎么完善路由模块也没有多大意义。

应用程序需要新的部件,因此加入新的模块——已经无需为此感到新奇了。我们来创建一个叫做requestHandlers的模块,并对于每一个请求处理程序,添加一个占位用函数,随后将这些函数作为模块的方法导出:

这样我们就可以把请求处理程序和路由模块连接起来,让路由“有路可寻”。

在这里我们得做个决定:是将requestHandlers模块硬编码到路由里来使用,还是再添加一点依赖注入?虽然和其他模式一样,依赖注入不应该仅仅为使用而使用,但在现在这个情况下,使用依赖注入可以让路由和请求处理程序之间的耦合更加松散,也因此能让路由的重用性更高。

这意味着我们得将请求处理程序从服务器传递到路由中,但感觉上这么做更离谱了,我们得一路把这堆请求处理程序从我们的主文件传递到服务器中,再将之从服务器传递到路由。

那么我们要怎么传递这些请求处理程序呢?别看现在我们只有2个处理程序,在一个真实的应用中,请求处理程序的数量会不断增加,我们当然不想每次有一个新的URL或请求处理程序时,都要为了在路由里完成请求到处理程序的映射而反复折腾。除此之外,在路由里有一大堆if request == x then call handler y也使得系统丑陋不堪。

仔细想想,有一大堆东西,每个都要映射到一个字符串(就是请求的URL)上?似乎关联数组(associative array)能完美胜任。

不过结果有点令人失望,JavaScript没提供关联数组——也可以说它提供了?事实上,在JavaScript中,真正能提供此类功能的是它的对象。

在这方面

有一个不错的介绍,我在此摘录一段:

在C++或C#中,当我们谈到对象,指的是类或者结构体的实例。对象根据他们实例化的模板(就是所谓的类),会拥有不同的属性和方法。但在JavaScript里对象不是这个概念。在JavaScript中,对象就是一个键/值对的集合——你可以把JavaScript的对象想象成一个键为字符串类型的字典。

但如果JavaScript的对象仅仅是键/值对的集合,它又怎么会拥有方法呢?好吧,这里的值可以是字符串、数字或者……函数!

好了,最后再回到代码上来。现在我们已经确定将一系列请求处理程序通过一个对象来传递,并且需要使用松耦合的方式将这个对象注入到route()函数中。

我们先将这个对象引入到主文件index.js中:

虽然handle并不仅仅是一个“东西”(一些请求处理程序的集合),我还是建议以一个动词作为其命名,这样做可以让我们在路由中使用更流畅的表达式,稍后会有说明。

在完成了对象的定义后,我们把它作为额外的参数传递给服务器,为此将server.js修改如下:

这样我们就在start()函数里添加了handle参数,并且把handle对象作为第一个参数传递给了route()回调函数。

然后我们相应地在route.js文件中修改route()函数:

通过以上代码,我们首先检查给定的路径对应的请求处理程序是否存在,如果存在的话直接调用相应的函数。我们可以用从关联数组中获取元素一样的方式从传递的对象中获取请求处理函数,因此就有了简洁流畅的形如handle[pathname]();的表达式,这个感觉就像在前方中提到的那样:“嗨,请帮我处理了这个路径”。

有了这些,服务器、路由和请求处理程序就在一起了。现在我们启动应用程序并在浏览器中访问http://localhost:8888/start,以下日志可以说明系统调用了正确的请求处理程序:

让请求处理程序作出响应

很好。不过现在要是请求处理程序能够向浏览器返回一些有意义的信息而并非全是“Hello World”,那就更好了。

这里要记住的是,浏览器发出请求后获得并显示的“Hello World”信息仍是来自于我们server.js文件中的onRequest函数。

其实“处理请求”说白了就是“对请求作出响应”,因此,我们需要让请求处理程序能够像onRequest函数那样可以和浏览器进行“对话”。

不好的实现方式

对于我们这样拥有PHP或者Ruby技术背景的开发者来说,最直截了当的实现方式事实上并不是非常靠谱:看似有效,实则未必如此。

这里我指的“直截了当的实现方式”意思是:让请求处理程序通过onRequest函数直接返回(return())他们要展示给用户的信息。

我们先就这样去实现,然后再来看为什么这不是一种很好的实现方式。

让我们从让请求处理程序返回需要在浏览器中显示的信息开始。我们需要将requestHandler.js修改为如下形式:

好的。同样的,请求路由需要将请求处理程序返回给它的信息返回给服务器。因此,我们需要将router.js修改为如下形式:

正如上述代码所示,当请求无法路由的时候,我们也返回了一些相关的错误信息。

最后,我们需要对我们的server.js进行重构以使得它能够将请求处理程序通过请求路由返回的内容响应给浏览器,如下所示:

如果我们运行重构后的应用,一切都会工作的很好:请求http://localhost:8888/start,浏览器会输出“Hello Start”,请求http://localhost:8888/upload会输出“Hello Upload”,而请求http://localhost:8888/foo 会输出“404 Not found”。

好,那么问题在哪里呢?简单的说就是:当未来有请求处理程序需要进行非阻塞的操作的时候,我们的应用就“挂”了。

没理解?没关系,下面就来详细解释下。

未完,请移步Node入门(三)

点击展开全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 优才学院 的精彩文章:

TAG:优才学院 |

您可能感興趣

DOM探索之-DOM的nodeType、nodeName、nodeValue
徹底理解 Node.js 中的回調(Callback)函數
詳解node + mongoDb(mongoDb安裝、運行,在node中連接增刪改查)
nodejs連接mongodb,對數據增刪改查操作(跳過坑)Windows版
Node.js進階:cluster模塊深入剖析
React、頁面渲染、任務隊列、Node.js
XML DOM-NamedNodeMap 對象
RPM命令的——nodeps 和——force參數解釋
blogfoster-scripts:一款簡化 Node.js 項目初始化的工具
大規模集群下的Hadoop NameNode
川崎病:小兒皮膚黏膜淋巴結綜合征(mucocutaneous lymph node syndrome,MCLS)
Node Worthy 第十八期:隱私和虛擬晶元
Node Worthy 第 16 期:區塊鏈技術在台灣
Node Worthy第16 期:區塊鏈技術在台灣
Node Worthy第16 期:區塊鏈技術在台灣
瀏覽器與Node的事件循環(Event Loop)有何區別?
Shader Graph著色器視圖自定義節點API:Code Function Node
拒絕 Python、C 和 Go,我只用 Node.js!
拒絕 Python、C#和Go,我只用 Node.js!
log4js-node配置