乐趣区

关于前端:Nodejs-服务性能翻倍的秘密一

前言

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

性能晋升的要害

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

# 装置 fastify
npm 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 库)。本文只是介绍了其中的一种体现最重要显著优化思路,心愿大家浏览之后能有所播种。

退出移动版