The first Egg project and the first egg+typescript backend development framework of its own architecture.

In 2019, I will gradually transition from the front end to the back end. In fact, I want to start from the full stack rather than the back end, because I think only by learning more technologies can I have broader thinking. The back end the company started with was KOA, wrote for a while, wrapped its own version of KOA + decorator (automatic routing recognition), and then slowly added Egg + TS, and the scaffolding was there.

The project address

Github.com/CB-ysx/egg-…

function

  • Support for configuring routes automatically (by directory path and decorator injection)
  • Supports blocking and filtering of various parameters
  • One-click generation of routing and service files
  • Support multiple database model configurations (automatically generate X.d. ts files)
  • Multiple Redis configurations are supported
  • Path aliases are supported and long ones are rejected. /.. /.. /The introduction of

Project directory structure

E-ts-base ├─.autod.conf. Js ├─.DockerIgnore ├─.EditorConfig ├─.travis. Yml ├─Jenkinsfile // Jenkinsfile Build File ├─ readme.md ├─ AppVeyor.yml ├─package.json ├─plopfile.js // To generate file ├─tsconfig.json ├─ tShelper.js // To automatically generate database for Model ├─tslint.json ├ ─ example / / test database | ├ ─ egg_test. SQL | └ egg_test2. SQL ├ ─ docker / / docker file | └ Dockerfile. API. Prod ├ ─ dev - scripts / / Scripts for development, Mainly used to generate the file | ├ ─ the plop - templates | | ├ ─ util. Js | | ├ ─ service / / generated service file | | | ├ ─ index. The HBS | | | └ prompt. Js | | ├ ─ the router / / generate routing file | | | ├ ─ index. The HBS | | | └ prompt. Js ├ ─ config / / project configuration file | ├ ─ config. The default. The ts / / the default configuration | ├ ─ config. Local. Ts / / Local configuration | ├ ─ config. Prod. Ts / / production environment configuration | └ plugin. Ts / / plug-in configuration ├ ─ app | ├ ─ the router. The ts / / entrance | ├ ─ util / / for some tools | | ├ ─ error. The ts / / Unified management error message | | ├ ─ redisKey. The unified management of ts / / redisKey | | └ sessionKey. The unified management of ts / / cookiekey | ├ ─ service/store/service | | ├ ─test| | | └ test. Ts | | ├ ─ lib | | | └ Oss. Ts | ├ ─ public | ├ ─ model / / database model | | ├ ─test2 | | | ├ ─ Admin. Ts | | | └ User. The ts | | ├ ─test| | | └ User. Ts | ├ ─ middleware / / middleware | | └ response. The ts | ├ ─ lib / / put some third-party packages such as | | ├ ─ auth | | | └ authUser. Ts | | ├ ─ aRouter / / routing identification code in the | | | ├ ─ but ts | | | └ the ts | ├ ─ the extend / / extension | | ├ ─ application. The ts | | ├ ─ context. The ts | | └ helper. Ts | ├ ─ controller / / routing | | ├ ─ but ts | | ├ ─ example | | | ├ ─ but ts | | | └ test. The ts | ├ ─ base / / storage of some basic classes | | ├ ─ baseController. Ts / / if you want to automatically identify the routing, we need to inherit the class | | └ baseService. TsCopy the code

Automatic identification configures the route function

This makes use of decorators. If you don’t understand decorators, take a look at my other article: The use of javascript decorators

The file is at: github.com/CB-ysx/egg-…

Basically see:

ARouter: // entry, user import app and do some configuration AController: The ARouterHelper class uses this to automatically configure routes POST, GET, PUT, DEL, PATCH, ALLCopy the code

Let’s start with the ARouter function:

/** * Throw hwrouter, use ARouter(app) directly in router.ts; * @param app Application * @param Options * @param Options */export functionARouter(app: Application, options? : {prefix? : string}) { const { router } = app; / / access to the routerif(options && options.prefix) { router.prefix(options.prefix); / / configure routing prefix} aRouterHelper injectRouter (router); // Inject route}Copy the code

ARouterHelper class:

/** ** * {/** ** ** ** / controllers: {[key: string]: {prefix? : string, // prefix target? : any, // Corresponding class routers: Array<{// Route handler under controller: string, // Method name path: string, // Route path: method: RequestMethods}>}} = {}; Public injectRouter(router: router) {const keys = object.keys (this.controllers); keys.forEach(key => { const controller = this.controllers[key]; Controller. Routers. ForEach (r = > {/ / before writing is the router get ('/xxx', xxx, controller.xxx.xxx); Router [r.mode](Controller.prefix + r.mode, async (CTX: Context) => {// Get class instance const instance = new controller.target(CTX); / / for use in class decorator middleware const middlewares = controller. The target. The prototype. _middlewares;if(middlewares) {// All is bound to the class, so all of the following methods need to pass through the all middleware const all = middlewares.for (leti = 0; i < all.length; ++i) { const func = all[i]; await func(ctx); } / / this is the way to the middleware const self = middlewares [r.h. andler] | | [];for (leti = 0; i < self.length; ++i) { const func = self[i]; await func(ctx); }} // Go through all the middleware before actually executing the called method await instance[r.handler](); }); }); }); }}Copy the code

You can see that the above injection is configured using controllers in aRouterHelper. Where do controllers get their data? Create an ARouterHelper instance and store data in controllers in AController, GET, and POST decorators. AController is used in the class to indicate that the class needs to be used as a route. Methods such as GET are used in the class to indicate that the method needs to be used as a route entry.

Const aRouterHelper = new aRouterHelper (); /** * controller decorator * @param prefix */function AController (prefix: string) {
    prefix = prefix ? prefix.replace(/\/+$/g, ' ') : '/';
    if (prefix === '/') {
        prefix = ' ';
    }
    return(target: any) => {// Get class name const key = target.arouterGetName (); BaseController (BaseController); BaseController (BaseController)if(! aRouterHelper.controllers[key]) { aRouterHelper.controllers[key] = { target, prefix, routers: [] }; }else{ aRouterHelper.controllers[key].target = target; // target is the class that uses the decorator. aRouterHelper.controllers[key].prefix = prefix; }}; } /** * route decorator * @param path path * @param method request method (get, POST, etc.) */functionRequest (path: string, method: RequestMethods)return function (target: any, value: any, des: PropertyDescriptor & ThisType<any>) {
        const key = target.constructor.toString().split(' ') [1];if(! aRouterHelper.controllers[key]) { aRouterHelper.controllers[key] = { routers: [] }; } aRouterHelper.controllers[key].routers.push({ handler: // Request method (get, post, etc.)}); // Request method (get, post, etc.); }; }function POST (path: string) {
    return request(path, RequestMethods.POST);
}
Copy the code

The above list of automatic route generation code ideas, then automatic route generation is complete, that middleware how to use? This also uses the decorator to temporarily store the middleware, and when the corresponding route is requested, the middleware is executed first and then the target method is executed last. See the implementation of injectRouter method above. So where does middlewares come from? Here I write an install method that binds the decorator handling to middlewares.

Middlewares. All means that all routes in a class need to be processed by middlewares. Middlewares [R.handler] means that all routes in a class need to be processed by middlewares. R.handler is the function name.

Install implementation:

/** * encapsulates routing middleware decorator injection, supports class and methods */exportdefault (target: any, value: any, des: PropertyDescriptor & ThisType<any> | undefined, fn: Function) => {// there is no value, indicating that it is applied to classif (value === undefined) {
        const middlewares = target.prototype._middlewares;
        if(! middlewares) { target.prototype._middlewares = { all: [ fn ] }; }else{ target.prototype._middlewares.all.push(fn); }}else {
        const source = target.constructor.prototype;
        if(! source._middlewares) { source._middlewares = { all: [] }; } const middlewares = source._middlewares;if (middlewares[value]) {
            middlewares[value].push(fn);
        } else{ middlewares[value] = [ fn ]; }}};Copy the code

Install using:

/** * is used to filter the header argument and attach it to the context filterHeaders */function Headers (opt: {[key: string]: Function[]}) {
    return(target: any, value? : any, des? : PropertyDescriptor & ThisType<any> | undefined) => {returninstall(target, value, des, async (ctx: Context) => { ctx.filterHeaders = getValue(ctx.headers, opt); }); }; } // The basic new middleware is written like this // XXXXXX is the decorator namefunction XXXXXX (opt: {[key: string]: Function[]}) {
    return(target: any, value? : any, des? : PropertyDescriptor & ThisType<any> | undefined) => {returninstall(target, value, des, async (ctx: Context) => { // ... Own processing}); }; } const auth = async (CTX: Context) => {const authorizeHeader = ctx.get();'Authorization');
    if(! authorizeHeader) { throw ctx.customError.USER.UNAUTHORIZED; } const token = authorizeHeader.split(' ').pop();
    if(! token) { throw ctx.customError.USER.UNAUTHORIZED; } ctx.jwtInfo = ctx.helper.jwtVerify(token, ctx.app.config.jwtSecret, ctx.customError.USER.UNAUTHORIZED);if(! ctx.jwtInfo || ! ctx.jwtInfo.userInfo) { throw ctx.customError.USER.UNAUTHORIZED; }};export default function (allowNull: boolean = false) {
    return(target: any, value? : any, des? : PropertyDescriptor & ThisType<any> | undefined) => {returninstall(target, value, des, async (ctx: Context) => { try { await auth(ctx); } catch (e) {catch (e) {if(allowNull) {// If the authentication fails, set userInfo to null and no more errors will be reported. Ctx.jwtinfo = {userInfo: null }; }else {
                    throw e;
                }
            }
        });
    };
}

// 使用
@GET('/auth'@authUser() public asyncauth() {
    const {
        service: { test}, jwtInfo: {userInfo}} = this.ctx; this.returnSuccess(await test.test.showData()); } @GET('/auth2')
@authUser(truePublic async can be skipped if async failsauth2() {
    const {
        service: { test}, jwtInfo: {userInfo} // Verify success message, otherwise null} = this.ctx; this.returnSuccess(await test.test.showData()); }Copy the code

Multi-database model configuration and TS identification function

Multiple database configurations are supported by egg-sequelize-ts, but by default, egg-ts-Helper does not generate x.d.ts files based on directory names and their own aliases.

Starting with egg-ts-Helper, the library provides its own generator to generate x.d.ts files for your own needs.

I want to implement the function:

Sequelize = {datasources: [getSqlConfig({delegate:'model.testModel',
            baseDir: 'model/test',
            database: 'egg_test'
        }),
        getSqlConfig({
            delegate: 'model.test2Model',
            baseDir: 'model/test2',
            database: 'egg_test2'}})]; // Service file asyncshowData() {
    const {
        model: {
            testModel,
            test2Model,
        }
    } = this.ctx;
    const [[ userTest ]] = await testModel.query('select * from `user`');
    const admin = await test2Model.Admin.findByPk(1);
    const userTest2 = await test2Model.User.findByPk(1);
    return { userTest, admin, userTest2 };
}
Copy the code

You can see that two models are configured, using databases egg_test and egg_test2, respectively, and are hung on context.model. By the way, this implementation binds the Query method to the Model, which would have failed because ts could not recognize it. For details, add the tshelper.js file to the root directory. For details, see github.com/whxaxes/egg…

Const sequelizeModelContent = 'interface model {query(SQL: string, options? : any):function;
}
`

function selfGenerator(config) {
    if(! config.modelMap) { throw'modelMap must not be undefined'
    }

    const modelMap = {};
    Object.entries(config.modelMap).forEach(item=> {
        modelMap[item[0]] = {
            name: item[1],
            pathList: []
        }
    })
    const modelList = config.fileList.map(item=> ({name: item.split('/')[0], path: item}));
    for (leti = 0; i < modelList.length; ++i) { const map = modelMap[modelList[i].name];if(! map) { throw'modelName must not be null';
        }
        map.pathList.push(modelList[i].path);
    }
    const importContent = [];
    const modelInterface = [];
    const modelContent = [];
    Object.entries(modelMap).forEach(([k, v])=> {
        const item = {name: v.name, contentList: []};
        modelContent.push(item);
        v.pathList.forEach(path=> {
            const name = path[0].toUpperCase() + path.replace('/'.' ').slice(1, -3);
            importContent.push(`import Export${name} from '.. /.. /.. /${config.directory}/${path.slice(0, -3)}'`);
            item.contentList.push(`        ${path.split('/')[1].slice(0, -3)}: ReturnType<typeof Export${name}>; `); }) modelInterface.push(`${v.name}: T_${v.name}& Model; `) }) const content = `${importContent.join('\n')}
${sequelizeModelContent}
declare module 'egg' {
    interface Context {
        model: {
${modelInterface.join('\n')}
        }
    }
${modelContent.map(item=> {
        return `    interface T_${item.name} {
${item.contentList.join('\n')}
    }`
    }).join('\n')}} `;return {
        dist: config.dtsDir + '/index.d.ts',
        content
    }
}

module.exports = {
    watchDirs: {
        model: {
            directory: 'app/model',
            modelMap: {
                test: 'testModel'// Add a mapping for each model directory. Key is the directory name and value is the delegate set in configtest2: 'test2Model'
            },
            generator: selfGenerator
        }
    }
}
Copy the code

More redis configuration

The egg-redis plugin also supports multiple redis configurations, but it is not mounted to the app, causing this to happen every time you use it:

(this.redis as Singleton<Redis>).get('test').xxx()
Copy the code

Here’s one way to do it: mount it to your app and add an application extension file

import { Context, Singleton, Application } from 'egg';
import { Redis } from 'ioredis'; /** * extend application */export default {
    get testRedis(this: Application) {
        return (this.redis as Singleton<Redis>).get('test');
    },
    get test2Redis(this: Application) {
        return (this.redis as Singleton<Redis>).get('test2'); }}; // Const {app: {const {app: {testRedis,
        test2Redis
    }
} = this.ctx;
const set1 = await testRedis.set('test', 1);
const set2 = await test2Redis.set('test', 2); Redis = {clients: {test: getRedisConfig({ db: 13 }),
        test2: getRedisConfig({ db: 14 })
    }
};
Copy the code

If you add a redis library, you will need to write the library fetch in the application extension.

Path alias

In projects, we usually refer to files and often encounter **.. /.. /.. /.. / XXX ** this is a very long path, I don’t like this very much, because if you copy to another file, the path might change, and if you are not careful, you will get an error, so the study introduced a path alias, which can be used like this:

require('module-alias/register');
import BaseService from '@base/baseService'; // @base points to the app/base directory.Copy the code

Tsconfig. json: tsconfig.json

"compilerOptions": {..."baseUrl": "."// Be sure to set the project path"paths": {// Configure the corresponding alias here"@base/*": ["./app/base/*"]."@util/*": ["./app/util/*"]."@lib/*": ["./app/lib/*"],}},Copy the code

After ts is configured, only TS can recognize, but it does not work. Finally, we still run js, also need to configure JS to recognize. Here we use the third-party library module-alias

Configure the alias in package.json

"_moduleAliases": {
    "@base": "./app/base"."@lib": "./app/lib"."@util": "./app/util"
},
Copy the code

Also add a sentence above the file where the alias is to be used

require('module-alias/register');
Copy the code

At this point, you can use path aliases instead of long paths.

Require (‘module-alias/register’); In the ts version, the path can be automatically converted according to the configured alias at compile time.

A one-line command generates the route and service

Because of the changes to the project, each Controller class has many common parts

require('module-alias/register');
import BaseController from '@base/baseController'; BaseController import {AController, GET} from'@lib/aRouter'; Const __CURCONTROLLER = __filename.substr(__filename.indexof ())'/app/controller')).replace('/app/controller'.' ').split('. ')[0].split('/').filter(item => item ! = ='index').join('/').toLowerCase(); // Configure route @acontroller (__CURCONTROLLER)export default class indexController extends BaseController {

}
Copy the code

It would have been too much trouble to do this every time I created a new file (I’m too lazy haha), so I thought of generating files using commands.

At first, THE idea was to write a JS file to generate, and then I learned about Plop. So that’s what this project is going to use. In the project dev-script directory.

The Router is the routing template that generates the controller.

Service is a Service template.

You also need to add the plopfile.js file to the root directory

const routerGenerator = require('./dev-scripts/plop-templates/router/prompt');
const serviceGenerator = require('./dev-scripts/plop-templates/service/prompt');

module.exports = function (plop) {
    plop.setGenerator('router', routerGenerator);
    plop.setGenerator('service', serviceGenerator);
};
Copy the code

Add a command to package.json

"scripts": {
    "new": "plop"
},
Copy the code

This allows you to generate files automatically with a single command

/ / in the app/controller /testCreate an index.ts file NPM run new routertest/index
Copy the code

I’m not going to go into plop syntax here, but you can check it out online.

The last

This scaffold has been used in many projects of the company and there is no problem at present.

Welcome to experience and comments ~.

Egg +typescript builds backend projects