第二章简单的Node.js框架——2.2 预定义的PHP变量

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

当一个支持 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 最后添加添加一个问号(?)并将名称/值对以&符号分割添加到后面。键值对本身之间以等号分割。这有一个例子:

http://localhost:1337/index.php?theme=green&tab=user&fastload=true

在这个例子中的键值对有:theme=green、tab=users 和 fastload=true。当一个PHP页面获取到一个像这个例子中一样的 GET request 时,PHP 引擎从原始 HTTP GET request 中提取出这些键值对并把它们放到预定义的PHP$_GET数组。名字作为$_GET 数组中的键值或索引,值就是值。对于之前的URL 例子,$_GET数组看起来就像这样:

$_GET['theme'] = 'green';
$_GET['tab'] = 'users';
$_GET['fastload'] = 'true';

当 PHP 页面被转换成 Node.js 代码后,Node.js 仍然需要这些预定义的数组存在并被正确填充。接下来的代码展示了一个 Node.js 函数 initGET(),它可以被用在任何转换过的 PHP 页面的本地模块中用来填充一个 Node.js_GET 变量,这个变量就像PHP 中的$_GET 变量一样工作:

function initGET(req,pre,cb){
  pre._GET = {};
  var urlparts = req.url.split('?');
  if(urlparts.length >= 2){
    var query = urlparts[urlparts.length-1].split('&');
    for (var p=0; p < query.length; ++p){
      var pair = query[p].split('=');
      pre._GET[pair[0]] = pair[1];
    }
  }
  cb();
   }

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 = function(req,res){
  res.weiteHead(200,{'Content-Type': 'text/plain'});
  res.end('admin/index.njs');
}

这里我们并不是在  exports.serve()函数中实现真实的页面,而是使用一个新的函数叫作 page(),exports.serve()将被保留作为初始化和完成其他 PHP 引擎为 PHP 页面做的工作:

function page(req,res,pre,cb){
  res.weiteHead(200,{'Content-Type': 'text/plain'});
  res.end('admin/index.njs');
  cb();
}

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 中:

var util = require('util');
function page(req,res,pre,cb){
  res.weiteHead(200,{'Content-Type': 'text/plain'});
  res.end('admin/index.njs\n'+util.inspect(pre));
  cb();
}

现在处理页面的操作已经移动到 page()函数中了,exports.serve()函数被修改成进行初始化工作,包括调用 initGET()函数:

exports.serve = function(req,res){
  var pre = {};
  ininGET(req,pre,function(){
    page(req,res,pre,function(){
    });
  });
};

pre 变量是最先被创建的。然后调用 initGET()函数,当它完成时,则调用 page()函数。在 page()函数之后并没有终止化或清除操作,所以它的回调函数是空的。

当 _GET 属性被实现之后,就可以修改 page()函数使用查询参数。修改 page()函数,期待接收一个 x 查询参数并返回对应的值:

function page(req,res,pre,cb){
  res.writeHead(200,{'Content-Type':'text/plain'});
  if(pre._GET['x']){
   res.end('The value of x is '+pre._GET['x']+'.');
  } else{
    res.end('There is no value for x.');
    }
}

如果 Node.js 服务器正在运行,并且浏览器被指向到http://localhost: 1337/index.php?x=4, 浏览器会显示“The value of x is 4.”。

HTTP POST request 和 HTTP GET request 基本一样,除了键值对是通过 request 正文而不是 URL 最后的查询字符串进行发送的。一个 HTTP request 包括 HTTPheader 和一个 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 事件跟一个数据处理函数绑定在一起,这个数据处理函数会将数据写到控制台:

req.on('data',function(chunk){
  sonsole.log(chunk);
});

对于 initPOST()函数,pre 的 POST 属性会被初始化。就像从 initGET()函数中重新调用后一样,pre 参数是一个包含了所有预定义的全局变量的 Node.js 对象。一个body 变量会被创建出来保存到被读取的 HTTP body。on()函数把一个事件处理程序和data 事件联系到一起,这个事件处理程序会在数据变为可用之后将其写入到body变量中:

pre._POST = {};
var body = '';
req.on('data',function(chunk){
  body += chunk;
  if(body.length > 1e6){
    req.connection.destroy();
}
});

在 data 事件处理程序中添加的 if 语句是用来检测 HTTP 正文是否太长,如果是则会中断连接。写得不好或恶意的客户端可能会发送无限量的数据,在这时该 if 语句就需要放弃那个发送的大量数据的 HTTP request 来保护 Node.js 服务器。

最后,on()函数为 end 事件绑定一个事件处理程序,它会在整个 HTTP 正文被读取之后触发。通过简单的 split()函数调用,end 事件处理程序提取出数据并把它们放到 pre._POST 变量中:

req.on('end',function(){
  var pairs = body.dplit('&');
  for (var p=0; p < pairs.length; ++p){
    var pair = pairs[p].split('=');
    pre._POST[pair[0]] = pair[1];
  }
  cb();
});

cb()在最后被调用时,已经有正确的 pre._POST 变量值可以使用了。将所有代码放到一起,initPOST()函数的全文显示如下:

function initPOST(req,pre,cb){
  pre._POST = {};
  var body = '';
  req.on('data',function(){
    body += chunk;
  if(body.length > 1e6){
    req.connection.destroy();
}
});
req.on('end',function(){
  var pairs = body.split('&');
  for (var p=0; p < apirs.length; ++p){
    var pair = pairs[p].split('=');
    pre._POST[pair[0]] = pair[1];
  }
  cb();
});
}

对于那些期待 HTTP GET request 的页面,必须修改 exports.serve()函数。而对于那些期待 HTTP POST request 的页面,exports.serve()函数的代码则是一样的,除了调用 initGET()函数的地方替换为 initPOST()函数调用。设计就是这样的。尽管 initGET() 函数不需要一个回调函数,但是给 initGET()函数一个回调函数让它和 initPOST() 函数有一样的参数并且代码几乎相同。

exports.serve = function(req,res){
  var pre = {};
  ininPOST(req,pre,function(){
    page(req,res,pre,function(){
    });
  });
};

到目前为止,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”:

Cookie: user=admin;sessid=21EC33203BEA1169A2EA08332B313090

在 Node.js 的 HTTP 请求中,cookies 是存储在 HTTP 请求的 headers.cookie 属性中的,在本书中的例子里就是 req。

initCOOKIE()与initGET()函数都用于将cookie 从相应的HTTP 头中提取出来并放到pre._COOKIE 变量中,非常相似。获取 cookie 会更方便:cookie 不是附加 在 URL 最后作为查询字符串而是有自己的属性值。pre._COOKIE 变量将会 用来替代 PHP 引擎为 PHP 页面提供的 PHP $_COOKIE 变量:

function initCOOKIE(req,pre,cb._COOKIE = {};
  if(req.headers.cookie){
    var cookies = req.headers.cookie.split(';');
    for (var c=0; c < cookies.length; ++c){
      var pair = cookies[c].split('=');
      pre._COOKIE[0] = pair[1];
    }
  }
  cb();
}

就像那些期待 HTTP GET 和 HTTP POST 请求的页面,那些需要 cookie 的页面需要修改它们的  exports.serve()函数来调用  initCOOKIE()函数。initCOOKIE()函数跟initGET()和 initPOST()有同样的参数,因此可以使用同样的代码调用 initCOOKIE()函数:

exprts.serve = function(req,res){
  var pre = {};
  initCOOKIE(req,pre,function(){
    page(req,res,pre,function(){
    });
 });
};

当一个页面同时需要处理 HTTP GET 和 HTTP POST 两种请求并且还需要用到cookie 时,可以通过将一个函数当作另一个函数的回调函数的方法来增强exports.serve()函数初始化的操作。以下代码用于加载 pre._GET、pre._POST 和 pre._COOKIE 属性,它们将用于替代 PHP 预定义变量$_GET 、$_POST 和$_COOKIE:

exports.serve = function(req,res){
  var re = {};
  initGET(rwq,pre,function(){
    initPOST(req,pre,function(){
      initCOOKIE(req,pre.function(){
        page(req,res,pre,function(){
        });
      });
    });
  });
}

在 PHP 里,$_REQUEST 预定义变量包含了在$_GET、$_POST 和$_COOKIE 中所有的键值对。我们需要将 pre._GET、pre_POST 和 pre._COOKIE 变量复制一份来创建pre._REQUEST:

function initREQUEST(req,pre,cb){
  pre._REQUEST = {};
  if(PRE._GET){
    for(var k in pre._GET){
      pre._RWQUEST[k] = pre._GET[k];
    }
  }
  if(pre._POST){
    for(var k in pre._POST){
      pre._REQUEST[k] = pre._POST[k];
    }
  }
  if(pre._COOKIE){
    for(var k in pre._COOKIE){
    pre._REQUEST[k] = pre._COOKIE[K];
    }
  }
  cb();
}

Node.js 中的 for…in 语句可以从一个 Node.js 对象找出所有的属性名。下面的代码显示了一个对象 obj 并且把它所有的属性名打印出来:

var obj = {'a': '1','b': '2','c': '3','d': '4','e': '5'};
for (var name in obj){
  console.log('name:'+name);
}

