前言
本篇仅仅是本人自己搭建的一个模板项目而已,并不是什么行业争论的问题,只是提供一个开箱即用的基础项目配置
本人介绍
class BEON {
name: string;
sex: string;
ability: number;
constructor() {
this.name = 'BEON';
this.sex = '♂';
this.ability = -1;
}
}
项目目标
完成一个express+ts+redis+mysql的后台模板
不多bb直接上项目整体逻辑
当然这个结构只是大概情况而已,看着了解了解就可以了
接下来我对几个地方做点讲解
router配置
说实话对于整个router的配置我不知道我的做法是算好还是不好,但是也算给大家一个思路而已。
第一个路由文件
/**
* @author WYX
* @date 2020/5/9
* @Description: 首页一级路由类
*/
import * as express from 'express';
import TemplateHTML from '../template/defaultIframe';
import usersMapper from '../mapper/usersMapper';
import {RouterDec, MyType} from '../decorators/routerDec';
import UsersServices from '../services/usersServices/usersServices';
const routerDec: RouterDec = new RouterDec();
@routerDec.BaseRequest('')
export class Index {
private static UsersServices: UsersServices = new UsersServices()
/**
* 首页渲染
* @param {express.Request} req 请求
* @param {express.Response} res 返回
* @returns {Promise<void>} async异步
*/
@routerDec.RequestMapping('/', MyType.get)
async welCome(
req: express.Request,
res: express.Response
): Promise<void> {
const ceshi = await usersMapper.getAllUser();
console.log(this);
await Index.UsersServices.getUserInfo('a2', 'bbbb');
res.render('index', ceshi);
}
/**
* 返回标准html页面方法
* @param {express.Request} req 请求
* @param {express.Response} res 返回
* @returns {Promise<void>} async异步
*/
@routerDec.RequestMapping('/index.html', MyType.get)
async backHtml(
req: express.Request,
res: express.Response
): Promise<void> {
Index.UsersServices.changeUserInfo();
res.writeHead(200, {
'Content-Type': 'text/html',
'Expires': new Date().toUTCString()
});
res.write(TemplateHTML.startHTML);
res.write('测试\n');
res.end(TemplateHTML.endHTML);
}
}
export default routerDec;
如果第一次直看整个路由注册文件感觉可能真的比较懵逼,因为这里面并没有任何一句router.use的语句,取而代之的是我们不怎么常用的装饰器
这个路由文件注册了两个路由一个是根路径的('/'),另一个是('/index.html')。这哈细心的大佬可能就看到了路由注册是由 @routerDec.RequestMapping这个装饰器来进行注册的。
private static UsersServices: UsersServices = new UsersServices()这个东东后面再做解释
routerDec是怎么注册的
对于routerDec的工作方式我其实进行了两种方式的设计:
方法1
方法2
方法2利用了装饰器在类中的调用顺序,关于这个执行顺序详情可以查看官方文档ts装饰器
那么现在就来说一下这两种方法的优缺点吧:
方法1:
- 编写简单,注册使用装饰器相对而言更容易查找
- 使用方便,并且对于routerDec类的使用方便,只需要进行RequesMapping方法编写
方法2:
- 编写简单,注册使用装饰器相对而言更容易查找
- 使用方便,但对于routerDec类的需要进行构造对象导出
- 可以自定义基础路由,以及多级路由
其实看到这基本上就知道两者的差距了,方法2可以进行基础路由的设定这个功能,但是方法1使用和编写都要简单点,那么对于我们程序员来说肯定要麻烦的功能丰富的啊!(其实是为了拓展性)
那么可以得到我们的代码应该是这样的:
/**
* @author WYX
* @date 2020/5/13
* @Description: 路由注册装饰器类
*/
import * as express from 'express';
export enum MyType {
'get' = 'get',
'post' = 'post'
}
interface RouterSet {
type: string;
path: string;
fn: express.RequestHandler;
}
export class RouterDec {
router: express.Router
baseUrl: string
routerList: RouterSet[] = []
/**
* 构造函数
*/
constructor() {
this.router = express.Router();
}
/**
* 类的基础路由
* @param {String} path 基础路由
* @returns {ClassDecorator} 返回构造函数
*/
BaseRequest(path: string): ClassDecorator {
return (): void => {
this.baseUrl = path;
this.routerList.forEach((item) => {
this.router[item.type](this.baseUrl + item.path, item.fn);
});
};
}
/**
* 注册路由
* @param {String} path 路由路径
* @param {MyType} type 请求方式
* @returns {MethodDecorator} ts方法装饰器
*/
RequestMapping(path: string, type: MyType): MethodDecorator {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor): void => {
this.routerList.push({
type: type,
path: path,
fn: descriptor.value
});
};
}
}
当然了这其实也只是一个基础模板,具体使用比如需要添加put、delete等类型需要进行自主的添加。
注册文件
当然了,大佬可能要质疑我了,看了两个文件了还是没看到具体的注册地方呢?
其实这就是很简单的一个文件了,功能就是遍历了下路由文件夹里面导出的路由进行注册构建对象。
/**
* @author WYX
* @date 2020/5/9
* @Description: 路由加载类
*/
import * as express from 'express';
import * as fs from 'fs';
export default class MyRouter {
protected router: express.Router
/**
* 构造函数
*/
constructor() {
this.router = express.Router();
this.getRouter();
}
/**
* 获取router实例(写个protected感觉比较牛皮)
* @returns {e.Router} 返回router实例
*/
getMyRouter(): express.Router {
return this.router;
}
/**
* 注入传入路径的路由
* @param {String} routeUrl 传入需要遍历的路径
* @returns {void}
*/
private getRouter(routeUrl = ''): void {
const files = fs.readdirSync((__dirname + routeUrl) as fs.PathLike);
files
.filter(
function(file: string): boolean{
return file.split('.')[0] !== 'loader';
}
)
.forEach(
(file: string) => {
if (file.split('.').length === 1) {
this.getRouter(routeUrl + '/' + file.split('.')[0]);
} else {
import(`.${routeUrl}/${file.split('.')[0]}`)
.then((route) => {
route = route.default.router;
this.router.use(`/`, route);
})
.catch((e) => {
console.log(e);
throw Error('读取路由文件失败,请检查');
});
}
}
);
}
}
看了这么多都不点个赞么?你的良心不会痛么?
redis开启
接下来的就比较简单了,首先我们开启一个redis服务
/**
* @author WYX
* @date 2020/5/12
* @Description: redis基础信息配置
*/
import * as redis from 'redis';
const $redisConfig: redis.ClientOpts = {
host: '127.0.0.1',
port: 6379
};
export const $redisOutTime = {
outTime: 60 * 30
};
export default $redisConfig;
别、开个玩笑,我们先看看redis的配置开启我们放后面再来嘛
我们将所有的这种服务的地址作为config配置来进行管理可以达到功能和配置分开管理的目的,防止代码误操作以及更清晰明了的进行配置管理。
然后我封装了一个redis操作类,包括了登录使用在里面,并且所有获取是一个promise对象保证异步执行
/**
* @author WYX
* @date 2020/5/12
* @Description: 对常用进行了封装(未封装无序和有序的内容)MyRedis类
* 注: 对于ts类型检测,调用getFun方法的类型检测有问题(所以使用any),多重调用后类型检测的包含关系混乱
*/
import * as Redis from 'redis';
import $redisConfig from './config/configRedis';
const redisClient = Redis.createClient($redisConfig);
redisClient.auth('', function () {
console.log('redis登录成功');
});
redisClient.on('error', function (err) {
console.log(err);
});
class MyRedis {
static redisClient: Redis.RedisClient = redisClient
/**
* set 普通的set/get 字符串型
* @param {String} key 键值
* @param {String} value 保存值
* @returns {void}
*/
static set(key: string, value: string): void {
this.redisClient.set(key, value, this.errFun);
}
/**
* get 普通的set/get 字符串型
* @param {String} key 键值
* @returns {Promise<String>} 返回值
*/
static get(key): Promise<string> {
return this.getFun((fn: Redis.Callback<string>) => {
this.redisClient.get(key, fn);
}) as Promise<string>;
}
/**
* 设置key失效时间
* @param {String} key 设置键
* @param {Number} time 失效时长
* @returns {void}
*/
static exp(key: string, time: number): void {
this.redisClient.expire(key, time, this.errFun);
}
/**
* 删除key
* @param {String} key 删除键
* @returns {void}
*/
static remove(key: string): void {
this.redisClient.del(key, this.errFun);
}
/**
* 单个key存储 hash
* @param {String} key 设置键
* @param {String} field hash的key
* @param {String} value hash的值
* @returns {void}
*/
static hset(key: string, field: string, value: string): void {
this.redisClient.hset(key, field, value, this.errFun);
}
/**
* 单个key获取 hash
* @param {String} key hash设置键
* @param {String} field 获取的可以
* @returns {void}
*/
static hget(key: string, field: string): Promise<string> {
return this.getFun((fn: Redis.Callback<string>) => {
this.redisClient.hget(key, field, fn);
}) as Promise<string>;
}
/**
* 多个key存储 hash
* @param {String} key hash设置键
* @param {Object} argObj 需要传入的对象(暂时只封装对象传输)
* @returns {void}
*/
static hmset(key: string, argObj: { [key: string]: string | number }): void {
this.redisClient.hmset(key, argObj, this.errFun);
}
/**
* 多个key获取 hash
* @param {String} key hash设置键
* @param {Array<String>} argList 需要查询的数组
* @returns {Promise<string[]>} 返回查询结果
*/
static hmget(key: string, argList: Array<string>): Promise<string[]> {
return this.getFun((fn: Redis.Callback<string[]>) => {
this.redisClient.hmget(key, argList, fn);
}) as Promise<string[]>;
}
/**
* 全部hash获取
* @param {String} key hash设置键
* @returns {Promise<string[]>} 返回查询结果
*/
static hgetall(key: string): Promise<string[]> {
return this.getFun((fn: Redis.Callback<{ [key: string]: string }>) => {
this.redisClient.hgetall(key, fn);
}) as Promise<string[]>;
}
/**
* 设定无返回操作错误处理
* @param {null|Error} err 错误
* @returns {void}
*/
private static errFun(err: null | Error): void {
if (err) {
console.log(err);
}
}
/**
* 对获取函数进行Promise封装
* @param {Function} fn 传入执行方法
* @returns {Promise<string|string[]>} 返回Promise对象
*/
private static getFun(fn: Function): Promise<string | string[]> {
return new Promise<string | string[]>((resolve, reject): void => {
fn((err: null | Error, getRslt: string | string[]): void => {
if (err) {
reject();
throw err;
}
resolve(getRslt);
});
});
}
}
export default MyRedis;
这个文件的注释比较多我就不做详细的解释了,大家使用的时候可以直接通过注释查看功能,在这大家需要注意以下这个地方哦
redisClient.auth('', function () {
console.log('redis登录成功');
});
自己使用的时候需要把第一个参数改成自己redis的密码就行了。
本人封装的时候只封装了几个常用的,如果大家有需求可以自行进行封装哦,代码都是死的人是活的可以参照现有方法进行改造就行了。
redis重点
看到这里按理来说redis应该已经完了呀,怎么还重点都来了。当然接下来的才是redis使用的灵魂!
使用过java的大佬肯定很喜欢使用Cache注解吧,所以我们这个地方就简简单单的进行了redis注解的封装
/**
* @author WYX
* @date 2020/5/13
* @Description: redis注解类
*/
import MyRedis from '../cache';
import {$redisOutTime} from '../config/configRedis';
interface CacheType {
time: number;
}
export default class RedisDec {
private static CacheNamespace: {string: { string: {string: CacheType} }} | {} = {}
/**
* 进行key设置
* @param {String} key 传入key的namespace
* @param {String} params 传入参数
* @param {Number} outTime 过期时间
* @returns {MethodDecorator} 返回方法
*/
static Cacheable(key: string, params = 'redis', outTime = $redisOutTime.outTime): MethodDecorator {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor): void => {
// 保存现有方法
const setFunction = descriptor.value;
// 获取方法需要的传参
const getParams = RedisDec.getFunParams(setFunction);
// 重写方法
descriptor.value = async (...args): Promise<JSON> => {
let reqParams = '';
if (params !== 'redis') {
params.split('#').forEach((item) => {
const index = getParams.indexOf(item);
if (args[index]) {
reqParams += item + '-' + args[index] + '&';
}
});
} else {
reqParams = 'redis';
}
const getValue: string = await MyRedis.get(`${key}:${propertyKey}:${reqParams}`);
if (getValue) {
RedisDec.changeCacheTime(key, propertyKey, outTime, reqParams);
return JSON.parse(getValue);
}
const dueBack: JSON = await setFunction(...args);
MyRedis.set(`${key}:${propertyKey}:${reqParams}`, JSON.stringify(dueBack));
RedisDec.changeCacheTime(key, propertyKey, outTime, reqParams);
return dueBack;
};
};
}
/**
* 删除redis缓存
* @param {String} key 需要删除namespace
* @returns {MethodDecorator} 返回构造方法
*/
static CacheEvict(key: string): MethodDecorator{
return (target: any, propertyKey: string, descriptor: PropertyDescriptor): void => {
// 保存现有方法
const setFunction = descriptor.value;
// 重写方法
descriptor.value = (): any => {
RedisDec.removeAllCache(key);
return setFunction();
};
};
}
/**
* 改变过期时间
* @param {String} key 传入namespace空间
* @param {String} propertyKey 传入方法名
* @param {Number} outTime 传入过期时间
* @param {String} params 传入请求参数
* @returns {void}
*/
private static changeCacheTime(key: string, propertyKey: string, outTime: number, params: string): void {
const setOutTime = Math.round((new Date()).getTime() / 1000) + outTime;
if (this.CacheNamespace[key]) {
if (this.CacheNamespace[key][propertyKey]) {
if (this.CacheNamespace[key][propertyKey][params]) {
this.CacheNamespace[key][propertyKey][params].time = setOutTime;
} else {
this.CacheNamespace[key][propertyKey][params] = { time: setOutTime } as CacheType;
}
} else {
this.CacheNamespace[key][propertyKey] = {[params]: {time: setOutTime}} as {string: CacheType};
}
} else {
this.CacheNamespace[key] = {[propertyKey]: {[params]: {time: setOutTime}}} as{ string: {string: CacheType} };
}
MyRedis.exp(`${key}:${propertyKey}:${params}`, outTime);
}
/**
* 删除传入namespace所有key
* @param {String} key 需要删除key
* @returns {void}
*/
private static removeAllCache(key: string): void {
if (this.CacheNamespace[key]) {
Object.keys(this.CacheNamespace[key]).forEach((propertyKey) => {
Object.keys(this.CacheNamespace[key][propertyKey]).forEach((params) => {
if (this.CacheNamespace[key][propertyKey][params].time > Math.round((new Date()).getTime() / 1000)) {
MyRedis.remove(`${key}:${propertyKey}:${params}`);
}
});
});
delete this.CacheNamespace[key];
}
}
/**
* 返回
* @param {Function} fn 传入需要进行获取参数的函数
* @returns {Array} 返回获取参数数组
*/
private static getFunParams(fn: Function): string[] {
const regex1 = /\((.+?)\)/g; // () 小括号
const getList = fn.toString().match(regex1);
const dealString = getList[0].substring(1, getList[0].length - 1).replace(' ', '');
return dealString.split(',');
}
}
可能大家看着代码还不太清楚怎么使用,那么直接看看使用的地方吧,基本上和java的是一样的
/**
* 获取用户信息服务
* @param {String} userId 用户id
* @param {String} ceshi 测试参数
* @returns {string[]} 返回获取的信息
*/
@RedisDec.Cacheable('keyList', '#userId#ceshi')
getUserInfo(userId: string, ceshi?: string): string[] {
console.log('获取用户信息');
return ['a', 'b'];
}
是的,只需要添加一句话就可以实现redis缓存的保存和读取以及redis过期的设置
第一个参数主要是用作命名空间使用,第二个参数是处理传过来的参数也作为条件,只有当条件完全一样的时候才会进行读取redis返回,那我们来看一下在redis中是进行怎样的设置我们的key值的。
keyList:getUserInfo:userId-a2&ceshi-bbbb&
可以清晰的看到我们的保存的key是由:命名空间+方法名+各个参数的key、value对应
这样可以减少我们对于redis的直接代码操作(偷懒美滋滋啊)
mysql数据库
只剩下简简单单、朴实无华的mysql了,还是一样的采用配置文件的方式在这就不拿出来了,没有营养
简简单单看看mysql的封装得了
/**
* @author WYX
* @date 2020/5/12
* @Description: mysql连接池配置类
*/
import * as mysql from 'mysql';
import $dbConfig from './config/configMysql';
class MySql{
private static pool: mysql.Pool = mysql.createPool($dbConfig);
/**
* 封装query之sql不带带占位符func
* @param {String} sql 执行sql
* @param {Function} MapperReject 执行错误回调方法(推荐传入mapper的reject这样服务层可以捕获catch)
* @returns {void}
*/
static query(
sql: string | mysql.Query,
MapperReject?: Function
): Promise<object> {
return new Promise((resolve, reject) => {
this.pool.getConnection(
function(
err: mysql.MysqlError,
connection: mysql.PoolConnection
): void {
if (err) {
MapperReject && MapperReject(err);
reject();
}
connection.query(
sql,
function(
err: mysql.MysqlError,
rows: object
) {
if (err) {
MapperReject && MapperReject(err);
reject(err);
}
resolve(rows);
// 释放链接
connection.release();
}
);
}
);
});
}
/**
* 封装query之sql不带带占位符func
* @param {String} sql 执行sql
* @param {*} args 传入占位符
* @param {Function} MapperReject 执行错误回调方法(推荐传入mapper的reject这样服务层可以捕获catch)
* @returns {void}
*/
static queryArgs(
sql: string,
args: any,
MapperReject?: Function
): Promise<object> {
return new Promise((resolve, reject) => {
this.pool.getConnection(
function(
err: mysql.MysqlError,
connection: mysql.PoolConnection
): void {
if (err) {
MapperReject && MapperReject(err);
reject();
}
connection.query(
sql,
args,
function(
err: mysql.MysqlError,
rows: object
) {
if (err) {
MapperReject && MapperReject(err);
reject(err);
}
resolve(rows);
// 释放链接
connection.release();
}
);
}
);
});
}
}
export default MySql;
只是在这个地方用了以下mapper的方式,把所有接口请求给封装起来了,对于数据库操作的封装可以更好的管理我们的sql语句,以及减少我们的sql语句代码
/**
* @author WYX
* @date 2020/5/12
* @Description: 用户请求sql映射类
*/
import MySql from '../db';
class UsersMapper {
/**
* 获取全部用户
* @returns {void}
*/
static getAllUser(): Promise<any> {
return new Promise((resolve) => {
MySql.query('select * from users')
.then((results) => {
console.log(results);
resolve(results);
});
});
}
}
export default UsersMapper;
这哈基本上就看完了整个项目的主要配置了
结束
现在这简单的放两个截图吧,一个是欢迎页面一个是报错的处理的
gif图可能加载有点慢。。。
最后放上git地址:https://gitee.com/missshen/node-express-ts-model (各位大佬欢迎使用啊提意见啊,本人是个菜鸡)
最后说一句,github是真滴慢- -