Koa+TypeScript从0到1实现简易CMS框架(三):用户模型、参数校验与用户注册接口

目录

项目地址:koa-typescript-cmsjavascript

前言

用户系统是一个cms最重要的部分,也是最复杂的部分,须要进行不少安全处理。
每次用户请求接口时,咱们要进行参数校验,以防用户传入危险以及不规范数据。java

主要工具库

  • koa web框架
  • koa-bodyparser 处理koa post请求
  • koa-router koa路由
  • sequelize、sequelize-typescript、mysql2 ORM框架与Mysql
  • validator、class-validator 参数校验
  • jsonwebtoken jwt
  • bcryptjs 加密工具
  • reflect-metadata 给装饰器添加各类信息
  • nodemon 监听文件改变自动重启服务
  • lodash 很是好用的工具函数库

项目目录

├── dist                                        // ts编译后的文件
├── src                                         // 源码目录
│   ├── components                              // 组件
│   │   ├── app                                 // 项目业务代码
│   │   │   ├── api                             // api层
│   │   │   ├── service                         // service层
│   │   │   ├── model                           // model层
│   │   │   ├── validators                      // 参数校验类
│   │   │   ├── lib                             // interface与enum
│   │   ├── core                                // 项目核心代码
│   │   ├── middlewares                         // 中间件
│   │   ├── config                              // 全局配置文件
│   │   ├── app.ts                              // 项目入口文件
├── tests                                       // 单元测试
├── package.json                                // package.json                                
├── tsconfig.json                               // ts配置文件
复制代码

初始化Sequelize配置

src/core目录下建立db.ts文件,引入sequelize-typescriptconfig.ts配置文件。node

import { Sequelize, Model } from "sequelize-typescript";
import { config, databaseInterface } from "../config/config";
复制代码

初始化数据库信息

// 数据库配置信息
const { dbName, user, password, host, port }: databaseInterface = config.database;
// 初始化Sequelize
const sequelize: Sequelize = new Sequelize(
  dbName, // 数据库名称
  user, // 数据库用户名
  password, // 数据库密码
  {
    dialect: "mysql", // 数据库引擎
    host, // 数据库地址
    port, // 数据库端口
    logging: true, // 是否打印日志
    timezone: "+08:00", // 设置数据库市区,建议设置,mysql默认的时区比东八区少了八个小时
    define: {
      timestamps: true, // 为模型添加 createdAt 和 updatedAt 两个时间戳字段
      paranoid: true, // 使用逻辑删除。设置为true后,调用 destroy 方法时将不会删队模型,而是设置一个 deletedAt 列。此设置须要 timestamps=true
      underscored: true, // 转换列名的驼峰命名规则为下划线命令规则
      freezeTableName: true // 转换模型名的驼峰命名规则为表名的下划线命令规则
    }
  }
);
复制代码

设置sequelize是否自动建表

sequelize.sync({
  // 是否自动建表
  force: false
});
复制代码

JSON序列化

JSON序列化是使sequelize每次返回都默认排除咱们不想要的字段。 sequelizeModel的原型上会有一个toJSON方法,这个是Model默认的序列化方法,咱们要重写它这个方法:mysql

Model.prototype.toJSON = function(): object {
  // 浅拷贝从数据库获取到的数据
  let data = clone(this['dataValues'])
  // 删除指定字段
  unset(data, 'updatedAt')
  unset(data, 'deletedAt')
  // 这个是本身再Model原型上定义的变量
  // 用于控制咱们再某次查询数据时想要排除的其余字段
  // 类型为数组,数组的值即是想要排除的字段
  // 例如user.exclude['a', 'b'],这次查询将会增长排除a,b字段
  if(isArray(this['exclude'])) {
    this['exclude'].forEach(value => {
      unset(data, value)
    })
  }
  return data;
};
复制代码

所有代码

import { Sequelize, Model } from "sequelize-typescript";
import { config, databaseInterface } from "../config/config";
import { unset,clone, isArray } from "lodash";
// 数据库配置信息
const {
  dbName,
  user,
  password,
  host,
  port
}: databaseInterface = config.database;
// 初始化Sequelize
const sequelize: Sequelize = new Sequelize(
  dbName, // 数据库名称
  user, // 数据库用户名
  password, // 数据库密码
  {
    dialect: "mysql", // 数据库引擎
    host, // 数据库地址
    port, // 数据库端口
    logging: true, // 是否打印日志
    timezone: "+08:00", // 设置数据库市区,建议设置,mysql默认的时区比东八区少了八个小时
    define: {
      timestamps: true, // 为模型添加 createdAt 和 updatedAt 两个时间戳字段
      paranoid: true, // 使用逻辑删除。设置为true后,调用 destroy 方法时将不会删队模型,而是设置一个 deletedAt 列。此设置须要 timestamps=true
      underscored: true, // 转换列名的驼峰命名规则为下划线命令规则
      freezeTableName: true // 转换模型名的驼峰命名规则为表名的下划线命令规则
    }
  }
);

