基于koa的项目实践

1,365 阅读7分钟

前言

本篇会介绍下koa的项目实践中的一些项目搭建,以及一些解决方案。我会通过对koa基础知识的介绍,来更好的进入koa项目实践环节。一起看看吧。

前置知识

在项目中使用koa,那必然要先了解koa的运行机制,掌握中间件的实现原理。而koa是基于洋葱模型的来进行执行运作的。 这里我们就来了解下来KOA 的洋葱模型。

koa的洋葱模型

洋葱我们都知道,一层包裹着一层,层层递进,但是现在不是看其立体的结构,而是需要将洋葱切开来,从切开的平面来看:

image.png

可以看到要从洋葱中心点穿过去,就必须先一层层向内穿入洋葱表皮进入中心点,然后再从中心点一层层向外穿出表皮。

这里有个特点:进入时穿入了多少层表皮,出去时就必须穿出多少层表皮。先穿入表皮,后穿出表皮,符合我们所说的栈列表,先进后出的原则。这也是我们的洋葱模型原理。

然后再回到 Node.js 框架,洋葱的表皮我们可以思考为中间件

  • 外向内的过程是一个关键词 next()
  • 而从内向外则是每个中间件执行完毕后,进入下一层中间件,一直到最后一层。

而koa 的中间件运行都是基于洋葱模型来执行的。

中间件执行

为了理解上面的洋葱模型以及其执行过程,我们用 koa作为框架例子,来实现一个后台服务。这里需要做一些准备工作,你按照如下步骤初始化项目即可。

mkdir myapp
cd myapp
npm init
npm install koa --save
touch app.js

以下代码,其中的 app.use 部分的就是 4个中间件。其中一个为异步中间件。

const Koa = require("koa");
const app = new Koa();

/**
 * 中间件 1
 */
app.use(async (ctx, next) => {
  console.log("first");
  await next();
  console.log("first end");
});

/**
 * 中间件 2
 */
app.use(async (ctx, next) => {
  console.log("second");
  await next();
  console.log("second end");
});

/**
 * 异步中间件
 */
app.use(async (ctx, next) => {
  console.log("async");
  await next();
  await new Promise((resolve) =>
    setTimeout(() => {
      console.log(`wait 1000 ms end`);
      resolve();
    }, 1000)
  );
  console.log("async end");
});

/**
 * 中间件 2
 */
app.use(async (ctx, next) => {
  console.log("third");
  await next();
  console.log("third end");
});

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

app.listen(3000, () => console.log(`Example app listening on port 3000!`));

接下来我们运行如下命令,启动项目。

node app.js

启动成功后,打开浏览器,输入如下浏览地址:

http://127.0.0.1:3000/

然后在命令行窗口,你可以看到打印的信息如下:

Example app listening on port 3000!
first
second
async
third
third end
wait 1000 ms end
async end
second end
first end

你会发现,koa 严格按照了洋葱模型的执行,从上到下,也就是从洋葱的内部向外部,输出 first、second、async、third;接下来从内向外输出 third end、async end、second end、first end。

搭建项目

好了,基本原理说完,就进入搭建项目的环节。我们会从统一中间件管理==>路由管理==>参数解析==>统一数据格式==>全局捕获错误处理==>跨域处理==>日志记录==>请求参数校验等方面,来介绍koa项目搭建所需的必要中间件运用,和流程管理。

首先,建立项目文件目录:

├── server                 // 代码文件夹
    ├── common             // 公共资源
    ├── config             // 环境配置
    ├── controllers        // 业务逻辑
    ├── middlewares        // 中间件集合
    ├── router             // 路由
    ├── schema             // 参数校验
    ├── utils              // 工具集
    ├── package.json       // 包配置
    ├── app.js             // 运行文件

注意,以下示例代码中require('xxx')xxx npm包未写npm 引用(npm install xxx --save)。这里做简要说明。

server/app.js文件中,写入基本的启动服务代码:

const Koa = require("koa");
const app = new Koa();

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

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`启动成功,${ "http://127.0.0.1"}:${PORT}`);
});

中间件合并

首先,我们使用koa-compose将所有的中间件处理集中起来,统一管理,方便查阅。

server/middlewares/index.js文件下进行中间件的集中处理,输出中间件列表

module.exports = [
  ...,
];

然后在server/app.js 文件中引入使用。

const compose = require("koa-compose");
const middlewares = require("./server/middlewares");
app.use(compose(middlewares));

路由管理

