第二章简单的 Node.js 框架——2.1 ​HTTP 服务器

字号+ 编辑: Snake 修订: 种花家 来源: 《为PHP开发者写的Node.js学习指南》 2023-09-09 我要说两句(0)

在之前的章节,我介绍了一个用于 PHP 到Node.js 转换的开发环境,以及如何使用它进行转换。在本章,我们将开始使用这个开发环境并进行实际的转换。

在 PHP 中,一个 PHP 文件代表一个 HTML 页面。一个 Web 服务器,比如 Apache, 当请求一个 PHP 页面时,Web 服务器会运行 PHP。但是在 Node.js 里,Node.js 的main 文件代表了整个服务器。Node.js 并不是运行在类似 apache 这样的 Web 服务器中,而是取代 apache. 因此,我们需要一些启动代码来让 Web 服务器正常工作。

我们来看看上一个章中作为例子的 httpsvr.njs,下面是 httpsvr.njs 中的 Node.js 代码:

var http s require('http');
var static = require('node-static');
var file = new static.Server();
http.createServer(function (req, res) {
file.serve(req, res);
}).listen(l337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/')

它是如何工作的呢?

如同在前面章节中提到的,require()函数可以加载一个模块并使用该模块。前两行代码分别展示了加载一个内建模块和外部模块:

var http = require('http'); //built-in module
var static = require('node-static');// external module

如果你按照上一章的内容安装了 Node.js 并且实现了所有的例子,那么应该已经安装了包含 node-static 外部模块的 node-static npm 包。如果没有,你可以执行下面的npm 命令进行安装:

npm install node-static

第三行代码有一点技巧:

var file = new static.Server();

non-static 模块想要在Node.js 服务器提供多个文件服务对象给客户端使用而不仅仅是被限制在单个文件服务对象上。因此,它没有使用模块本身作为文件服务对象, 而是通过调用构造函数来创建一个文件服务对象。构造函数被设计成跟 new 关键字一起使用。使用 new 关键字调用构造函数就可以创建一个新的对象。

在这个例子里,模块对象被命名为 static。在这个模块对象内部有一个键值对(key-value),key  是  Server,value  是一个构造函数。点操作符(.)指明了这种关系,使得 new 关键字正确作用在构造函数上。创建一个新的文件服务对象并存在file 变量里。

文件服务对象的构造函数接受零个或一个参数。当没有提供参数的时候,文件服务对象会使用当前的目录(文件夹)作为 HTTP 服务器的顶级目录。例如,如果httpsvr.njs  是在  ferris  目录下运行的,那么当浏览器如  Google  Chrome  访问http://127.0.0.1:1337/hello.html 时,文件服务对象会在 ferris 目录下寻找 hello.html文件。当浏览器访问 http://127.0.0.1:1337/exit/goodbye.html 时,文件服务对象会在ferris 目录下的 exit 目录里寻找 goodbye.html 文件。

然而,当使用有参的构造函数时,文件服务对象会到参数所指定的目录内查询文件。例如,当“..”字符串作为构造函数的参数时,被创建出来的文件服务对象会去当前目录的父目录里查询文件。require()函数只接受一个参数,即要加载的模块的名字。这里并没有提供更便捷的方式在用来在模块加载过程中传递额外参数。虽然它需要指定加载文件的目录作为参数,但是最好还是把加载模块和指定从哪里加载文件的操作完全分开。

在文件服务对象创建好以后,HTTP 服务器对象就可以接受 HTTP 请求并把文件返回给客户端,例如 Web 浏览器:

http.createServer(function(req,res){
file.serve(req,res);
}).listen(1337,'127.0.0.1');

上面的代码可以改写成下面三条语句,更容易理解:

var handleReq (function(req, res) {
file.serve(req, res);
};
var svr = http.createServer(handle'leReq);
svr.listen(1337, '127.0.0.1');

第一条语句由三行代码组成。它定义了一个叫 handleReq 的变量,这个变量的值不是类似字符串或数字这样“一般”的值,而是一个函数。在 Node.js 里面,可以将函数赋值给一个变量,像字符串和数字一样。当一个函数被赋值给一个变量时,这个函数被叫做回调函数,并为了方便起见,被赋值的变量被叫做回调变量。回调变量的定义和常规函数定义基本相同,不同的是回调函数不需要命名可以通过赋值给变量来使用。

在这个例子里,回调函数需要两个变量。第一个变量 req,包含进行 HTTP 请求的所有数据。第二个变量,res 包含 HTTP 响应的所有数据。在这种实现里,文件服务对象 file 负责解析 HTTP 请求,在磁盘上找到对应的文件,然后把数据写入到HTTP 响应中,文件就这样被返回到浏览器中。Node-static 模块就是依据这种思想来设计的,仅需一行代码文件服务对象就能返回磁盘文件。

第四行代码创建了一个 HTTP 服务器和一个处理 HTTP 请求的循环,它会一直接收新的 HTTP 请求并使用 handleReq 回调函数来执行这些请求:

var svr = http.createServer(handleReq);

在 createServer()函数内部,handleReq 变量会按如下方式被调用:

function createServer(handleReq){
white (true){
//wait for HTTP request
var req = decodeHttppequest(); // somehow decode the request
var res = createHttpRequest(); // somehow create a response object handleReq(req, res); // invoke handleReq()
// send WTTP response back to client based on "res" object

回调变量与一般的函数一样(但不同于其他类型的变量)可以调用它所包含的函数。正如你可以看到的,调用 handleReq 回调函数的参数与调用一般的函数是相同的。事实就是这样的,即使 handleReq 不是函数的名字仅仅是回调变量或参数的名字。回调变量可以作为参数传递给其他函数,就像其他类型的变量一样。

为什么不直接将 file.serve()调用直接硬编码到 createServer()函数中呢?难道提供文件不是一个 Web 服务器该做的事情吗?

function createServer(handleReq){
  while (true){
    // wait for HTTP request
    var res = decodeHttpRequest(); // somehow decode the request
    var res = createHttpRequest(); // somehow create a request object
    file.serve(req,res); // hardcode a reference to the "node static" module
    // send HTTP response back to the client based on "res" object
    }
}

这样也是可以的,但是把回调函数传递给 createServer 函数会更加灵活。记住:http模块是  node.js  内建的,而  node  static  模块是一个独立安装的  npm  包。如果将file.serve()调用写到 createServer()函数中,当我们使用另一个模块替代 node static 模块或者添加一些自定义的处理 HTTP 请求的代码时,就需要复制粘贴整个createServer()函数才能调整其中一行代码。因此,它使用回调函数。所以,你仔细想想,回调函数是一种调用代码的时将自己的代码插入到被调用的函数中的方法。它是在不修改函数本身代码的情况下改变函数的行为的方法。在本例中被调用的函数 createServer()必须期望和支持回调函数,但是在编写程序的时候就考虑到回调函数的话,那么调用者可以创建一个匹配它的期望的回调函数,这样函数就可以使用整个回调而不用调用代码的任何详细内容。回调函数使得两段代码能在一起工作, 即使它们是不同人在不同时间编写的。

在这个例子中,通过传递作为参数的回调函数,调用者可以以适合它的任何方式来处理 HTTP 请求。但是,在大多数应用场景中,作为参数传递进去的回调函数会在异步请求完成后被调用。在下一个章节中,将会详细讲解回调函数的这类应用。

第五行代码使用 svr 对象监听‘127.0.0.1’计算机,又名‘localhost’计算机(即运行Node.js 服务器的计算机)上的 1337 端口:

svr.listen(1337,'127.0.0.1');

指出的是, 处理 HTTP 请求的循环更可能在 listen() 函数里而不是createServer()函数。但是这里的目的是解释回调函数,所以这不是大问题。

因为 svr 变量和 handleReq 变量仅被使用了一次因此可以使用更简洁的代码来替换他们,这样三条语句可以合并成一条:

http.createServer(function(req,res){
    file.serve(req,res);
}).listen(1337,'127.0.0.1');

httpsvr.njs 的最后一行代码会向控制台中输出一条信息,这样当某人启动 HTTP 服务器时,他可以知道该如何访问它:

console.log('Server running at http://127.0.0.1/');

httpsvr.njs 文件构建了一个基本的 Node.js HTTP 服务器。现在我们将组成 Web 应用程序的所有的文件从 PHP 服务器中移动到 httpsvr.njs 所在的文件夹中。当 httpsvr.njs 启动后,所有这些文件包括 HTML、CSS、客户端 JavaScript、图片文件(如 PNG 文件),以及其他相关的文件都可以被传送给客户端了,可能是浏览器,它们就会像之前那样正常工作。客户端仅需要指向正确的端口(例如,1337)就可以从 Node.js创建的服务器加载文件了。现在 Web 应用程序会出问题的唯一原因是它仍然是用PHP 写的,但是因为 HTML,CSS,客户端 JavaScript 和图片文件都是完全在客户端处理和执行的,所以除非需要 PHP 文件的支持,它们都能正常工作。.php 文件也可以被移动到 Node.js 服务器,但是因为 Node.js 不能解释.php 文件,所以它们并不能正常工作。

.php 文件和其他文件的最大的不同点是.php 文件会被服务器解释,解释结果会作为HTTP 响应返回给客户端。而对于其他文件,服务器会读取它们的内容并直接写入到 HTTP 响应中。如果.php 文件没经过解析,那么客户端会接收到 PHP 源码,而它并不知道如何使用这些源码。客户端需要这些源码的解析结果才能工作,而不是源码本身。PHP 到 Node.js 的转换过程归结来说就是,使用 Node.js 代码来产生 PHP 代码完全相同的输出结果。这看起来相当简单,但是工作量很大。

首先,需要为每一个.php 文件创建一个本地模块。对于本书来说,一个本地模块就是一个本地的.njs 文件,main 函数文件可以通过 require()函数来加载这个模块。为了给一个.php 文件创建一个 Node.js 模块,我们可以在.php 文件相同的文件夹创建一个相同文件名的.njs 空文件。例如,对于 admin/index.php,创建一个空的 admin/index.njs 文件。

对于这些转换工作,你需要自己做出判断。在某些情况下,有相当多的.php 文件, 创建对应的.njs 文件会做很多无用功,因此开始的时候只创建一小部分会比较好。但是在其他情况,只有很少的.php 文件需要转换,所以一次性创建所有的对应的空文件会比较有效率。

一旦你创建好了对应的.njs 文件后,选择一个.php 文件并修改它对应的.njs 文件:

ecports.serve = function(req,res){
    res,writHead(200,{'Content-Type':'text/plain'});
res.end('admin/index.njs');
};

把这些代码写到.njs 文件中并且修改 res.end()函数调用的路径(在这个例子中是admin/index.njs)指向正在被修改的这个.njs 文件。

通过这段简单的代码,这个.njs 文件就是一个实现了 exports.serve()这个存根函数的Node.js 模块了。存根实现是指那些使用简单代码作为占位符,稍后再用一个更可靠的实现来替换。exports.serve()存根函数有两个参数,它们对应于 httpsvr.njs 中传递给 http.createServer()的回调函数的两个参数。Req 参数是 HTTP request 对象,res 参数是 HTTP response 对象。

当 exports.serve()函数被调用的时候,存根实现会返回一个 HTTP response,它的一个 HTTP  头的 Content-Type 被设置为“text/plain”,内容则是正在被调用的文件。通过自定义的存根实现使得后期更容易调试,以免一个编码错误导致请求一个文件的 HTTP request 意外结束在另一个文件中。

一旦你有了一个实现了 exports.serve 的.njs 文件,你应该在适当的时机修改httpsvr.njs 文件来调用 exports.serve 函数。但是首先,必须使 httpsvr.njs 能访问这个模块。通过使用 Node.js 中的 require()函数可以像加载内建模块一样加载本地模块:

var admin_index = require('./admin/index.njs');

在加载本地模块的时候,参数需要一个以./开头的路径名。./指明了一个路径名是一个本地模块而不是内建模块或 npm 包。

在这个例子中,该本地模块被赋值给 admin index 变量。使用那些包含了实现了.php文件同等功能的 Node.js 代码的模块文件的路径名的变体作为存储模块的变量的名字会带来很大的便利并且不容易混淆,当然这取决这个 Web 应用程序中的.php 文件的数量(即对应的.njs 文件的数量)。

为了将对某个 PHP 页面的 HTTP request 传递到正确的 Node.js 本地模块,我们需要修改传递给  http.createServer()的回调函数验证并传递  HTTP  request  到适当的Node.js 本地模块。通过在 if 语句中使用一个简单比较,可以检查该 HTTP request 是否正在请求一个特定的.php 文件,如果是,则把它传递到合适的 Node.js 本地模块上,否则传递到 node static npm 包(例如,file 变量)。

http.createServer((req,rea){
    if (url.parse(req.url).pathname == '/admin/index.php'){
        admin_index.serve(req,res);
    } else {
        file.serve(req,res);
    }
}).listen(1337,'127.0.0.1');

这段源码使用了 Node.js 内建的 http 和 url 模块实现了一个简单但是功能完整的 Node.js 服务器。如果需要的话,可以安装使用一些更复杂的包,如 express Node.js 包。

在这个实现中,需要使用内建的 url 模块来从 HTTP request 中解析 URL。下面的代码使用 require()函数访问内建的 url 模块:

var url = require('url');

让我们继续前面的例子,当 HTTP request 正在请求/admin/index.php URL 资源,那么本地模块 admin index 会解释这个请求并返回一个 HTTP response,而不是使用file 变量读取一个文件并以静态文件返回。

你应该已经注意到了 HTTP request 中 URL 是与/admin/index.php 进行对比的,而不是/admin/index.njs。因为浏览器使用自定义的代码处理对.php 文件的请求,它可以以任何方式来处理.php 文件的请求,包括忽略磁盘上.php 文件并使用 Node.js 代码来代替运行。一个 HTTP request 是一个请求,而不是一个需求,Node.js 服务器可以不适用完全不适用  PHP  的情况下处理.php  文件的请求。在这段  Node.js  代码中,.php  文件引用变成了进行一个特定操作的唯一标示符,.php  扩展符不再代表PHP。回调函数中 if-else 语句仅仅是将一个唯一的路径匹配到一段 Node.js 代码上。这个语义表明.php 扩展符已经被忽略了。

最有可能的是,客户端正在请求一个.php 文件或者是由于用户点击了一个 HTML页面中的链接,又或者这是 JavaScript AJAX 调用的一部分。为了改变这种情况,HTML 和客户端 JavaScript 中对.php 文件的引用需要改成.njs。这些改变取决你的实际情况。从有利的一方面来说,移除.php 文件扩展符可以消除访问你代码的程序员的困惑,他们可能会奇怪为什么在 Node.js 代码库里会有 PHP 引用。从不利的方面来看,移除.php 文件引用会在 PHP 和Node.js 代码库之间产生一些不必要的技术上的差异。

在一步一步解释完单个.php 文件后,让我们把所有这些融合到一起来处理多个.php 文件:

var http = require('http');
var static = require('node-static');
var file = new static.Server();
var url = require('url');
var index = require('./index.njs');
var login = require('./login.njs');
var admin_index = require('./admin/index.njs');
var admin_login = require('./admin/login.njs');
http.createServer(function(req,res){
    if(url.parse(req.url).pathname == '/index.php'){
        index.serve(req,res);
    } else if (url.parse(req.url).pathname == '/login.php'){
        login.serve(req,res);
    } sele if (url.parse(req,res).pathname == '/admin/index.php'){
        admin_index.serve(req,res);
    } else if (url.parse(req,res).pathname == '/admin/login.php'){
        admin_login.serve(req,res);
    } sele {
        file.serve(req,res);
    }
    }).listen(1337,'127.0.0.1');
    console.log('Server running at http://127.0.0.1/');

这里有两个重要的修改:(1)添加了一组 require()函数调用,每一个 require()函数对应于一个.php 文件;(2)添加了一组 else-if 语句与每一个.php 文件一一对应。在这个例子中,这个修改过的 httpsvr.njs 函数会处理四个.php 文件:index.njs、login.njs、 admin/index.njs 和 admin/login.njs。这些文件里面的代码都相同,除了对指向那个文件的路径名的修改:

exports.serve = function(req,res){
    res.writeHead(200,{'Content-Type':'text/plain'});
    res.end('admin/index.njs');
    };

每一个.njs 文件中的 exports.serve()函数中所包含的代码完整地模拟了它对应的.php 文件的操作。我们将会移除存根实现中仅返回.njs 文件的名字作为 HTTP response 的操作,而将对应的.php 文件中的 PHP 代码复制粘贴到 exports.serve()函数中,并且通过一系列的变形和转换的技巧将这些 PHP 代码变成相同功能的 Node.js 代码。但是我们还没有准备好这样做。

阅完此文,您的感想如何?
  • 有用

    0

  • 没用

    0

  • 开心

    0

  • 愤怒

    0

  • 可怜

    0

1.如文章侵犯了您的版权,请发邮件通知本站,该文章将在24小时内删除;
2.本站标注原创的文章,转发时烦请注明来源;
3.交流群: PHP+JS聊天群

相关课文
  • JS如何防止父节点的事件运行

  • nodejs编写一个简单的http请求客户端代码demo

  • 使用Sublime Text3 开发React-Native的配置

  • 说一则为什么后端开发人员不选择node.js的原因

我要说说
网上宾友点评