sequelize.sync({
  // 是否自动建表
  force: false
});
Model.prototype.toJSON = function(): object {
  // 浅拷贝从数据库获取到的数据
  let data = clone(this['dataValues'])
  // 删除指定字段
  unset(data, 'updatedAt')
  unset(data, 'deletedAt')
  // 这个是本身再Model原型上定义的变量
  // 用于控制咱们再某次查询数据时想要排除的其余字段
  // 类型为数组,数组的值即是想要排除的字段
  // 例如user.exclude['a', 'b'],这次查询将会增长排除a,b字段
  if(isArray(this['exclude'])) {
    this['exclude'].forEach(value => {
      unset(data, value)
    })
  }
  return data;
};
export { sequelize };
复制代码

建立Users模型

sequelize-typescript建立模型和sequelize建立模型区别仍是挺大的,sequelize-typescript中大部分字段的配置都是基于装饰器来实现。下面直接贴上代码,基本看一遍就知道怎么回事了。git

注意事项:github

  • 千万不要忘记@Table装饰器,少写这个装饰器会报错
  • 也不要忘记向Model里传入泛型
import { sequelize } from "../../core/db";
import {
  Model,
  Table,
  Column,
  DataType,
  PrimaryKey,
  AutoIncrement,
  Unique,
  Comment,
} from "sequelize-typescript";
// 千万不要忘记Table装饰器,少写这个装饰器会报错
// 也不要忘记向Model里传入泛型
@Table
class Users extends Model<Users> {
  @PrimaryKey
  @AutoIncrement
  @Comment("ID")
  @Column(DataType.INTEGER)
  id?: number;

  @Comment("用户昵称")
  @Column(DataType.STRING(128))
  nickname?: string;

  @Unique
  @Comment("用户邮箱")
  @Column(DataType.STRING(128))
  email?: string;

  @Comment("用户密码")
  @Column(DataType.STRING(64))
  password?: string;

  @Unique
  @Comment("微信小程序openid")
  @Column(DataType.STRING(128))
  openid?: string;
}

sequelize.addModels([Users]);

export default Users;

复制代码

参数校验

参数校验是一个系统中必不可少的部分,尤为是先后端分离的架构模式,为了更方便的使用参数校验,咱们须要本身封装一个类,实现代码更高的复用性,此类模仿lin-cms-koa的参数校验的基本功能进行封装。web

Validator封装

src/core文件夹下建立validator.ts文件,引入须要的依赖:sql

import { validateOrReject } from "class-validator";
import { Context } from "koa";
import { cloneDeep } from "lodash";
import { ParametersException } from "./exception";
复制代码

Validator类封装思路:typescript

  1. 解析koa的Context,获取到可能接收到用户传来的参数的字段,进行拍平(扁平化)
  2. 遍历全部参数,将它们的key挂载到原型上。
  3. 使用class-validator进行参数校验。

实现代码:数据库

export class Validator {
  async validate(ctx: Context) {
    const params = {
      ...ctx.request.body,
      ...ctx.request.query,
      ...ctx.params
    };
    const data = cloneDeep(params);
    for (let key in params) {
      this[key] = params[key];
    }
    try {
      await validateOrReject(this);
      return data;
    } catch (errors) {
      let errorResult: string[] = [];
      errors.forEach(error => {
        let messages: string[] = [];
        for (let msg in error.constraints) {
          messages.push(error.constraints[msg]);
        }
        errorResult = errorResult.concat(messages)
      });
      throw new ParametersException({ msg: errorResult });
    }
  }
}
复制代码

具体使用方式在用户注册接口时进行演示

用户注册接口

建立/v1/user/register路由

src/app/api/v1目录下建立users.ts文件,因为咱们以前写了路由自动注册功能,因此咱们只须要将路由导出便可,不须要再app.ts中引入路由。

引入koa-router

import Router from "koa-router";
const router: Router = new Router();
复制代码

设置路由的prefix

router.prefix("/v1/user");
复制代码

建立路由:

router.post("/register", async ctx => {});
复制代码

参数校验

上文咱们已经将Validator类封装好了,在src/app/validators目录下建立UsersValidator.ts文件,参数校验是基于class-validator,具体使用方式能够观看官网文档,直接上基础代码:

/** * 注册验证类 * * @export * @class RegistorValidator * @extends {Validator} */
export class RegistorValidator extends Validator {
  constructor() {
    super();
  }
  @Length(3, 10, {
    message: "用户名长度为3~10个字符"
  })
  nickname?: string;
  @IsEmail({},{ message: "电子邮箱格式错误" })
  email?: string;
  @Validate(CheckPassword)
  // 至少8-16个字符,至少1个大写字母,1个小写字母和1个数字,其余能够是任意字符:
  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/, {
    message: "密码至少8-16个字符,至少1个大写字母,1个小写字母和1个数字"
  })
  password1?: string;
  password2?: string;
}
复制代码

