Typescript微软开发的自由和开源的变成语言,是 Javascript 的超集,它可以编译成 Javascript。Typescript 支持 Javascript语法,同时它又包含类型定义接口枚举泛型等很多后端语言的特点,能在编译支持类型检查,因此可以很好的提升代码质量

本文演示如何使用 Typescipt 搭建一个 Nodejs Server(非常像 Spring MVC),所使用的主要框架插件如下

主要框架和插件概述

Koa2 框架

Nodejs 自诞生以来,产生了很多用于构建 web service框架,其中 express 框架是比较出名的,一套快速、极简、开放web 开发框架,我们可以先看下创建 Nodejs Server一个变化(以 Javascript 作为示例)。

最初,只使用 Nodejs 创建Server 和 路由

const http = require('http');

const routes = {
    '/': indexHandler
}

const indexHandler = (req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-type', 'text/plain');
    res.end('&lt;h1&gt;Welcome!</h2>');
}

const server = http.createServer((req, res) => {
    const url = req.url;
    if(routes[url]) {
        routes[url](req, res);
    } else {
        res.statusCode = 404;
        res.setHeader('Content-type', 'text/plain');
        res.end('404 - Not Found')
    }
});

server.listen(3000, () => {
    console.log('server start at 3000');
})

然后,使用 express 框架创建 Server 和 路由可以看到创建 server 的过程express 框架处理完成了,开发者可以更加关注业务实现,而不用花很多时间在通用代码逻辑上:

const express = require("express");
const app = express();

app.get("/", (req, res) => {
    res.write('<h1>Welcome!</h2>');
    res.end();
});

app.listen(3000, () => {
    console.log('server start at 3000')
});

koa2 框架是 express 框架的开发人员原班人马,基于ES6的新特性而开发的敏捷框架,相比于 expresskoa2 框架更加的轻量化,用 asyncawait实现异步流程控制解决地狱回调和麻烦的错误处理。Express 和 Koa2 框架的主要区别如下

  1. express 框架中集成了很多的中间件比如路由视图等等,而koa2框架不集成任何的中间件(因此它更轻量),需要时由开发人员自主安装中间件比如路由中间件 koa-router,这看起来虽然麻烦,但是不一定是坏事,这让整体代码变得更加可控

  2. 对于异步流程控制express 框架使用回调函数方式callback 或者 promise),随着代码变得复杂,一层一层的回调足以让开发者调试时变得头疼(所以被称作地狱回调),而 koa2 框架使用 asyncawait处理异步控制,使得代码运行时看起来像是同步,所以开发人员可以更方便的进行代码调试,也使得代码逻辑变得更容易理解

    async 将函数声明异步,所以此函数不会阻塞后续代码的执行async自动将函数转换成 Promise,但是必须要等到 async 函数内部执行完毕之后,才会执行 then() 回调函数;async 函数内部的 await ,会让函数内部代码执行阻塞住,只要 await这个函数返回 Promise 对象 resolve,才会继续执行 await 后面的代码,因此,所有的代码在最终表现上看起来就像是同步执行了。

  3. 对于错误处理express 框架使用回调函数来处理,对于深层次的错误无法捕获koa2 框架使用 trycatch捕获异常,能很好的解决异步捕获(可见后面代码示例koa 定义全局error handler)。

  4. express 是线性模型koa2 是洋葱模型,即所有请求在经过中间件时会执行两次,所有可以比较方便的进行前置和后置的处理。关于洋葱模型更直观的解释,请看如下示例

    const Koa = require('koa');
    
    const app = new Koa();
    const mid1 = async (ctx, next) => {
        ctx.body = '';
        ctx.body += 'request: mid1 中间件n';
        await next();
        ctx.body += 'response: mid1 中间件n';
    }
    const mid2 = async (ctx, next) => {
        ctx.body += 'request: mid2 中间件n';
        await next();
        ctx.body += 'response: mid2 中间件n';
    }
    app.use(mid1);
    app.use(mid2);
    
    app.use(async (ctx, next) => {
        ctx.body += 'This is bodyn'
    })
    
    app.listen(3000);
    

    访问 http://localhost:3000,将会看到如下结果

    request: mid1 中间件
    request: mid2 中间件
    This is body
    response: mid2 中间件
    response: mid1 中间件
    
  5. express 框架中有 request 和 response 两个对象,而 koa2 框架把这两个对象统一到了 context 对象中。

