preface

This is just a template project I set up myself, not an industry debate, just provide a basic project configuration out of the box

I introduce

class BEON {
    name: string;
    sex: string;
    ability: number;

    constructor() {
        this.name = 'BEON';
        this.sex = 'came here'; this.ability = -1; }}Copy the code

Project objectives

Complete a background template for Express + TS + Redis +mysql

No bb directly on the overall project logic

Of course, this structure is only a general situation, just look to understand it

I’m going to talk about a couple of places

The router configuration

To be honest, I don’t know whether my approach is good or bad for the configuration of the whole router, but it is also calculated to give you an idea.

The first routing file

/** * @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 Request @param {express.Response} res returns @returns {Promise
      
       } async 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);
    }

    @param {express.Request} req Request @param {express.Response} res returns @returns {Promise
      
       } Async asynchronous * /
      
    @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('test \ n'); res.end(TemplateHTML.endHTML); }}export default routerDec;

Copy the code

The first time you look at the entire routing registration file, you might feel really confused because it doesn’t contain any router.use statements, but instead has a decorator that you don’t use very often

This route file registers two routes, one for the root path (‘/’) and the other for (‘/index.html’). You might have noticed that route registration is done by the @RouterDec.requestMapping decorator.

Private static UsersServices: UsersServices = new UsersServices(

How is routerDec registered

I actually designed routerDec to work in two ways:

Method 1

Method 2

Method 2 makes use of the order in which the decorators are called in the class. For details on this execution order, see the official TS decorator documentation

So let’s talk about the pros and cons of these two methods:

Method 1:

  • Simple to write, registration using decorators is relatively easy to find
  • Easy to use, and easy to use routerDec class, just need to write the RequesMapping method

Method 2:

  • Simple to write, registration using decorators is relatively easy to find
  • Easy to use, but need to construct object export for routerDec class
  • You can customize basic routes and multi-level routes

In fact, see this basically know the gap between the two, method 2 can be set this function of basic routing, but method 1 to use and write are simple, so for our programmers must trouble rich function ah! (Actually for extensibility)

Our code should look like this:

/** * @author WYX * @date 2020/5/13 * @description: Route registration decorator class */
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 */
    constructor() {
        this.router = express.Router();
    }

    @param {String} path @returns {ClassDecorator} returns the constructor
    BaseRequest(path: string): ClassDecorator {
        return() :void= > {
            this.baseUrl = path;
            this.routerList.forEach((item) = > {
                this.router[item.type](this.baseUrl + item.path, item.fn);
            });
        };
    }

    /** * register route * @param {String} path route path * @param {MyType} type request method * @returns {MethodDecorator} ts MethodDecorator */
    RequestMapping(path: string, type: MyType): MethodDecorator {
        return (target: any, propertyKey: string, descriptor: PropertyDescriptor): void= > {
            this.routerList.push({
                type: type,
                path: path,
                fn: descriptor.value }); }; }}Copy the code

Of course, this is only a basic template, and you need to add types like PUT and delete.

Registration file

Of course, the big guy may question me, read two documents or did not see the specific registration place?

In fact, this is a very simple file, the function is to traverse the next route folder exported routes to register the construction object.

/** * @author WYX * @date 2020/5/9 * @description: Route loading class */
import * as express from 'express';
import * as fs from 'fs';

export default class MyRouter {
    protected router: express.Router

    /** * constructor */
    constructor() {
        this.router = express.Router();

        this.getRouter();
    }

    @returns {e.outer} returns the router instance */
    getMyRouter(): express.Router {
        return this.router;
    }

    /** * Inject a route to the incoming path * @param {String} routeUrl Passes the path to be traversed * @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('Failed to read routing file, please check'); }); }}); }}Copy the code

You don’t give a thumbs up after all this? Doesn’t your conscience ache?

Redis open

The next step is relatively simple. First we start a Redis service

/** * @author WYX * @date 2020/5/12 * @description: Redis basic information configuration */
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;
Copy the code

No, just kidding. Let’s look at the configuration of Redis and come back later

We manage the address of all such service as config configuration, which can achieve the purpose of separating function and configuration management, prevent code misoperation and make configuration management more clear.

I then encapsulate a Redis action class that includes login usage, and all fetch is a Promise object that guarantees asynchronous execution

/** * @author WYX * @date 2020/5/12 * @description: MyRedis class * For TS type detection, the type detection that calls getFun is problematic (so any is used), and the inclusion relation of type detection after multiple calls is messy */
import * as Redis from 'redis';
import $redisConfig from './config/configRedis';

const redisClient = Redis.createClient($redisConfig);

redisClient.auth(' '.function () {
    console.log('Redis login successful');
});

redisClient.on('error'.function (err) {
    console.log(err);
});

class MyRedis {
    static redisClient: Redis.RedisClient = redisClient

