文章摘要: 这是一个将Typescript与node框架(本模板暂时只支持koa,后续可以支持express,或者使用者自行配置)、以及vue同构框架nuxt结合的模板。
改造上的文章地址:xinsirnuxt改造(上)—— 基础改造
正常情况下,routing-controllers、class-validator,typedi都是组合使用的,所以后续都是添加这些配置。如果读者对这些都比较熟悉的话,可以尝试自己优化一下项目目录,然后跟笔者做的处理进行对比。进阶改造开始:
1.目录改造
src├ bin // 启动文件
├ controller // 方法
├ middlewares // 中间件
├ ...
├ index.ts // 入口文件
├ nuxt.config.ts // nuxt配置文件
可以类似上面的目录结构将代码进行划分,这样能帮助你更好的管理代码。
2.全局异常捕获中间件
编写全局异常捕获中间件,让你在写代码的时候不用过多关注有关异常的处理。
// src/middlewares/errorHandle.ts
/**
* 异常捕获
* @param ctx 上下文
* @param next
*/
export async function errorHandler (ctx, next) {
let status:number = 200;
let errorMsg:any;
try {
await next();
} catch (err) {
status = 500;
errorMsg = err;
}
if (status === 200) {
return;
}
try {
const errorRes = {
errorCode: 1000,
msg: '系统发生错误,请稍后重试'
};
ctx.response.status = 200;
// 系统异常错误
console.log('❌ 系统发生错误', errorMsg);
// TODO: 可以在此处添加告警处理
ctx.response.body = errorRes;
} catch (error) {
console.error('❌ 抵达兜底错误', errorMsg, error);
}
}
改写启动文件,将全局捕获异常中间件放在最前方,利用洋葱模型捕获后面代码的异常。
// src/bin/app.ts
import 'reflect-metadata';
import Koa from "koa";
import KoaBodyParser from "koa-bodyparser";
import { useKoaServer,useContainer } from "routing-controllers";
import { Container } from "typedi";
import { Nuxt, Builder } from 'nuxt';
import path from "path";
import config from '../nuxt.config';
import { errorHandler } from '../middlewares/errorHandle';
const PORT = 3000
class App {
protected app = new Koa();
constructor () {
this.init();
}
init () {
// typedi 注入到 routing-controllers
useContainer(Container);
// 中间件处理
this.combinationPlugins();
// 项目启动
this.start();
}
// 组装中间件
combinationPlugins() {
const pluginsList = [
errorHandler, // 处理异常,保证接口返回值正确;必须放在最上面(洋葱模型)
KoaBodyParser({
limit: '20mb',
formLimit: '20mb',
jsonLimit: '20mb',
textLimit: '20mb'
})
];
// 执行中间件
pluginsList.forEach((v) => this.app.use(v));
}
// 启动
async start () {
try {
useKoaServer(this.app, {
defaultErrorHandler: false, // 将默认处理错误设置为false,不让会比我们写的错误处理中间件提前捕获到错误
controllers: [`${path.join(__dirname, '../controller/**.controller{.ts,.js}')}`]
});
// 先匹配接口路由,再匹配页面路由
const nuxt = new Nuxt(config);
await nuxt.ready();
// Build in development
if (config.dev) {
const builder = new Builder(nuxt);
await builder.build();
}
this.app.use((ctx:any) => {
if (ctx.status !== 404) { // 非404,说明是接口路由,如果不return,会被下面的覆盖
return;
}
ctx.status = 200;
ctx.respond = false; // Bypass Koa's built-in response handling
ctx.req.ctx = ctx; // This might be useful later on, e.g. in nuxtServerInit or with nuxt-stash
nuxt.render(ctx.req, ctx.res);
});
console.log(`server run on localhost:${PORT}`);
this.app.listen(PORT);
} catch (e) {
console.error(`❌ 启动出错:${e.message}`);
}
this.errorCatch();
}
// 错误捕捉Unsupported type
private errorCatch (): any {
this.app.on('error', (err, ctx, next) => {
console.error(`❌ 主程序捕捉到错误:${err.message}`);
});
}
}
export default App;
顺便把入口文件也改一下:
// src/index.ts
import App from "./bin/app";
new App();
ps:如果node框架选择的是express,routing-controllers是支持直接配置的,具体见文档:github.com/typestack/r…
目前,笔者只做了koa的处理,以后会推出express版本,还有中间件形式xinsirnuxt模板,敬请期待~
3.参数校验的处理
利用‘class-validator’这个库以及编写model来代替我们之前的if判断
// 以前:
if (!ctx.query.message) // doing anything
// 现在:
// src/controller/user.model.ts
import { IsNotEmpty } from 'class-validator';
export class GetAllQuery {
@IsNotEmpty({
message: '缺少参数test'
})
test: string;
}
做完上面的处理后,我们可以试一下,当缺少参数message的时候,确实是没有继续往下走,但接口返回的是errorCode:1000。此时,我们应该也可以明白,‘class-validator’对于参数校验拦截的原理是通过抛出错误,打断js执行来实现。同时,我们也可以在上方全局捕获异常的中间件添加日志,我们可以发现,他的结构是这样的:
{
httpCode: 400,
name: 'BadRequestError',
message: 'Invalid queries, check \'errors\' property for more info.',
errors:
[ ValidationError {
target: GetAllQuery {},
value: undefined,
property: 'test',
children: [],
constraints: [Object] } ],
paramName: ''
}
// 将errors[0].constraints打印出来
{ isNotEmpty: '缺少参数test' }
至此,我们就可以对全局捕获异常的中间件进行升级,让他也帮我们处理一下参数校验拦截抛出的错误:
// src/middlewares/errorHandle.ts
/**
* 异常捕获
* @param ctx 上下文
* @param next
*/
export async function errorHandler (ctx, next) {
let status:number = 200;
let errorMsg:any;
try {
await next();
} catch (err) {
status = 500;
errorMsg = err;
}
if (status === 200) {
return;
}
try {
const errorRes = {
errorCode: 1000,
msg: '系统发生错误,请稍后重试'
};
ctx.response.status = 200;
console.log('errorMsg', errorMsg.errors[0].constraints);
// 参数校验的错误
if (errorMsg.errors && errorMsg.errors.length) {
const validatorErr = errorMsg.errors[0];
const { constraints = {}, contexts = {} } = validatorErr;
errorRes.errorCode = 1001;
errorRes.msg = (Object.values(constraints)[0] as string) || 'error';
const context = Object.values(contexts)[0];
if (context instanceof Object) {
Object.assign(errorRes, context);
}
ctx.response.body = errorRes;
return;
}
// 系统异常错误
console.log('❌ 系统发生错误', errorMsg);
// TODO: 可以在此处添加告警处理
ctx.response.body = errorRes;
} catch (error) {
console.error('❌ 抵达兜底错误', errorMsg, error);
}
}
4.如何进行开发
最后再大概说明一下,我们接下来如何去写controller
// user.controller.ts
// 引入需要的依赖包
import 'reflect-metadata'
import { JsonController, Param, Body, Get, Post, Put, Delete, QueryParams } from 'routing-controllers'
import { Service, Inject } from 'typedi'
import { UserRepository } from '../repository/UserRepository'
import { GetAllQuery } from './user.model' // 上方提到的model文件
@Service()
@JsonController()
export class UserController {
// 需要注入的service等,写法1:
userRepository = new UserRepository()
// 写法2
@Inject() userRepository: UserRepository
constructor() {
this.userRepository = new UserRepository()
}
// 路由装饰器,不需要写router文件
@Get('/user')
getAll(@QueryParams() query:GetAllQuery /** 使用class-validator来实现参数拦截 **/) {
// 方法内部不需要写处理异常的错误,只专注于业务代码
return this.userRepository.queryAll()
}
@Get('/user/:id')
getOne(@Param('id') id: number) {
return this.userRepository.query(id)
}
@Post('/user')
post(@Body() input: any) {
return this.userRepository.insert(input)
}
@Delete('/user/:id')
remove(@Param('id') id: number) {
return this.userRepository.delete(id)
}
}
如果读者之前写过nuxt,应该知道vue页面的router利用约定路由来实现了免写router文件,而routing-controllers利用来装饰器来实现了免写router文件,而xinsirnuxt正式结合了两种框架的优点,这样我们在xinsirnuxt中开发时,是不需要去单独去写router文件这样的冗余代码,使得开发更加高效。
最后,如果你喜欢这个项目,或者觉得这个项目改造对你有帮助,有启发,请给这个项目点一个赞吧(比心❤️)。
GitHub:github.com/xinsira/xin… Gitee:gitee.com/xinsiroffic… 脚手架:www.npmjs.com/package/xin… 脚手架源码Github:github.com/xinsira/xin… 脚手架源码Gitee:gitee.com/xinsiroffic…