因为咱们须要判断password1password2是否相等,class-validator没有类似功能,咱们本身建立一个校验方法:

/** * 验证密码自定义装饰器 * * @class CheckPassword * @implements {ValidatorConstraintInterface} */
@ValidatorConstraint()
class CheckPassword implements ValidatorConstraintInterface {
  validate(text: string, args: ValidationArguments): boolean {
    const obj: any = args.object;
    return obj.password1 === obj.password2;
  }
  defaultMessage() {
    return "两次输入密码不一致";
  }
}
复制代码

password1属性上能够直接使用装饰器挂载这个自定义方法:

@Validate(CheckPassword)
password1?: string;
复制代码

至此注册接口的校验器完成,所有代码:

import {
  Length,
  IsEmail,
  Matches,
  Validate,
  ValidatorConstraintInterface,
  ValidatorConstraint,
  ValidationArguments
} from "class-validator";

import { Validator } from "../../core/validator";

/** * 验证密码自定义装饰器 * * @class CheckPassword * @implements {ValidatorConstraintInterface} */
@ValidatorConstraint()
class CheckPassword implements ValidatorConstraintInterface {
  validate(text: string, args: ValidationArguments): boolean {
    const obj: any = args.object;
    return obj.password1 === obj.password2;
  }
  defaultMessage() {
    return "两次输入密码不一致";
  }
}

/** * 注册验证类 * * @export * @class RegistorValidator * @extends {Validator} */
export class RegistorValidator extends Validator {
  constructor() {
    super();
  }
  @Length(3, 10, {
    message: "用户名长度为3~10个字符"
  })
  nickname?: string;
  @IsEmail({},{ message: "电子邮箱格式错误" })
  email?: string;
  @Validate(CheckPassword)
  // 至少8-16个字符,至少1个大写字母,1个小写字母和1个数字,其余能够是任意字符:
  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/, {
    message: "密码至少8-16个字符,至少1个大写字母,1个小写字母和1个数字"
  })
  password1?: string;
  password2?: string;
}
复制代码

在路由文件中使用校验器,调用校验器类上的validate方法,将koaContext传入。
若是校验成功则将请求参数封装成一个对象并返回,
若是失败则直接利用全局异常处理中间件 向客户抛出错误信息:

router.post("/register", async ctx => {
  const v: registerInterface = await new RegistorValidator().validate(ctx);
});
复制代码

registerInterface接口存放了注册所须要的参数,代码:

export interface registerInterface {
  email: string;
  nickname: string;
  password1: string;
  password2: string;
}
复制代码

实现注册功能

src/app/service目录下建立users.ts文件,此目录专门存放进行数据库业务操做的文件。

users.ts中建立UsersService类,在类中建立静态方法userRegister,此方法进行注册操做。

注册步骤:

  1. 判断数据库中是否存在此用户
  2. 若是存在则向用户抛出异常
  3. 若是不存在则将数据插入数据库

业务代码:

static async userRegister(params: registerInterface) {
  const { email, nickname, password1 } = params;
  const data = {
    email,
    nickname,
    password: password1
  };
  const isExistEmail = await Users.findOne({
    where: {
      email
    }
  });
  if (isExistEmail) {
    throw new Failed({ msg: "Email已存在" });
  }
  const r = await Users.create(data);
  return r;
}
复制代码

所有代码:

import Users from "../models/users";
import { Failed } from "../../core/exception";
import { registerInterface } from "../lib/interface/UsersInterface";
class UsersService {
  static async userRegister(params: registerInterface) {
    const { email, nickname, password1 } = params;
    const data = {
      email,
      nickname,
      password: password1
    };
    const isExistEmail = await Users.findOne({
      where: {
        email
      }
    });
    if (isExistEmail) {
      throw new Failed({ msg: "Email已存在" });
    }
    const r = await Users.create(data);
    return r;
  }
}
export default UsersService;
复制代码

在路由中引入注册功能代码:

router.post("/register", async ctx => {
  const v: registerInterface = await new RegistorValidator().validate(ctx);
  const r = await UsersService.userRegister(v);
  if (r) {
    throw new Success();
  } else {
    throw new Failed({msg: '注册失败'});
  }
});
复制代码

路由文件所有代码:

import Router from "koa-router";
import { RegistorValidator } from "../../validators/UsersValidator";
import { Success, Failed } from "../../../core/exception";
import { registerInterface } from '../../lib/interface/UsersInterface';
import UsersService from '../../service/users';

const router: Router = new Router();
router.prefix("/v1/user");

router.post("/register", async ctx => {
  const v: registerInterface = await new RegistorValidator().validate(ctx);
  const r = await UsersService.userRegister(v);
  if (r) {
    throw new Success();
  } else {
    throw new Failed({msg: '注册失败'});
  }
});
// 这里必定要用commonjs规范导出
module.exports = router;
复制代码

更新中......

相关文章
相关标签/搜索