2
头图

What is koa?

Koa is the next generation of Express web framework based on Node.js. Use koa to write web applications, by combining different generators, you can avoid repeated and tedious nesting of callback functions, and greatly improve the efficiency of common error handling. Koa does not bind any middleware in the kernel method, it only provides a lightweight and elegant function library, which makes writing web applications and APIs handy.

What can Koa do?

The main purpose

  • Website (for example, forums like cnode)
  • api (three terminals: pc, mobile terminal, h5)
  • Match with other modules, such as writing bullet screens with socket.io, im (live chat), etc.

Koa is a micro web framework, but it is also a Node.js module, which means we can also use it to do some http-related things. For example: to achieve a function similar to http-server, in the development of vue or react, in the crawler, use the route to trigger the crawler task, etc. For example, in the bin module, it is very easy to integrate the koa module and start a function such as static-http-server.

Set up project start-up service

// 1. 创建项目文件夹后初始化npm
npm init
// 2. 安装koa环境
npm install koa
// 3. 根目录下创建app文件夹作为我们源代码的目录
// 4. app下新建index.js作为入口文件

The generated directory structure is as follows:

Write app/index.js

const Koa = require('koa');
const app = new Koa();
const port = '3333';
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}`)
});

node app/index.js root directory. After the startup is successful, API server listening on 0.0.0.0:3333 appears on the console. Open the browser and visit native ip: 3333

image-20210627084008790

Routing processing

koa processing corresponding to a respective route returned in response to this development process similar java programmed in controller , restful style routing can be very semantic prepared according to the traffic scenario corresponding handlers, front end use axios access server finds the corresponding function (routing Name) to get the corresponding desired result.

Write app/index.js :

// app/index.js
const Koa = require('koa');
const app = new Koa();

const port = '3333';
const host = '0.0.0.0';

app.use(async ctx => {
  const { path } = ctx;
  console.log(path)
  if (path === '/test1') {
    ctx.body = 'response for test1';
  } else if (path === '/test2') {
    ctx.body = 'response for test2';
  } else if (path === '/test3') {
    ctx.body = 'response for test3';
  } else {
    ctx.body = 'Hello World';
  }
});

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

Note: Every time you want to update the code in koa to take effect, you must restart the koa service

At this time, let's try again:

image-20210627085327472

The result returned as we expected. At this time, we will solve the appeal problem first. How to update the code to help us improve development efficiency

  1. Install nodemon

    npm install nodemon
    npm i nodemon -g // 建议直接全局安装
  1. Modify package.js
"scripts": {
  "start": "nodemon app/index.js"
}

Run npm run start to start the service again. At this time, after modifying the code, you only need to refresh the browser instead of restarting the node service!

It is foreseeable that the above processing of routing is not feasible in actual combat. After the api is gradually increased, the maintainability of the system needs to be considered, and koa-router came into being.

koa-router: A middleware that centrally processes URLs. It is responsible for processing URL mapping and calling different processing functions according to different URLs. In this way, we can concentrate on writing processing functions for each URL

Create a new app router directory, as shown below:

image-20210627090757106

Install koa-router

npm install koa-router

Write app/router/index.js

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

router.get('/test1', ctx  => {
  ctx.body = 'response for test1';
});

router.get('/test2', ctx  => {
  ctx.body = 'response for test2';
});

router.get('/test3', ctx  => {
  ctx.body = 'response for test3';
});

module.exports = router;

Visit the test again in the browser, http://192.168.0.197:3333/test3 , return response for test3 , the returned result is the same as before. Think about it again. In the actual company's business scenario, router/index.js may be very large. Therefore, we only need to care about the specific route for the routing file, and its corresponding processing function can be separately proposed for unified management. We can put the business logic processing function in controller , as follows:

image-20210627092430698

We have added three new files:

  • app/router/routes.js routing list file
  • app/contronllers/index.js business processing unified export
  • app/contronllers/test.js Business Process Document

All business logic codes are managed in the controller, as shown in app/contronllers/test.js

const echo = async ctx => {
  ctx.body = '这是一段文字...';
}

module.exports = {
  echo
}

app/contronllers/index.js unified entrance, management export

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

module.exports = {
  test
}

app/router/routes.js routing file concentrates on managing all routing, no need to maintain the corresponding business logic code

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

const routes = [
  {
    path: 'test1',
    method: 'get',
    controller: test.echo
  }
];

module.exports = routes;

Transform app/router/index.js

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

routes.forEach(route => {
  const { method, path, controller } = route;
  //  router 第一个参数是 path, 后面跟上路由级中间件 controller(上面编写的路由处理函数)
  router[method](path, controller);
});

module.exports = router;

Open the browser to visit http://192.168.0.197:3333/test1

image-20210627093721213

If the result is overdue, it returns normally, and the test is successful.

Parameter analysis

After testing the get request, request a post request. The path is /postTest and the parameter is name: wangcong . The request is as follows:

image-20210627102933947

Print out console.log('postTest', ctx) as follows. It seems that the parameter'name' we passed in is not found. How to get the request body of the post?

image-20210627102601854

koa-bodyparser : For POST request processing, the koa-bodyparser middleware can parse the formData data of the koa2 context into ctx.request.body

Before installing the middleware, we can transform the management of the middleware in the same way as the router.

Create a new app/midllewares directory, add index.js files to manage all middleware

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

// 路由处理,router.allowedMethods()用在了路由匹配router.routes()之后,所以在当所有路由中间件最后调用.此时根据ctx.status设置response响应头
const mdRoute = router.routes();
const mdRouterAllowed = router.allowedMethods();

// 导出数组是为后面使用koa-compose做准备,koa-compose支持传入数组,数组里的中间件一次同步执行
// 洋葱模型, 务必注意中间件的执行顺序!!!
module.exports = [
  mdRoute,
  mdRouterAllowed
];

index.js file contains all the middleware used, and then the startup file app/index.js :

const Koa = require('koa');
const app = new Koa();
const compose = require('koa-compose');
const MD = require('./midllewares'); // 引入所有的中间件


const port = '3333';
const host = '0.0.0.0';

app.use(compose(MD)); // compose接收一个中间件数组, 按顺序同步执行!!!

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

compose is a tool function, Koa.js middleware function after this tool combination, according app.use() sequential synchronous execution , i.e. the formation of onion rings and called.

Introduce koa-bodyparser to process the request parameters uniformly, Note: In order to process the information in each Request, bodyParser must be placed in front of the route and let him process it before entering the route

// midllewares/index.js
const router = require('../router');
const koaBody = require('koa-bodyparser'); // bodyParser 就是为了处理每个 Request 中的信息,要放到路由前面先让他处理再进路由

// 路由处理,router.allowedMethods()用在了路由匹配router.routes()之后,所以在当所有路由中间件最后调用.此时根据ctx.status设置response响应头
const mdRoute = router.routes();
const mdRouterAllowed = router.allowedMethods();

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

// 洋葱模型, 务必注意中间件的执行顺序!!!
module.exports = [
  mdKoaBody,
  mdRoute,
  mdRouterAllowed
];

postman request test

image-20210627102758998

Successfully obtained ctx.request.body

Quoting a sentence from the koa-bodyparser document, it can be seen that it does not support binary streams for uploading, and we hope that we use co-busboy to parse multipart format data

Notice: this module don't support parsing multipart format data, please use co-busboy to parse multipart format data.

Replace koa-bodyparser with koa-body , koa-body mainly depends on the following two:

"co-body": "^5.1.1",
"formidable": "^1.1.1"

The official introduced it like this

A full-featured koa body parser middleware. Supports multipart, urlencoded, and json request bodies. Provides the same functionality as Express's bodyParser - multer.

Modify app/midllewares/index.js :

const { tempFilePath } = require('../config');
const { checkDirExist } = require('../utils/file');
const router = require('../router');
const koaBody = require('koa-body'); // koa-body 就是为了处理每个 Request 中的信息,要放到路由前面先让他处理再进路由

// 路由处理,router.allowedMethods()用在了路由匹配router.routes()之后,所以在当所有路由中间件最后调用.此时根据ctx.status设置response响应头
const mdRoute = router.routes();
const mdRouterAllowed = router.allowedMethods();

/**
 * 参数解析
 * https://github.com/koajs/bodyparser
 */
const mdKoaBody = koaBody({
  multipart: true, // 支持文件上传
  // encoding: 'gzip', // 启用这个会报错
  formidable: {
    uploadDir: tempFilePath, // 设置文件上传目录
    keepExtensions: true,    // 保持文件的后缀
    maxFieldsSize: 200 * 1024 * 1024, // 设置上传文件大小最大限制,默认2M
    onFileBegin: (name,file) => { // 文件上传前的设置
      // 检查文件夹是否存在如果不存在则新建文件夹
      checkDirExist(tempFilePath);
      // 获取文件名称
      const fileName = file.name;
      // 重新覆盖 file.path 属性
      file.path = `${tempFilePath}/${fileName}`;
    },
    onError:(err)=>{
      console.log(err);
    }
  }
})

// 洋葱模型, 务必注意中间件的执行顺序!!!
module.exports = [
  mdKoaBody,
  mdRoute,
  mdRouterAllowed
];

Among them, two folders, config and utils, are created, and the respective directories are:

image-20210627163251606

config file in 060de6c2217d33 currently only configures the temporary path of the uploaded file. Later, you can also configure some configuration related to different environments:

image-20210627163452123

file.js tool file and a unified export file of `index.js are created under the utils folder, which mainly deals with the logic related to the file (path, file name, etc.):