koa2 框架创建 Server 和 路由示例需要额外安装 @koa/router 插件):

const Koa = require('koa');
const router = require('@koa/router')();
const app = new Koa();

router.get('/', async (ctx, next) => {
    ctx.body = '<h1>Welcome!</h2>';
})

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000,()=>{
    console.log('server start at 3000')
});

routing-controllers

routing-controllers一个基于 express/koa2 的 nodejs 框架,它提供了大量的装饰器(就像是 SpringMVC 中的注解,但是装饰器和注解是完全不同概念,虽然在语法上非常相似),比如下面这个示例

@Controller('/user')
@UseBefore(RequestFilter)
export class UserController {

    private logger = LogUtil.getInstance().getLogger();

    constructor(
        private userService: UserService
    ) {
        this.logger.debug('UserController init');
    }

    @Get('/list')
    async getUserList() {
        const users = await this.userService.getAllUser();
        return users;
    }

    @Get('/:uuid')
    async getUserByUuid(@Param('uuid') uuid: string) {
        this.logger.debug(`get user by uuid ${uuid}`);
        const user = await this.userService.getUserByUuid(uuid);
        return user;
    }

    @Post()
    createUser(@Body() userParam: UserParam) {
        this.logger.debug('create user with param ', JSON.stringify(userParam));
        return this.userService.saveUser('', userParam);
    }

    @Put('/:uuid')
    updateUser(@Param('uuid') uuid: string, @Body() userParam: UserParam) {
        this.logger.debug(`update user ${uuid} with param `, JSON.stringify(userParam));
        return this.userService.saveUser(uuid, userParam);
    }

    @Delete('/:uuid')
    deleteUser(@Param('uuid') uuid: string) {
        this.logger.debug(`delete user ${uuid}`);

        return this.userService.deleteUser(uuid);
    }
}

用 @Get @Post 这种装饰器,可以极大的方便我们定义路由,整个代码风格也更偏向于后端代码风格需要注意的是,因为 routing-controllers基于 express/koa2 上的二次开发,所以我们开发时可以尽量用 routing-controllers 提供的语法糖来实现代码逻辑

Typescript装饰

Typescript装饰器是一种特殊类型的声明,它能被附加到类、方法属性或者参数上,使用 @expression 这种格式。expression求值后必须为一个函数,它会在运行时被调用,被装饰声明信息做为参数传入。

TypeScript装饰器有如下几种

  1. 装饰器,用于类的构造函数
  2. 方法装饰器,用于方法属性描述符上。
  3. 方法参数装饰器,用于方法参数上。
  4. 属性装饰器,用于类的属性上。

多个参数装饰器时,从最后一个参数依次向前执行,方法装饰器和方法参数装饰器中方法参数装饰器先执行,类装饰器总是最后执行,方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。

routing-controllers 装饰器的实现以及 MetadataArgsStorage

MetadataArgsStorage 是一个全局单例对象用来保存整个代码运行期间的全局数据

routing-controllers 实现的主要逻辑就是先实现一些装饰器,将信息存储到 MetadataArgsStorage 中,然后 createServer 时,再从 MetadataArgsStorage 将信息取出来,进行 express/koa2 框架需要初始化工作

比如 Get 装饰器源码,将路由信息存入 MetadataArgsStorage:

export function Get(route?: string|RegExp): Function {
    return function (object: Object, methodName: string) {
        getMetadataArgsStorage().actions.push({
            type: "get",
            target: object.constructor,
            method: methodName,
            route: route
        });
    };
}

然后比如创建 koa server时,通过 registerAction 等函数,将 MetadataArgsStorage 中的信息取出来,注册到 koa2 框架中。

/**
 * Integration with koa framework.
 */
export class KoaDriver extends BaseDriver {
    constructor(public koa?: any, public router?: any) {
        super();
        this.loadKoa();
        this.loadRouter();
        this.app = this.koa;
    }

