前言
用過 Node.js 開發過的同學肯定都上手過 koa,因為他簡單優雅的寫法,再加上豐富的社群生態,而且現存的許多 Node.js 框架都是基於 koa 進行二次封裝的。但是說到效能,就不得不提到一個知名框架: fastify
,聽名字就知道它的特性就是快,官方給出的Benchmarks甚至比 Node.js 原生的 http.Server
還要快。
效能提升的關鍵
我們先看看 fastify
是如何啟動一個服務的。
# 安裝 fastify
npm i -S [email protected]
複製程式碼
// 建立服務例項
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
對請求的響應體定義了一個 schema
,fastify
除了可以定義響應體的 schema
,還支援對如下資料定義 schema
:
body
:當為 POST 或 PUT 方法時,校驗請求主體;query
:校驗 url 的 查詢引數;params
:校驗 url 引數;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:github.com/douglascroc…
// 只展示 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,邏輯比較簡單,便於理解。
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
庫)。本文只是介紹了其中的一種體現最重要明顯優化思路,希望大家閱讀之後能有所收穫。