本节书摘来自异步社区《写给php开发者的node.js学习指南》一书中的第2章,第2.1节,作者【美】daniel howard,更多章节内容可以访问云栖社区“异步社区”公众号查看
写给php开发者的node.js学习指南
当一个支持php的web服务器执行一个php页面时,它并不是仅提供一个未处理的对某个页面的http request,然后执行这个页面。如果它这样做的话,那么每一个php页面都需要大量额外的代码来解析原始的http request并且把这些值用更方便的方式存储起来。相反,php引擎解码原始的http请求,并将数据填充到一堆众所周知的php全局变量中。这些全局变量被正确填充才能保证php页面正常工作。
由于我们采用的基本方法是将php页面拷贝到本地模块中并将其转换成node.js代码,那么我们需要自己在node.js中实现这些全局变量以保证转换过的页面能正常工作。通过分析php页面,我们可以决定它依赖于哪些变量。并不是每一个php引擎提供的全局变量都需要实现。相反,我们仅实现那些被使用的到的变量。
有五个php预定义的全局变量是最常用的:$_get、$_post、$_cookie、$_request和$_session。
一个http request被发送时总是有一个http操作,它被叫作方法或者动词。一个http get操作是非常简单的:客户端向服务器请求获取一个页面。当用户在浏览器的地址栏里键入url时,他就是在输入一个http get request。
http get request可能有一些以名称/值对形式的参数。这些参数通常叫做查询参数或者查询字符串。用户可以手动向浏览器的地址栏中的url最后添加添加一个问号(?)并将名称/值对以&符号分割添加到后面。键值对本身之间以等号分割。这有一个例子:

在这个例子中的键值对有:theme=green、tab=users和fastload=true。当一个php页面获取到一个像这个例子中一样的get request时,php引擎从原始http get request中提取出这些键值对并把它们放到预定义的php$_get数组。名字作为$_get数组中的键值或索引,值就是值。对于之前的url例子,$_get数组看起来就像这样:
当php页面被转换成node.js代码后,node.js仍然需要这些预定义的数组存在并被正确填充。接下来的代码展示了一个node.js函数 initget(),它可以被用在任何转换过的php页面的本地模块中用来填充一个node.js_get变量,这个变量就像php中的$_get变量一样工作:
node.js函数initget()需要三个参数:req、pre和cb。req参数包含一个原始的http request。pre参数是一个包含了所有预定义的全局变量的node.js对象。所有预定义的变量都存储在pre变量中,而不是一堆不同的变量,这样可以方便地传递它。而cb包含了一个在initget()函数结束时会被调用的回调函数。由于initget()函数仅进行了一些简单的内存操作并且无需进行有回调函数的操作,因此在技术上并不需要回调函数。但是,由于稍后将要实现的initpost()函数将会需要一个回调函数cb()作为参数,所以最好让initget()和initpost()函数保持一致。
initget()函数的第一行代码在参数pre中创建了一个名为_get的数组。pre._get 就是相当于php中$_get数组的node.js对象。接下来再从main url中提取出查询参数,而这个main url则是从req.url属性获取的。通过使用split()函数来将每一个url查询参数分离开来进而区分它们的名字/值对使得填充pre._get变量也非常简单。最后,调用cb参数让回调函数知道pre._get变量已经可用。
为了初始化node.js pre._get变量,需要对exports.serve()函数进行一些修改。这里是最初的exports.serve()函数:
这里我们并不是在exports.serve()函数中实现真实的页面,而是使用一个新的函数叫作page(),exports.serve()将被保留作为初始化和完成其他php引擎为php页面做的工作:
page()函数需要四个参数:req、res、pre和cb。req和res参数代表http request和http response。pre参数是预定义的变量,包含了存储查询参数的_get属性。cb参数是一个回调函数,它可以让exports.serve()函数知道什么时候页面被完全处理完了。
将pre对象打印出来可以帮助调试。通过使用require()函数加载内建的util模块,并在res.end()函数调用中添加一个util.inspect()函数调用就可以把pre变量中包括_get属性的所有内容显示在http response中:
现在处理页面的操作已经移动到page()函数中了,exports.serve()函数被修改成进行初始化工作,包括调用initget()函数:
pre变量是最先被创建的。然后调用initget()函数,当它完成时,则调用page()函数。在page()函数之后并没有终止化或清除操作,所以它的回调函数是空的。
当_get属性被实现之后,就可以修改page()函数使用查询参数。修改page()函数,期待接收一个x查询参数并返回对应的值:
http post request和http get request基本一样,除了键值对是通过request正文而不是url最后的查询字符串进行发送的。一个http request包括http header和一个http body。包含查询字符串的url便是http header中的一个。http header内容精简并且有长度限制的。尤其是包含查询字符串的url,需要限制在一定长度,不推荐使用过长的url。相应地,当需要在http request中包含很多数据时,推荐把数据放到http body中作为http post的一部分。http body跟被限制长度的http header不一样,它可以处理非常大量的数据。对于http post,http body通常被称为post数据。
由于http post中的正文可能非常巨大,所以post数据通常不是一次性发送的;它在收到变成可用的事件时会被发送。事件是node.js用来指示某件事情发生的一种方法。例如,一个data事件表明下一个数据块已经从http body中被读出。假如这里有很多数据,node.js会在读取每一块数据时触发若干事件。
一个时间可以跟回调函数联系到一起,这也被叫作事件处理程序。事件处理程序会在一个事件发生时被执行。
on()函数可以将一个事件和事件处理函数联系到一起。下面的例子说明了如何使用on函数把data事件跟一个数据处理函数绑定在一起,这个数据处理函数会将数据写到控制台:
对于initpost()函数,pre的_post属性会被初始化。就像从initget()函数中重新调用后一样,pre参数是一个包含了所有预定义的全局变量的node.js对象。一个body变量会被创建出来保存到被读取的http body。on()函数把一个事件处理程序和data事件联系到一起,这个事件处理程序会在数据变为可用之后将其写入到body变量中:
在data事件处理程序中添加的if语句是用来检测http正文是否太长,如果是则会中断连接。写得不好或恶意的客户端可能会发送无限量的数据,在这时该if语句就需要放弃那个发送的大量数据的http request来保护node.js服务器。
最后,on()函数为end事件绑定一个事件处理程序,它会在整个http正文被读取之后触发。通过简单的split()函数调用,end事件处理程序提取出数据并把它们放到pre._post变量中:
cb()在最后被调用时,已经有正确的pre._post变量值可以使用了。
将所有代码放到一起,initpost()函数的全文显示如下:
对于那些期待http get request的页面,必须修改exports.serve()函数。而对于那些期待http post request的页面,exports.serve()函数的代码则是一样的,除了调用initget()函数的地方替换为initpost()函数调用。设计就是这样的。尽管initget()函数不需要一个回调函数,但是给initget()函数一个回调函数让它和initpost()函数有一样的参数并且代码几乎相同。
到目前为止,http get和http post是最常用的http操作,并且在大多数的情况下,这也是一个web应用程序仅需要的http操作。还有一些其他的http操作,如http put、http delete和http head,但是php引擎并没有对这些操作提供支持,所有node.js移植通常也不需要提供支持。
cookie是一个服务器发送到客户端(通常是浏览器)的键值对,客户端会将cookie存起来并将它作为一个http header添加到每一个后续的http请求中。客户端通常将cookie存储在一个可持久化的地方,如硬盘中,因此能在将来的http请求中使用这个cookie,即使客户端被关闭并重新启动后。cookie是服务器提供给客户端的一小段数据,它可以帮助服务器验证客户端,例如让客户端可以自动登录自己的账户。
cookie的http header的名字就是“cookie”:
在node.js的http请求中,cookies是存储在http请求的headers.cookie属性中的,在本书中的例子里就是req。
initcookie()与initget()函数都用于将cookie从相应的http头中提取出来并放到pre._cookie变量中,非常相似。获取cookie会更方便:cookie不是附加 在url最后作为查询字符串而是有自己的属性值。pre._cookie变量将会 用来替代php引擎为php页面提供的php $_cookie变量:
就像那些期待http get和http post请求的页面,那些需要cookie的页面需要修改它们的exports.serve()函数来调用initcookie()函数。initcookie()函数跟initget()和initpost()有同样的参数,因此可以使用同样的代码调用initcookie()函数:
当一个页面同时需要处理http get和http post两种请求并且还需要用到cookie时,可以通过将一个函数当作另一个函数的回调函数的方法来增强exports.serve()函数初始化的操作。以下代码用于加载pre._get、pre._post和pre._cookie属性,它们将用于替代php预定义变量$_get、$_post和$_cookie:
在php里,$_request预定义变量包含了在$_get、$_post和$_cookie中所有的键值对。我们需要将pre._get、pre_post和pre._cookie变量复制一份来创建 pre._request:
node.js中的for…in语句可以从一个node.js对象找出所有的属性名。下面的代码显示了一个对象obj并且把它所有的属性名打印出来:
这里是node.js的for…in循环的输出:
在php中,foreach…as与node.js中的for…in工作行为差不多,除了foreach…as会返回php中的属性值而不是像for…in在node.js中返回属性名。
就像其他预定义变量的初始化函数一样,initrequest()函数必须被exports.serve()函数调用。因为_request值是_get、_post和_cookie的一个复合体,构造_request值需要的三个值已经赋值给pre变量,因此 initrequest()函数必须要在其他函数初始化之后被调用:
initrequest()函数就像期待的那样与其他初始化函数使用完全相同的参数并且表现也相同。
在php还有一个常用的预定义变量:$_session变量。$_session变量代表了每一个用户对 php页面的调用。
initsession()函数使用pre._cookie变量维护当前用户在sessions变量中的会话。当会话不存在时,它会被创建出来;否则,将从sessions对象中获取:
同样也需要让页面处理会话。通过将一个函数添加到另一个函数的回调函数中来加强exports.serve()初始化函数的能力。下面的代码会加载pre._get、pre._post、pre._cookie和pre._request属性,它们是用来代替php中预定义的变量$_get、$_post、$_cookie和$_request的。
initsession()函数是在exports.serve()函数中调用的最后一个函数。它依赖于php变量$_cookie来维护这个会话。为了保证cookie处于被激活的状态,需要修改page()函数的回调函数将cookie返回给调用者:
就像所期待的那样,initsession()函数跟其他初始化函数一样采用完全相同的参数,并且执行方式也几乎相同。
这里我们创建一个本地模块initreq.njs用来将initget()、initpost()、initcookie()、initrequest()和initsession()函数共享给所有其他模块。并且这些函数作为属性赋值给exports变量,从而使它们可以暴露给加载该模块的调用者:
为了使用这个initreq.njs本地模块,首先需要用require()函数加载它。然后,需要将模块的名字initreq置于initreq.njs文件暴露出来的每一个初始函数的引用之前来进行调用。下面的代码显示了这些改变:
现在你对于从php到node.js的转换应该有更好的想法并且知道整个过程是如何工作的了。
httpsvr.njs文件是一个node.js http服务器。在一个常见的php设置中,httpsvr.njs文件类似于一个安装了php模块的apache web服务器。如果你想要调整node.js web服务器来添加一些页面,将url指定到特定的页面或者执行其他通用的web服务器的配置时,那么就需要修改httpsvr.njs文件。我们把之前的httpsvr.njs例子再放到这里以便于引用:
对于每一个php页面,都会有一个index.njs文件或其他模块文件被创建出来。exports.serve()是node.js中对应于php引擎用来处理某个特定页面代码的函数。如果需要一些额外的预定义变量,初始化代码或者结束代码(例如,在页面完成之后执行的代码),就需要修改exports.serve()函数。exports.serve()函数并不是页面本身,而是“包裹”这个页面的代码: