跳至主要內容

服务器架构设计

njrProjectkitchen大约 3 分钟约 997 字

分层架构

route

分发路由,如:/category。匹配后的路由包含 controller 和 路由特定的 middleware

interface Router: { [key: string]: Route }

export interface Route {
  controller: Controller;
  middlewares?: Middleware[]; // route-specific middleware
}

middleware

中间件,处理请求中间逻辑,比如:corsauth 等。

export type Middleware = (req: CustomIncomingMessage, res: ServerResponse, next: () => void) => void

/**
 * Manage the execution order of middleware functions.
 * It allows you to add multiple middleware functions to a queue
 * and execute them sequentially during a request.
 *
 * @export
 * @class MiddlewareManager
 */
export default class MiddlewareManager {
  private readonly middlewares: Middleware[]

  constructor() {
    this.middlewares = []
  }

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

  run(req: CustomIncomingMessage, res: ServerResponse) {
    const runner = async (index: number) => {
      const middleware = this.middlewares[index]
      if (middleware) {
        await middleware(req, res, () => {
          return runner(index + 1)
        })
      }
    }

    runner(0)
  }
}

corsMiddleware 为例:

const corsMiddleware = (options: { origin: string; methods: string; credentials?: boolean }): Middleware => {
  return (req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', options.origin)
    res.setHeader('Access-Control-Allow-Methods', options.methods)
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
    if (options.credentials) {
      res.setHeader('Access-Control-Allow-Credentials', 'true')
    }
    // Handle preflight requests
    if (req.method === 'OPTIONS') {
      res.statusCode = 204
      res.end()
    } else {
      next()
    }
  }
}

export default corsMiddleware

controller

控制层,处理请求参数和请求体数据,分发业务逻辑。一个抽象基类,提供 sendResponsesendError 方法。

export type Controller = (req: CustomIncomingMessage, res: ServerResponse) => void

export abstract class BaseController {
  protected static sendResponse(statusCode: number, data: unknown, res: ServerResponse) {
    res.setHeader('content-type', 'application/json')
    res.writeHead(statusCode)

    // handle bigint
    let responseData: unknown
    if (typeof data === 'bigint') {
      if (data <= Number.MAX_SAFE_INTEGER && data >= Number.MIN_SAFE_INTEGER) {
        responseData = Number(data)
      } else {
        responseData = data.toString()
      }
    } else {
      responseData = data
    }

    const response: Response = {
      code: statusCode,
      message: 'OK',
      data: responseData
    }

    res.write(JSON.stringify(response))
    res.end()
  }

  protected sendError(statusCode: number, message: string, res: ServerResponse) {
    res.setHeader('content-type', 'application/json')
    res.writeHead(statusCode)
    const response: Response = {
      code: statusCode,
      message: JSON.stringify({ error: message })
    }
    res.write(response)
    res.end()
  }
}

UserController 为例:

export default class UserController extends BaseController {
  private userService: UserService

  constructor() {
    super()
    this.userService = new UserService()
  }

  async signIn(req: CustomIncomingMessage, res: ServerResponse) {
    try {
      const start = Date.now()
      const data = await getRequestBody<SignInParams>(req)

      // TODO: verify parameters.

      const response = await this.userService.signIn(data)

      console.log('All time: ', Date.now() - start)
      UserController.sendResponse(response.code, { message: response.message }, res)
    } catch (error) {
      UserController.sendResponse(500, { message: 'Internal Server Error' }, res)
      throw error
    }
  }
}

service

服务层,处理业务逻辑。BaseService 为抽象基类,提供 db 属性。

export abstract class BaseService {
  protected db: DB

  constructor() {
    this.db = DB.createInstance()
  }
}

UserService 为例:

export default class UserService extends BaseService {
  constructor() {
    super()
  }

