使用 koa + typescript + 装饰器搭建 mock 服务

Posted 左手121

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 koa + typescript + 装饰器搭建 mock 服务相关的知识,希望对你有一定的参考价值。

使用 koa + typescript + 装饰器搭建 mock 服务

基本的包信息和运行的脚本


  "name": "mock",
  "version": "1.0.0",
  "license": "MIT",
  "main": "index.js",
  "scripts": 
    "start": "node index.js"
  ,
  "dependencies": 
    "chalk": "^4.1.0",
    "jsonwebtoken": "^8.5.1",
    "koa": "^2.13.0",
    "koa-bodyparser": "^4.3.0",
    "koa-convert": "^2.0.0",
    "koa-cors": "^0.0.16",
    "koa-jwt": "^4.0.0",
    "koa-router": "^9.4.0",
    "mockjs": "^1.1.0",
    "nodemon": "^2.0.9",
    "reflect-metadata": "^0.1.13",
    "ts-node": "^10.0.0",
    "typescript": "^4.3.5"
  ,
  "devDependencies": 
    "@types/jsonwebtoken": "^8.5.3",
    "@types/koa": "^2.13.3",
    "@types/koa-bodyparser": "^4.3.1",
    "@types/koa-convert": "^1.2.3",
    "@types/koa-cors": "^0.0.0",
    "@types/koa-router": "^7.4.2",
    "@types/mockjs": "^1.0.3",
    "@types/node": "^16.0.0"
  

启动服务的脚本 index.js

const chalk = require('chalk')
const nodemon = require('nodemon')
nodemon(
  script: 'src/app.ts',
  watch: 'src',
  // 使用 ts-node 执行程序
  // tsconfig-paths 作用是为了识别 tsconfig 中的别名
  exec: 'ts-node -r tsconfig-paths/register',
  ext: 'js,json,ts',
)
nodemon
  .on('restart', () => 
    console.log(chalk.green('Service Restart'))
  )
  .on('quit', () => 
    process.exit()
  )

tsconfig 的基本配置


  "compilerOptions": 
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    // 改成 commonjs,我们就可以使用 import/export
    "module": "commonjs",
    // --- 开启装饰器功能 ----
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    // --- 结束         ----
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "noEmit": true,
    "baseUrl": "./",
    "paths": 
      "@/*": ["src/*"]
    
  ,
  "include": ["src/**/*"]

app.ts

// 引入这个包,为了添加和读取元数据信息
import 'reflect-metadata'
import Koa from 'koa'
import chalk from 'chalk'
import cors from 'koa-cors'
import convert from 'koa-convert'
import bodyParser from 'koa-bodyparser'
import koaJwt from 'koa-jwt'
import  verify, secret  from '@/verify'
import router from '@/router'
const app = new Koa()
app
  // 可以通过 ctx.request.body 拿到请求的 body 信息
  .use(bodyParser())
  // 开启 cors 解决跨域问题
  .use(convert(cors()))
  // 使用 jwt 认证机制
  .use((ctx, next) =>
    next().catch(err => 
      if (err.status === 401) 
        ctx.status = 401
        ctx.body =
          'Protected resource, use Authorization header to get access\\n'
       else 
        throw err
      
    ),
  )
  .use(koaJwt( secret ).unless( path: [/^\\/mock\\/login/] ))
  .use(verify)
  // 使用 jwt 认证机制 -结束
  .use(router.routes())
  .use(router.allowedMethods())

app.listen(3000)

console.log(`\\nserver is running at $chalk.cyan('http://localhost:3000/')\\n`)

jwt 认证

import jwt from 'jsonwebtoken'
import  IContext, INext  from './utils'
const user =  username: 'admin', password: '123456', nickname: '超级管理员' 
export const secret = 'self-mock'
export async function verify(ctx: IContext, next: INext) 
  if (ctx.path === '/mock/login' && ctx.method === 'POST') 
    const  username, password  = ctx.request.body as 
      username: string
      password: string
    
    if (user.username === username && password === user.password) 
      ctx.body = 
        statusCode: 200,
        message: 'success',
        data: jwt.sign(user, secret,  expiresIn: 60 * 60 * 1 ),
      
     else 
      ctx.body = 
        statusCode: 400,
        message: '用户名或者密码错误',
      
    
   else if (ctx.path === '/mock/getUserInfo' && ctx.method === 'GET') 
    ctx.body = 
      statusCode: 200,
      message: 'success',
      data: 
        username: user.username,
        nickname: user.nickname,
      ,
    
   else 
    await next()
  

定义装饰器的元数据信息

import  IRouterParamContext  from 'koa-router'
import  ParameterizedContext, Next as KoaNext  from 'koa'

export const METHOD_METADATA = 'method'
export const PARAM_METADATA = 'param'
export const CONTROLLER_METADATA = 'controller'

// 定义 Controller 装饰器
export const Controller =
  (url?: string): ClassDecorator =>
  target => 
    Reflect.defineMetadata(CONTROLLER_METADATA, url || '$auto$', target)
  

// 定义请求的装饰器
const createMethodMappingDecorator =
  (method: string) =>
  (url?: string): MethodDecorator =>
  (target, name) => 
    Reflect.defineMetadata(METHOD_METADATA,  method, url , target, name)
  
export type IMethod =
  | 'get'
  | 'post'
  | 'put'
  | 'link'
  | 'unlink'
  | 'delete'
  | 'del'
  | 'head'
  | 'options'
  | 'patch'
  | 'all'