    /** * set Common set/get String * @param {String} key * @param {String} value Save value * @returns {void} */
    static set(key: string, value: string): void {
        this.redisClient.set(key, value, this.errFun);
    }

    /** * get Common set/get String * @param {String} key value * @returns {Promise
      
       } Return value */
      
    static get(key): Promise<string> {
        return this.getFun((fn: Redis.Callback<string>) = > {
            this.redisClient.get(key, fn);
        }) as Promise<string>;
    }

    @param {String} Key setting key * @param {Number} time Expiration duration * @returns {void} */
    static exp(key: string, time: number): void {
        this.redisClient.expire(key, time, this.errFun);
    }

    /** * Delete key * @param {String} key delete key * @returns {void} */
    static remove(key: string): void {
        this.redisClient.del(key, this.errFun);
    }

    /** * Single key stores hash * @param {String} key sets the key * @param {String} field hash key * @param {String} value hash value * @returns {void} */
    static hset(key: string, field: string, value: string): void {
        this.redisClient.hset(key, field, value, this.errFun);
    }

    /** * Single key to get hash * @param {String} Key hash set key * @param {String} field can get * @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>;
    }

    /** * Multiple keys store hash * @param {String} Key hash sets the key * @param {Object} argObj needs to pass the Object (for now only encapsulates the Object transfer) * @returns {void} */
    static hmset(key: string, argObj: { [key: string]: string | number }): void {
        this.redisClient.hmset(key, argObj, this.errFun);
    }

    @param {Array
      
       } argList Array to query * @returns {Promise< String []>} The query result is displayed */
      
    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[]>;
    }

    /** * All hash to get * @param {String} key hash setting key * @returns {Promise< String []>} Return query result */
    static hgetall(key: string): Promise<string[]> {
        return this.getFun((fn: Redis.Callback<{ [key: string]: string }>) = > {
            this.redisClient.hgetall(key, fn);
        }) as Promise<string[]>;
    }

    / set no return Error handling operation * * * * @ param {null | Error} err Error * @ returns {void} * /
    private static errFun(err: null | Error) :void {
        if (err) {
            console.log(err); }}/ * * * to Promise for Function encapsulation * @ param Function fn into execution method attach * @ returns {Promise < string | string [] >} returns the 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();
                    throwerr; } resolve(getRslt); }); }); }}export default MyRedis;
Copy the code

This file annotations more I will not do a detailed explanation, you can use the time to view the function directly through annotations, in this we need to pay attention to the following place oh

redisClient.auth(' '.function () {
    console.log('Redis login successful');
});
Copy the code

You need to change the first parameter to redis password on the line.

I encapsulate only a few commonly used packaging, if you have the need to carry out their own packaging oh, the code is dead people are alive can refer to the existing method for transformation on the line.

Redis key

See here supposedly redis should have finished ah, how to also come to the point. Of course, next is the soul that Redis uses!

If you’ve ever used Java, you’d love to use Cache annotations, so we’ve simply wrapped redis annotations here

/** * @author WYX * @date 2020/5/13 * @description: Redis annotation class */
import MyRedis from '.. /cache';
import {$redisOutTime} from '.. /config/configRedis';

interface CacheType {
    time: number;
}

export default class RedisDec {
    private static CacheNamespace: {string: { string: {string: CacheType} }} | {} = {}

