Chuyển đến nội dung chính

レッスン 13: HTTP フレームワークをゼロから構築する

ミニ フレームワークを構築します: ルーター、ミドルウェア パイプライン、リクエスト解析、レスポンス ヘルパー。 Express/Fastify の内部構造と比較してください。コンテンツネゴシエーション、CORS。

💻 プログラミング — レッスン 13 レッスン 13: HTTP フレームワークをゼロから構築する

Node.js コア: 基本から高度まで

パート 4: フレームワークを使用しない構築

xdev.asia

1.ルーターエンジン

type Handler = (req: AppRequest, res: AppResponse) => Promise<void> | void
type Middleware = (req: AppRequest, res: AppResponse, next: () => Promise<void>) => Promise<void> | void

interface Route {
  method: string
  pattern: RegExp
  paramNames: string[]
  handler: Handler
}

class Router {
  private routes: Route[] = []

  private addRoute(method: string, path: string, handler: Handler) {
    const paramNames: string[] = []
    const pattern = new RegExp(
      '^' + path.replace(/:(\w+)/g, (_, name) => {
        paramNames.push(name)
        return '([^/]+)'
      }) + '$'
    )
    this.routes.push({ method, pattern, paramNames, handler })
  }

  get(path: string, handler: Handler) { this.addRoute('GET', path, handler) }
  post(path: string, handler: Handler) { this.addRoute('POST', path, handler) }
  put(path: string, handler: Handler) { this.addRoute('PUT', path, handler) }
  delete(path: string, handler: Handler) { this.addRoute('DELETE', path, handler) }

  match(method: string, url: string) {
    for (const route of this.routes) {
      if (route.method !== method) continue
      const match = url.match(route.pattern)
      if (match) {
        const params: Record<string, string> = {}
        route.paramNames.forEach((name, i) => params[name] = match[i + 1])
        return { handler: route.handler, params }
      }
    }
    return null
  }
}

2. ミドルウェアパイプライン

class App {
  private router = new Router()
  private middlewares: Middleware[] = []

  use(middleware: Middleware) {
    this.middlewares.push(middleware)
  }

  private async runMiddleware(req: AppRequest, res: AppResponse, handler: Handler) {
    let index = 0
    const next = async (): Promise<void> => {
      if (index < this.middlewares.length) {
        const mw = this.middlewares[index++]
        await mw(req, res, next)
      } else {
        await handler(req, res)
      }
    }
    await next()
  }

  async handleRequest(req: IncomingMessage, res: ServerResponse) {
    const url = new URL(req.url!, `http://${req.headers.host}`)
    const appReq = new AppRequest(req, url)
    const appRes = new AppResponse(res)

    const matched = this.router.match(req.method!, url.pathname)
    if (matched) {
      appReq.params = matched.params
      await this.runMiddleware(appReq, appRes, matched.handler)
    } else {
      appRes.status(404).json({ error: 'Not Found' })
    }
  }

  listen(port: number) {
    const server = createServer((req, res) => this.handleRequest(req, res))
    server.listen(port, () => console.log(`Server on :${port}`))
  }
}

3. リクエストとレスポンスのヘルパー

class AppRequest {
  params: Record<string, string> = {}
  query: Record<string, string>
  private _body: unknown

  constructor(public raw: IncomingMessage, public url: URL) {
    this.query = Object.fromEntries(url.searchParams)
  }

  async body<T>(): Promise<T> {
    if (this._body) return this._body as T
    const chunks: Buffer[] = []
    for await (const chunk of this.raw) chunks.push(chunk as Buffer)
    const text = Buffer.concat(chunks).toString()
    this._body = JSON.parse(text)
    return this._body as T
  }

  get headers() { return this.raw.headers }
  get method() { return this.raw.method! }
}

class AppResponse {
  private statusCode = 200

  constructor(private raw: ServerResponse) {}

  status(code: number) { this.statusCode = code; return this }

  json(data: unknown) {
    this.raw.writeHead(this.statusCode, { 'Content-Type': 'application/json' })
    this.raw.end(JSON.stringify(data))
  }

  send(text: string) {
    this.raw.writeHead(this.statusCode, { 'Content-Type': 'text/plain' })
    this.raw.end(text)
  }
}

4. フレームワークを使用する

const app = new App()

// Middleware: logging
app.use(async (req, res, next) => {
  const start = Date.now()
  await next()
  console.log(`${req.method} ${req.url.pathname} ${Date.now() - start}ms`)
})

// Middleware: CORS
app.use(async (req, res, next) => {
  res.raw.setHeader('Access-Control-Allow-Origin', '*')
  res.raw.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')
  await next()
})

// Routes
app.router.get('/api/users/:id', async (req, res) => {
  const { id } = req.params
  res.json({ id, name: 'Nguyen Van A' })
})

app.router.post('/api/users', async (req, res) => {
  const body = await req.body<{ name: string }>()
  res.status(201).json({ id: '1', name: body.name })
})

app.listen(3000)

次の記事: データベースドライバーと接続プーリング — pg、mysql2、better-sqlite3。