Fastify 零基础学习手册

Fastify 零基础学习手册

最近根据项目需求学习一下fastify框架,同步整理一下学习笔记记录。


目录

  • [Fastify 零基础学习手册](#Fastify 零基础学习手册)
    • [Fastify 是什么?为什么选它?](#Fastify 是什么?为什么选它?)
    • [每种语言都从最朴实的Hallo Word开始](#每种语言都从最朴实的Hallo Word开始)
      • [1. 初始化](#1. 初始化)
      • [2. 新建 app.js](#2. 新建 app.js)
      • [3. 启动 & 测试](#3. 启动 & 测试)
    • 官方脚手架:一步到位目录规范
      • [1. 脚手架脚本](#1. 脚手架脚本)
      • [2. 目录结构](#2. 目录结构)
    • 关键核心理念
      • [1. 路由(Routes)](#1. 路由(Routes))
      • [2. 插件(Plugins)](#2. 插件(Plugins))
      • [3. Hook(生命周期钩子)](#3. Hook(生命周期钩子))
      • [4. Schema(校验与序列化)](#4. Schema(校验与序列化))
      • [5. 日志(Pino)](#5. 日志(Pino))
    • 项目基础功能
    • 注意点(避坑点)

Fastify 是什么?为什么选它?

特性 简述
性能 路由+序列化预编译,官方 benchmark 30k+ req/s
插件化 任何功能皆可写成插件,天然作用域隔离
TypeScript 内置类型声明,零成本 TS
日志 基于 Pino,开发/生产开箱即用
校验&序列化 JSON Schema 驱动,自动校验请求/响应

每种语言都从最朴实的Hallo Word开始

1. 初始化

bash 复制代码
mkdir fastify-demo && cd fastify-demo
npm init -y
npm install fastify

2. 新建 app.js

typescript 复制代码
const fastify = require('fastify')({ logger: true });

fastify.get('/', async (req, reply) => {
  return { hello world };
});

const start = async () => {
  try {
    await fastify.listen({ port: 3000 });
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
start();

3. 启动 & 测试

执行项目启动脚本,浏览器打开 http://localhost:3000

shell 复制代码
node app.js

官方脚手架:一步到位目录规范

1. 脚手架脚本

复制代码
脚手架已集成 nodemon、ESLint、tap,可直接开发。
shell 复制代码
npx fastify-cli generate .
// 如果执行时出现
npm install
npm run dev        # 带热重载

如果执行时出现如下问题,表示当前目录已经存在 package.json,fastify-cli 为了避免覆盖,要求你显式地加一个 --integrate 参数,以用于把生成的内容整合进已有项目

shell 复制代码
npx fastify-cli generate . --integrate

npx : NPM 包运行器,用于在不同的 Node.js 版本上执行运行脚本和运行本地的二进制文件. 在 npm@5.2.0 版本以上可用.

成功示例

2. 目录结构

html 复制代码
.
├── app.js                 # 应用入口
├── package.json
├── plugins/               # 全局插件
│   ├── sensible.js        # 错误处理
│   └── support.js         # 工具函数
├── routes/                # 自动加载的路由
│   ├── example/
│   │   └── index.js       # /example 路由
│   └── root.js            # / 路由
├── test/                  # 测试
│   ├── example.test.js
│   └── root.test.js
└── README.md

关键核心理念

1. 路由(Routes)

路由配置为根据routes目录中对应文件夹名+方法中注册路由名称组合形式

typescript 复制代码
// routes/user.js
module.exports = async function (fastify, opts) {
  fastify.get('/users/:id', async (req, reply) => {
    const { id } = req.params;
    return { id, name: 'Tom' };
  });
};

注册:

typescript 复制代码
fastify.register(require('./routes/user'));

2. 插件(Plugins)

插件 = 可复用功能单元。

示例:把数据库连接写成插件 plugins/db.js

typescript 复制代码
const fp = require('fastify-plugin');
module.exports = fp(async (f, opts) => {
  f.register(require('@fastify/postgres'), {
    connectionString: 'postgres://user:pwd@localhost:5432/db'
  });
});

3. Hook(生命周期钩子)

typescript 复制代码
fastify.addHook('onRequest', async (req, reply) => {
  req.startTime = Date.now();
});
fastify.addHook('onResponse', async (req, reply) => {
  req.log.info({ responseTime: Date.now() - req.startTime });
});

4. Schema(校验与序列化)

typescript 复制代码
const getUserSchema = {
  params: { type: 'object', properties: { id: { type: 'number' } } },
  response: {
    200: {
      type: 'object',
      properties: { id: { type: 'number' }, name: { type: 'string' } }
    }
  }
};
fastify.get('/users/:id', { schema: getUserSchema }, async (req, reply) => {
  const { id } = req.params;
  return { id, name: 'Tom' };
});

5. 日志(Pino)

typescript 复制代码
const fastify = require('fastify')({
  logger: { level: 'info' }   // 开发 debug,生产 warn
});

项目基础功能

1. 数据库连接 Database

这里用 pgsql 和 redis 为例

pgsql
shell 复制代码
npm i pg @fastify/postgres
typescript 复制代码
1. 单文件闭包写法
const fastify = require('fastify')()

fastify.register(require('@fastify/postgres'), {
  connectionString: 'postgres://postgres@localhost/postgres'
})

fastify.get('/user/:id', function (req, reply) {
  fastify.pg.query(
    'SELECT id, username, hash, salt FROM users WHERE id=$1', [req.params.id],
    function onResult (err, result) {
      reply.send(err || result)
    }
  )
})

fastify.listen({ port: 3000 }, err => {
  if (err) throw err
  console.log(`server listening on ${fastify.server.address().port}`)
})

2. esm写法
module.exports = async function (fastify, opts) {
  fastify.register(require('@fastify/postgres'), {
    connectionString: 'postgres://postgres@localhost/postgres'
  })
  
  fastify.get('/user/:id', function (req, reply) {
    fastify.pg.query(
      'SELECT id, username, hash, salt FROM users WHERE id=$1', [req.params.id],
      function onResult (err, result) {
        reply.send(err || result)
      }
    )
  })
}
redis
shell 复制代码
npm i @fastify/redis
typescript 复制代码
1. 单文件闭包写法
'use strict'

const fastify = require('fastify')()

fastify.register(require('@fastify/redis'), { host: '127.0.0.1' })
// or
fastify.register(require('@fastify/redis'), { url: 'redis://127.0.0.1', /* other redis options */ })

fastify.get('/foo', function (req, reply) {
  const { redis } = fastify
  redis.get(req.query.key, (err, val) => {
    reply.send(err || val)
  })
})

fastify.post('/foo', function (req, reply) {
  const { redis } = fastify
  redis.set(req.body.key, req.body.value, (err) => {
    reply.send(err || { status: 'ok' })
  })
})

fastify.listen({ port: 3000 }, err => {
  if (err) throw err
  console.log(`server listening on ${fastify.server.address().port}`)
})

2. esm写法
  fastify.register(require('@fastify/redis'), {
    url: 'redis://127.0.0.1:51865',
    connectTimeout: 2000,
    maxRetriesPerRequest: 1,
    enableOfflineQueue: false,
    lazyConnect: true
  })
	fastify.post('/saveRedis', function (req, reply) {
	    if (!fastify.redis) return reply.code(503).send({ error: 'redis unavailable' })
	    const { redis } = fastify
	    redis.set(req.body.key, req.body.value, (err) => {
	      reply.send(err || { status: 'ok' })
	    })
	  })

  fastify.get('/getRedis', function (req, reply) {
  if (!fastify.redis) return reply.code(503).send({ error: 'redis unavailable' })
  const { redis } = fastify
  redis.get(req.query.key, (err, val) => {
    reply.send(err || val)
  })

注意点(避坑点)

  1. 请求格式支持(Content-Type
    Fastify 原生只支持 'application/json' 和 'text/plain' content type。默认的字符集是 utf-8。如果你需要支持其他的 content type,你需要使用 addContentTypeParser API。默认的 JSON 或者纯文本解析器也可以被更改或删除。

注:假如你决定用 Content-Type 自定义 content type,UTF-8

便不再是默认的字符集了。请确保如下包含该字符集:text/html; charset=utf-8。