xinsirnuxt改造(下)—— 进阶改造

243 阅读5分钟

文章摘要: 这是一个将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…

原文地址:xinsirnuxt改造(下)—— 进阶改造