statuses:用于操作 HTTP 状态码的小型工具库

statuses 是一个在操作 HTTP 状态码时会用到的一个小型工具库。著名 Node.js 后端 Web 框架 Koa/Express 的源码中都有用到它。

本文就来介绍。

基本使用

安装。

$ npm install statuses

安装好后,我们来看看 statuses 的基本使用。

const status = require('statuses')

status()

status() 是一个函数,接收的参数类型可以是数字,也可以是字符串。

status(403) // => 'Forbidden'
status('Forbidden') // => 403
  • 当接收的是一个数字时,会执行 HTTP 状态码到对应英文信息的转换
  • 当接收的是一个字符串时,会执行英文信息到对应的 HTTP 状态码的转换

数字字符串也会作为数字处理。

status('403') // => 'Forbidden'

另外,做英文信息到对应的 HTTP 状态码的转换时,不区分大小写。

status('forbidden') // => 403

如果传入的是无效状态码就会报错。

status(306) // throws
status('foo') // throws

status.message[code]/.code[msg]

如果不想报错,可以使用 status.message[code]status.code[msg]

status.message[306] // undefined
status.code['foo'] // undefined

status() 函数内部其实就是依据传入的参数类型,访问对应的 status.message[code]status.code[msg] 的映射对象,不过加了一层抛错判断。

status.empty[code]

除了上述的转换方法,statuses 还提供了一些便捷映射对象。比如:status.emptystatus.empty 中存储了响应体为空时的一个状态码即可。方便你进行响应体为空判断。

status.empty[200] // => undefined
status.empty[204] // => true
status.empty[205] // => true
status.empty[304] // => true

这样的响应码有 3 个:204(No Content,比如 PUT 请求,Etag 头更新)、205(Reset Content,比如清空表单内容、重置 canvas 状态等)和 304(Not Modified,协商缓存使用)

除此之外,还有一个 status.redirect[code]

status.redirect[code]

status.redirect[code] 表示当前是否属于重定向请求。

status.redirect[200] // => undefined
status.redirect[301] // => true

重定向请求对应的状态码比较多,包括:300(Multiple Choices)、301(Moved Permanently)、302(Found)、303(See Other)、305(Use Proxy)、307(Temporary Redirect) 和 308(Permanent Redirect)。

需要注意的是,301、308 都表示永久重定向,资源永久移动了,浏览器会根据返回的 Location 头信息将地址重定向。一般来说,301 状态码用作 GET 或 HEAD 方法的响应,而对于 POST 则使用 308 Permanent Redirect,因为 308 状态码能够确保请求方法和消息主体不会发生改变。

另外,302 跟 307 语义相同,都表示临时重定向,浏览器会根据返回的 Location 头信息将地址重定向。不过,307 状态码能够确保请求方法和消息主体不会发生改变。

还有最后一个 status.retry[code]

status.retry[code]

这些状态码表示这些请求是需要重试的。

status.retry[501] // => undefined
status.retry[503] // => true

statuses 中定义的重试状态码有 3 个,包括:502(Bad Gateway)、503(Service Unavailable)和 504(Gateway Timeout)。

接下来来看一下 statuses 的实现过程。

分析源码

基于 v2.0.1 版本

statuses 的所有操作都是围绕 http 模块提供的 STATUS_CODES 常量进行的。

statuses:用于操作 HTTP 状态码的小型工具库

status.message

首先,我们先将 STATUS_CODES 导出为 status.message

const codes = require('node:http').STATUS_CODES

module.exports = status

// status code to message map
status.message = codes

function status(code) {
  // ...
}

注意:statuses 源代码中为了兼容(”node”: “>= 0.8″),codes 是从 codes.json 文件导入的(require('./codes.json'))。http.STATUS_CODES 从 v0.1.22 就开始支持了,为避免实现冗余,我们就不再使用 code.json了。

status.code

然后,将 STATUS_CODES 处理成响应消息到 HTTP 状态码的映射对象 status.code

// status message (lower-case) to code map
status.code = function createMessageToStatusCodeMap(codes) {
  const map = {}

  Object.keys(codes).forEach(code => {
    const message = codes[code]
    const status = Number(code)

    // populate map
    map[message.toLowerCase()] = status
  })
  
  return map
}

注意,存储 message 时,为了方便后续比较,我们将英文信息都转成小写的了。

status()

现在 status.messagestatus.code 都有了,可以着手实现 status() 了。

module.exports = status

// ...

function status(code) {
  if (typeof code === 'number') {
    return status.message[code]
  }

  // ...
}

