前言

用过 Node.js 开发过的同学必定都上手过 koa,因为他简略优雅的写法,再加上丰盛的社区生态,而且现存的许多 Node.js 框架都是基于 koa 进行二次封装的。然而说到性能,就不得不提到一个出名框架: fastify ,听名字就晓得它的个性就是快,官网给出的Benchmarks甚至比 Node.js 原生的 http.Server 还要快。

性能晋升的要害

咱们先看看 fastify 是如何启动一个服务的。

# 装置 fastifynpm i -S fastify@3.9.1
// 创立服务实例const fastify = require('fastify')()app.get('/', {  schema: {    response: {      // key 为响应状态码      '200': {        type: 'object',        properties: {          hello: { type: 'string' }        }      }    }  }}, async () => {  return { hello: 'world' }})// 启动服务;(async () => {  try {    const port = 3001 // 监听端口    await app.listen(port)    console.info(`server listening on ${port}`)  } catch (err) {    console.error(err)    process.exit(1)  }})()

从下面代码能够看出,fastify 对申请的响应体定义了一个 schemafastify 除了能够定义响应体的 schema,还反对对如下数据定义 schema

  1. body:当为 POST 或 PUT 办法时,校验申请主体;
  2. query:校验 url 的 查问参数;
  3. params:校验 url 参数;
  4. response:过滤并生成用于响应体的 schema