// utils/file.js
const fs = require('fs');
const path = require('path');

function getUploadDirName(){
  const date = new Date();
  let month = Number.parseInt(date.getMonth()) + 1;
  month = month.toString().length > 1 ? month : `0${month}`;
  const dir = `${date.getFullYear()}${month}${date.getDate()}`;
  return dir;
}

// 创建目录必须一层一层创建
function mkdir(dirname) {
  if(fs.existsSync(dirname)){
    return true;
  } else {
    if (mkdir(path.dirname(dirname))) {
      fs.mkdirSync(dirname);
      return true;
    }

  }
}

function checkDirExist(p) {
  if (!fs.existsSync(p)) {
    mkdir(p)
  }
}

function getUploadFileExt(name) {
  let idx = name.lastIndexOf('.');
  return name.substring(idx);
}

function getUploadFileName(name) {
  let idx = name.lastIndexOf('.');
  return name.substring(0, idx);
}

module.exports = {
  getUploadDirName,
  checkDirExist,
  getUploadFileExt,
  getUploadFileName
}
// utils/index.js
const file = require('./file')

module.exports = {
  file
}

Introduce the global public part in app/index.js and mount it in the context of app.context

// app/index.js
const Koa = require('koa');
const app = new Koa();
const compose = require('koa-compose');
const MD = require('./midllewares');
const config = require('./config');
const utils = require('./utils');