数字直接丢给 status.message 查询。剩下的情况,如果参数不是字符串类型就报错。

// ...

function status(code) {
  if (typeof code === 'number') {
    return status.message[code]
  }

  if (typeof code === 'string') {
    throw new Error('code must be a number or string')
  }
  
  // ...
}

接下来再处理类似 '403' 这样的字符串数字。

// ...

function status(code) {
  if (typeof code === 'number') {
    return status.message[code]
  }

  if (typeof code === 'string') {
    throw new Error('code must be a number or string')
  }

  // '403'
  const n = parseInt(code, 10)
  if (!isNaN(n)) {
    return status.message[n] 
  }
  
  // ...
}

我们使用了 parseInt() 处理数字字符串,也就是说,像 "403.21" 这样的数组字符串会作为 "403" 状态码处理。

最后,处理英文信息到对应的 HTTP 状态码的转换。

// ...

function status(code) {
  if (typeof code === 'number') {
    return status.message[code]
  }

  if (typeof code === 'string') {
    throw new Error('code must be a number or string')
  }

  // '403'
  const n = parseInt(code, 10)
  if (!isNaN(n)) {
    return status.message[n] 
  }
  
  return status.code[msg.toLowerCase()] 
}

注意,msg 做了转小写的处理。

不过,status(code) 还需要对传入不合法的 HTTP 状态码/信息做抛错处理,因此,我们需要再额外抽象出两个方法,替换直接访问 status.message/status.code 的方式。

// ...

function status(code) {
  if (typeof code === 'number') {
    return getStatusMessage(code)
  }

  if (typeof code === 'string') {
    throw new Error('code must be a number or string')
  }

  // '403'
  const n = parseInt(code, 10)
  if (!isNaN(n)) {
    return getStatusMessage(n) 
  }
  
  return getStatusCode(msg) 
}

/**
 * Get the status code for given message.
 * @private
 */

function getStatusCode (message) {
  var msg = message.toLowerCase()

  if (!Object.prototype.hasOwnProperty.call(status.code, msg)) {
    throw new Error('invalid status message: "' + message + '"')
  }

  return status.code[msg]
}

/**
 * Get the status message for given code.
 * @private
 */

function getStatusMessage (code) {
  if (!Object.prototype.hasOwnProperty.call(status.message, code)) {
    throw new Error('invalid status code: ' + code)
  }

  return status.message[code]
}

status.redirect/.empty/.retry

最后还有 3 个预定义常量 status.redirect/status.empty/status.retry

// status codes for redirects
status.redirect = {
  300: true,
  301: true,
  302: true,
  303: true,
  305: true,
  307: true,
  308: true
}

// status codes for empty bodies
status.empty = {
  204: true,
  205: true,
  304: true
}

// status codes for when you should retry the request
status.retry = {
  502: true,
  503: true,
  504: true
}

到这里,差不多就写完了。感受一下全部代码。

全部代码

const codes = require('node:http').STATUS_CODES

module.exports = status

// status code to message map
status.message = codes

// status message (lower-case) to code map
status.code = function createMessageToStatusCodeMap(codes) {
  const map = {}

  Object.keys(codes).forEach(code => {
    const message = codes[code]
    const status = Number(code)

    // populate map
    map(message.toLowerCase()) = status
  })
  
  return map
}

// status codes for redirects
status.redirect = {
  300: true,
  301: true,
  302: true,
  303: true,
  305: true,
  307: true,
  308: true
}

// status codes for empty bodies
status.empty = {
  204: true,
  205: true,
  304: true
}

// status codes for when you should retry the request
status.retry = {
  502: true,
  503: true,
  504: true
}

function status(code) {
  if (typeof code === 'number') {
    return getStatusMessage(code)
  }

  if (typeof code === 'string') {
    throw new Error('code must be a number or string')
  }

  // '403'
  const n = parseInt(code, 10)
  if (!isNaN(n)) {
    return getStatusMessage(n) 
  }
  
  return getStatusCode(msg) 
}

function getStatusCode (message) {
  var msg = message.toLowerCase()

  if (!Object.prototype.hasOwnProperty.call(status.code, msg)) {
    throw new Error('invalid status message: "' + message + '"')
  }

  return status.code[msg]
}

function getStatusMessage (code) {
  if (!Object.prototype.hasOwnProperty.call(status.message, code)) {
    throw new Error('invalid status code: ' + code)
  }

  return status.message[code]
}

一共 82 行。

总结

本文我们讲解了 HTTP 状态码的工具库的基本使用和代码实现。总的来说,statuses 库其实就是基于 require('node:http').STATUS_CODES 常量之上的一套操作封装,Web 框架 Koa、Express 或多或少的都在使用它。

