Node.js 服務性能翻倍的祕密(一)

image

前言

用過 Node.js 開發過的同窗確定都上手過 koa,由於他簡單優雅的寫法,再加上豐富的社區生態,並且現存的許多 Node.js 框架都是基於 koa 進行二次封裝的。可是說到性能,就不得不提到一個知名框架: fastify ,聽名字就知道它的特性就是快,官方給出的Benchmarks甚至比 Node.js 原生的 http.Server 還要快。git

Benchmarks

性能提高的關鍵

咱們先看看 fastify 是如何啓動一個服務的。github

# 安裝 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,還支持對以下數據定義 schemanpm

  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

如何對 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,邏輯比較簡單,便於理解。bash

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 對應的對象進行序列化,具體使用方式以下:app

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 內的代碼的:koa

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 不一樣的類型進行二次處理,若是碰到新的對象,會構造一個新的函數進行處理。async

// 若是包含子對象
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 庫)。本文只是介紹了其中的一種體現最重要明顯優化思路,但願你們閱讀以後能有所收穫。

image

相關文章
相關標籤/搜索