前言
本篇会介绍下koa的项目实践中的一些项目搭建,以及一些解决方案。我会通过对koa基础知识的介绍,来更好的进入koa项目实践环节。一起看看吧。
前置知识
在项目中使用koa,那必然要先了解koa的运行机制,掌握中间件的实现原理。而koa是基于洋葱模型的来进行执行运作的。 这里我们就来了解下来KOA 的洋葱模型。
koa的洋葱模型
洋葱我们都知道,一层包裹着一层,层层递进,但是现在不是看其立体的结构,而是需要将洋葱切开来,从切开的平面来看:
可以看到要从洋葱中心点穿过去,就必须先一层层向内穿入洋葱表皮进入中心点,然后再从中心点一层层向外穿出表皮。
这里有个特点:进入时穿入了多少层表皮,出去时就必须穿出多少层表皮。先穿入表皮,后穿出表皮,符合我们所说的栈列表,先进后出的原则。这也是我们的洋葱模型原理。
然后再回到 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});
请求外部接口的方法
- 一种是 node 原生 request,request 已经废弃,不更新,具体原因。
- koa2-request 中间件,基于 request 做了封装,内部源码参考。
- bent 具有async / await 的 node.js 和浏览器的功能的 HTTP 客户端。
- got 为 Node.js 的友好和强大的 HTTP 请求库。
- 更多参考:
图片来源于替代库列表。
方案建议:
作为一名使用 request 一段时间的 Node.js 开发人员,bent 绝对是一个简单的过渡 - 强烈推荐 💖
并发请求限制
方案:
- 基于Promise.race的特性配合Promise.all 实现并行请求的限制请求数量,保持请求数量。
- 基于队列先进先出的特性配合Promise 构成微任务异步队列,实现并行请求的限制请求数量,保持请求数量。
具体参考:
多进程解决方案
使用PM2 是守护进程管理器;
CPU 过载保护设计
待补充。