    /**
     * 初始化server
     */
    initialize() {
        const bodyParser = require("koa-bodyparser");
        this.koa.use(bodyParser());
        if (this.cors) {
            const cors = require("kcors");
            if (this.cors === true) {
                this.koa.use(cors());
            } else {
                this.koa.use(cors(this.cors));
            }
        }
    }

    /**
     * 注册中间件
     */
    registerMiddleware(middleware: MiddlewareMetadata): void {
        if ((middleware.instance as KoaMiddlewareInterface).use) {
            this.koa.use(function (ctx: any, next: any) {
                return (middleware.instance as KoaMiddlewareInterface).use(ctx, next);
            });
        }
    }

    /**
     * 注册action
     */
    registerAction(actionMetadata: ActionMetadata, executeCallback: (options: Action) => any): void {
        // ...一些处理action逻辑

        const uses = actionMetadata.controllerMetadata.uses.concat(actionMetadata.uses);
        const beforeMiddlewares = this.prepareMiddlewares(uses.filter(use => !use.afterAction));
        const afterMiddlewares = this.prepareMiddlewares(uses.filter(use => use.afterAction));

        const route = ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute);
        const routeHandler = (context: any, next: () => Promise<any>) => {
            const options: Action = {request: context.request, response: context.response, context, next};
            return executeCallback(options);
        };

        // 将所有action注册到koa中
        this.router[actionMetadata.type.toLowerCase()](...[
            route,
            ...beforeMiddlewares,
            ...defaultMiddlewares,
            routeHandler,
            ...afterMiddlewares
        ]);
    }

    /**
     * 注册路由
     */
    registerRoutes() {
        this.koa.use(this.router.routes());
        this.koa.use(this.router.allowedMethods());
    }

    /**
     * 动态加载koa
     */
    protected loadKoa() {
        if (require) {
            if (!this.koa) {
                try {
                    this.koa = new (require("koa"))();
                } catch (e) {
                    throw new Error("koa package was not found installed. Try to install it: npm install koa@next --save");
                }
            }
        } else {
            throw new Error("Cannot load koa. Try to install all required dependencies.");
        }
    }

    /**
     * 动态加载koa-router
     */
    private loadRouter() {
        if (require) {
            if (!this.router) {
                try {
                    this.router = new (require("koa-router"))();
                } catch (e) {
                    throw new Error("koa-router package was not found installed. Try to install it: npm install koa-router@next --save");
                }
            }
        } else {
            throw new Error("Cannot load koa. Try to install all required dependencies.");
        }
    }

	...
}

routing-controllers 的装饰器功能强大,除了路由外,还支持对于 http request 参数解析、参数校验拦截器等很多的装饰器,具体可以参考 routing-controllers

题外话:

注解与装饰器:

Sequelize

sequelize 是一个功能强大的 nodejs 数据库插件,支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server 这几个常见数据库能够帮助开发者方便的进行数据库连接数据库增删改查操作,也支持事务、池化、钩子高级特性,具体请查看 Sequelize

搭建 Typescript 编写的 Nodejs Server

1. 项目初始化

首先执行 npm init初始化基本package.json 文件然后执行以下命令安装一些基本依赖

npm install -D typescript     // 基本typescript 依赖
npm install -D @types/node    // 在 typescriptimport nodejs类库需要的类型声明插件
npm install -D ts-node        // 直接运行 ts 代码的插件
npm install -D nodemon        // 检测文件变化的插件,方便调试部署

然后执行 npx tsc init生成默认tsconfig.json 文件,这个文件定义了 typescript 编译时的一些设置,具体的参数含义,可以参考生成文件中的说明比较重要的参数如下:

{
  "compilerOptions": {
    /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "target": "es2016",
    "experimentalDecorators": true,     /* Enable experimental support for legacy experimental decorators. */
    "emitDecoratorMetadata": true,      /* Emit design-type metadata for decorated declarations in source files. */
    "module": "commonjs",               /* Specify what module code is generated. */
    "rootDir": "./src",                 /* Specify the root folder within your source files. */
    "moduleResolution": "node",         /* Specify how TypeScript looks up a file from a given module specifier. */
    "types": [   /* Specify type package names to be included without being referenced in a source file. */
      "node"
    ],
    "sourceMap": true,                  /* Create source map files for emitted JavaScript files. */
    "outDir": "./dist",                 /* Specify an output folder for all emitted files. */
    /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
    "strict": true,         /* Enable all strict type-checking options. */
    "noImplicitAny": true,  /* Enable error reporting for expressions and declarations with an implied 'any' type. */
    "skipLibCheck": true    /* Skip type checking all .d.ts files. */
  }
}