const port = '3333';
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}`)
});

Test upload function:

image-20210627164254743

Note: In the KoaBody configuration, keepExtensions: true must be enabled, otherwise the upload will not succeed!

Check the app directory, the file we just uploaded is generated

image-20210627164506045

With the koa-body @ 4, the console print file information ctx.request.files , low version ctx.request.body.files

image-20210627164652759

Unified response body & error handling

To process the response in a unified format, we can make full use of the onion model for delivery. We can write two middleware, a unified return format middleware and an error handling middleware, as follows:

File app/midllewares/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;

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) {
        // 自己主动抛出的错误
        ctx.res.fail({ code: err.code, msg: err.message });
      } else {
        // 程序运行时的错误
        ctx.app.emit('error', err, ctx);
      }
    }
  };
};

module.exports = error;

app/middlewares/index.js quotes them:

const { tempFilePath } = require('../config');
const { checkDirExist } = require('../utils/file');
const router = require('../router');
const koaBody = require('koa-body'); // koa-body 就是为了处理每个 Request 中的信息,要放到路由前面先让他处理再进路由
const response = require('./response');
const error = require('./error');

// 路由处理,router.allowedMethods()用在了路由匹配router.routes()之后,所以在当所有路由中间件最后调用.此时根据ctx.status设置response响应头
const mdRoute = router.routes();
const mdRouterAllowed = router.allowedMethods();

/**
 * 参数解析
 * https://github.com/koajs/bodyparser
 */
const mdKoaBody = koaBody({
  multipart: true, // 支持文件上传, 必须设置为true!!!
  // encoding: 'gzip', // 启用这个会报错
  formidable: {
    uploadDir: tempFilePath, // 设置文件上传目录
    keepExtensions: true,    // 保持文件的后缀
    maxFieldsSize: 200 * 1024 * 1024, // 设置上传文件大小最大限制,默认2M
    onFileBegin: (name,file) => { // 文件上传前的设置
      // 检查文件夹是否存在如果不存在则新建文件夹
      checkDirExist(tempFilePath);
      // 获取文件名称
      const fileName = file.name;
      // 重新覆盖 file.path 属性
      file.path = `${tempFilePath}/${fileName}`;
    },
    onError:(err)=>{
      console.log(err);
    }
  }
})
// 统一返回格式
const mdResHandler = response();
// 错误处理
const mdErrorHandler = error();

// 洋葱模型, 务必注意中间件的执行顺序!!!
module.exports = [
  mdKoaBody,
  mdResHandler,
  mdErrorHandler,
  mdRoute,
  mdRouterAllowed
];

To reiterate, in app.use(), middleware execution is executed continuously and synchronously. mdResHandler defines two processing channels (success and failure). The real judgment logic is in the error.js middleware, one is a business error The code needs to be returned to the front-end for processing. The other is an error when the server code is running. For this type of error, we need to start koa's error handling event to handle it. After judgment and processing in error.js, mdResHandler is called to return the request response in a unified return format. For server-side runtime code errors, we still need to make changes. Modify the code app/index.js

app.on('error', (err, ctx) => {
  if (ctx) {
    ctx.body = {
      code: 9999,
      message: `程序运行时报错:${err.message}`
    };
  }
});

After completion, we still use the code of echo in the controller/ap/test.js

const echo = async ctx => {
  ctx.body = '这是一段文字...';
}

Request again to see what's different from before

image-20210627171126461

Late results returned, then return error simulation, modified test.js under echo function is as follows:

// test.js
const { throwError } = require('../utils/handle');
const echo = async ctx => {
  const data = '';
  ctx.assert(data, throwError(50002, 'token失效!'));
  // 不会往下执行了
  ctx.body = '这是一段文字...';
}


// utils/handle.js
const assert = require('assert');

const throwError = (code, message) => {
  const err = new Error(message);
  err.code = code;
  throw err;
};

module.exports = {
  assert,
  throwError
};

postman requests the test again:

image-20210627173550538

The result returned as expected

Modify test.js to the code error when koa is running:

const echo = async ctx => {
  const data = '';
  data = 'a'; // 模拟语法错误
  ctx.body = '这是一段文字...';
}

Request again and get the following results:

image-20210627173801715

At this point, the error handling is done, and the unified return format is also done.

Parameter verification

Parameter verification can greatly avoid the runtime error of the appealed program. In this example, we also put the parameter verification in controller to complete. test.js adds a new business processing function print to return the front-end name, which is printed in On the page:

const print = async ctx => {
  const { name } = ctx.request.query;
  if (!name) {
    ctx.utils.handle.assert(false, ctx.utils.handle.throwError(10001, '参数错误'));
  }
  ctx.body = '打印姓名: ' + name;
}

Request a test, the normal parameters are as follows:

image-20210628073806022

If no parameters are passed, the error status code 10001 :

image-20210628073852996

It can be expected that as the complexity of the business scenario increases, the part of the code behind the controller layer for parameter verification will become larger and larger, so this part must be optimized. The third-party plug-in joi is to deal with this scenario. , We can use this middleware to help us complete parameter verification. In app/middlewares/ added under validator.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();

    schemaKeys.some(item => {
      const validObj = paramMap[item];

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

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

Modify app/router/index.js :

const koaRouter = require('koa-router');
const router = new koaRouter();
const routes = require('./routes');
const validator = require('../midllewares/validator');

routes.forEach(route => {
  const { method, path, controller, valid } = route;
  router[method](path, validator(valid), controller);
});

module.exports = router;

Can be seen, route multi deconstruct a valid as validator parameter, app/router/routes.js in print add a route validation rules, as follows:

{
  path: '/print',
  method: 'get',
  valid: schTest.print,
  controller: test.print
}

koa-router allows the addition of multiple routing-level middleware, and we will handle the parameter verification here. Then create a new directory schema app directory to store the code of the parameter verification part, and add two files:

  1. app/schema/index.js:

    const schTest = require('./test');
    
    module.exports = {
     schTest
    };
  2. app/schema/test.js:

    const Joi = require('@hapi/joi');
    
    const print = {
     query: Joi.object({
    name: Joi.string().required(),
    age: Joi.number().required()
     })
    };
    
    module.exports = {
     list
    };

Before the app/controller/test.js manual calibration section deleted, the test joi middleware is in effect:

const print = async ctx => {
  const { name } = ctx.request.query;
  ctx.body = '打印姓名: ' + name;
}

Request interface test

image-20210628082630673

Here, even if the integration is completed calibration parameters, joi more of Use Please view the document

Configure cross-domain

Use @koa/cors plug-in for cross-domain configuration, app/middlewares/index.js add configuration, as follows:

// ...省略其他配置
const cors = require('@koa/cors'); // 跨域配置
// 跨域处理
const mdCors = cors({
  origin: '*',
  credentials: true,
  allowMethods: [ 'GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH' ]
});
module.exports = [
  mdKoaBody,
  mdCors,
  mdResHandler,
  mdErrorHandler,
  mdRoute,
  mdRouterAllowed
];

Log

Use log4js to record the request log and add 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));
      }
    }
  };
};

Introduce the log middleware written above in app/middlewares/index.js

const log = require('./log'); // 添加日志
// ...省略其他代码

// 记录请求日志
const mdLogger = log();

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

Use the postman request interface to test the effect:

image-20210627174730031

Open the log file and view the log:

[2021-06-27T17:45:53.803] [INFO] default - {"method":"GET","path":"/test1","origin":"http://192.168.0.197:3333","query":{},"body":{},"ip":"192.168.0.197","headers":{"host":"192.168.0.197:3333","connection":"keep-alive","cache-control":"no-cache","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36","postman-token":"ae200806-a92b-00c4-5f2d-d5afdb7d717c","accept":"*/*","accept-encoding":"gzip, deflate","accept-language":"zh-CN,zh;q=0.9,en;q=0.8"},"status":200,"params":{},"result":{"code":0,"data":"这是一段文字...","msg":"success"}}

At this point, the log module is successfully referenced!

Database operation

In app then add a lower service directory, database operation after on service directory, controller focus on business processes, service additions and deletions to change search focused database operations and other matters. You can also add a model directory to define the database table structure. The specific operations will be shown in the koa application actual combat .

to sum up

The basic koa actual combat project is over here. In the enterprise-level development, there will be more problems to be solved, and I look forward to closer to the enterprise-level actual combat project.

project remote address


MangoGoing
780 声望1.2k 粉丝

开源项目:详见个人详情