源码系列 —— http-errors

·  阅读 300

这是我参与更文挑战的第15天,活动详情查看:更文挑战

简介

http-errors 主要供 expresskoa 等后端框架使用,用于便捷地创建 HTTP 异常状态。简单使用如下所示:

var createError = require('http-errors')
var express = require('express')
var app = express()
const port = 3001

app.use(function (req, res, next) {
  console.log(req)
  if (!req.user) return next(createError(401, 'Please login to view this page.'))
  next()
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})
复制代码

依赖

http-errors 的外部依赖包有 depdsetprototypeofstatusesinheritstoidentifier

var deprecate = require('depd')('http-errors')
var setPrototypeOf = require('setprototypeof')
var statuses = require('statuses')
var inherits = require('inherits')
var toIdentifier = require('toidentifier')
复制代码

depd

depd 主要用于输出提示信息以及实现向后兼容操作,简单使用如下所示:

var deprecate = require('depd')('LvLin-test')
deprecate('Hello, I am LvLin.') // 输出提示语

function sayHello () {
  console.log('Hello world!')
}
// 实际运行了 sayHello,并输出了相关提示
const sayHi = deprecate.function(sayHello, 'sayHi; use "sayHello" instead')
sayHi()
复制代码

效果如下图所示:

Snipaste_2021-06-15_22-50-43.png

statuses

statuses 是一个处理 HTTP 状态码的工具库。需要注意的是 http-errors 使用的 statuses 低于 2.0 版本,因为在 2.x 版本中部分 api 已经不再支持。

"statuses": ">= 1.5.0 < 2",
复制代码

statuses 的简单使用如下所示:

status[404] // => 'Not Found'
status['Not Found'] // => 404
status.codes  // 获取所有状态码
// [
//     100, 101, 102, 103, 200, 201, 202, 203, 204,
//     205, 206, 207, 208, 226, 300, 301, 302, 303,
//     304, 305, 306, 307, 308, 400, 401, 402, 403,
//     404, 405, 406, 407, 408, 409, 410, 411, 412,
//     413, 414, 415, 416, 417, 418, 421, 422, 423,
//     424, 425, 426, 428, 429, 431, 451, 500, 501,
//     502, 503, 504, 505, 506, 507, 508, 509, 510,
//     511
// ]
复制代码

statuses 的源码可以简单看一下,相关解释见注释:

'use strict'

// 引入状态码映射表,结构如下
// {
//   "100": "Continue",
//   "101": "Switching Protocols",
//   "102": "Processing",
//   "103": "Early Hints",
//   ...
// }
var codes = require('./codes.json')

module.exports = status

// status.STATUS_CODES 保存了所有状态码的原因短语映射
status.STATUS_CODES = codes

// status.codes 保存了所有的状态码
status.codes = populateStatusesMap(status, codes)

// 与重定向相关的状态码
status.redirect = {
  300: true,
  301: true,
  302: true,
  303: true,
  305: true,
  307: true,
  308: true
}

// 响应报文没有 body 的状态码
status.empty = {
  204: true,
  205: true,
  304: true
}

// 暂时无法处理请求,需要重试请求的状态码
status.retry = {
  502: true,
  503: true,
  504: true
}

// 将状态码跟原因短语绑定到 statuses 上
function populateStatusesMap (statuses, codes) {
  var arr = []

  Object.keys(codes).forEach(function forEachCode (code) {
    var message = codes[code]
    var status = Number(code)

    // 状态码跟原因短语绑定到 statuses上
    // 全小写的原因短语也考虑
    statuses[status] = message
    statuses[message] = status
    statuses[message.toLowerCase()] = status

    // 将状态码保存,为了返回给 status.codes
    arr.push(status)
  })

  return arr
}