package.json 文件的 scripts 中定义一些基本脚本

"scripts": {
    。。。,
    
    "build": "tsc",
    "start": "nodemon --watch src/**/*.ts --exec "ts-node" src/app.ts local"
}

执行 npm run build 可以将 ts 代码编译为 js 文件

执行 npm run start 可以直接运行 ts 代码,并在修改代码时热部署而不必手动重启

2. 安装必要组件

routing-controllers 相关

根据 routing-controllers 的文档,执行以下命令安装必要的框架依赖和 types 声明依赖:

npm install koa koa-bodyparser @koa/router @koa/multer routing-controllers reflect-metadata class-transformer
npm install -D @types/koa @types/koa-bodyparser

可选的插件,需要使用相关功能时才选择安装

npm install @koa/cors class-validator typedi
npm install -D @types/validator

@koa/cors 允许跨域访问

class-transformer转换插件,后面会细说

class-validator: 类属性校验插件

typedi 自定义依赖注入所用的插件,比如将某个类注册成 Service 可供其他类注入并使用

数据库

npm install sequelize @journeyapps/sqlcipher

Log4js(可选)

npm install log4js
npm install -D @types/log4js

ini 文件操作(可选)

npm install ini
npm install @types/ini

3. 创建代码目录

创建 src 目录存放源码(这个目录要与 tsconfig.json 文件中定义的 rootDir 一致),然后按照实际业务,某个业务逻辑范围内的代码,放到同一个目录下,比如示例的目录结构如下:

/src             // 源代码文件夹
/--/app.ts       // 程序主入口
/--/common/      // 通用工具类,实体类
/--/filter/      // 过滤器
/--/user/        // 与用户相关的 controller/service/实体类
/--/others/      // 其他自定义业务代码文件
/package.json
/tsconfig.json

4. 程序主入口

import 'reflect-metadata';  // 此依赖为 routing-controllers 插件必须引入的依赖

import Koa, { Context, Next } from 'koa';
import { useContainer, useKoaServer } from 'routing-controllers';
import { Container } from "typedi";
import { UserController } from './user/user-controller';
import { LogUtil } from './common/log-util';
import { ConfigUtil, CONFIG_SECTION, CONFIG_KEY } from './common/config-util';
import { ResponseFilter } from './filter/response-filter';
import { RestJson } from './common/rest-json';
import { AnkonError, ERROR_CODE, ERROR_MSG } from './common/ankon-error';

const LOGGER = LogUtil.getInstance().getLogger();

// 启用依赖注入,目的是为了 @Service 注解能正常使用
useContainer(Container);

const app: Koa = new Koa();
// 自定义统一的全局 error handler
app.use(async (ctx: Context, next: Next) => {
    try {
        await next();
    } catch (err: any) {
        if (err.errorCode) {
            // 自定义错误
            ctx.status = 200;
            const result = new RestJson();
            const error = new AnkonError(err.errorCode, err.errorMsg, err.errorDetail);
            result.createFail(error);
            ctx.body = result;
        } else {
            // 未知异常
            ctx.status = err.status || 500;
            const result = new RestJson();
            const error = new AnkonError(ERROR_CODE.FAIL, ERROR_MSG.FAIL, err.message);
            result.createFail(error);
            ctx.body = result;
        }
    }
})

// 使用 routing-controllers 进一步初始化 app 配置
useKoaServer(app, {
    cors: true,
    // classTransformer: true, // 此配置可以将参数转换成类对象,并包含class的所有方法
    defaultErrorHandler: false, // 关闭默认的 error handler载入自定义 error handler
    controllers: [
        UserController
    ],
    interceptors: [
        ResponseFilter
    ]
});
const port = ConfigUtil.getInstance().getConfig(CONFIG_SECTION.SERVER, CONFIG_KEY.PORT);

app.listen(port, () => {
    LOGGER.info(`Node Server listening on port ${port}`);
});

5. routing-controllers 的实际应用

