on-headers: 一个用于在返回请求头数据前执行回调函数的工具库

本文是“源码解读”系列第 2 篇。

概述

on-headers: 一个用于在返回请求头数据前执行回调函数的工具库

on-headers 是一个 Node.js 端的工具库,用来在所有请求头数据设置之后、发送给客户端之前,为你提供一个回调函数用来执行。

下面是它的一个使用案例:

var http = require('http')
var onHeaders = require('on-headers')

http
  .createServer(onRequest)
  .listen(3000)

function addPoweredBy ({
  // set if not set by end of request
  if (!this.getHeader('X-Powered-By')) {
    this.setHeader('X-Powered-By''Node.js')
  }
}

function onRequest (req, res{
  onHeaders(res, addPoweredBy)

  res.setHeader('Content-Type''text/plain')
  res.end('hello!')
}

on-headers 的使用场景一般是在响应头发送给客户端之前做一些数据检查。

按照作者的话说[1],这个库是为了模拟 res 上的 headers 事件:res.on('headers', listener),目前 Node.js 原生还不支持。

实现思路

知道 on-headers 包的作用后,我们再来了解一下它的实现思路:通过复写 response.writeHead()[2] 方法。

认识 response.writeHead() 方法

与 res.setHeader() 的异同点

response.writeHead() 方法的作用类似 res.setHeader(),不过还需要我们指定 HTTP 状态码。

下面的例子[3]来说:

const http = require('node:http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type''text/plain');
  res.end('Hello Worldn');
});
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

换成 response.writeHead() 实现是这样的:

const http = require('node:http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type''text/plain' })
  res.end('Hello Worldn');
});
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

除了需要设置 HTTP 状态码,response.writeHead()response.setHeader() 的不同之处还包括。

一、res.writeHead() 可以同时设置多个响应头

res.writeHead(200, {
  'X-Powered-By''Node.js',
  'Content-Type''text/plain',
})

二、res.writeHead() 之后就不能再使用 res.setHeader() 了,会报错

res.writeHead(200, {'Content-Type''text/plain'});
res.setHeader('X-Foo''bar');

// Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

这是因为在调用 res.writeHead() 之后,请求头已经发送会客户端了。

response.writeHead() 总是会被调用

response.writeHead() 方法是作为最终返回响应头的操作。即使你没有显式调用,它也会在你调用 res.end() 方法的时候检查,确保 response.writeHead() 方法被调用过了。

我举一个例子:

// server.js
import http from 'node:http'

http
  .createServer(onRequest)
  .listen(3000)

