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
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:
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
Install
nodemon
npm install nodemon npm i nodemon -g // 建议直接全局安装
- 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:
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:
We have added three new files:
app/router/routes.js
routing list fileapp/contronllers/index.js
business processing unified exportapp/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
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:
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?
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
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-featuredkoa
body parser middleware. Supportsmultipart
,urlencoded
, andjson
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:
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:
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:
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
With the koa-body @ 4, the console print file informationctx.request.files
, low versionctx.request.body.files
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
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:
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:
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:
If no parameters are passed, the error status code 10001
:
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:
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 };
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
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:
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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。