  // sign-in
  async signIn(data: SignInParams): Promise<Response<boolean | null>> {
    const sql = 'SELECT * FROM users WHERE name = ?'
    const res = await this.db.execute({ sql, args: [data.name] })
    const usersInfo: User[] = res.rows.map((row) => ({
      id: Number(row.ID),
      name: String(row.name),
      password: String(row.password)
    }))

    // Check if the user exists.
    if (usersInfo.length === 0) {
      return {
        code: 401,
        message: 'Access to the requested resource is unauthorized. Please authenticate.',
        data: null
      }
    }
    // Check if the password matches.
    if (usersInfo[0].password !== data.password) {
      return {
        code: 401,
        message: 'Access to the requested resource is unauthorized. Please authenticate.',
        data: null
      }
    }

    // All checks passed, return success message or token
    // TODO: generate token.
    return { code: 200, message: 'Login successful', data: true }
  }
}

dao

数据访问层,处理数据查询。提供 execute 方法执行 sql

export class DB {
  private static instance: DB

  private constructor() {}

  public static createInstance() {
    if (!DB.instance) {
      DB.instance = new DB()
    }
    return DB.instance
  }

  public async execute(stmt: InStatement) {
    try {
      const start = Date.now()
      const res = await client.execute(stmt)
      console.log('Query time: ', Date.now() - start)
      return res
    } catch (error) {
      if (error instanceof LibsqlError) {
        throw new Error(`Execution failed: ${error.message}`)
      } else {
        throw new Error(`An error occurred: ${error}`)
      }
    }
  }
}

export default DB

其他细节

请求体处理

目前允许 POSTPUT 请求携带请求体。

export const getRequestBody = <T>(req: CustomIncomingMessage): Promise<T> => {
  return new Promise((resolve, reject) => {
    try {
      if (req.method !== 'POST' && req.method !== 'PUT') {
        resolve({} as T)
        return
      }
      if (req.headers['content-type'] !== 'application/json') {
        resolve({} as T)
        return
      }
      let postData = ''
      req.on('data', (chunk: Buffer) => {
        postData += chunk.toString()
      })
      req.on('end', () => {
        if (!postData) {
          resolve({} as T)
          return
        }
        resolve(JSON.parse(postData) as T)
      })
    } catch (error) {
      reject(error)
    }
  })
}

路径参数处理

路由规则:method + path + query,如 GET/category/:id

其中 path 需要进行参数处理,如:/category/:id。处理完后参数类型应为 params: Record<string, string>,因为可能存在多个参数。

解析完成后,controller 中需要接收 params,需要将 params 转换为 request.params

具体逻辑如下:

/**
 * Execute route's middleware and controller.
 *
 * @param {CustomIncomingMessage} req
 * @param {ServerResponse} res
 * @param {Route} route
 */
async function execute(
  req: CustomIncomingMessage,
  res: ServerResponse,
  route: Route,
  params: Record<string, string>
) {
  const mwManager = new MiddlewareManager()
  req.params = params

  const cors = corsMiddleware({
    origin: '*',
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
    credentials: false
  })
  mwManager.use(cors)

  route.middlewares?.forEach((func) => mwManager.use(func))
  mwManager.use(route.controller)
  await mwManager.run(req, res)
}

/**
 * Matches the provided method and pathname to a route in the Router.
 *
 * @param {string} method - The HTTP method to match.
 * @param {string} pathname - The URL pathname to match.
 * @return {Object} An object containing the matched route and parameters.
 */
const matchRoute = (method: string, pathname: string) => {
  for (const router in Router) {
    const [routeMethod, ...routeParts] = router.split('/')
    if (method === routeMethod) {
      const pathParts = pathname.slice(1).split('/')
      console.log(routeMethod, routeParts, pathParts, pathname)

      if (routeParts.length === pathParts.length) {
        const params: Record<string, string> = {}
        let match = true

        for (let i = 0; i < routeParts.length; i++) {
          if (routeParts[i].startsWith(':')) {
            params[routeParts[i].slice(1)] = pathParts[i]
          } else if (routeParts[i] !== pathParts[i]) {
            match = false
            break
          }
        }

        if (match) {
          return { route: Router[router], params }
        }
      }
    }
  }

  return { route: notFound, params: {} }
}