使用@koa/router来管理路由。这里我们要将路由和各个路由对应的处理函数区分开,不能写在一起,防止后期难以维护。

这里新建如下文件:

  • server/router/routes.js 路由列表文件;
  • server/router/index.js 路由处理导出文件;
  • server/contronllers/index.js 统一业务处理;
  • server/controllers/home.js 业务处理文件;

server/contronllers 文件夹来统一存放业务处理的代码逻辑。如下server/controllers/home.js是业务处理文件:

const home = async (ctx) => {
  ctx.body = "hello world";
};

module.exports = { home };

server/contronllers 文件夹统一导出业务处理文件:

const homeRouter = require("./home");
module.exports = {
  homeRouter,
};

server/router/routes.js 文件中,统一处理业务路由列表;

const { homeRouter } = require("../controllers");
const routes = [
  {
    method: "get",
    path: "/",
    controller: homeRouter.home,
  },
];

module.exports = routes;

server/router/index.js 文件中,循环业务路由列表,导出路由处理文件;

const router = require("@koa/router")();
const routeList = require("./routes");

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

module.exports = router;

参数解析

针对请求报文的处理。用koa-bodyparser来解析。server/middlewares/index.js文件下写入:

const koaBodyParser = require("koa-bodyparser");
const myKoaBodyParser = koaBodyParser();
module.exports = [
  ...,
  myKoaBodyParser,
]

统一返回数据格式

你是否遇到这样一个问题,你每一次的返回数据时,都写入如下的重复逻辑:

 ctx.body = {
        code,
        data,
        msg: msg || "fail",
 };

这里,我们运用中间层处理统一返回数据格式,在server/middlewares/response.js文件下写入:

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

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

    await next();
  };
};

module.exports = response;

server/middlewares/index.js文件下写入:

const response = require("./response");
const myResHandler = response();
module.exports = [
  ...,
  myResHandler,
]

写了以上的代码。我们还需要 错误处理中间件 来触发统一返回数据格式中间件。接着向下来。

错误处理

基于统一返回数据格式的处理,这里运用洋葱模型构建一个中间件,全局统一错误处理。server/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;

server/middlewares/index.js文件下写入:

const error = require("./error");
const myErrorHandler = error();
module.exports = [
  ...,
  myErrorHandler,
]

结合了错误处理中间件来触发统一返回数据格式中间件,我们只需要写下如下代码,中间件就可以自动补充返回数据格式了。

 ctx.body = data

跨域处理

使用@koa/cors处理跨域问题。server/middlewares/index.js文件下写入:

const koaCors = require("@koa/cors");
const myKoaCors = koaCors({
  origin: "*",
  credentials: true,
  allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
});

module.exports = [
 ...,
  myKoaCors,
]

日志记录

使用koa-logger 处理简易日志记录问题。server/middlewares/index.js文件下写入:

const logger = require("koa-logger");
const myLogger = logger();
module.exports = [
  ...,
  myLogger,
]

请求参数校验

实际项目中的一些解决方案

文件上传方案

const koaBody = require('koa-body');   // 文件上传
app.use(koaBody({
    multipart: true,
    formidable: {
        maxFileSize: config.LIMIT.UPLOAD_IMG_SIZE    // 设置上传文件大小最大限制,默认2M
    }

Koa2 设置全局变量

运用 app.context[xxx]

const Koa =  require('koa');
const app = new Koa();

//作用和express中的app.locals = {//xxx} 一样,全局生效
app.context.state = Object.assign(app.context.state, {key1 : value1, key2: value2});

请求外部接口的方法

  1. 一种是 node 原生 request,request 已经废弃,不更新,具体原因
  2. koa2-request 中间件,基于 request 做了封装,内部源码参考
  3. bent 具有async / await 的 node.js 和浏览器的功能的 HTTP 客户端。
  4. got 为 Node.js 的友好和强大的 HTTP 请求库。
  5. 更多参考:

image (1).png

图片来源于替代库列表

方案建议:

作为一名使用 request 一段时间的 Node.js 开发人员,bent 绝对是一个简单的过渡 - 强烈推荐 💖

并发请求限制

方案:

  • 基于Promise.race的特性配合Promise.all 实现并行请求的限制请求数量,保持请求数量。
  • 基于队列先进先出的特性配合Promise 构成微任务异步队列,实现并行请求的限制请求数量,保持请求数量。

具体参考:

多进程解决方案

使用PM2守护进程管理器

CPU 过载保护设计

待补充。