【實戰篇】koa2+Ts項目的優雅使用和封裝

背景

因爲最近學習到 SSR 相關的內容,而且須要作一些內部的工具系統;考慮先熟悉 Koa2+TypeScript 的方式;轉了一圈發現 TypeScript+Koa 的結合基本不多; 因此就有了,如今的總結輸出javascript

項目結構

跟js項目相比,主要是增長tsconfig.jsonsrc/@types的配置, 還有不少庫須要引入TypeScript類型聲明;前端

js-md5,同時須要引入@types/js-md5java

juejin.png

具體優化點

中間件

logger.ts

log4js這個庫,能夠很好地收集http請求方法、返回狀態、請求url、IP地址、請求時間等,來打印出自定義的日誌。能夠代替console.log()使用;在使用這個中間件的時候,必須放在第一個中間件,才能保證因此的請求及操做會先通過logger進行記錄再到下一個中間件。node

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-corsios

app.use(Cors(corsHandler))git

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不能設置爲*github

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’設置爲通配符‘*’web

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…

相關文章
相關標籤/搜索