以 UserController 为例

  • 使用 @Controller(‘/user’) 装饰器声明为路由,并定义基础路由路径
  • 使用 @UseBefore(RequestFilter) 声明引用 RequestFilter 这个中间件,并且会在函数执行之前使用,相当于 filter。对应的,还有 @UseAfter,相当于后置过滤器,它们都属于中间件
  • 构造函数 private userService: UserService 即为依赖注入,将 UserService 注入到 UserController 中进行使用
  • @Get(‘/list’)/@Post() 等方法使用对应的装饰器来实现路由以及内部业务逻辑
import { Body, Controller, Delete, Get, Param, Post, Put, UseBefore } from 'routing-controllers';
import { Service } from 'typedi';

import { LogUtil } from '../common/log-util';
import { UserService } from './user-service';
import { UserParam } from './user-param';
import { RequestFilter } from '../filter/request-filter';

@Service()  // 因为额外使用的 typedi,所以此处必须加上这个注解
@Controller('/user')
@UseBefore(RequestFilter)
export class UserController {

    private logger = LogUtil.getInstance().getLogger();

    constructor(
        private userService: UserService
    ) {
        this.logger.debug('UserController init');
    }

    @Get('/list')
    async getUserList() {
        const users = await this.userService.getAllUser();
        return users;
    }

    @Get('/:uuid')
    async getUserByUuid(@Param('uuid') uuid: string) {
        this.logger.debug(`get user by uuid ${uuid}`);
        const user = await this.userService.getUserByUuid(uuid);
        return user;
    }

    @Post()
    createUser(@Body() userParam: UserParam) {
        this.logger.debug('create user with param ', JSON.stringify(userParam));
        return this.userService.saveUser('', userParam);
    }

    @Put('/:uuid')
    updateUser(@Param('uuid') uuid: string, @Body() userParam: UserParam) {
        this.logger.debug(`update user ${uuid} with param `, JSON.stringify(userParam));
        return this.userService.saveUser(uuid, userParam);
    }

    @Delete('/:uuid')
    deleteUser(@Param('uuid') uuid: string) {
        this.logger.debug(`delete user ${uuid}`);

        return this.userService.deleteUser(uuid);
    }
}

UserService:

import { Service } from 'typedi';
import { randomUUID } from 'crypto';

import { UserModel } from './user-model';
import { UserParam } from './user-param';
import { LogUtil } from '../common/log-util';
import { UserDTO } from './user-dto';
import { ListDTO } from '../common/list-dto';
import { AnkonError, ERROR_CODE, ERROR_MSG } from '../common/ankon-error';

@Service()
export class UserService {

    private logger = LogUtil.getInstance().getLogger();

    /**
     * 更新或者新增一个用户信息
     * 如果 uuid 传值,则更新用户
     * 如果 uuid 不传值,则新建用户
     * 
     * @param uuid 
     * @param userParam 
     * @returns 
     */
    async saveUser(uuid: string, userParam: UserParam): Promise<UserDTO> {
        if (uuid) {
            // 更新
            const userFromDB = await UserModel.findByPk(uuid);
            if (userFromDB) {
                // 更新
                this.logger.debug(`find user ${uuid}, will update`);
                const userAfterUpdate = await userFromDB.update(userParam);

                return new UserDTO(userAfterUpdate);
            } else {
                throw new AnkonError(ERROR_CODE.USER_NOT_FOUND, ERROR_MSG.USER_NOT_FOUND, `user ${uuid} not found`);
            }
        } else {
            // 新增
            const uuid = randomUUID().replace(/-/g, '');
            const data: any = {};
            Object.assign(data, userParam);
            data.uuid = uuid;
            return UserModel.create(data);
        }
    }

    /**
     * 根据 uuid 获取用户信息(uuid 为主键)
     * 
     * @param uuid 
     * @returns 
     */
    async getUserByUuid(uuid: string): Promise<UserDTO> {
        const userModel = await UserModel.findByPk(uuid);
        if (!userModel) {
            throw new AnkonError(ERROR_CODE.USER_NOT_FOUND, ERROR_MSG.USER_NOT_FOUND, `user ${uuid} not found`);
        }
        return new UserDTO(userModel);
    }

