什么是koa?

koa是Express的下一代基于Node.js的web框架。应用 koa 编写 web 利用,通过组合不同的 generator,能够罢黜反复繁琐的回调函数嵌套,并极大地晋升罕用错误处理效率。Koa 不在内核办法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 Web 利用和API变得得心应手。

Koa能干什么?

主要用途

  • 网站(比方cnode这样的论坛)
  • api(三端:pc、挪动端、h5)
  • 与其余模块搭配,比方和socket.io搭配写弹幕、im(即时聊天)等

koa是微型web框架,但它也是个Node.js模块,也就是说咱们也能够利用它做一些http相干的事儿。举例:实现相似于http-server这样的性能,在vue或react开发里,在爬虫里,利用路由触发爬虫工作等。比方在bin模块里,集成koa模块,启动个static-http-server这样的性能,都是非常容易的。

搭建我的项目启动服务

// 1. 创立我的项目文件夹后初始化npmnpm init// 2. 装置koa环境npm install koa// 3. 根目录下创立app文件夹作为咱们源代码的目录// 4. 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 => {  ctx.body = 'Hello World';});app.listen(port, host, () => {  console.log(`API server listening on ${host}:${port}`)});

根目录下运行node app/index.js,启动胜利后控制台呈现API server listening on 0.0.0.0:3333,关上浏览器拜访本机ip:3333

路由解决

koa中解决相应的路由返回对应的响应这一开发过程相似java中编写controllerrestful格调的路由能够十分语义化的依据业务场景编写对应的处理函数,前端利用axios拜访服务端找到对应的函数(路由名字)来获取对应想要的后果。

编写app/index.js:

// app/index.jsconst 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}`)});

留神:每次在koa中更新代码后想要失效必须重启koa服务

这时,咱们再拜访试试:

后果按咱们预期返回了,这时咱们先解决上诉的问题,如何热更新代码来帮忙咱们进步开发效率

  1. 装置nodemon

    npm install nodemonnpm i nodemon -g // 倡议间接全局装置
  1. 批改package.js
"scripts": {  "start": "nodemon app/index.js"}

运行npm run start再次启动服务,这时批改代码后只须要刷新浏览器即可,不必重启node服务了!

能够预感的是:上面对路由的解决在实战中是不可行的,api逐步减少后须要思考到零碎的可维护性,koa-router应运而生。

koa-router: 集中处理URL的中间件,它负责解决URL映射,依据不同的URL调用不同的处理函数,这样,咱们就能能分心为每个URL编写处理函数

app 目录下新建 router 目录,如下所示:

装置koa-router

npm install koa-router

编写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;

浏览器中再次拜访测试,http://192.168.0.197:3333/test3,返回response for test3,返回后果与之前统一。再次细想一下,理论公司的业务场景中,router/index.js中可能一个处理函数就会十分宏大,因而,路由文件咱们只须要关怀具体的路由,它对应的处理函数能够独自提出来对立治理。咱们能够把业务逻辑处理函数放到controller中,如下:

咱们新增了三个文件:

  • app/router/routes.js 路由列表文件
  • app/contronllers/index.js 业务解决对立导出
  • app/contronllers/test.js 业务解决文件

所有的业务逻辑代码放到controller中治理,如app/contronllers/test.js所示:

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

app/contronllers/index.js对立入口,治理导出

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

app/router/routes.js路由文件分心治理所有路由,无需保护对应业务逻辑代码

const { test } = require('../controllers');const routes = [  {    path: 'test1',    method: 'get',    controller: test.echo  }];module.exports = routes;

革新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;

关上浏览器拜访http://192.168.0.197:3333/test1

后果如逾期失常返回,测试胜利。

参数解析

测试完get申请后再申请一个post申请,path为/postTest,参数为name: wangcong,申请如下:

打印出console.log('postTest', ctx)如下,如同并没有找到咱们传入的参数'name',那如何获取到post的申请体呢?

koa-bodyparser: 对于POST申请的解决,koa-bodyparser中间件能够把koa2上下文的formData数据解析到ctx.request.body中

装置中间件之前,咱们能够依照革新router的形式革新一下中间件的治理

新建app/midllewares目录,增加index.js文件对立治理所有中间件

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文件里集中了所有用到的中间件,接下来革新下启动文件 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 是一个工具函数,Koa.js 的中间件通过这个工具函数组合后,app.use() 的程序同步执行,也就是造成了 洋葱圈 式的调用。

引入 koa-bodyparser对立解决申请参数,留神:bodyParser 为了解决每个 Request 中的信息,要放到路由后面先让他解决再进路由

// midllewares/index.jsconst 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申请测试

获取ctx.request.body胜利!

援用koa-bodyparser文档的一句话,能够看进去它并不反对二进制流来进行上传,并且心愿咱们用co-busboy来解析multipart format data

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

