項目地址:koa-typescript-cmsjavascript
用戶系統是一個cms最重要的部分,也是最複雜的部分,須要進行不少安全處理。
每次用戶請求接口時,咱們要進行參數校驗,以防用戶傳入危險以及不規範數據。java
├── 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配置文件
複製代碼
再src/core
目錄下建立db.ts
文件,引入sequelize-typescript
與config.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序列化是使sequelize
每次返回都默認排除咱們不想要的字段。 sequelize
的Model
的原型上會有一個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 };
複製代碼
sequelize-typescript
建立模型和sequelize
建立模型區別仍是挺大的,sequelize-typescript
中大部分字段的配置都是基於裝飾器來實現。下面直接貼上代碼,基本看一遍就知道怎麼回事了。git
注意事項:github
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
在src/core
文件夾下建立validator.ts
文件,引入須要的依賴:sql
import { validateOrReject } from "class-validator";
import { Context } from "koa";
import { cloneDeep } from "lodash";
import { ParametersException } from "./exception";
複製代碼
Validator
類封裝思路:typescript
Context
,獲取到可能接收到用戶傳來的參數的字段,進行拍平(扁平化)key
掛載到原型上。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;
}
複製代碼
因爲咱們須要判斷password1
和password2
是否相等,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
方法,將koa
的Context
傳入。
若是校驗成功則將請求參數封裝成一個對象並返回,
若是失敗則直接利用全局異常處理中間件 向客戶拋出錯誤信息:
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
,此方法進行註冊操做。
註冊步驟:
業務代碼:
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;
複製代碼