    /**
     * 获取所有用户列表
     * 
     * @returns 
     */
    async getAllUser(): Promise<ListDTO<UserDTO>> {
        const result = await UserModel.findAndCountAll();
        const users: UserDTO[] = new Array();
        result.rows.forEach((userModel: UserModel) => {
            const user: UserDTO = new UserDTO(userModel);
            users.push(user);
        })
        return new ListDTO(users, result.count);
    }

    /**
     * 删除用户信息返回 true/false
     * 
     * @param uuid 
     * @returns 
     */
    async deleteUser(uuid: string): Promise<boolean> {
        const count = await UserModel.destroy({ where: { uuid: uuid } });
        return count > 0;
    }
}

6. 数据库操作

定义数据库 DBUtil,单例模式工具类,此工具实例化 Squelize 对象,并且根据 ini 文件的配置,决定数据库连接和参数

import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { Sequelize } from 'sequelize';
import { LogUtil } from './log-util';
import { CONFIG_KEY, CONFIG_SECTION, ConfigUtil } from './config-util';
import { InternalServerError } from 'routing-controllers';

const DATABASE_TYPE = {
    MYSQL: 'mysql',
    SQLITE: 'sqlite'
}

const iv = 'ankon_encryptkey';
const key = '86e1e84b81e5787a122441f9548ea2df';

export class DBUtil {

    private logger = LogUtil.getInstance().getLogger();

    private sequelize: Sequelize;

    private static dbUtil: DBUtil;

    private constructor() {
        this.logger.debug('DBUtil init');

        const dialect = ConfigUtil.getInstance().getConfig(CONFIG_SECTION.DATABASE, CONFIG_KEY.DIALECT);
        const host = ConfigUtil.getInstance().getConfig(CONFIG_SECTION.DATABASE, CONFIG_KEY.HOST);
        const database = ConfigUtil.getInstance().getConfig(CONFIG_SECTION.DATABASE, CONFIG_KEY.DATABASE);
        const username = ConfigUtil.getInstance().getConfig(CONFIG_SECTION.DATABASE, CONFIG_KEY.USER_NAME);

        if (dialect == DATABASE_TYPE.MYSQL) {
            const password = ConfigUtil.getInstance().getConfig(CONFIG_SECTION.DATABASE, CONFIG_KEY.PASSWORD);
            this.sequelize = new Sequelize({
                dialect: 'mysql',
                host: host,
                username: username,
                password: password,
                database: database,
                logging: (msg) => this.logger.debug(msg),
                define: {
                    charset: 'utf8mb4'
                }
            })
        } else if (dialect == DATABASE_TYPE.SQLITE) {
            this.sequelize = new Sequelize({
                dialect: 'sqlite',
                storage: path.join(host, database),
                logging: (msg) => this.logger.debug(msg),
                password: this.getSqlitePassword(),
                dialectModulePath: '@journeyapps/sqlcipher'
            })
        } else {
            throw new InternalServerError(`database ${dialect} not suppoorted`);
        }

        if (this.sequelize) {
            this.sequelize.sync();
        }
    }

    public static getInstance(): DBUtil {
        if (!this.dbUtil) {
            this.dbUtil = new DBUtil();
        }
        return this.dbUtil;
    }

    /**
     * 获取 Sequelize 实例化的对象
     * 
     * @returns 
     */
    public getSequelize() {
        return this.sequelize;
    }

    /**
     * 获取 sqlite密码
     * 如果本地密码文件存在,则读取解密
     * 如果不存在,则创建一个新的密码文件
     * 
     * @returns 
     */
    private getSqlitePassword(): string {
        let password = '';

        const basePath = ConfigUtil.getInstance().getConfig(CONFIG_SECTION.DATABASE, CONFIG_KEY.HOST);
        const databaseKey = ConfigUtil.getInstance().getConfig(CONFIG_SECTION.DATABASE, CONFIG_KEY.PASSWORD);
        const keyFilePath = path.join(basePath, databaseKey);
        if (fs.existsSync(keyFilePath)) {
            // 读取文件内容解密
            const pwdBuffer = fs.readFileSync(keyFilePath);
            const cipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
            let passwordBuffer = cipher.update(pwdBuffer);
            passwordBuffer = Buffer.concat([passwordBuffer, cipher.final()]);
            password = passwordBuffer.toString();
        } else {
            // 动态生成密码加密之后写入文件
            const passwordBuffer = crypto.randomBytes(32);
            const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
            let data = cipher.update(passwordBuffer);
            data = Buffer.concat([data, cipher.final()]);
            fs.writeFileSync(keyFilePath, data);

            password = passwordBuffer.toString();
        }

        this.logger.debug('init sqlite database with password ', password);

        return password;
    }
}