// 接受一个code,只能是数字或者字符串
// 如果是数字:如果是支持的状态码,返回该数字,否则抛出异常
// 如果字符串:1、尝试转成数字,如果非 NaN,判断是否是支持的状态码,返回该数字或者抛出异常
//           2、当成原因短语处理,判断是否有符合的原因短语,返回相应的状态码或者抛出异常
function status (code) {
  if (typeof code === 'number') {
    if (!status[code]) throw new Error('invalid status code: ' + code)
    return code
  }

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

  // '403'
  var n = parseInt(code, 10)
  if (!isNaN(n)) {
    if (!status[n]) throw new Error('invalid status code: ' + n)
    return n
  }

  n = status[code.toLowerCase()]
  if (!n) throw new Error('invalid status message: "' + code + '"')
  return n
}

复制代码

setprototypeof

setprototypeof 是一个Object.setPrototypeOf的垫片库,跨平台,兼容到 IE8

看一下源码实现:

'use strict'
/* eslint no-proto: 0 */
// 优先使用 Object.setPrototypeOf
// 判断使用 __proto__ 能不能改变原型链,如果可以就用 setProtoOf
// 如果不行就用 mixinProperties
module.exports = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array ? setProtoOf : mixinProperties)

// 可以通过 __proto__ 修改原型链,直接修改
function setProtoOf (obj, proto) {
  obj.__proto__ = proto
  return obj
}

// 无法通过 __proto__ 修改原型链
function mixinProperties (obj, proto) {
  for (var prop in proto) {
      // 找出非本对象已有的属性,绑定到对象上
    if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
      obj[prop] = proto[prop]
    }
  }
  return obj
}
复制代码

inherits

inherits 用于实现对象的原型继承,简单使用如下所示:

function Base() { 
    this.name = 'base'; 
    this.base = 1991; 
} 
function Sub() { 
    this.name = 'sub'; 
}
inherits(Sub, Base); 

let objSub = new Sub;
objSub.base // 1991
复制代码

看一下源码:

// inherits.js
try {
   // 如果是 node 环境,用 util.inherits
  var util = require('util');
  /* istanbul ignore next */
  if (typeof util.inherits !== 'function') throw '';
  module.exports = util.inherits;
} catch (e) {
  // 如果是浏览器环境就使用 inherits_browser.js
  /* istanbul ignore next */
  module.exports = require('./inherits_browser.js');
}

// inherits_browser.js
if (typeof Object.create === 'function') {
  // 如果支持 Object.create,使用 object.create 实现寄生组合式继承
  module.exports = function inherits(ctor, superCtor) {
    if (superCtor) {
      ctor.super_ = superCtor
      ctor.prototype = Object.create(superCtor.prototype, {
        constructor: {
          value: ctor,
          enumerable: false,
          writable: true,
          configurable: true
        }
      })
    }
  };
} else {
  // 利用空的构造函数实现寄生组合式继承
  module.exports = function inherits(ctor, superCtor) {
    if (superCtor) {
      ctor.super_ = superCtor
      var TempCtor = function () {}
      TempCtor.prototype = superCtor.prototype
      ctor.prototype = new TempCtor()
      ctor.prototype.constructor = ctor
    }
  }
}

复制代码

toIdentifier

toIdentifier 用于将字符串转成符合规范的变量名,源码及解释如下所示:

function toIdentifier (str) {
  return str
    .split(' ') // 按照空格将字符串分割成数组
    .map(function (token) {
      // 各单词首字母大写
      return token.slice(0, 1).toUpperCase() + token.slice(1)
    })
    .join('') // 所有单词重新拼接在一起
    .replace(/[^ _0-9a-z]/gi, '')  // 去掉特殊字符
}
复制代码

http-errors 源码分析

先从几个简单的工具函数看起。

// 将状态码根据开头数字进行归类,比如 401、402 的 codeClass 为 400
function codeClass (status) {
  return Number(String(status).charAt(0) + '00')
}

// 从名称标符获取类名,以 Error 结尾,
function toClassName (name) {
  return name.substr(-5) !== 'Error'
    ? name + 'Error'
    : name
}

