【实战篇】koa2+Ts项目的优雅使用和封装

8,420 阅读4分钟

背景

由于最近学习到 SSR 相关的内容,并且需要做一些内部的工具系统;考虑先熟悉 Koa2+TypeScript 的方式;转了一圈发现 TypeScript+Koa 的结合基本很少; 所以就有了,现在的总结输出

项目结构

跟js项目相比,主要是增加tsconfig.jsonsrc/@types的配置, 还有很多库需要引入TypeScript类型声明;

js-md5,同时需要引入@types/js-md5

juejin.png

具体优化点

中间件

logger.ts

log4js这个库,可以很好地收集http请求方法、返回状态、请求url、IP地址、请求时间等,来打印出自定义的日志。可以代替console.log()使用;在使用这个中间件的时候,必须放在第一个中间件,才能保证所以的请求及操作会先经过logger进行记录再到下一个中间件。

import { Context, Next } from 'koa'
import { LogPath } from '../config/constant'

const fs = require('fs')
const path = require('path')
const log4js = require('log4js')

// 这个是判断是否有logs目录,没有就新建,用来存放日志
const logsDir = path.parse(LogPath).dir
if (!fs.existsSync(logsDir)) {
  fs.mkdirSync(logsDir)
}
// 配置log4.js
log4js.configure({
  appenders: {
    console: { type: 'console' },
    dateFile: {
      type: 'dateFile',
      filename: LogPath,
      pattern: '-yyyy-MM-dd',
    },
  },
  categories: {
    default: {
      appenders: ['console', 'dateFile'],
      level: 'error',
    },
  },
})

export const logger = log4js.getLogger('[Default]')

// logger中间件
export const loggerMiddleware = async (ctx: Context, next: Next) => {
  // 请求开始时间
  const start = +new Date()
  await next()
  // 结束时间
  const ms = +new Date() - start
  // 打印出请求相关参数
  const remoteAddress = ctx.headers['x-forwarded-for'] || ctx.ip || ctx.ips
  const logText = `${ctx.method} ${ctx.status} ${
    ctx.url
  } 请求参数: ${JSON.stringify(ctx.request.body)}  响应参数: ${JSON.stringify(
    ctx.body
  )} - ${remoteAddress} - ${ms}ms`
  logger.info(logText)
}

cors.ts

在前后端接口请求中,由于浏览器的限制,不同域名则会出现跨域的情况。本实例是采用koa中设置跨域; 采用koa2-cors

app.use(Cors(corsHandler))

import { Context } from 'koa'

export const corsHandler = {
  origin: function (ctx: Context) {
    return '*'
  },
  exposeHeaders: ['Authorization'],
  maxAge: 5 * 24 * 60 * 60,
  // credentials: true,
  allowMethods: ['GET', 'POST', 'OPTIONS', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Requested-With'],
}

配置跨域的时候要注意:设置withCredentialstrue时,Access-Control-Allow-Origin不能设置为*

const instance = axios.create({
  baseURL: baseURL,
  timeout: 30000,
  // withCredentials: true,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8',
  },
})

在CORS中,Credential不接受http响应首部中的‘Access-Control-Allow-Origin’设置为通配符‘*’

response.ts

新建response.ts这个中间件主要是用来对返回前端的响应进行统一处理

import { logger } from './logger'
import { Context, Next } from 'koa'

export const responseHandler = async (ctx: Context, next: Next) => {
  if (ctx.result !== undefined) {
    ctx.type = 'json'
    ctx.body = {
      code: 200,
      msg: ctx.msg || '成功',
      data: ctx.result,
    }
    await next()
  }
}

export const errorHandler = (ctx: Context, next: Next) => {
  return next().catch((err) => {
    if (err.code == null) {
      logger.error(err.stack)
    }
    if (err.status === 401) {
      ctx.status = 401
      ctx.body = 'Protected resource, use Authorization header to get access\n'
    } else {
      ctx.body = {
        code: err.code || -1,
        data: null,
        msg: err.message.trim() || '失败',
      }
      ctx.status = 200
    }
    return Promise.resolve()
  })
}

jwt.ts

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案;

本项目主要是用到koa-jwtjsonwebtoken这两个插件

import jsonwebtoken from 'jsonwebtoken'
const { jwtSecret } = require('../../config/index')

export interface UserParams {
  username: string
  name?: string
  avatar?: string
  email?: string
  gender?: number
  phone?: number
  accessToken: string
}
export default class JwtAuth {
  /**
   * 获取用户token
   * @static
   * @param {UserParams} userData
   * @param {*} [options]
   * @return {*}  {string}
   * @memberof JwtAuth
   */
  public static signUserToken(userData: UserParams, options?: any): string {
    try {
      return jsonwebtoken.sign(userData, jwtSecret, options)
    } catch (error) {
      console.log(error)
    }
  }

  /**
   * 验证用户token值
   * @static
   * @param {string} token
   * @return {*}  {Object}
   * @memberof JwtAuth
   */
  public static verifyUserToken(token: string): any {
    try {
      const authorization = token && token.split(' ')[1]
      return jsonwebtoken.verify(authorization, jwtSecret)
    } catch (error) {
      console.log(error)
      throw { code: 401, message: 'no authorization' }
    }
  }
}

集成 swagger 生成接口文档

写接口,怎么能少了接口文档呢;这里采用koa集成swagger的方式生成接口文档。

具体步骤:

  1. 引入koa2-swagger-ui, swagger-jsdoc
  2. 建立swagger.config
