Lu Yangyang, a front-end development engineer of the front-end technology department of Weimi, makes a quiet salted fish.

Why Koa

Why did you choose Koa? Lao Wang: Because Koa is relatively lightweight, there are almost no built-in additional functions. Also for this reason, Koa’s flexibility is very high, people who like to toss around can try xiao Wang: light weight and almost no additional features? So why not use native Node? Isn’t that lighter? Lao Wang: Well… Why don’t I tell you how to use it

A little long, impatient can view the full code github.com/JustGreenHa…

Set up the project and start the service

After a series of base operations, a directory structure like this is generated:

Next we will write the simplest startup service code in the startup file app/index.js:

const Koa = require('koa');

const app = new Koa();

const port = '8082'
const host = '0.0.0.0'

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(port, host, () = > {
  console.log(`API server listening on ${host}:${port}`);
});
Copy the code

By running the node app/index.js command, the simplest Node service is already started. Access http://localhost:8082 in the browser has been able to respond normally

Modified routing

As the project becomes more functional, so does the routing. The app/index.js code looks like this:

const Koa = require('koa');

const app = new Koa();

const port = '8082'
const host = '0.0.0.0'

app.use(async ctx => {
  const { path } = ctx
  if (path === '/a') {
    A / / function
    ctx.body = 'a';
  } else if (path === '/b') {
    B / / function
    ctx.body = 'b';
  } else if (path === '/c') {
    C / / function
    ctx.body = 'c';
  } else {
    ctx.body = 'hello world'}}); app.listen(port, host,() = > {
  console.log(`API server listening on ${host}:${port}`);
});
Copy the code

This way of writing all the routes and route handlers together can be difficult to maintain later, and it’s easy to lose focus as the amount of code grows. So we took the routing part out of the startup file app/index.js, maintained a separate routing file, and managed the routing using the third-party route management plug-in KOA-Router. Let’s create a new router directory in the app directory, as shown below:

NPM install koa-router-s, and then write the route processing part of the code in app/router/index.js

const koaRouter = require('koa-router');
const router = new koaRouter();

router.get('/a'.ctx= > {
  ctx.body = 'a'
});

router.get('/b'.ctx= > {
  ctx.body = 'b'
});

router.get('/c'.ctx= > {
  ctx.body = 'c'
});

module.exports = router;

Copy the code

Modify app/index.js code to import route processing from app/router/index.js file:

const Koa = require('koa');

const router = require('./router')

const app = new Koa();

const port = '8082'
const host = '0.0.0.0'

app.use(router.routes());
/* If the route exists and the request mode does not match, the request mode will not be allowed */
app.use(router.allowedMethods());


app.listen(port, host, () = > {
  console.log(`API server listening on ${host}:${port}`);
});

Copy the code

An attempt to access http://localhost:8082/a returns the same result as before the modification. So far, the individual files seem to be doing their job, and the functional split is fairly clear. However, as the number of interfaces increases, our routing file will become larger and larger. Our goal is that the routing file only cares about routing processing, not the specific business logic. So again, the routing file is split.

As shown above, we added three files at this stage

  • app/router/routes.jsRoute list file
  • app/contronllers/index.jsExport service processing in a unified manner
  • app/contronllers/test.jsBusiness processing file

Code on the business logic controllers, the sample file app/contronllers/test. The js:

const list = async ctx => {
  ctx.body = 'Results of route modification'
}

module.exports = {
  list
}
Copy the code

Will this part of the business processing code into the app/contronllers/index, js:

const test = require('./test');

module.exports = {
  test
};

Copy the code

The advantage of this is that all business processes are unified in one entrance, which is conducive to maintenance. Next, write the file app/router/routes.js:

const { test } = require('.. /controllers');

const routes = [
  {
    / / test
    method: 'get'.path: '/a'.controller: test.list
  }
];

module.exports = routes;

Copy the code

The original app/router/index.js file needs to be modified accordingly:

/* const Router = require('koa-router'); const router = new Router(); router.get('/a', ctx => { ctx.body = 'a' }); router.get('/b', ctx => { ctx.body = 'b' }); router.get('/c', ctx => { ctx.body = 'c' }); module.exports = router; * /


const koaRouter = require('koa-router');
const router = new koaRouter();

const routeList = require('./routes');

routeList.forEach(item= > {
  const { method, path, controller } = item;
  // The first argument to the router is path, followed by the routing middleware controller.
  router[method](path, controller);
});

module.exports = router;

Copy the code

After the modifications above, visit http://localhost:8082/a again and return the results:

It looks like no problem, so far the routing transformation has been completed, and by the way, the startup file, routing file, routing processing file three parts apart

Argument parsing

After the actual operation, the post request was not obtainedbodyIn. Here is the screenshot:

The expected return value is{"a": 4}, the actual is:

After reading the data, we need to add a middleware for parameter analysis. In the app/middlewares directory, we will add the index.js file: app/middlewares

const router = require('.. /router');

/** * Route processing */
const mdRoute = router.routes();
const mdRouterAllowed = router.allowedMethods();

module.exports = [
  mdRoute,
  mdRouterAllowed
];

Copy the code

All the middleware used is concentrated in the above file. So far, there are two middleware for routing processing. Next, we will transform the startup file app/index.js:

const Koa = require('koa');

const compose = require('koa-compose');
const MD = require('./middlewares/');

const app = new Koa();

const port = '8082'
const host = '0.0.0.0'

app.use(compose(MD));

app.listen(port, host, () = > {
  console.log(`API server listening on ${host}:${port}`);
});

Copy the code

A plug-in, KOA-Compose, is introduced to simplify the writing of the reference middleware. At this point, you’re ready to deal with parameter parsing

Install the third-party parameter parsing plugin KoA-BodyParser to handle the parameters in the body of the POST request. Modify the app/middlewares/index.js file:

const koaBody = require('koa-bodyparser');

const router = require('.. /router');

/ * * * * https://github.com/koajs/bodyparser * /
const mdKoaBody = koaBody({
  enableTypes: [ 'json'.'form'.'text'.'xml'].formLimit: '56kb'.jsonLimit: '1mb'.textLimit: '1mb'.xmlLimit: '1mb'.strict: true
});

/** * Route processing */
const mdRoute = router.routes();
const mdRouterAllowed = router.allowedMethods();

module.exports = [
  mdKoaBody,
  mdRoute,
  mdRouterAllowed
];

Copy the code

Since we have already modified the boot file, there is no need to add it to the boot fileapp.use(). Here’s another attempt with a POST request:

Discovery is what we expected. But there’s a hole:

const mdKoaBody = koaBody({
  enableTypes: [ 'json'.'form'.'text'.'xml'].formLimit: '56kb'.jsonLimit: '1mb'.textLimit: '1mb'.xmlLimit: '1mb'.strict: true
});
Copy the code

The koa-BodyParser plugin can only parse four types of data [‘ JSON ‘, ‘form’, ‘text’, and ‘XML’]. When we upload the file, we can’t get the file. Formidable: A formidable wall is a formidable wall. A formidable wall is a formidable wall. A formidable wall is a formidable wall in the app/middlewares directory. The code is as follows:

const Formidable = require('formidable');

const { tempFilePath } = require('.. /config');

module.exports = () = > {
  return async function (ctx, next) {
    const form = new Formidable({
      multiples: true.// Save path of the uploaded temporary file
      uploadDir: `${process.cwd()}/${tempFilePath}`
    });

    // eslint-disable-next-line promise/param-names
    await new Promise((reslove, reject) = > {
      form.parse(ctx.req, (err, fields, files) = > {
        if (err) {
          reject(err);
        } else{ ctx.request.body = fields; ctx.request.files = files; reslove(); }}); });await next();
  };
};

Copy the code

Formidable: formidable: Formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable: formidable

/** * Introduce third-party plugins */
const koaBody = require('koa-bodyparser');

/** * Import custom files */
const router = require('.. /router');
const formidable = require('./formidable');

/ * * * * https://github.com/koajs/bodyparser * /
const mdFormidable = formidable();
const mdKoaBody = koaBody({
  enableTypes: [ 'json'.'form'.'text'.'xml'].formLimit: '56kb'.jsonLimit: '1mb'.textLimit: '1mb'.xmlLimit: '1mb'.strict: true
});

/** * Route processing */
const mdRoute = router.routes();
const mdRouterAllowed = router.allowedMethods();

module.exports = [
  mdFormidable,
  mdKoaBody,
  mdRoute,
  mdRouterAllowed
];

Copy the code

Let’s test it out:

Judging from the results, we have got what we expected. At this point, parameter parsing is done

Formidable. Js const {tempFilePath} = require(‘.. / config ‘). There are 5 files in the app/config directory, which are:

  • app/config/index.js
  • app/config/base.js
  • app/config/dev.js
  • .

Except for the index.js file, other files can be created according to the actual situation of the project

App /config/index.js

const base = require('./base');
const dev = require('./dev');
const pre = require('./pre');
const pro = require('./pro');

const env = process.env.NODE_ENV || 'dev';

const configMap = {
  dev,
  pre,
  pro
}


module.exports = Object.assign(base, configMap[env]);

Copy the code

Uniform return format & error handling

Before we implement error handling and a uniform return format, let’s do a little more tweaking. We created the config directory, which contains some constant configurations, and then we created a common/utils.js directory to store utility functions. It would be cumbersome to import every reference to a common/utils.js directory. So we put the utility functions and constant configuration on the app.context property, and then we don’t have to introduce it so often, we can do it through CTX. To access, modify app/index.js as follows:

const Koa = require('koa');
const compose = require('koa-compose');

const MD = require('./middlewares/');
const config = require('./config')
const utils = require('./common/utils')

const app = new Koa();

const port = '8082'
const host = '0.0.0.0'

app.context.config = config;
app.context.utils = utils;

app.use(compose(MD));

app.listen(port, host, () = > {
  console.log(`API server listening on ${host}:${port}`);
});

Copy the code

Let’s get down to business. Let’s fix the uniform return format. The first response is to write a utility function, not too simple:

const successRes = (data, msg) = > {
    return {
        code: 0,
        data,
        msg: msg || 'success',}}const failRes = (code = 1, data, msg) = > {
    return {
        code,
        data,
        msg: msg || 'fail',
    }
}

ctx.body = ctx.utils.successRes('aaa')
/ / or
ctx.body = ctx.utils.failRes(10001)
Copy the code

This is not a problem, but in fact, it can be more pure, take full advantage of koA onion model, make ctx.body more concise, return the correct result, such as: ctx.body = data, think of here, it is still added middleware. I need to add two here, an error handling, a uniform return format, and these two are related, so THEY’re written together

The file app/middlewares/response. Js

const response = () = > {
  return async (ctx, next) => {
    ctx.res.fail = ({ code, data, msg }) = > {
      ctx.body = {
        code,
        data,
        msg,
      };
    };

    ctx.res.success = msg= > {
      ctx.body = {
        code: 0.data: ctx.body,
        msg: msg || 'success'}; };await next();
  };
};

module.exports = response;
Copy the code

The file app/middlewares/error. Js

const error = () = > {
  return async (ctx, next) => {
    try {
      await next();
      if (ctx.status === 200) { ctx.res.success(); }}catch (err) {
      if (err.code) {
        // The error is thrown voluntarily
        ctx.res.fail({ code: err.code, msg: err.message });
      } else {
        // Error at runtime
        ctx.app.emit('error', err, ctx); }}}; };module.exports = error;

Copy the code

Introduce the above two middleware in app/middlewares/index.js:

/** * Introduce third-party plugins */
const koaBody = require('koa-bodyparser');

/** * Import custom files */
const router = require('.. /router');
const formidable = require('./formidable');
const response = require('./response');
const error = require('./error');

/ * * * * https://github.com/koajs/bodyparser * /
const mdFormidable = formidable();
const mdKoaBody = koaBody({
  enableTypes: [ 'json'.'form'.'text'.'xml'].formLimit: '56kb'.jsonLimit: '1mb'.textLimit: '1mb'.xmlLimit: '1mb'.strict: true
});

/** * Return format */
const mdResHandler = response();
/** * Error handling */
const mdErrorHandler = error();

/** * Route processing */
const mdRoute = router.routes();
const mdRouterAllowed = router.allowedMethods();

module.exports = [
  mdFormidable,
  mdKoaBody,
  mdResHandler,
  mdErrorHandler,
  mdRoute,
  mdRouterAllowed
];

Copy the code

App /middlewares/error.js (); if the status code is 200, we will return it with a successful utility function. If not, we will return it in two cases: One is the case we throw ourselves that contains a business error code (which we return wrapped in a failed utility function); For the second case, we need to modify the startup file app/index.js and add the following code:

app.on('error'.(err, ctx) = > {
  if (ctx) {
    ctx.body = {
      code: 9999.message: 'Error when program is running:${err.message}`}; }});Copy the code

We’ll test our code once we’ve done that: app/controllers/test.js

const list = async ctx => {
  ctx.body = 'Return result'
}
Copy the code

Request the interface and return the following values:

That’s what we expected

Next, we change the app/controllers/test.js to throw a business error code

const list = async ctx => {
  const data = ' '
  ctx.utils.assert(data, ctx.utils.throwError(10001.'Verification code invalid'))
  ctx.body = 'Return result'
}
Copy the code

The request is sent back again with the following results:

In line with expectations

Update the app/controllers/test.js file as shown in figure 1

const list = async ctx => {
  const b = a;
  ctx.body = 'Return result'
}
Copy the code

Send the request again to see the result:

In line with expectations

So far, we’ve got the error handling, we’ve got the uniform return format, and we can move on

Cross domain set

This should be the simplest, just use @koa/cors (see the documentation), because this is a small amount of code, so just add it to app/middlewares/index.js:

const cors = require('@koa/cors');

/** * cross-domain processing */
const mdCors = cors({
  origin: The '*'.credentials: true.allowMethods: [ 'GET'.'HEAD'.'PUT'.'POST'.'DELETE'.'PATCH']});module.exports = [
  mdFormidable,
  mdKoaBody,
  mdCors,
  mdResHandler,
  mdErrorHandler,
  mdRoute,
  mdRouterAllowed
];

Copy the code

You’re done

Add the log

Use log4js to view the document to log the request, add the file app/middlewares/log.js:

const log4js = require('log4js');
const { outDir, flag, level } = require('.. /config').logConfig;

log4js.configure({
  appenders: { cheese: { type: 'file'.filename: `${outDir}/receive.log`}},categories: { default: { appenders: [ 'cheese'].level: 'info'}},pm2: true
});

const logger = log4js.getLogger();
logger.level = level;

module.exports = () = > {
  return async (ctx, next) => {
    const { method, path, origin, query, body, headers, ip } = ctx.request;
    const data = {
      method,
      path,
      origin,
      query,
      body,
      ip,
      headers
    };
    await next();
    if (flag) {
      const { status, params } = ctx;
      data.status = status;
      data.params = params;
      data.result = ctx.body || 'no content';
      if(ctx.body.code ! = =0) {
        logger.error(JSON.stringify(data));
      } else {
        logger.info(JSON.stringify(data)); }}}; };Copy the code

App /middlewares/index.js introduces the log middleware written above:

const log = require('./log');

/** * Log requests */
const mdLogger = log();

module.exports = [
  mdFormidable,
  mdKoaBody,
  mdCors,
  mdLogger,
  mdResHandler,
  mdErrorHandler,
  mdRoute,
  mdRouterAllowed
];

Copy the code

Let’s look at the request effect:

[2021-03-31T21:44:40.919] [INFO] default - {"method":"GET"."path":"/a"."origin":"http://localhost:8082"."query": {"name":"Zhang"."age":"12"},"body": {},"ip":"127.0.0.1"."headers": {"user-agent":"PostmanRuntime / 7.26.8"."accept":"* / *"."cache-control":"no-cache"."postman-token":"d8be0c95-10f6-438c-aa37-1006be317081"."host":"localhost:8082"."accept-encoding":"gzip, deflate, br"."connection":"keep-alive"},"status":200."params": {},"result": {"code":0."data":"Return result"."msg":"success"}}
[2021-03-31T21:54:55.595] [ERROR] default - {"method":"GET"."path":"/a"."origin":"http://localhost:8082"."query": {"name":"Zhang"."age":"12"},"body": {},"ip":"127.0.0.1"."headers": {"user-agent":"PostmanRuntime / 7.26.8"."accept":"* / *"."cache-control":"no-cache"."postman-token":"86b581e4-07cf-4b04-9b01-5e56c19f696f"."host":"localhost:8082"."accept-encoding":"gzip, deflate, br"."connection":"keep-alive"},"status":200."params": {},"result": {"code":9999."message":"Program running error: b is not defined"}}
Copy the code

Here’s the log module.

Parameter calibration

I forgot to check the parameters, which is necessary both for business logic and to avoid errors at runtime. Again, we can put parameter validation in the corresponding controller, like this:

const list = async ctx => {
  const { name, age } = ctx.request.query
  if(! name) ctx.utils.assert(false, ctx.utils.throwError(10001.'Name is required'))
  if(! age) ctx.utils.assert(false, ctx.utils.throwError(10001.'Age is a must'))
  ctx.body = name + age
}
Copy the code

But when you have a lot of parameters, the controller gets really big, and we don’t see the point of this function, and I have to write a lot of repetitive code, Like ctx.utils.assert(false, ctx.utils.throwError(10001, ‘name is required ‘)), I wish I could write some business code right at the controller layer, @hapi/joi/app/middlewares/ add paramvalidater.js file:

module.exports = paramSchema= > {
  return async function (ctx, next) {
    let body = ctx.request.body;
    try {
      if (typeof body === 'string' && body.length) body = JSON.parse(body);
    } catch (error) {}
    const paramMap = {
      router: ctx.request.params,
      query: ctx.request.query,
      body
    };

    if(! paramSchema)return next();

    const schemaKeys = Object.getOwnPropertyNames(paramSchema);
    if(! schemaKeys.length)return next();

    // eslint-disable-next-line array-callback-return
    schemaKeys.some(item= > {
      const validObj = paramMap[item];

      const validResult = paramSchema[item].validate(validObj, {
        allowUnknown: true
      });

      if (validResult.error) {
        ctx.utils.assert(false, ctx.utils.throwError(9998, validResult.error.message)); }});await next();
  };
};

Copy the code

This parameter verification middleware is not used in app/middlewares/index.js, we will transform app/router/index.js:

// const koaRouter = require('koa-router');
// const router = new koaRouter();

// const routeList = require('./routes');


// routeList.forEach(item => {
// const { method, path, controller } = item;
// router[method](path, controller);
// });

// module.exports = router;

const koaRouter = require('koa-router');
const router = new koaRouter();

const routeList = require('./routes');
const paramValidator = require('.. /middlewares/paramValidator');

routeList.forEach(item= > {
  const { method, path, controller, valid } = item;
  router[method](path, paramValidator(valid), controller);
});

module.exports = router;

Copy the code

The KOA-Router allows you to add multiple router-level middleware, and this is where parameter validation is handled. Then we add a new directory schema to hold the code for the parameter validation section. Add two files:

  • app/schema/index.js
const scmTest = require('./test');

module.exports = {
  scmTest
};

Copy the code
  • app/schema/test.js
const Joi = require('@hapi/joi');

const list = {
  query: Joi.object({
    name: Joi.string().required(),
    age: Joi.number().required()
  })
};

module.exports = {
  list
};

Copy the code

We can see the following code in app/router/index.js:

  const { method, path, controller, valid } = item;
  router[method](path, paramValidator(valid), controller);
Copy the code

We need a valid property to validate the parameter, so we can modify the app/router/routes.js file as follows:

const { test } = require('.. /controllers');
const { scmTest } = require('.. /schema/index')

const routes = [
  {
    / / test
    method: 'get'.path: '/a'.valid: scmTest.list,
    controller: test.list
  }
];

module.exports = routes;

Copy the code

Now let’s change the code in controller to remove the parameter validation we wrote manually and change it to:

const list = async ctx => {
  const { name, age } = ctx.request.query
  ctx.body = name + age
}
Copy the code

Here we haven’t checked the parameters, so let’s try sending the request and see what happens:

In the request parameters, we have removed the age parameter, you can see that the return result is what we expected, so far the parameter verification has been done, @hapi/joi for more usage, please see the documentation

Database operation

When it comes to database operations, we can add a service directory under the app. Separate the database operations from the controller directory and place them in the Service directory. Each of the two directories focuses on business processing and the other on adding, deleting, modifying, and checking the database. In addition, add a model directory to define the database table structure, the details of which will not be covered here.

Directory structure so far

conclusion

More common logic can be done at the middleware level, such as login verification, permission verification, etc. Up to now, there are still many problems in the above details that have not been dealt with. For example, the middleware that returns a uniform format will actually have problems if it returns a file, and many details will be optimized later.

The above is my personal summary of practice, there are unreasonable places or suggest that we help to point out, to learn and exchange progress together

The resources

  • The complete code: https://github.com/JustGreenHand/koa-app