声明数据库实体类,以 UserModel 为例

import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
import { DBUtil } from '../common/db-util';

export class UserModel extends Model<
    InferAttributes<UserModel>,
    InferCreationAttributes<UserModel>> {
        declare uuid: string;
        declare userName: string;
        declare nickName: CreationOptional<string>
}

UserModel.init({
    uuid: {
        type: DataTypes.STRING(32),
        primaryKey: true,
        allowNull: false,
        comment: '主键,用户唯一标识'
    },
    userName: {
        type: DataTypes.STRING(64),
        allowNull: false,
        comment: '用户姓名'
    },
    nickName: {
        type: DataTypes.STRING(64),
        comment: '用户昵称'
    }
}, {
    sequelize: DBUtil.getInstance().getSequelize(),
    timestamps: false,
    createdAt: false,
    updatedAt: false,
    freezeTableName: true,
    tableName: 'user'
})

7. 自定义中间件和拦截器

实现一个自定义中间件,只要继承 KoaMiddlewareInterface 并实现 use 方法即可,KoaMiddlewareInterface 为 routing-controllers 封装实现的 Koa 的中间件。

import { KoaMiddlewareInterface } from 'routing-controllers';
import { Service } from 'typedi';
import { LogUtil } from '../common/log-util';

/**
 * 请求拦截器
 * 可用于签名校验、用户校验等
 * 
 */
@Service()
export class RequestFilter implements KoaMiddlewareInterface {

    private logger = LogUtil.getInstance().getLogger();

    use(context: any, next: (err?: any) => Promise<any>): Promise<any> {
        this.logger.debug('in request filter');
        this.logger.debug(`get request header content-type = ${context.headers['content-type']}`);
        return next();
    }
}

中间件使用时,可以用 @UseBefore 或者 @UseAfter,在路由之前或者之后应用(这就对应了 koa2 的洋葱模型,每个中间件可以执行两次)。@UseBefore 或者 @UseAfter 可以声明在 Controller 类上,也可以声明到某个具体的函数之上。

全局中间件的定义,需要使用 @Middleware 这个装饰器来声明,并且在 app.ts 初始化指定使用,具体参考 routing-controllers 说明文档

拦截器 Interceptor,本质还是个中间件,其实就是相当于实现一个 KoaMiddlewareInterface 并且 @UseAfter,routing-controllers 定义了 InterceptorInterface 来更方便的实现拦截器,并且可以全局应用

如下,以ResponseFilter为例,定义一个全局的拦截器,该拦截拦截所有 response,将结果封装为 RestJson 对象:

import { Action, Interceptor, InterceptorInterface } from 'routing-controllers';
import { LogUtil } from '../common/log-util';
import { Service } from 'typedi';
import { ListDTO } from '../common/list-dto';
import { RestJson } from '../common/rest-json';

/**
 * 返回值拦截器
 * 可以在此对于返回值做一些处理
 * 
 */
@Service()
@Interceptor()
export class ResponseFilter implements InterceptorInterface {

    private logger = LogUtil.getInstance().getLogger();

    intercept(action: Action, result: any) {
        this.logger.debug('in response filter ', result);
        const restJson = new RestJson<any>();
        restJson.createSuccess();
        if (result instanceof ListDTO) {
            restJson.setTotal(result.count);
            restJson.setData(result.data);
        } else {
            restJson.setData(result);
        }

        return restJson;
    }

}

同时,app.ts 中需要声明:
useKoaServer(app, {
    。。。
    interceptors: [
        ResponseFilter
    ]
});

非全局的拦截器,只需要删除拦截器类声明上的 @Interceptor(),在具体需要使用的地方用 @UseInterceptor(ResponseFilter) 来装饰即可,可参考 routing-controllers