当然,学习使用和实现的过程,也帮助我们更好地理解了常用 HTTP 状态码的含义和使用场景。想全面了解的同学,可以参考 MDN 上的文档《HTTP 响应状态码》进行学习。

好了,关于 statuses 的学习,我们暂时讲到这里了。希望对大家能有所帮助,感谢阅读,再见!

福利内容:真实框架中的使用

这一节展示 statuses 在 Express、Koa 框架中的使用。

Koa

链接:github.com/koajs/koa/b…

/**
 * Response helper.
 */

function respond(ctx) {
  // ...

  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  // ...
}

使用 statuses 进行空响应检查。如果是空响应状态码,就把响应体明确指定为空(null)。

链接:github.com/koajs/koa/b…

/**
 * Context prototype.
 */

const proto = module.exports = {
  /**
   * Default error handling.
   *
   * @param {Error} err
   * @api private
   */

  onerror (err) {
    // ...

    // default to 500
    if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500

    // respond
    const code = statuses[statusCode]
    const msg = err.expose ? err.message : code
    this.status = err.status = statusCode
    this.length = Buffer.byteLength(msg)
    res.end(msg)
  },
}

用于获取 5xx 状态码对应的报错信息。

链接:github.com/koajs/koa/b…

/**
 * Prototype.
 */

module.exports = {
  /**
   * Set response status code.
   *
   * @param {Number} code
   * @api public
   */

  set status (code) {
    this.res.statusCode = code
    if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]
    if (this.body && statuses.empty[code]) this.body = null
  },
}

在显式设置 HTTP 状态码时,再一次执行空响应状态码的判断。

Express

链接:github.com/expressjs/e…

/**
 * Response prototype.
 * @public
 */

var res = Object.create(http.ServerResponse.prototype)

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

module.exports = res

/**
 * Send given HTTP status code.
 *
 * Sets the response status to `statusCode` and the body of the
 * response to the standard description from node's http.STATUS_CODES
 * or the statusCode number if no description.
 *
 * Examples:
 *
 *     res.sendStatus(200);
 *
 * @param {number} statusCode
 * @public
 */

res.sendStatus = function sendStatus(statusCode) {
  var body = statuses.message[statusCode] || String(statusCode)

  this.statusCode = statusCode;
  this.type('txt');

  return this.send(body);
};

注意,Express 中使用的是 statuses.message[statusCode] 而非 statuses(statusCode) 方式获取状态码信息,这样即使 statusCode 不合法,也不会报错,而是返回它的字符串表示。

链接:github.com/expressjs/e…

/**
 * Redirect to the given `url` with optional response `status`
 * defaulting to 302.
 *
 * The resulting `url` is determined by `res.location()`, so
 * it will play nicely with mounted apps, relative paths,
 * `"back"` etc.
 *
 * Examples:
 *
 *    res.redirect('/foo/bar');
 *    res.redirect('http://example.com');
 *    res.redirect(301, 'http://example.com');
 *    res.redirect('../login'); // /blog/post/1 -> /blog/login
 *
 * @public
 */

res.redirect = function redirect(url) {
  var address = url;
  var body;
  var status = 302;

  // allow status / url
  if (arguments.length === 2) {
    if (typeof arguments[0] === 'number') {
      status = arguments[0];
      address = arguments[1];
    } else {
      deprecate('res.redirect(url, status): Use res.redirect(status, url) instead');
      status = arguments[1];
    }
  }

  // Set location header
  address = this.location(address).get('Location');

  // Support text/{plain,html} by default
  this.format({
    text: function(){
      body = statuses.message[status] + '. Redirecting to ' + address
    },

    html: function(){
      var u = escapeHtml(address);
      body = '<p>' + statuses.message[status] + '. Redirecting to <a href="'%20+%20u%20+%20'">'%20+%20u%20+%20'</a></p>'
    },

    default: function(){
      body = '';
    }
  });

  // Respond
  this.statusCode = status;
  this.set('Content-Length', Buffer.byteLength(body));

  if (this.req.method === 'HEAD') {
    this.end();
  } else {
    this.end(body);
  }
};

用于重定向请求时的错误消息拼接,使用的依然是 statuses.message[status] 获取英文信息,避免抛错。

原文链接:https://juejin.cn/post/7337247324680929289 作者:zhangbao90s

(0)
上一篇 2024年2月21日 下午4:05
下一篇 2024年2月21日 下午4:15

相关推荐

发表评论

登录后才能评论