import path from 'path'
import swaggerJSDoc from 'swagger-jsdoc'
import AddressIp from 'ip'
import { PORT } from '../../config/constant'

const swaggerDefinition = {
  info: {
    // API informations (required)
    title: '账号系统', // Title (required)
    version: '1.0.0', // Version (required)
    description: '账号和权限', // Description (optional)
  },
  host: `http://${AddressIp.address()}:${PORT}`, // Host (optional)
  basePath: '/', // Base path (optional)
}

const options = {
  swaggerDefinition,
  apis: [path.join(__dirname, '../../routes/*.ts')], // all api
}

const jsonSpc = swaggerJSDoc(options)
export default jsonSpc

  1. 配置/doc路由
import Router from 'koa-router'
import { Context } from 'koa'
import swaggerJSDoc from '../middlewares/swagger/swagger.conf'
const routerInit = new Router()

routerInit.get('/docs', (ctx: Context) => {
  ctx.body = swaggerJSDoc
})
export default routerInit
  1. 应用
// swagger
app.use(
  koaSwagger({
    routePrefix: '/swagger',
    swaggerOptions: {
      url: '/docs',
    },
  })
)

应用的时候,是需要自己手动写注释的例如:

/**
 * @swagger
 * /v1/menu/list/${appId}:
 *   post:
 *     description: 获取菜单列表
 *     tags: [菜单模块]
 *     produces:
 *       - application/json
 *     parameters:
 *     - in: "body"
 *       name: "body"
 *       description: "查询参数"
 *       schema:
 *         $ref: "#/definitions/Menu"
 *     responses:
 *       200:
 *         description: 获取成功
 *         schema:
 *           type: object
 *           properties:
 *             total:
 *               type: number
 *             rows:
 *               type: array
 *               items:
 *                   $ref: '#/definitions/MenuModel'
 *
 */

来看看成果:

image.png

路由配置

目前的路由配置是手动添加和注册的

// 路由
app.use(Role.routes()).use(Role.allowedMethods())
app.use(User.routes()).use(User.allowedMethods())
app.use(Menu.routes()).use(Menu.allowedMethods())
app.use(Auth.routes()).use(Auth.allowedMethods())

在搜索路由自动加载的方案,暂时没找到适合TypeScript的库,卒

如果是用js可以考虑这个库require-directory

joi 参数校验

我们用nodejs实现一些功能时,往往需要对用户输入的数据进行验证。然而,验证是一件麻烦的事情,很有可能你需要验证数据类型,长度,特定规则等等,在前端做表单验证时,我们常用的做法是使用正则,正则表达式也许可以一步到位,但是他只会给你true or false,如果想要知道数据不符合哪些条件时,那么你要进一步判断,下面和大家分享一种可读性和易用性更好的实现方法。

Joi 是 hapijs 自带的数据校验模块,他已经高度封装常用的校验功能,本文就是介绍如何优雅地使用 joi 对数据进行校验。相信你会喜欢上他。便于大家理解,以登录为例,一般分两种方式:A或B (输入密码或二维码),那么 joi 的配置如下即可实现检验:

const Joi = require('joi')
const schema = Joi.object({
  name: Joi.string().empty(''),
  pageSize: Joi.number().required(),
  pageNo: Joi.number().required(),
  appId: Joi.number().required(),
})
schema.validateAsync({ ...request })

sequelize 的事务解决数据不一致问题

当一个业务要进行多项数据库的操作时,拿点赞功能为例,首先你得在点赞记录的表中增加记录,然后你要将对应对象的点赞数加1,这两个操作是必须要一起完成的,如果有一个操作成功,另一个操作出现了问题,那就会导致数据不一致,这是一个非常严重的安全问题。

具体实践(就是把多个数据库操作置于一个事务中):

await sequelize.transaction(async (t: any) => {
  await roleModel.update(
    {
      roleName: request.roleName,
      remark: request.remark || '',
    },
    {
      where: {
        id: request.id,
      },
      transaction: t,
    }
  )
  await roleMenuModel.destroy({
    where: {
      roleId: request.id,
    },
    force: true,
    transaction: t,
  })
})

pm2 配置

PM2是可以用于生产环境的Nodejs的进程管理工具,并且它内置一个负载均衡。它不仅可以保证服务不会中断一直在线,并且提供0秒reload功能,还有其他一系列进程管理、监控功能。并且使用起来非常简单。pm2的官方文档已经进行详细的配置说明,在这里就不进行一一简述,主要讲的时我的koa项目怎样配合PM2进行相关管理或者说部署。也可以结合在package.json里面,用自定义命令运行。我们在package.jsonscript配置

  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon --exec ts-node src/app.ts",
    "build-ts": "tsc",
    "build:test": "rm -fr dist && npm run lint && npm run build-ts",
    "serve:test": "cross-env NODE_ENV=development pm2 startOrReload pm2-start.json --no-daemon",
}

pm2-start.json

{
  "apps": [
    {
      "name": "xl-account-server",
      "script": "./dist/app.js",
      "instances": "2",
      "exec_mode": "cluster",
      "watch": false,
      "watch_delay": 4000,
      "ignore_watch" : [
        "node_modules",
        "src"
      ],
      "max_memory_restart": "1000M",
      "min_uptime": "5s",
      "max_restarts": 5,
      "error_file": "./logs/pm2_xl_account_err.log",
      "out_file": "/dev/null",
      "log_date_format": "YYYY-MM-DD HH:mm Z"
    }
  ]
}


koa2 + TypeScript github模板

github.com/kkxiaojun/k…