# koa和typescript搭建中台

# 依赖和入口

koa 依赖包

  • koa
  • koa-body
  • koa-router
  • koa-static
  • koa-xtime
npm install koa koa-static koa-body koa-xtime -S

typescript 依赖包

  • typescript
  • ts-node-dev
  • tslint
  • @types/node
npm install typescript ts-node-dev tslint @types/node -D

package.json 替换 scripts

"scripts": {
    "dev": "ts-node-dev ./src/index.ts -P tsconfig.json --no-cache",
    "build": "tsc -P tsconfig.json && node ./dist/index.js",
    "tslint": "tslint --fix -p tsconfig.json"
}

tsconfig.json

{
    "compilerOptions": {
        "outDir": "./dist",
        "target": "es6", // 指定编译后的版本
        "module": "commonjs", // 指定模版标准 cjs
        "sourceMap": true, // 编译时需要生成sourceMap文件
        "moduleResolution": "node", // 模块解析策略
        "experimentalDecorators": true, // 启用装饰器特性
        "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块默认导出
        "lib": ["es2015"], // 指定要包含在编译中的库文件
        "typeRoots": ["./node_modules/@types"] // 这里面是刚才安装的@types/node
    },
    "include": ["src/**/*"], // 指定要编译的路径列表
    "exclude": [ // 排除不编译的文件
        "node_modules"
    ]
}

# 项目搭建

./src/index.ts

import * as Koa from 'koa'
import * as bodify from 'koa-body'
import * as serve from 'koa-static'
import * as timing from 'koa-xtime' // 插入X-response-time
import {load} from './utils/route-decors'
import {resolve} from 'path'
const app = new Koa()

app.use(serve(`${__dirname}/public`))
app.use(timing())
app.use(bodify({
    multipart: true,
    strict: false
}))

let router = load(resolve(__dirname, './routes')) // 执行指定目录生成路由

app.use(router.routes())
app.use((ctx: Koa.Context) => {
    ctx.body = 'hello world'
})

app.listen(3000, () => {
    console.log('服务器启动成功')
})

./utils/route-decors.ts

import * as Koa from 'Koa'
import * as KoaRouter from 'koa-router'
import * as glob from 'glob'

// 装饰器类型 通过@get('/list')等形式调用
type HTTPMethod = 'get' | 'put' | 'del' | 'post' | 'patch';
type LoadOptions = {
    /**
     * 路由⽂件扩展名,默认值是`.{js,ts}`
     */
    extname?: string;
};
type RouteOptions = {
    /**
     * 适⽤于某个请求⽐较特殊,需要单独制定前缀的情形
     */
    prefix?: string;
    /**
     * 给当前路由添加⼀个或多个中间件
     */
    middlewares?: Array<Koa.Middleware>;
};
const router = new KoaRouter()

// 柯里化 方便后面的装饰器生成
const decorate = (method: HTTPMethod, path: string, options:RouteOptions = {}, router: KoaRouter) =>  {
    // target是类 property是修饰的方法 descriptor是方法的内容
    return (target, property, descriptor) {
        const middlewares = []; // 缓存中间件
        if(options.middlewares) {
            middlewares.push(...options.middlewares);
        }
        middlewares.push(target[property]); // 最后执行当前方法
        const url = options.prefix ? options.prefix + path : path
        router[method](url, ...middlewares) //注册路由
    }
}
const method = method => (path: string, options?:RouteOptions) => decorate(method, path, options, router)
// 这些是实实在在的装饰器函数
export const get = method('get')
export const post = method('post')
export const put = method('put')
export const del = method('del')
export const patch = method('patch')

// 调用接口会打印log
export const log = function log(target, name, descriptor) {
    var oldValue = descriptor.value;

    descriptor.value = function() {
        console.log(`Calling "${name}" with`, arguments);
        return oldValue.apply(null, arguments);
    }
    return descriptor;
}
// 执行函数注册router
export const load = (folder: string, options: LoadOptions = {}) : KoaRouter => {
    const extname = options.extname || '.{js,ts}'
    glob.sync(require('path').join(folder, `./**/*${extname}`)).forEach(item => require(item))
    return router
}

./routes/user.ts

import * as Koa from 'koa'
import {get, post, log} from '../utils/route-decors'
const users = [{name: 'abc'}, {name: 'def'}, {name: 'joy'}] 
export default class User {
    @get('/users')
    @log
    public list(ctx: Koa.Context) {
        sleep(4);
        ctx.body = {ok: 1, data: users}
    }
    
    @post('/users', {
        middlewares: [
            async (ctx, next) => {
                const name = ctx.request.body.name
                // 用户名必传
                if(!name) {
                    throw '请输入用户名'
                } else {
                    await next()
                }
            }
        ]
    })
    @log
    public add(ctx: Koa.Context) {
        users.push(ctx.request.body)
        ctx.body = {ok: 1}
    }
}

# 实现类装饰器

假如获取用户前需要先登录

import {classValid} from '../utils/route-decors.td';
@classValid([
    async function guard(ctx: Koa.Context, next: () => Promise<any>) {
        console.log('guard', ctx.header);

        // 校验token
        if(ctx.header.token) {
            await next();
        } else {
            throw '请登录';
        }
    }
])
export default class User {
    @log
    @get('/users')
    public list(ctx: Koa.Context) {
        sleep(4);
        ctx.body = {ok: 1, data: users}
    }

    @post('/users', {
        middlewares: [
            async (ctx, next) => {
                const name = ctx.request.body.name
                // 用户名必传
                if(!name) {
                    throw '请输入用户名'
                } else {
                    await next()
                }
            }
        ]
    })
    public add(ctx: Koa.Context) {
        users.push(ctx.request.body)
        ctx.body = {ok: 1}
    }
}

装饰器的顺序是从外到里定义,从里到外执行的,所以如果按正常思路加到队列后面是不行的 ./utils/route-decors.ts

// 柯里化 方便后面的装饰器生成
const decorate = (method: HTTPMethod, path: string, options:RouteOptions = {}, router: KoaRouter) =>  {
    // target是类 property是修饰的方法 descriptor是方法的内容
    return (target, property, descriptor) {
        process.nextTick(() => {
            const middlewares = []; // 缓存中间件
            if(target.middlewares) {
                middlewares.push(...target.middlewares);
            }
            if(options.middlewares) {
                middlewares.push(...options.middlewares);
            }
            middlewares.push(target[property]); // 最后执行当前方法
            const url = options.prefix ? options.prefix + path : path
            router[method](url, ...middlewares) //注册路由
        })
    }
}
...
export const classValid = function(middlewares: Koa.middlewares[]) {
    return function(target) {
        target.prototype.middlewares = middlewares; // 加到原型链上 decorate再处理
    }
}

这就相当于在每次调用接口时都会执行classValid检查是否登录。