使用 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 = ''
使用 koa + typescript + 装饰器搭建 mock 服务