8. 自定义错误以及错误处理

routing-controllers 中预置了很多通用错误:

  • HttpError
  • BadRequestError
  • ForbiddenError
  • InternalServerError
  • MethodNotAllowedError
  • NotAcceptableError
  • NotFoundError
  • UnauthorizedError

HttpError extends Error,其他的 Error 都是 extends HttpError。如果这些错误不能涵盖业务代码的所有错误,可以自定义错误类,同样 extends HttpError 即可。

import { HttpError } from "routing-controllers";

export class AnkonError extends HttpError  {
    errorCode!: number;
    errorMsg!: string;
    errorDetail?: string;

    constructor(errorCode: number, errorMsg: string, errorDetail?: string) {
        super(500, errorMsg);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
        this.errorDetail = errorDetail;
    }
}

export const ERROR_CODE = {
    SUCCESS: 0,
    FAIL: -1,
    // user 相关错误
    USER_NOT_FOUND: 10000
}

export const ERROR_MSG = {
    SUCCESS: 'success',
    FAIL: 'fail',
    // user 相关错误
    USER_NOT_FOUND: 'user not found'
}

使用时,
throw new AnkonError(ERROR_CODE.USER_NOT_FOUND, ERROR_MSG.USER_NOT_FOUND, `user ${uuid} not found`);

当错误发生时,请求被意外中断,因此 @UseAfter 的中间件不会被调用,所以不能通过定义全局的中间件或者拦截器来处理异常,只能在 app.ts 中事先处理掉,并且关闭 defaultErrorHandler

const app: Koa = new Koa();
// 自定义统一的全局 error handler
app.use(async (ctx: Context, next: Next) => {
    try {
        await next();
    } catch (err: any) {
        if (err.errorCode) {
            // 自定义错误
            ctx.status = 200;
            const result = new RestJson();
            const error = new AnkonError(err.errorCode, err.errorMsg, err.errorDetail);
            result.createFail(error);
            ctx.body = result;
        } else {
            // 未知异常
            ctx.status = err.status || 500;
            const result = new RestJson();
            const error = new AnkonError(ERROR_CODE.FAIL, ERROR_MSG.FAIL, err.message);
            result.createFail(error);
            ctx.body = result;
        }
    }
})

useKoaServer(app, {
    。。。
    defaultErrorHandler: false, // 关闭默认的 error handler,载入自定义 error handler
});

9. 其他装饰器

routing-controllers 中提供了多种多样的装饰器,具体可以参考 routing-controllers 装饰器参考

class-transformer

大家应该注意到,在 app.ts 中,有一行注释掉的代码 classTransformer: true

useKoaServer(app, {
    cors: true,
    // classTransformer: true, // 此配置可以将参数转换成类对象,并包含class的所有方法
    defaultErrorHandler: false, // 关闭默认的 error handler,载入自定义 error handler
    controllers: [
        UserController
    ],
    interceptors: [
        ResponseFilter
    ]
});

routing-controllers 框架中,使用 classTransformer 来将用户参数转换成类对象实例,classTransformer 为 true 和 false 的区别在于,为 true 时实例化的对象包含类的所有属性和方法,而为 false 时,实例化的对象仅包含基础属性,例如

export class User {
  firstName: string;
  lastName: string;

  getName(): string {
    return this.lastName + ' ' + this.firstName;
  }
}

@Controller()
export class UserController {
  post(@Body() user: User) {
    console.log('saving user ' + user.getName());
  }
}

// 当 classTransformer = true 时,可以调用 user.getName() 方法
// 当 classTransformer = false 时,调用 user.getName() 会报错

当然,在我们日常开发中,也可以使用 class-transformer转换比如 JSON 数据为一个具体的类对象

import { plainToClass } from 'class-transformer';

const userJson = {
    firstName: 'zhang',
    lastName: 'san'
}
const user = plainToClass(User, userJson);

编译运行

按照 package.json 文件中的定义,执行 npm run build 将所有 src 目录下的 ts 文件编译成 js 文件,并且输出dist 目录下,然后将 dist 目录下的所有文件,以及 node_modules 文件夹一起打包即可正常运行 node app.js

原文地址:https://blog.csdn.net/zxcvqwer19900720/article/details/131249759

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_35664.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注