/** * 核心Http * TKServer * @author ranyunlong<549510622@qq.com> * license MIT */ import * as http from 'http' import * as https from 'https'; import { ServerOptions } from 'https' import * as Koa from 'koa' import { Middleware } from 'koa'; import * as Knex from 'knex' import * as KoaBody from 'koa-body' import * as KoaStatic from 'koa-static' import * as KoaSession from 'koa-session' import * as KoaRouter from 'koa-router' import proxy from '@tkrjs/proxy' import logger from './util/logger'; import chalk from 'chalk' import { HttpExceptionInterface } from './exceptions'; import { I18n } from './i18n'; import { Config } from 'http-proxy-middleware'; import * as fs from 'fs' import * as path from 'path'; const app = new Koa() const router: TKServer.Router = new KoaRouter() export class TKServer { readonly app: Koa = app readonly port: number; constructor(options: TKServer.Options) { options.configs = options.configs || {} options.debug = options.debug || false options.language = options.language || 'en-US' app.keys = options.keys || ['tkr']; this.port = options.port || 3000; // create https | http if (options.https as ServerOptions) { https .createServer(options.https, app.callback()) .listen(this.port) } else { http.createServer(app.callback()) .listen(this.port) } // 1. 代理请求处理 // http proxy if ((options.proxy || options.configs.proxy) && (options.proxyTable || options.configs.proxyTable)) { // 开启代理请求 app.proxy = options.proxy || options.configs.proxy const proxyTable = options.proxyTable || options.configs.proxyTable Object.keys(proxyTable).forEach((route: string) => { app.use(proxy(route, proxyTable[route])) }) } // 2. 基础中间件处理 // body parser if (options.configs.body as KoaBody.IKoaBodyOptions) { app.use(KoaBody(options.configs.body || {})) } else { app.use(KoaBody()) } // session if (options.configs.session as TKServer.ServerSessionOptions) { app.use(KoaSession(options.configs.session, app)) } // 中间件处理 if (Array.isArray(options.middlewares)) { options.middlewares.forEach((middleware:Koa.Middleware) => { app.use(middleware) }) } // 3. 控制器处理 // map controllers if (Array.isArray(options.controllers)) { // 遍历所有控制器 options.controllers.forEach((Controller: TKServer.Controller) => { // 控制器根路由 const $root: string = Controller.prototype.$options.baseRoute // 获取方法装饰器 const $methods: TKServer.ControllerOptionsMethod = Controller.prototype.$options.methods // 获取控制器的构造函数所注入的服务 const $registerServices:Function = Controller.prototype.$options.registerServices // 获取处理方法的参数装饰器 const $mixinsParameters: Function = Controller.prototype.$options.mixinsParameters // 遍历方法装饰器 Object.keys($methods).forEach((k: string) => { // 组合路由路径 let routePath = $root + k; // 替换路径中的多余 '/' 路径 if(/[\/]{2,}/g.test(routePath)) { routePath = routePath.replace(/[\/]{2,}/g, '/'); } /** * addRouter * @param method 请求方式 * @param propertyKey 控制器处理方法名称 */ function addRouter(method: TKServer.RequestMethodTypes, propertyKey: string) { // 注册路由 router[method](routePath, async function(ctx: TKServer.Context){ // 注入服务 const services = $registerServices(ctx) // 参数装饰器数据混入 const parameters = $mixinsParameters(ctx, propertyKey, options) // 属性装饰器数据混入 Controller.prototype.$options.mixinsPropertys(ctx) // 数据库装饰器混入 Controller.prototype.$options.mixinsDatabases(ctx, options.configs.database) // 实例化控制器 const controller:{ [key:string]: any } = new Controller(...services) try { const data = await controller[propertyKey](...parameters) if (data) ctx.body = data } catch (error) { const err: HttpExceptionInterface = error ctx.status = err.status || 500 // 获取响应方式 options.configs.response = options.configs.response || {} options.configs.response.type = options.configs.response.type || 'json' // 响应结果 if (options.configs.response.type === 'json') { ctx.type = 'json' ctx.body = { error: err.error, debug: err.properties, message: error.message || ctx.response.message } } else { ctx.throw(err.status, err.message, err.properties) } // 输出错误调试 if(options.debug) { err.message = ctx.response.message console.log(err.stack) } } }) } // 遍历方法装饰器注册路由 if(Array.isArray($methods[k].type)) { ($methods[k].type as TKServer.RequestMethodTypes[]).forEach((key: TKServer.RequestMethodTypes)=>{ addRouter(key, $methods[k].propertyKey) }) } else { addRouter($methods[k].type as TKServer.RequestMethodTypes, $methods[k].propertyKey) } }) }) } app.use(router.routes()) // 4. ssr处理 if (typeof options.ssr === 'function') { app.use(options.ssr) } // 5. 静态目录 // static if (!options.ssr && options.configs.static as TKServer.ServerStaticOptions) { if (options.configs.spa || options.spa) { options.configs.static.options.defer = false } app.use(KoaStatic(options.configs.static.root, options.configs.static.options)) } // 6.spa if (options.spa || options.configs.spa && !options.ssr) { const spa = options.spa || options.configs.spa const filePath = path.join(spa.root, spa.filename) if (fs.existsSync(filePath)) { let file = fs.readFileSync(filePath) if (options.debug) { const wacher = fs.watch(filePath) wacher.on('change', function () { file = fs.readFileSync(filePath) }) wacher.on('error', function() { wacher.close() }) } app.use(async function(ctx: TKServer.Context, next: () => Promise) { if (ctx.status !== 200 && !path.extname(ctx.req.url) || path.extname(ctx.req.url) === 'html') { ctx.type = 'text/html' ctx.body = file } await next() }) } } // server debug if (options.debug) { // use logger middware console.log(`[${chalk.gray(`server`)}] ${chalk.green(`http://localhost:${this.port}`)}`) app.use(logger) } } } export namespace TKServer { export interface Context extends Koa.Context { } export interface Response extends Koa.Response {} export interface Request extends Koa.Request {} export interface Router extends KoaRouter { [index: string]: any; } export type ClassDecorator = (target: C) => C | void; export type Decorator = ClassDecorator | ParameterDecorator | MethodDecorator | PropertyDecorator; export type RequestMethodTypes = 'all' | 'delete' | 'get' | 'post' | 'head' | 'options' | 'patch' | 'put'; export type PropertyDecoratorTypes = 'query' | 'body' | 'session' | 'files' | 'params' | 'header' | 'headers' | 'request' | 'response'; export type Database = Knex export interface ServerStaticOptions { root: string; options: KoaStatic.Options; } export interface ServerSessionOptions { key?: string; maxAge?: number; overwrite: boolean; httpOnly: boolean; signed?: boolean; rolling?: boolean; } export interface ServerBodyOptions extends KoaBody.IKoaBodyOptions { } export interface ServerDatabaseOptions extends Knex.Config { } export interface SpaOptions { root: string; filename: string; } export interface ProxyTableOptions { [key: string]: Config; } export interface OptionsConfigs { database?: ServerDatabaseOptions; static?: ServerStaticOptions; session?: ServerSessionOptions; body?: KoaBody.IKoaBodyOptions; i18n?: I18n; spa?: SpaOptions; proxy?: boolean; proxyTable?: ProxyTableOptions; response?: { type?: 'json' | 'raw' } } export interface Options { debug?: boolean; https?: ServerOptions; port?: number; language?: 'zh-CN' | 'en-US'; configs?: OptionsConfigs; controllers?: Array; middlewares?: Array proxy?: boolean; proxyTable?: ProxyTableOptions; keys?: string[]; spa?: SpaOptions; ssr?: Middleware; } export interface Server { app: Koa; port: number; } export interface ServerConstructor { app: Koa; port: number; new(options: Options): Server; } export interface Parameters { type?: PropertyDecoratorTypes; parameterIndex?: number; args?: string | string[] | object; } export interface ControllerOptionsParameter{ [key: string]: Array } export interface ControllerOptionsMethod { [key: string]: { propertyKey?: string; type?: RequestMethodTypes | RequestMethodTypes[]; method?: () => Promise }; } export interface ControllerOptionsDatabase { [key:string]: string | null; } export interface ControllerOptions { propertys?: { [key:string]: PropertyDecoratorTypes }; parameters?: ControllerOptionsParameter; databases?: ControllerOptionsDatabase; methods?: ControllerOptionsMethod; registerServices?: (ctx: Context) => any []; mixinsPropertys?: (ctx: Context) => void; mixinsParameters?: (ctx: Context,propertyKey: string, option: Options) => any[]; mixinsDatabases?: (ctx: Context, config: ServerDatabaseOptions) => void; baseRoute?: string; metaData?: { new (...args:any[]):any }[]; target?: Controller; } export interface Controller { new(...args: Array): any | void; $options?: ControllerOptions; [key:string]: any; } export interface Validator { data:T; } }