// 修改函数的名称
function nameFunc (func, name) {
  var desc = Object.getOwnPropertyDescriptor(func, 'name')

  if (desc && desc.configurable) {
    // 如果可修改,通过 Object.defineProperty 修改函数名称
    desc.value = name
    Object.defineProperty(func, 'name', desc)
  }
}
复制代码

create.HttpError 是用于继承的抽象类,不能直接调用。

module.exports.HttpError = createHttpErrorConstructor() // 生成一个抽象基类

function createHttpErrorConstructor () {
  // 这是一个抽象类,不能直接调用,只是用于继承
  // 如果直接调用,抛出异常
  function HttpError () {
    throw new TypeError('cannot construct abstract class')
  }
  inherits(HttpError, Error)
  return HttpError
}
复制代码

createError.isHttpError 用于判断实例是否是 HttpError 类型。

module.exports.isHttpError = createIsHttpErrorFunction(module.exports.HttpError)

function createIsHttpErrorFunction (HttpError) {
  return function isHttpError (val) {
    if (!val || typeof val !== 'object') {
      return false
    }
    // 如果继承自 HttpError,返回 true
    if (val instanceof HttpError) {
      return true
    }
	// 通过 createError() 创建的自定义错误类型条件
    return val instanceof Error &&
      typeof val.expose === 'boolean' &&
      typeof val.statusCode === 'number' && val.status === val.statusCode
  }
}
复制代码

初始化时,创建各个状态码异常类的构造函数,并绑定到 createError 上。

// 将各类错误的 constructor 绑定到 createError 上,方便调用,比如 new createError.NotFound()
populateConstructorExports(module.exports, statuses.codes, module.exports.HttpError)

function populateConstructorExports (exports, codes, HttpError) {
  codes.forEach(function forEachCode (code) {
    var CodeError
    var name = toIdentifier(statuses[code])

    switch (codeClass(code)) {
      case 400: // 4xx 状态码,提供客户端异常类的构造函数
        CodeError = createClientErrorConstructor(HttpError, name, code)
        break
      case 500: // 5xx 状态码,提供服务端异常类的构造函数
        CodeError = createServerErrorConstructor(HttpError, name, code)
        break
    }

    if (CodeError) {
      // createError[404] === createError.NotFound
      // 相应的 code 和 name 对应同一个构造函数
      exports[code] = CodeError
      exports[name] = CodeError
    }
  })

  // 兼容旧版本的 I'mateapot
  exports["I'mateapot"] = deprecate.function(exports.ImATeapot,
    '"I\'mateapot"; use "ImATeapot" instead')
}
复制代码

再来看看客户端异常类和服务端异常类的构造函数是怎么生成的。

createServerErrorConstructor 返回 ServerError 类构造函数。

function createServerErrorConstructor (HttpError, name, code) {
  var className = toClassName(name)

  function ServerError (message) {
    // 创建 Error 实例,错误描述为传入值或者
    var msg = message != null ? message : statuses[code]
    var err = new Error(msg)

    // 捕获构造点的堆栈跟踪,具体使用见 http://nodejs.cn/api/errors.html#errors_error_capturestacktrace_targetobject_constructoropt
    Error.captureStackTrace(err, ServerError)

    // err.__proto__ = ServerError.prototype
    // 即让 err 成为 ServerError 类实例
    setPrototypeOf(err, ServerError.prototype)

    // 重定义 err 的错误描述
    Object.defineProperty(err, 'message', {
      enumerable: true,
      configurable: true,
      value: msg,
      writable: true
    })

    // 重定义 err 的 name
    Object.defineProperty(err, 'name', {
      enumerable: false,
      configurable: true,
      value: className,
      writable: true
    })

    return err
  }

  // ServerError 类继承自 HttpError 类
  inherits(ServerError, HttpError)
  // 将构造函数重命名为 name + 'Error',避免重名
  nameFunc(ServerError, className)

  ServerError.prototype.status = code
  ServerError.prototype.statusCode = code
  ServerError.prototype.expose = false

  return ServerError
}
复制代码