    @param {String} key Passes in the namespace of the key * @param {String} params passes in the parameter * @param {Number} outTime expiration time * Returns {MethodDecorator} returns the method */
    static Cacheable(key: string, params = 'redis', outTime = $redisOutTime.outTime): MethodDecorator {
        return (target: any, propertyKey: string, descriptor: PropertyDescriptor): void= > {
            // Save existing methods
            const setFunction = descriptor.value;

            // Get the pass parameter required by the method
            const getParams = RedisDec.getFunParams(setFunction);

            // Override the method
            descriptor.value = async(... args):Promise<JSON> = > {let reqParams = ' ';
                if(params ! = ='redis') {
                    params.split(The '#').forEach((item) = > {
                        const index = getParams.indexOf(item);
                        if (args[index]) {
                            reqParams += item + The '-' + 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 = awaitsetFunction(... args); MyRedis.set(`${key}:${propertyKey}:${reqParams}`.JSON.stringify(dueBack));
                RedisDec.changeCacheTime(key, propertyKey, outTime, reqParams);
                return dueBack;
            };
        };
    }

    /** * delete redis cache * @param {String} key need to delete namespace * @returns {MethodDecorator} return constructor */
    static CacheEvict(key: string): MethodDecorator{
        return (target: any, propertyKey: string, descriptor: PropertyDescriptor): void= > {
            // Save existing methods
            const setFunction = descriptor.value;
            // Override the method
            descriptor.value = (): any= > {
                RedisDec.removeAllCache(key);
                return setFunction();
            };
        };
    }

    /** * Change expiration time * @param {String} key is passed into namespace space * @param {String} propertyKey is passed into method name * @param {Number} outTime Is passed into expiration time * @param {String} params Incoming request parameters * @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 } asCacheType; }}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);
    }

    /** * Delete all incoming keys * @param {String} keys You need to delete keys * @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]; }}/** * returns @param {Function} fn passes in a Function that needs to get parameters * @returns {Array} returns an Array of parameters */
    private static getFunParams(fn: Function): string[] {
        const regex1 = / \ ((. +?) \)/g; // () parentheses
        const getList = fn.toString().match(regex1);
        const dealString = getList[0].substring(1, getList[0].length - 1).replace(' '.' ');
        return dealString.split(', '); }}Copy the code

Maybe you don’t know how to use it when you look at the code, so let’s just look at where it’s used, it’s basically the same as Java, right

@param {String} userId userId * @param {String} ceshi test parameters * @returns {String []} returns the obtained information */
@RedisDec.Cacheable('keyList'.'#userId#ceshi') getUserInfo(userId: string, ceshi? : string): string[] {console.log('Get user information');
    return ['a'.'b'];
}
Copy the code

Yes, the redis cache can be saved and read and redis expiration can be set with just one sentence

The first parameter is used primarily as a namespace, and the second parameter is used to process the passed parameter as a condition, and only when the condition is exactly the same is read redis returns, so let’s look at how we set our key in Redis.

keyList:getUserInfo:userId-a2&ceshi-bbbb&
Copy the code

We can clearly see that the saved key is: namespace + method name + key and value of each parameter

This will reduce our direct code operations on Redis.

The mysql database

Only simple, unpretentious mysql, or the same use of configuration files in this way is not taken out, no nutrition

Just look at the mysql package

/** * @author WYX * @date 2020/5/12 * @description: mysql connection pool configuration class */
import * as mysql from 'mysql';
import $dbConfig from './config/configMysql';

class MySql{
    private static pool: mysql.Pool = mysql.createPool($dbConfig);

    Func * @param {String} SQL execute SQL * @param {Function} MapperReject Execute the error callback method (it is recommended to pass mapper reject so that the service layer can catch) * @returns {void} */
    staticquery( 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);
                            // Release the linkconnection.release(); }); }); }); }Func * @param {String} SQL execute SQL * @param {*} args pass placeholder * @param {Function} MapperReject Execute the error callback method (it is recommended to pass mapper reject so that the service layer can 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);
                            // Release the linkconnection.release(); }); }); }); }}export default MySql;
Copy the code

But in this place with the following mapper way, all interface requests to encapsulate, for the database operation encapsulation can better manage our SQL statements, and reduce our SQL statement code

/** * @author WYX * @date 2020/5/12 * @description: SQL mapping class */
import MySql from '.. /db';

class UsersMapper {

    /** * Obtain all users * @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;
Copy the code

This is basically the main configuration of the entire project

The end of the

Now let’s simply put two screenshots, one for the welcome page and one for error handling

GIF images may load a little slowly…

Git address: gitee.com/missshen/no… (You are welcome to use ah suggestions ah, I am a vegetable chicken)

Finally, Github is really slow