这里是 Node.js 的 for…in 循环的输出:

name:a
name:b
name:c
name:d
name:e

在 PHP 中,foreach…as 与 Node.js 中的 for…in 工作行为差不多,除了 foreach…as 会返回 PHP 中的属性值而不是像 for…in 在 Node.js 中返回属性名。

就像其他预定义变量的初始化函数一样,initREQUEST()函数必须被 exports.serve()函数调用。因为 REQUEST 值是 GET、 POST 和 COOKIE 的一个复合体,构造REQUEST 值需要的三个值已经赋值给 pre 变量,因此 initREQUEST()函数必须要在其他函数初始化之后被调用:

exports.serve = function(req,res){
  var pre = {};
  initGET(req,pre,function(){
    initPOST(req,pre,function(){
      initCOOKIE(req,pre,function(){
        initREQUEST(req,pre,function(){
          page(req,pre,function(){
          });
        });
      });
    });
  });
}

initREQUEST()函数就像期待的那样与其他初始化函数使用完全相同的参数并且表现也相同。

在 PHP 还有一个常用的预定义变量:$_SESSION 变量。$_SESSION 变量代表了每一个用户对 PHP 页面的调用。

initSESSION()函数使用 pre._COOKIE 变量维护当前用户在sessions 变量中的会话。当会话不存在时,它会被创建出来;否_则,将从 sessions 对象中获取:

/** All the session of all the users.*/
var session = {};
function initSESSION(req,pre,cb){
  if((typeof pre._COOKIE['NODESESSID'] == 'undefined'){
    var pool = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZzbcdefghijkklmopqrstuvwxyz';
    for (var i = 0; i < 26; ++i) {
      var r = Math.floor(Math.random()*pool.length);
      newid += pool.charAt(r);
    }
    pre._COOKIE['NODESESSID'] = newid;
    sessions[pre._COOKIE['NODESESSID']] = {};
  }
  var id = pre._COOKIE['NODESESSID'];
  if((typeof session[id]) == 'undefinnde'){
    session[id] = {};
  }
  pre._SESSION = sessions[id];
}

同样也需要让页面处理会话。通过将一个函数添加到另一个函数的回调函数中来加强 exports.serve()初始化函数的能力。下面的代码会加载 pre._GET、pre._POST、pre._COOKIE 和 pre_REQUEST 属性,它们是用来代替 PHP 中预定义的变量$_GET、$_POST、$_COOKIE 和$_REQUEST 的。

initSESSION()函数是在 exports.serve()函数中调用的最后一个函数。它依赖于 PHP变量$_COOKIE 来维护这个会话。为了保证 cookie 处于被激活的状态,需要修改page()函数的回调函数将 cookie 返回给调用者:

exports.serve = function(req,res){
  var pre = {};
  initGET(req,pre,function(){
    initPOST(req,pre,function(){
      initCOOKIR(req,pre,function(){
        initREQUEST(req,pre,function(){
          initSESSION(req,pre,function(){
            page(req,res,pre,function(){
              var cookies = [];
              for(var c in pre._COOKIE){
                cookies.push(c + '=' + pre._COOKIE[c];);
              }
              res.setHeader('Set-Cookie',cookies);
              reswriteHead(200,{'Content-Type':'text/plain'});
              res.end(res.content);
            });
         });
        });
      });
    });
  });
};

就像所期待的那样,initSESSION()函数跟其他初始化函数一样采用完全相同的参数,并且执行方式也几乎相同。

这里我们创建一个本地模块 initreq.njs 用来将 initGET()、initPOST()、initCOOKIE()、initREQUEST()和 initSESSION()函数共享给所有其他模块。并且这些函数作为属性赋值给 exports 变量,从而使它们可以暴露给加载该模块的调用者:

exports.initGET = function(req,pre,cb){
  pre._GET = {};
  var urlparts = req.url.split('?');
  if(urlparts.length >= 2){
    var query = urlparts[urlparts.length-1].split('&');
    for(var p=0; = < query.length; ++p){
      var pair = query[p].split('=');
    }
  }
  cb();
};
exports.initPOST = function(req,pre,cb){
  pre._POST = {};
  var body = '';
  req.on('data',function(chunk){
    body += chunk;
    if(body.length > 1e6){
      req.connection.destroy();
    }
});
    req.on('end',function(){
      var pairs = body.split('&');
      for(var p=0; p < pairs.length; ++p){
        var pair = pairs[p].length('=');
        pre._POST[pair[0]] = pair[1];
      }
      cb();
    });
};
export.initCOOKIE = function(req,pre,cb){
  pre._COOKIE = {};
  if(req.headers.cookie){
    var cookies = req.headers.cookie.split(';');
    for(var c=0; c < cookies.length; ++c){
      var pair = cookies[c].split('=');
    }
  }
  cb();
};
export.iitREQUEST = function(req,pre,cb){
  pre._REQUEST = {};
  if(pre._GET){
    for(var k in pre._GET){
      pre._REQUEST[k] = pre._GET[k];
    }
  }
  if(pre._POST){
    for(va k in pre._POSST){
      pre._REQUEST[k] = pre._COOKIE[k];
    }
  }
  cb();
};
/** All the sessions of all the users. */
var sessions = {};
exports.initSESSION = function(req,pre,cb){
  if((typeof pre._COOKIE['NODESESSID']) == 'undefined'){
    var pool = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZzbcdefghijkklmopqrstuvwxyz';
    var newif = '';
    for(var i = 0; i <  26; ++i){
      var r = Math.floor(Math.random()*pool.length);
      newid += pool.charAt(r);       
    }
    pre._COOKIE['NODESESSID'] = newid;
    sessiions[pre._COOKIE['NODESESSID']] = {};
  }
  var id = pre._COOKIE['NODESESSID'];
  if((typeof sessions[id] == 'undefined')){
    sessions[id] {};
  }
  pre._SESSION = sessions[id];
  cb();
}

为了使用这个 initreq.njs 本地模块,首先需要用 require()函数加载它。然后,需要将模块的名字 initreq 置于 initreq.njs 文件暴露出来的每一个初始函数的引用之前来进行调用。下面的代码显示了这些改变:

var initreq = require('./initreq.njs');
exports.serve = function(req,res){
  var pre = {};
  initreq.initGET(req,pre,function(){
    initreq.initPOST(req,pre,function(){
      initreqCOOKIE(req,pre,function(){
        initreqREQUEST(req,pre,function(){
          initreqSESSION(req,pre,function(){
            page(req,res,pre,function(){
              var cookies = [];
              for(var c in pre._COOKIE){
                cookies.push(c + '=' + pre._COOKIE[C]);
              }
              res.setHeader('Set-Cookie',cookies);
              res.writeHead(200,{'Content-Type':'text/plain'});
              res.end(res.content);
            });
          });
        });
      });
    });
  });
};

现在你对于从 PHP 到Node.js 的转换应该有更好的想法并且知道整个过程是如何工作的了。

httpsvr.njs 文件是一个 Node.js HTTP 服务器。在一个常见的 PHP 设置中,httpsvr.njs 文件类似于一个安装了 PHP 模块的 Apache Web 服务器。如果你想要调整 Node.js Web 服务器来添加一些页面,将 URL 指定到特定的页面或者执行其他通用的 Web 服务器的配置时,那么就需要修改 httpsvr.njs 文件。我们把之前的 httpsvr.njs 例子再放到这里以便于引用:

var http = require('http');
var static = require('node-static');
var file = new satic.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 = reauire('./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):
  } else if (url.parse(req.url).pathname == '/admin/index.php'){
    admin_index.serve(req,res);
  } else if (url.parse(req.url).pathname == '/admin/login.php'){
    admin_login.serve();
  } else {
    file.serve(req,res);
  }
}).listen(1337,'127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

对于每一个 PHP 页面,都会有一个 index.njs 文件或其他模块文件被创建出来。exports.serve()是 Node.js 中对应于 PHP 引擎用来处理某个特定页面代码的函数。如果需要一些额外的预定义变量,初始化代码或者结束代码(例如,在页面完成之后执行的代码),就需要修改 exports.serve()函数。exports.serve()函数并不是页面本身, 而是“包裹”这个页面的代码:

var initreq = require('./initreq.njs');
exports.serve = function(req,res){
  var pre = {};
  initGET(req,pre,function(){
    initPOST(req,pre,function(){
      initCOOKIR(req,pre,function(){
        initREQUEST(req,pre,function(){
          initSESSION(req,pre,function(){
            page(req,res,pre,function(){
              var cookies = [];
              for(var c in pre._COOKIE){
                cookies.push(c + '=' + pre._COOKIE[c];);
              }
              res.setHeader('Set-Cookie',cookies);
              reswriteHead(200,{'Content-Type':'text/plain'});
              res.end(res.content);
            });
         });
        });
      });
    });
  });
};
function page(req,res,pre,cb){
  res.writeHead(200,{'Content-Type':'text/plain'});
  res.end('admin/index.njs\n'+ntil.inspect(pre));
  cb();
}
阅完此文,您的感想如何?
  • 有用

    1

  • 没用

    1

  • 开心

    1

  • 愤怒

    1

  • 可怜

    1

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

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

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

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

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

我要说说
网上嘉宾点评