app.post('/user/:id', {  schema: {    params: {      type: 'object',      properties: {          id: { type: 'number' }      }    },    response: {      // 2xx 示意 200~299 的状态都实用此 schema      '2xx': {        type: 'object',        properties: {          id: { type: 'number' },          name: { type: 'string' }        }      }    }  }}, async (req) => {  const id = req.params.id  const userInfo = await User.findById(id)  // Content-Type 默认为 application/json  return userInfo})

fastify 性能晋升的的秘诀在于,其返回 application/json 类型数据的时候,并没有应用原生的 JSON.stringify,而是本人外部从新实现了一套 JSON 序列化的办法,这个 schema 就是 JSON 序列化性能翻倍的要害。

如何对 JSON 序列化

在摸索 fastify 如何对 JSON 数据序列化之前,咱们先看看 JSON.stringify 须要通过如许繁琐的步骤,这里咱们参考 Douglas Crockford (JSON 格局的创建者)开源的 JSON-js 中实现的 stringify 办法。

JSON-js:https://github.com/douglascrockford/JSON-js/blob/master/json2.js
// 只展现 JSON.stringify 外围代码,其余代码有所省略if (typeof JSON !== "object") {  JSON = {};}JSON.stringify = function (value) {  return str("", {"": value})}function str(key, holder) {  var value = holder[key];  switch(typeof value) {    case "string":      return quote(value);    case "number":      return (isFinite(value)) ? String(value) : "null";    case "boolean":    case "null":      return String(value);    case "object":      if (!value) {        return "null";      }      partial = [];      if (Object.prototype.toString.apply(value) === "[object Array]") {        // 解决数组        length = value.length;        for (i = 0; i < length; i += 1) {          // 每个元素都须要独自解决          partial[i] = str(i, value) || "null";        }        // 将 partial 转成 ”[...]“        v = partial.length === 0          ? "[]"          : "[" + partial.join(",") + "]";        return v;      } else {        // 解决对象        for (k in value) {          if (Object.prototype.hasOwnProperty.call(value, k)) {            v = str(k, value);            if (v) {              partial.push(quote(k) + ":" + v);            }          }        }        // 将 partial 转成 "{...}"        v = partial.length === 0          ? "{}"            : "{" + partial.join(",") + "}";        return v;      }  }}

从下面的代码能够看出,进行 JSON 对象序列化时,须要遍历所有的数组与对象,逐个进行类型的判断,并对所有的 key 加上 "",而且这里还不包含一些特殊字符的 encode 操作。然而,如果有了 schema 之后,这些状况会变得简略很多。fastify 官网将 JSON 的序列化独自成了一个仓库:fast-json-stringify,前期还引入了 ajv 来进行校验,这里为了更容易看懂代码,抉择看比拟晚期的版本:0.1.0,逻辑比较简单,便于了解。

fast-json-stringify@0.1.0: https://github.com/fastify/fast-json-stringify/blob/v0.1.0/index.js
function $Null (i) {  return 'null'}function $Number (i) {  var num = Number(i)  if (isNaN(num)) {    return 'null'  } else {    return String(num)  }}function $String (i) {  return '"' + i + '"'}function buildObject (schema, code, name) {  // 序列化对象 ...}function buildArray (schema, code, name) {  // 序列化数组 ...}function build (schema) {  var code = `    'use strict'    ${$String.toString()}    ${$Number.toString()}    ${$Null.toString()}  `  var main  code = buildObject(schema, code, '$main')  code += `    ;    return $main  `  return (new Function(code))()}module.exports = build

fast-json-stringify 对外裸露一个 build 办法,该办法承受一个 schema,返回一个函数($main),用于将 schema 对应的对象进行序列化,具体应用形式如下:

const build = require('fast-json-stringify')const stringify = build({  type: 'object',  properties: {    id: { type: 'number' },    name: { type: 'string' }  }})console.log(stringify)const objString = stringify({  id: 1, name: 'shenfq'})console.log(objString) // {"id":1,"name":"shenfq"}

通过 build 结构后,返回的序列化办法如下:

function $String (i) {  return '"' + i + '"'}function $Number (i) {  var num = Number(i)  if (isNaN(num)) {    return 'null'  } else {    return String(num)  }}function $Null (i) {  return 'null'}// 序列化办法function $main (obj) {  var json = '{'  json += '"id":'  json += $Number(obj.id)  json += ','  json += '"name":'  json += $String(obj.name)  json += '}'  return json}

能够看到,有 schema 做撑持,序列化的逻辑霎时变得无比简略,最初失去的 JSON 字符串只保留须要的属性,简洁高效。咱们回过头再看看 buildObject 是如何生成 $main 内的代码的:

function buildObject (schema, code, name) {  // 结构一个函数  code += `    function ${name} (obj) {      var json = '{'  `  var laterCode = ''  // 遍历 schema 的属性  const { properties } = schema  Object.keys(properties).forEach((key, i, a) => {    // key 须要加上双引号    code += `      json += '${$String(key)}:'    `    // 通过 nested 转化 value    const value = properties[key]    const result = nested(laterCode, name, `.${key}`, value)    code += result.code    laterCode = result.laterCode    if (i < a.length - 1) {      code += 'json += \',\''    }  })  code += `      json += '}'      return json    }  `  code += laterCode  return code}function nested (laterCode, name, key, schema) {  var code = ''  var funcName  // 判断 value 的类型,不同类型进行不同的解决  const type = schema.type  switch (type) {    case 'null':      code += `      json += $Null()      `      break    case 'string':      code += `      json += $String(obj${key})      `      break    case 'number':    case 'integer':      code += `      json += $Number(obj${key})      `      break    case 'object':      // 如果 value 为一个对象,须要一个新的办法进行结构      funcName = (name + key).replace(/[-.\[\]]/g, '')      laterCode = buildObject(schema, laterCode, funcName)      code += `        json += ${funcName}(obj${key})      `      break    case 'array':      funcName = (name + key).replace(/[-.\[\]]/g, '')      laterCode = buildArray(schema, laterCode, funcName)      code += `        json += ${funcName}(obj${key})      `      break    default:      throw new Error(`${type} unsupported`)  }  return {    code,    laterCode  }}

其实就是对 type"object"properties 进行一次遍历,而后针对 value 不同的类型进行二次解决,如果碰到新的对象,会结构一个新的函数进行解决。

// 如果蕴含子对象const stringify = build({  type: 'object',  properties: {    id: { type: 'number' },    info: {      type: 'object',      properties: {        age: { type: 'number' },        name: { type: 'string' },      }    }  }})console.log(stringify.toString())
function $main (obj) {  var json = '{'  json += '"id":'  json += $Number(obj.id)  json += ','  json += '"info":'  json += $maininfo(obj.info)  json += '}'  return json}// 子对象会通过另一个函数解决function $maininfo (obj) {  var json = '{'  json += '"age":'  json += $Number(obj.age)  json += ','  json += '"name":'  json += $String(obj.name)  json += '}'  return json}

总结

当然,fastify 之所以号称本人快,外部还有一些其余的优化办法,例如,在路由库的实现上应用了 Radix Tree 、对上下文对象可进行复用(应用 middie 库)。本文只是介绍了其中的一种体现最重要显著优化思路,心愿大家浏览之后能有所播种。