替换koa-bodyparserkoa-bodykoa-body 次要是上面两个依赖:

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

官网这样对它做了介绍

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

批改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];

其中,创立了config和utils两个文件夹,各自目录别离为:

config中文件目前只配置了上传文件的长期门路,前面还能够配置一些不同环境下的配置相干:

utils文件夹下创立了一个file.js工具文件和`index.js对立导出文件,次要解决对文件相干(门路、文件名等)的逻辑:

// utils/file.jsconst 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.jsconst file = require('./file')module.exports = {  file}

app/index.js引入全局公共局部,挂载到app.context上下文中:

// app/index.jsconst 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}`)});

测试上传性能:

留神:KoaBody配置中,keepExtensions: true必须开启,否则上传不会胜利!

查看app目录下,生成了咱们刚刚上传的文件

在koa-body @4中,控制台打印文件相干信息用ctx.request.files,低版本应用ctx.request.body.files

对立响应体 & 错误处理

对立 格局解决返回响应,能够充分利用洋葱模型进行传递,咱们能够编写两个中间件,一个对立返回格局middleware,一个错误处理middleware,别离如下:

文件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;

文件 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援用它们:

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];

再次强调一遍,app.use()中,中间件执行是按续同步执行,mdResHandler定义了两种解决通道(胜利和失败),真正判断逻辑在error.js中间件中,一种是业务型错误码,须要返回给前端进行解决,另一种是服务端代码运行时报错,这种谬误类型咱们须要登程koa的谬误处理事件去解决。error.js中判断解决后都是调用mdResHandler对立返回格局返回申请响应。针对服务端运行时代码谬误,咱们还须要做出批改,在app/index.js中批改代码如下:

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

实现后,咱们还是利用之前的controller/ap/test.js中echo的代码:

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

再次申请看看跟之前有什么不一样

后果如逾期返回,再进行模仿谬误的返回,批改test.js下的echo函数如下:

// test.jsconst { throwError } = require('../utils/handle');const echo = async ctx => {  const data = '';  ctx.assert(data, throwError(50002, 'token生效!'));  // 不会往下执行了  ctx.body = '这是一段文字...';}// utils/handle.jsconst assert = require('assert');const throwError = (code, message) => {  const err = new Error(message);  err.code = code;  throw err;};module.exports = {  assert,  throwError};

postman再次申请测试:

后果如预期返回

批改test.js为koa运行时的代码谬误:

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

再次申请,失去后果如下:

至此,错误处理搞定了,对立返回格局也搞定。

参数校验

参数校验能够极大的防止上诉的程序运行时的谬误,在这个例子里,咱们也将参数校验放在controller外面去实现,test.js新增一个业务处理函数print用于返回前端姓名,打印在页面上:

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

申请测试,失常传参如下 :

不传参数,返回谬误状态码10001:

能够意料的是,随着业务场景复杂度的回升,controller层前面对于参数校验的局部代码会变得越来越宏大,所以这部分肯定是能够优化的,第三方插件 joi 就是应答这种场景,咱们能够借助此中间件帮忙咱们实现参数校验。在 app/middlewares/ 下增加 validator.js 文件:

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();  };};

批改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;

能够看到,route中多解构了一个valid来作为validator的参数,app/router/routes.jsprint路由新增一条校验规定,如下:

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

koa-router 容许增加多个路由级中间件,咱们将参数校验放在这里解决。随后在 app目录下新建目录 schema,用来寄存参数校验局部的代码,增加两个文件:

  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};

把之前app/controller/test.js手动校验局部删掉 ,测试joi中间件是否失效:

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

申请接口测试

到这里,参数校验就算整合实现,joi 更多的应用办法请查看文档

配置跨域

应用@koa/cors插件来进行跨域配置,app/middlewares/index.js增加配置,如下:

// ...省略其余配置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];

日志

采纳 log4js 来记录申请日志,增加文件 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));      }    }  };};

app/middlewares/index.js 中引入下面写的日志中间件:

const log = require('./log'); // 增加日志// ...省略其余代码// 记录申请日志const mdLogger = log();module.exports = [  mdKoaBody,  mdCors,  mdLogger,  mdResHandler,  mdErrorHandler,  mdRoute,  mdRouterAllowed];

利用postman申请接口测试成果:

关上日志文件,查看日志 :

[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"}}

到这里,日志模块援用胜利!

数据库操作

app 下再新增一个 service 目录, 之后的数据库操作放在 service 目录下,controller专一业务解决,service专一数据库的增删改查等事务操作。还能够增加一个 model 目录,用来定义数据库表构造,具体的操作将在之后的koa利用实战中具体展现。

总结

根本的koa实战型我的项目到这里就完结了,企业级开发中,还会有更多的问题须要解决,期待更加贴近企业级的实战我的项目。

我的项目近程地址