Node笔记2
关于函数式编程
将函数作为参数传递并不仅仅出于技术上的考量。对软件设计来说,这其实是个哲学问题。
想想这样的场景:在index文件中,我们可以将router对象传递进去,服务器随后可以调用这个对象的route函数。就像这样,我们传递一个东西,然后服务器利用这个东西来完成一些事。然后,那个叫路由的东西,能帮我把这个路由一下吗?但是服务器其实不需要这样的东西。它只需要把事情做完就行,其实为了把事情做完,你根本不需要东西,你需要的是动作。
也就是说,你不需要名词,你需要动词。理解了这个概念里最核心、最基本的思想转换后,我自然而然地理解了函数编程。也就是说我直接执行动作,不必给动作定一个名称,然后让那个对象根据动作名称再去执行动作,有的时候不觉得这很啰嗦么?哈哈…
路由给真正的请求处理程序
路由: 是指我们要针对不同的URL有不同的处理方式。
我们暂时把作为路由目标的函数称为请求处理程序。现在我们不要急着来开发路由模块,因为如果请求处理程序没有就绪的话,再怎么完善路由模块也没有多大意义。
现在给应用程序增加一个新的部件。我们来创建一个叫做requestHandlers的模块,并对于每一个请求处理程序,添加一个占位用函数,随后将这些函数作为模块的方法导出:
|
|
这样就可以把请求处理程序和路由模块连接起来,让路由“有路可寻”。
在requestHandlers模块添加一点依赖,使用依赖注入可以让路由和请求处理程序之间的耦合更加松散,也因此能让路由的重用性更高。
那么什么是依赖注入?
依赖注入(Dependency Injection)是用于实现控制反转(Inversion of Control)的最常见的方式之一。依赖注入不是目的,它是一系列工具和手段,最终的目的是帮助我们开发出松散耦合(loose coupled)、可维护、可测试的代码和程序。
这意味着我们得将请求处理程序(即handle)从服务器传递到路由中,但感觉上这么做更离谱了,我们得一路把这堆请求处理程序(handle)从我们的主文件(index.js)传递到服务器中(server.start函数),再将hangle从服务器(server.start函数)传递到路由(route函数)。
那么我们要怎么传递这些请求处理程序呢?我们不想每次有一个新的URL或请求处理程序时,都要为了在路由里完成请求到处理程序的映射而反复折腾。除此之外,在路由里有一大堆if request == x then call handler y
也使得系统丑陋不堪。
仔细想想,每个都要映射到一个字符串(就是请求的URL)上?似乎关联数组(associative array)能完美胜任。不过JavaScript没提供关联数组,事实上,在JavaScript中,真正能提供此类功能的是它的对象, 在python中是字典.不过javascript的对象更加强大.
在C++或C#中,当我们谈到对象,指的是类或者结构体的实例。对象根据他们实例化的模板(就是所谓的类),会拥有不同的属性和方法。但在JavaScript里对象不是这个概念。在JavaScript中,对象就是一个键/值对的集合 – 你可以把JavaScript的对象想象成一个键为字符串类型的字典。
JavaScript的对象仅仅是键/值对的集合,它又怎么会拥有方法呢?
这里的值可以是字符串、数字或者函数.
现在我们已经确定将一系列请求处理程序通过一个对象(即handle)来传递,并且需要使用松耦合的方式将这个对象注入到route()函数中。
我们先将这个对象引入到主文件index.js中:
将不同的URL映射到相同的请求处理程序上是很容易的:只要在对象中添加一个键为”/“的属性,对应requestHandlers.start即可,这样就可以干净简洁地配置/start和/的请求都交由start这一处理程序处理。
在完成了对象的定义后,我们把它作为额外的参数传递给服务器,为此将server.js修改如下:
在start()函数里添加了handle参数,并把handle对象作为第一个参数传递给了route()回调函数。
然后我们相应地在route.js文件中修改route()函数:
在命令行运行:node index.js
, 通过以上代码,首先检查给定的路径对应的请求处理程序是否存在,如果存在直接调用相应的函数。我们可以用从关联数组中获取元素一样的方式从传递的对象中获取请求处理函数,因此就有了简洁流畅的形如handle[pathname]();
的表达式,这个感觉就像在前方中提到的那样:“嗨,请帮我处理了这个路径”。
让请求处理程序作出响应
其实“处理请求”说白了就是“对请求作出响应”,因此,我们需要让请求处理程序能够像onRequest函数那样可以和浏览器进行“对话”。
不好的实现方式
不好的实现方式是指:让请求处理程序通过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”。
那么问题在哪里呢?
简单的说就是: 当未来有请求处理程序需要进行非阻塞的操作的时候,我们的应用就“挂”了。
为什么呢?
阻塞与非阻塞
直接来看,当在请求处理程序中加入阻塞操作时会发生什么?
修改下start请求处理程序,让它等待10秒以后再返回“Hello Start”。因为,JavaScript中没有类似sleep()这样的操作,所以这里只能够来点小Hack来模拟实现。
将requestHandlers.js修改成如下形式:
上述代码中,当函数start()被调用的时候,Node.js会先等待10秒,之后才会返回“Hello Start”。当调用upload()的时候,会和此前一样立即返回。
(这里只是模拟休眠10秒,实际场景中,这样的阻塞操作有很多,比如耗时的计算操作等。)
我们重启下服务器。为了看到效果,我们要进行一些相对复杂的操作:
首先,打开两个浏览器窗口或者标签页。在第一个浏览器窗口的地址栏中输入http://localhost:8888/start, 但是先不要打开它!在第二个浏览器窗口的地址栏中输入http://localhost:8888/upload, 同样的,先不要打开它!接下来,做如下操作:在第一个窗口中(“/start”)按下回车,然后快速切换到第二个窗口中(“/upload”)按下回车。
注意,发生了什么: /start URL加载花了10秒,但是,/upload URL居然也花了10秒,而它在对应的请求处理程序中并没有类似于sleep()这样的操作!
这到底是为什么呢?
原因就是start()包含了阻塞操作。形象的说就是“它阻塞了所有其他的处理工作”。
这显然是个问题,因为Node一向是这样来标榜自己的:“在node中除了代码,所有一切都是并行执行的”。这句话的意思是说,Node.js可以在不新增额外线程的情况下,依然可以对任务进行并行处理 —— Node.js是单线程的。它通过事件轮询(event loop)来实现并行操作,对此,应该尽可能的避免阻塞操作,取而代之,多使用非阻塞操作。然而,要用非阻塞操作,需要使用回调,通过将函数作为参数传递给其他需要花时间做处理的函数(比方说,休眠10秒,或者查询数据库,又或者是进行大量的计算)。对于Node.js来说,它是这样处理的:“嘿,probablyExpensiveFunction()(译者注:这里指的就是需要花时间处理的函数),你继续处理你的事情,我(Node.js线程)先不等你了,我继续去处理你后面的代码,请你提供一个callbackFunction(),等你处理完之后我会去调用该回调函数的,谢谢!”
一种错误的使用非阻塞操作的方式
还是从start请求处理程序开始。将其修改成如下形式:
上述代码中,引入了一个新的Node.js内建模块child_process。之所以用它,是为了实现一个既简单又实用的非阻塞操作:exec()。
exec()做了什么呢?它从Node.js来执行一个shell命令。在上述例子中,我们用它来获取当前目录下所有的文件(“ls -lah”),然后,当/startURL请求的时候将文件信息输出到浏览器中。
上述代码是非常直观的: 创建了一个新的变量content(初始值为“empty”),执行“ls -lah”命令,将结果赋值给content,最后将content返回。
和往常一样,我们启动服务器,然后访问“http://localhost:8888/start” 。之后会载入一个漂亮的web页面,其内容为“empty”, 却不是”hello start”。怎么回事?
原因是exec()在非阻塞这块发挥了神奇的功效。它其实是个很好的东西,有了它,我们可以执行非常耗时的shell操作而无需迫使应用停下来等待该操作。然而,针对浏览器显示的结果来看,我们并不满意我们的非阻塞操作,对吧?好,接下来,我们来修正这个问题。在这过程中,让我们先来看看为什么当前的这种方式不起作用。
问题就在于:为了进行非阻塞工作,exec()使用了回调函数。
在我们的例子中,该回调函数就是作为第二个参数传递给exec()的匿名函数:
现在就到了问题根源所在了:代码是同步执行的,这就意味着在调用exec()之后,Node.js会立即执行return content;
, 而此时content仍然是“empty”,因为传递给exec()的回调函数还未执行到, 因为exec()的操作是异步的。这里“ls -lah”的操作其实是非常快的(除非当前目录下有上百万个文件)。这也是为什么回调函数也会很快的执行到, 不过,不管怎么说它还是异步的。为了让效果更加明显,我们使用一个更耗时的命令: “find /”,尽管在请求处理程序中,把“ls -lah”换成“find /”,当打开/start URL的时候,依然能够立即获得HTTP响应 —— 很明显,当exec()在后台执行的时候,Node.js自身会继续执行后面的代码。并且我们这里假设传递给exec()的回调函数,只会在“find /”命令执行完成之后才会被调用。
那究竟我们要如何才能实现将当前目录下的文件列表显示给用户呢?
了解了这种不好的实现方式之后,我们接下来来介绍如何以正确的方式让请求处理程序对浏览器请求作出响应。
以非阻塞操作进行请求响应
正确的方式是Node.js有这样一种实现方案:函数传递。
目前,我们的应用已经可以通过应用各层之间传递值的方式(请求处理程序 -> 请求路由 -> 服务器)将请求处理程序返回的内容(请求处理程序最终要显示给用户的内容)传递给HTTP服务器。
现在采用新的实现方式:相对采用将内容传递给服务器的方式,这次采用将服务器“传递”给内容的方式。 从实践角度来说,就是将response对象(从服务器的回调函数onRequest()获取)通过请求路由传递给请求处理程序。 随后,处理程序就可以采用该对象上的函数来对请求作出响应。
原理就是如此,接下来一步步实现这种方案。
先从server.js开始:
相对此前从route()函数获取返回值的做法,这次将response对象作为第三个参数传递给route()函数,并且,将onRequest()处理程序中所有有关response的函数调都移除,因为我们希望这部分工作让route()函数来完成。
下面就来看看我们的router.js:
同样的模式:相对此前从请求处理程序中获取返回值,这次取而代之的是直接传递response对象。
如果没有对应的请求处理器处理,我们就直接返回“404”错误。
最后,我们将requestHandler.js修改为如下形式:
我们的处理程序函数需要接收response参数,为了对请求作出直接的响应。
start处理程序在exec()的匿名回调函数中做请求响应的操作,而upload处理程序仍然是简单的回复“Hello World”,只是这次是使用response对象而已。
这时再次我们启动应用(node index.js),一切都会工作的很好。
如果想要证明/start处理程序中耗时的操作不会阻塞对/upload请求作出立即响应的话,可以将requestHandlers.js修改为如下形式:
这样一来,当请求http://localhost:8888/start的时候,会花10秒钟的时间才载入,
而当请求http://localhost:8888/upload的时候,会立即响应,纵然这个时候/start响应还在处理中。