function onRequest (req, res{
  const old = res.writeHead
  res.writeHead = function (...args{
    console.log(';)', args)
    old.call(res, ...args)
  }

  res.statusCode = 200
  res.setHeader('Content-Type''text/html');
  res.end('ok');
}

这里们复写了 res.writeHead() 的实现,仅仅做了一个 log 打印操作。node server.js 启动后,浏览器访问 http://localhost:3000/,可以看见在终端上看到打印结果。

$ node .index.js
;) [ 200 ]

分析源码

在理解了 on-headers 的实现原理和 res.writeHead() 方法的作用后,再来看一下 on-headers 源码。

我们基于最新的 v1.0.2[4] 版本讲解。

package.json 里没有指定 "main" 字段,就知道入口源码是在项目根目录下的 index.js 中。由于 on-headers 的实现思路不复杂,因此代码都写在 index.js 里面了,连注释一共才 132 行。

先来看默认导出。

/**
 * Module exports.
 * @public
 */


module.exports = onHeaders

再看一下 onHeaders() 函数。

onHeaders()

onHeaders() 函数接收 reslistener 两次参数。res 就是响应对象,listener 就是我们要注册的回调函数了。

/**
 * Execute a listener when a response is about to write headers.
 *
 * @param {object} res
 * @return {function} listener
 * @public
 */


function onHeaders (res, listener{
  if (!res) {
    throw new TypeError('argument res is required')
  }

  if (typeof listener !== 'function') {
    throw new TypeError('argument listener must be a function')
  }

  res.writeHead = createWriteHead(res.writeHead, listener)
}

这里先检查了参数是否合规,然后使用 createWriteHead() 的返回值复写了 res.writeHead() 函数。

接下来,再来看 createWriteHead() 的实现。

createWriteHead()

createWriteHead() 函数接收原始版本的 res.writeHead() 方法和监听函数 listener

/**
 * Create a replacement writeHead method.
 *
 * @param {function} prevWriteHead
 * @param {function} listener
 * @private
 */


function createWriteHead (prevWriteHead, listener{
  // return function with core name and argument list
  return function writeHead (statusCode{
    // ...
  }
}

这里返回的 writeHead (statusCode) {} 就是我们的复写版本了。在这里我们会在调用原始版本的 res.writeHead() 方法前,执行监听函数。

调用 listener

首先添加调用 listener 的逻辑。

function createWriteHead (prevWriteHead, listener{
  var fired = false
  
  // return function with core name and argument list
  return function writeHead (statusCode{
    // fire listener
    if (!fired) {
      fired = true
      listener.call(this)
    }

    // ...
  }
}

调用 listener 时将 response(即这里的 this)作为它的上下文对象。同时,为了保证监听函数只被唯一调用一次,我们引入了标识变量 fired

当然,我们的 on-headers listener 执行时机是在所有请求头数据设置之后、发送给客户端之前。因此,我们还需要调用在 listener 之前,设置好响应头。

  // return function with core name and argument list
  return function writeHead (statusCode{
   // set headers from arguments
    var args = setWriteHeadHeaders.apply(thisarguments)
    
    // fire listener
    if (!fired) {
      fired = true
      listener.call(this)
    }

    // ...
  }

这里额外引入了一个 setWriteHeadHeaders() 函数,我们来看一下。

setWriteHeadHeaders()

setWriteHeadHeaders() 函数的作用就是将 writeHead() 接收到的 headers 数据,通过 res.setHeader() 设置,这样在调用 listener 时,就能确保所有请求头数据都设置好了。

/**
 * Set headers and other properties on the response object.
 *
 * @param {number} statusCode
 * @private
 */


function setWriteHeadHeaders (statusCode{
  var length = arguments.length
  var headerIndex = length > 1 && typeof arguments[1] === 'string'
    ? 2
    : 1

  var headers = length >= headerIndex + 1
    ? arguments[headerIndex]
    : undefined

  this.statusCode = statusCode

  if (Array.isArray(headers)) {
    // handle array case
    setHeadersFromArray(this, headers)
  } else if (headers) {
    // handle object case
    setHeadersFromObject(this, headers)
  }

  // copy leading arguments
  var args = new Array(Math.min(length, headerIndex))
  for (var i = 0; i < args.length; i++) {
    args[i] = arguments[i]
  }

  return args
}

setWriteHeadHeaders() 的实现不是很复杂,主要是做了 4 件事。

  1. 找到 headers 参数。因为 writeHead 函数签名[5]response.writeHead(statusCode[, statusMessage][, headers]),就是说第二个可能是表示 statusMessage 的字符串,那这个时候 headers 参数应该是在第 3 个位置(对应索引 2
  2. 通过 res.setHeader() 设置请求头数据。由于 headers 参数既可能是对象(常用)也可能是数组(不常用),因此对应分别实现了 setHeadersFromArray()setHeadersFromObject() 两个工具函数。实现比较简单,我们直接贴出代码——就是循环遍历每个 Header 的 key 和 value,代入 res.setHeader() 中即可
/**
 * Set headers contained in array on the response object.
 *
 * @param {object} res
 * @param {array} headers
 * @private
 */


function setHeadersFromArray (res, headers{
  for (var i = 0; i < headers.length; i++) {
    res.setHeader(headers[i][0], headers[i][1])
  }
}

/**
 * Set headers contained in object on the response object.
 *
 * @param {object} res
 * @param {object} headers
 * @private
 */


function setHeadersFromObject (res, headers{
  var keys = Object.keys(headers)
  for (var i = 0; i < keys.length; i++) {
    var k = keys[i]
    if (k) res.setHeader(k, headers[k])
  }
}
  1. 设置 res.statusCode。由于在调用 .writeHead() 之前,外部可能已经手动设置 res.statusCode 了,以 .writeHead() 接收到的为准
  2. 将 headers 之前的参数返回。比如,我们是以 [200, {'Content-Type': 'text/plain'}] 调用的,那么返回的 args 就是 [200]

现在回到 createWriteHead() 再来看一下。

回到 createWriteHead()

我们将调用 setWriteHeadHeaders() 返回的结果 args,在调用 listener 之后,透传给原始的 res.writeHead() 方法。

  // return function with core name and argument list
  return function writeHead (statusCode{
   // set headers from arguments
    var args = setWriteHeadHeaders.apply(thisarguments)
    
    // fire listener
    if (!fired) {
      fired = true
      listener.call(this)
    }

    // 将调用 setWriteHeadHeaders() 返回的结果透传给原始 res.writeHead 方法
    return prevWriteHead.apply(this, args)
  }

这就完成了 on-headers 的所有代码实现。

总结

在完成了 on-headers 包的实现之后,我们就能知道下面的 2 块代码是等价的。

function onRequest (req, res{
  onHeaders(res, addPoweredBy)
  res.writeHead(200, { 'Content-Type''text/plain' })
  res.end('hello!')
}
function onRequest (req, res{
  res.setHeader('Content-Type''text/plain')
  addPoweredBy(res)
  res.writeHead(200)
  res.end('hello!')
}

知其然,知其所以然之后,我们就能更有信心的去使用这个工具库啦。

希望本篇文章能对你有所帮助。感谢阅读,Happy Coding!

参考资料

[1]

按照作者的话说: https://Github.com/jshttp/on-headers/pull/7#issuecomment-222843840

[2]

response.writeHead(): https://nodejs.org/api/http.html#outgoingmessageflushheaders

[3]

下面的例子: https://nodejs.org/en/docs/guides/getting-started-guide#an-example-nodejs-application

[4]

v1.0.2: https://github.com/jshttp/on-headers

[5]

writeHead 函数签名: https://nodejs.org/api/http.html#responsewriteheadstatuscode-statusmessage-headers


原文始发于微信公众号(写代码的宝哥):on-headers: 一个用于在返回请求头数据前执行回调函数的工具库

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/243695.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!