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;
複製代碼

更新中......

相關文章
相關標籤/搜索