export const Get = createMethodMappingDecorator('get')
export const Post = createMethodMappingDecorator('post')
export const Put = createMethodMappingDecorator('put')
export const Link = createMethodMappingDecorator('link')
export const UnLink = createMethodMappingDecorator('unlink')
export const Delete = createMethodMappingDecorator('delete')
export const Del = createMethodMappingDecorator('del')
export const Head = createMethodMappingDecorator('head')
export const Options = createMethodMappingDecorator('options')
export const Patch = createMethodMappingDecorator('patch')
export const All = createMethodMappingDecorator('all')

export type IContext<StateT = any, CustomT = > = ParameterizedContext<
  StateT,
  CustomT & IRouterParamContext<StateT, CustomT>,
  any
>
export type INext = KoaNext

// 定义参数装饰器
const createParamsMappingDecorator =
  (type: string, required = false) =>
  (key?: string): ParameterDecorator =>
  (target, propertyKey, propertyIndex) => 
    Reflect.defineMetadata(
      PARAM_METADATA,
       type, key, required ,
      target,
      `$propertyKey.toString()-$propertyIndex`,
    )
  
const createParamsMappingDecoratorRequire = (type: string) => 
  const base = createParamsMappingDecorator(type)
  const required = createParamsMappingDecorator(type, true) as (key: string) => ParameterDecorator
  const ans: any = base
  ans.required = required
  return ans as typeof base &  required: typeof required 

// @Query('id')
// @Query.required('id')
export const Query = createParamsMappingDecoratorRequire('query')
export const Params = createParamsMappingDecoratorRequire('params')
export const Body = createParamsMappingDecorator('body')()
export const Ctx = createParamsMappingDecorator('ctx')()
export const Next = createParamsMappingDecorator('next')()

将 controller 注册到 koa 路由中

/* eslint-disable import/no-dynamic-require */
import Router from 'koa-router'
import path from 'path'
import fs from 'fs'
import 
  CONTROLLER_METADATA,
  METHOD_METADATA,
  PARAM_METADATA,
  IMethod,
  IContext,
  INext,
 from './utils'

const router = new Router()

// 获取 target 的所有属性(函数且非constructor)
function getProtos(value: any) 
  const protos = Object.getPrototypeOf(value)
  return Object.getOwnPropertyNames(protos).filter(
    key => key !== 'constructor' && typeof protos[key] === 'function',
  )


// 获取需要注入的参数信息
function getParams(
  target: any,
  proto: string,
  length: number,
  ctx: IContext,
  next: INext,
) 
  // 判断函数需要的入参个数
  const args: any[] = Array.from( length )
  // 循环得到每个入参的值
  for (let i = 0; i < length; i += 1) 
    const metadata:
      | 
          type: 'body' | 'params' | 'query' | 'ctx' | 'next'
          key?: string
          required: boolean
          propertyKey: string
        
      | undefined = Reflect.getMetadata(PARAM_METADATA, target, `$proto-$i`)
    // eslint-disable-next-line no-continue
    if (!metadata) continue
    const  type, key, required  = metadata
    // params.required 和 query.required 的 key 必须填写
    if (required && !key) 
      throw new Error(`$type.required('key') key is required`)
    
    switch (type) 
      case 'ctx':
        args[i] = ctx
        break
      case 'next':
        args[i] = next
        break
      case 'body':
        args[i] = ctx.request.body
        break
      default:
        args[i] = ctx[type]
        if (key) 
          args[i] = args[i][key]
          if (required && !args[i]) 
            return  status: 400, message: `Parameter $key is required` 
          
        
        break
    
  
  return  status: 200, args 


// 注册路由
function setRouter(target: any, proto: string, base: string) 
  const metadata:  method: IMethod; url?: string  | undefined =
    Reflect.getMetadata(METHOD_METADATA, target, proto)
  if (!metadata) return
  const  method, url  = metadata
  const cb = target[proto]
  // 注册路由
  router[method](base + (url || ''), async (ctx, next) => 
    // 获取需要注入的参数信息
    const  status, message, args  = getParams(
      target,
      proto,
      cb.length,
      ctx,
      next,
    )
    if (status === 200) 
      // 使用 apply 调用防止 this 指向有误
      const body = await cb.apply(target, args)
      if (body) 
        // 返回值直接就是请求的数据
        ctx.body = body
       else 
        await next()
      
     else 
      ctx.body = 
        statusCode: status,
        message,
      
    
  )


// 获取 controllers 文件夹下的所有文件
const dir = path.join(__dirname, 'controllers')
fs.readdirSync(dir).forEach(filename => 
  // .js .ts 后缀
  if (!/^[^.]+?\\.(t|j)s$/.test(filename)) return
  const C = require(path.join(dir, filename)).default
  // 必须 export default 导出
  if (!C) return
  let metadata = Reflect.getMetadata(CONTROLLER_METADATA, C)
  // 只识别 @Controller 装饰过的类信息
  if (!metadata) return
  // 请求 url 的前缀
  if (metadata === '$auto$') metadata = ''
  const target = new C(以上是关于使用 koa + typescript + 装饰器搭建 mock 服务的主要内容,如果未能解决你的问题,请参考以下文章

使用 koa + typescript + 装饰器搭建 mock 服务

Typescript中的装饰器原理

Typescript:使用装饰器时的类型推断

TypeScript基础入门之装饰器(三)

使用TypeScript开发微信小程序(10)——装饰器(Decorator)

typeScript 装饰器