共计 14288 个字符,预计需要花费 36 分钟才能阅读完成。
什么是 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. 创立我的项目文件夹后初始化 npm
npm 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
中编写 controller
,restful
格调的路由能够十分语义化的依据业务场景编写对应的处理函数,前端利用 axios
拜访服务端找到对应的函数(路由名字)来获取对应想要的后果。
编写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}`)
});
留神:每次在 koa 中更新代码后想要失效必须重启 koa 服务
这时,咱们再拜访试试:
后果按咱们预期返回了,这时咱们先解决上诉的问题,如何热更新代码来帮忙咱们进步开发效率
-
装置
nodemon
npm install nodemon npm i nodemon -g // 倡议间接全局装置
- 批改
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.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 申请测试
获取 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-bodyparser
为koa-body
,koa-body
次要是上面两个依赖:
"co-body": "^5.1.1",
"formidable": "^1.1.1"
官网这样对它做了介绍
A full-featured
koa
body parser middleware. Supportsmultipart
,urlencoded
, andjson
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.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}
在 app/index.js
引入全局公共局部,挂载到 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}`)
});
测试上传性能:
留神: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.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 再次申请测试:
后果如预期返回
批改 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.js
中 print
路由新增一条校验规定,如下:
{
path: '/print',
method: 'get',
valid: schTest.print,
controller: test.print
}
koa-router
容许增加多个路由级中间件,咱们将参数校验放在这里解决。随后在 app
目录下新建目录 schema
,用来寄存参数校验局部的代码,增加两个文件:
-
app/schema/index.js
:const schTest = require('./test'); module.exports = {schTest};
-
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 实战型我的项目到这里就完结了,企业级开发中,还会有更多的问题须要解决,期待更加贴近企业级的实战我的项目。
我的项目近程地址