createClientErrorConstructor 返回 ClientError 类构造函数,其实现与ServerError 基本一一致。

function createClientErrorConstructor (HttpError, name, code) {
  var className = toClassName(name)

  function ClientError (message) {
    // 创建 Error 类实例
    var msg = message != null ? message : statuses[code]
    var err = new Error(msg)

    // 捕获构造点的堆栈跟踪,具体使用见 http://nodejs.cn/api/errors.html#errors_error_capturestacktrace_targetobject_constructoropt
    Error.captureStackTrace(err, ClientError)

    // err.__proto__ = ClientError.prototype
    // 即让 err 成为 ClientError 类实例
    setPrototypeOf(err, ClientError.prototype)

    // 重定义 err 的错误描述
    Object.defineProperty(err, 'message', {
      enumerable: true,
      configurable: true,
      value: msg,
      writable: true
    })

    // 重定义 err 的 name
    Object.defineProperty(err, 'name', {
      enumerable: false,
      configurable: true,
      value: className,
      writable: true
    })

    return err
  }
  // ClientError 类继承自 HttpError 类
  inherits(ClientError, HttpError)
  // 将构造函数重命名为 name + 'Error',避免重名
  nameFunc(ClientError, className)

  ClientError.prototype.status = code
  ClientError.prototype.statusCode = code
  ClientError.prototype.expose = true

  return ClientError
}
复制代码

最后来看 createError() 方法的实现,详细解释见注释:

// 有两种调用方式,但是由于参数都是可省略的,所以需要考虑的参数情况比较多
// createError([status], [message], [properties])
// createError([status], [error], [properties])
function createError () {
  var err
  var msg
  var status = 500
  var props = {}

  // 遍历参数,判断各个参数类型
  for (var i = 0; i < arguments.length; i++) {
    var arg = arguments[i]
    // 如果是继承自 Error,就是参数列表中对应的 error
    if (arg instanceof Error) {
      err = arg
      status = err.status || err.statusCode || status
      continue
    }

    switch (typeof arg) {
      case 'string': // 参数列表中的 message
        msg = arg
        break
      case 'number': // 参数列表中的 status
        status = arg
        if (i !== 0) { // 如果 status 不是第一个参数,就报异常
          deprecate('non-first-argument status code; replace with createError(' + arg + ', ...)')
        }
        break
      case 'object': // 如果是 object,就是参数列表中的 properties
        props = arg
        break
    }
  }
  // 只支持处理 4xx 和 5xx 类型的异常状态
  if (typeof status === 'number' && (status < 400 || status >= 600)) {
    deprecate('non-error status code; use only 4xx or 5xx status codes')
  }

  // 如果 status 不满足条件,status 置为 500
  if (typeof status !== 'number' ||
    (!statuses[status] && (status < 400 || status >= 600))) {
    status = 500
  }

  // 获取对应状态码的构造函数,已经挂载到 createError 上,没有就获取 400 或 500 对应的构造函数
  var HttpError = createError[status] || createError[codeClass(status)]

  if (!err) {
    // 异常类实例创建
    err = HttpError
      ? new HttpError(msg)
      : new Error(msg || statuses[status])
    // // 捕获构造点的堆栈跟踪,具体使用见 http://nodejs.cn/api/errors.html#errors_error_capturestacktrace_targetobject_constructoropt
    Error.captureStackTrace(err, createError)
  }

  if (!HttpError || !(err instanceof HttpError) || err.status !== status) {
    err.expose = status < 500
    err.status = err.statusCode = status
  }

  for (var key in props) {
    if (key !== 'status' && key !== 'statusCode') {
      err[key] = props[key]
    }
  }

  return err
}
复制代码

相关资料

每天一个npm包:http-errors

http-errors 源码

nodejs-depd 源码

toidentifier 源码

statuses 源码

inherits 源码

setprototypeof 源码

最后

如果文章对你有帮助,给个赞呗~

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改