文章目录
- [Koa 使用总结](#Koa 使用总结)
Koa 使用总结
概述
Koa.js 是一个由 Express 团队开发的现代、轻量级 Web 框架,它使用 ES2017 的 async/await 语法,提供了更优雅的中间件处理方式。Koa 的设计理念是 "精简",它只提供最核心的功能,其余功能通过中间件实现。
环境搭建
安装依赖
npm install koa koa-router koa-body koa-static
npm install nodemon --save-dev
- koa:核心库
- koa-router:路由中间件
- koa-body:请求体解析中间件,支持文件上传
- koa-static:静态文件服务
- nodemon:开发环境下自动重启服务器
package.json
{
"name": "koademo",
"version": "1.0.0",
"type": "module"
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"koa": "^3.1.1",
"koa-body": "^7.0.1",
"koa-router": "^14.0.0",
"koa-static": "^5.0.0"
},
"devDependencies": {
"nodemon": "^3.1.11"
}
}
使用 ESM 模块系统时注意:
- 使用 import 和 export 替代 require 和 module.exports
__dirname和__filename不可用,需要使用 import.meta.url 获取
GET请求处理
处理简单请求
javascript
import Koa from "koa";
import KoaRouter from "koa-router";
const app = new Koa();
const router = new KoaRouter();
router.get("/api/users", async (ctx) => {
ctx.body = [
{name: "小白", age: 18},
{name: "小黑", age: 28}
];
});
app.use(router.routes()).use(router.allowedMethods());
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
处理查询参数
javascript
import Koa from "koa";
import KoaRouter from "koa-router";
const app = new Koa();
const router = new KoaRouter();
router.get("/api/user", async (ctx) => {
const {name, age} = ctx.query;
ctx.body = {name, age};
});
app.use(router.routes()).use(router.allowedMethods());
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
处理路径参数
javascript
import Koa from "koa";
import KoaRouter from "koa-router";
const app = new Koa();
const router = new KoaRouter();
router.get("/api/user/:name/:age", async (ctx) => {
const {name, age} = ctx.params;
ctx.body = {name, age};
});
app.use(router.routes()).use(router.allowedMethods());
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
POST请求处理
koa-body
处理请求体需要使用 koa-body 中间件,Koa 是用于解析请求体的强大工具,支持多种数据格式,包括:
- JSON 格式:
application/json - 表单数据:
application/x-www-form-urlencoded - 多部分表单数据:
multipart/form-data(用于文件上传)
koa-body 中间件会根据请求头中的 Content-Type 自动选择合适的解析方式,无需手动配置。
javascript
app.use(koaBody({
multipart: true, // 启用多部分表单数据解析
urlencoded: true, // 启用 x-www-form-urlencoded 解析
json: true, // 启用 JSON 解析
text: false, // 禁用纯文本解析
encoding: 'utf-8', // 编码格式
formLimit: '56kb', // 表单数据大小限制
jsonLimit: '1mb', // JSON 数据大小限制
textLimit: '1mb', // 文本数据大小限制
formidable: {
uploadDir: path.join(__dirname, 'uploads'), // 文件上传目录
keepExtensions: true, // 保留文件扩展名
maxFileSize: 200 * 1024 * 1024, // 文件大小限制
onFileBegin: (name, file) => {
console.log(`开始上传文件: ${file.originalFilename}`);
}
}
}));
处理表单和JSON格式的数据
无论客户端发送哪种格式的数据,解析后的数据都会存放在 ctx.request.body 中,可以直接访问和使用。
javascript
import Koa from "koa";
import KoaRouter from "koa-router";
import koaBody from "koa-body";
const app = new Koa();
const router = new KoaRouter();
app.use(koaBody());
router.post("/api/user", async (ctx) => {
const contentType = ctx.request.headers['content-type'];
console.log(contentType); // 输出格式
const {name, age} = ctx.request.body;
ctx.status = 200;
ctx.body = {name, age};
});
app.use(router.routes()).use(router.allowedMethods());
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
文件处理
单文件上传
javascript
import fs from "fs";
import Koa from "koa";
import koaBody from "koa-body";
import KoaRouter from "koa-router";
import path from "path";
import {fileURLToPath} from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = new Koa();
const router = new KoaRouter();
// 确保上传目录存在
const uploadDir = path.join(__dirname, "uploads");
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
const uploadBody = koaBody({
multipart: true, // 启用多部分表单数据解析
urlencoded: true, // 启用 x-www-form-urlencoded 解析
json: true, // 启用 JSON 解析
text: false, // 禁用纯文本解析
encoding: "utf-8", // 编码格式
formLimit: "56kb", // 表单数据大小限制
jsonLimit: "1mb", // JSON 数据大小限制
textLimit: "1mb", // 文本数据大小限制
formidable: {
uploadDir: uploadDir, // 文件上传目录
keepExtensions: true, // 保留文件扩展名
maxFileSize: 200 * 1024 * 1024, // 文件大小限制 200MB
onFileBegin: (name, file) => {
console.log(`开始上传文件: ${file.originalFilename}`);
}
}
});
router.post("/api/single-upload", uploadBody, async (ctx) => {
const files = ctx.request.files;
if (!files) {
ctx.status = 400;
ctx.body = {
err: "请选择上传文件"
};
return;
}
const {file} = files;
const originalName = file.originalFilename;
const newName = file.newFilename;
ctx.body = {
originalName,
newName,
size: file.size,
type: file.mimetype,
filePath: file.filepath,
fileUrl: `/api/download/${newName}`
};
});
app.use(router.routes()).use(router.allowedMethods());
const PORT = 3001;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
多文件上传
javascript
import fs from "fs";
import Koa from "koa";
import koaBody from "koa-body";
import KoaRouter from "koa-router";
import path from "path";
import {fileURLToPath} from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = new Koa();
const router = new KoaRouter();
const uploadDir = path.join(__dirname, "uploads");
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
const uploadBody = koaBody({
multipart: true, // 启用多部分表单数据解析
urlencoded: true, // 启用 x-www-form-urlencoded 解析
json: true, // 启用 JSON 解析
text: false, // 禁用纯文本解析
encoding: "utf-8", // 编码格式
formLimit: "56kb", // 表单数据大小限制
jsonLimit: "1mb", // JSON 数据大小限制
textLimit: "1mb", // 文本数据大小限制
formidable: {
uploadDir: uploadDir, // 文件上传目录
keepExtensions: true, // 保留文件扩展名
maxFileSize: 200 * 1024 * 1024, // 文件大小限制 200MB
onFileBegin: (name, file) => {
console.log(`开始上传文件: ${file.originalFilename}`);
}
}
});
router.post("/api/multiple-upload", uploadBody, async (ctx) => {
const files = ctx.request.files;
if (!files) {
ctx.status = 400;
ctx.body = {
err: "请选择上传文件"
};
return;
}
const fileArray = Array.isArray(files.files) ? files.files : [files.files];
const results = fileArray.map(file => {
const originalName = file.originalFilename;
const newName = file.newFilename;
return {
originalName,
newName,
size: file.size,
type: file.mimetype,
filePath: file.filepath,
fileUrl: `/api/download/${newName}`
};
});
ctx.body = results;
});
app.use(router.routes()).use(router.allowedMethods());
const PORT = 3001;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
文件下载
javascript
import fs from "fs";
import Koa from "koa";
import koaBody from "koa-body";
import KoaRouter from "koa-router";
import path from "path";
import {fileURLToPath} from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = new Koa();
const router = new KoaRouter();
// 确保上传目录存在
const uploadDir = path.join(__dirname, "uploads");
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
router.get(`/api/download/:filename`, async (ctx) => {
const {filename} = ctx.params;
const filePath = path.join(uploadDir, filename);
if (!fs.existsSync(filePath)) {
ctx.status = 404;
ctx.body = {
err: "文件不存在"
};
return;
}
// 设置响应头
ctx.set("Content-Type", "application/octet-stream");
ctx.set("Content-Disposition", `attachment; filename="${filename}"`);
ctx.set("Content-Length", fs.statSync(filePath).size);
ctx.body = fs.createReadStream(filePath);
});
app.use(router.routes()).use(router.allowedMethods());
const PORT = 3001;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
响应头说明:
Content-Type: application/octet-stream: 表示二进制流,浏览器会触发下载Content-Disposition: attachment; filename="${filename}": 告诉浏览器以附件形式下载,并指定文件名Content-Length: 文件大小,帮助浏览器显示下载进度
路由
简单路由
javascript
import Router from "koa-router";
const router = new Router();
// 基础路由示例
router.get('/', async (ctx) => {
ctx.body = 'Hello Koa!';
});
// 注册路由到应用
app.use(router.routes()).use(router.allowedMethods());
路由分组
javascript
const userRouter = new KoaRouter({prefix: "/api/user"});
userRouter.get("/getUsers", async (ctx) => {
ctx.body = [
{name: "小白", age: 18},
{name: "小黑", age: 28}
];
});
userRouter.post("/createUser", async (ctx) => {
const {name, age} = ctx.request.body;
ctx.body = {name, age};
});
const articleRouter = new KoaRouter({prefix: "/api/article"});
articleRouter.get("/getArticles", async (ctx) => {
ctx.body = [
{articleName: "红楼梦", author: "曹雪芹"},
{articleName: "三国演义", author: "罗贯中"},
];
});
articleRouter.post("/createArticle", async (ctx) => {
const {articleName, author} = ctx.request.body;
ctx.body = {articleName, author};
});
app.use(userRouter.routes()).use(userRouter.allowedMethods());
app.use(articleRouter.routes()).use(articleRouter.allowedMethods());
中间件
中间件执行流程
Koa 中间件采用洋葱模型,请求从外层中间件进入,进过内层中间件处理后,再从内层中间件返回外层中间件。
请求 -> 中间件1 -> 中间件2 -> 处理 -> 中间件2 -> 中间件1 -> 响应
注册顺序
中间件的执行顺序决定了请求处理的流程:
- 全局中间件:放在最前面,如:错误处理、日志记录等
- 第三方中间件:如:koa-body、koa-static等
- 路由中间件:放在最后面,用于处理具体的请求
javascript
// 正确的中间件顺序
app.use(globalErrorHandler); // 全局错误处理(最先)
app.use(loggerMiddleware); // 日志记录
app.use(koaBody()); // 请求体解析
app.use(serveStatic); // 静态文件服务
app.use(router.routes()); // 路由处理(最后)
app.use(router.allowedMethods());
自定义日志中间件
javascript
const loggerMiddleware = async (ctx, next) => {
const startTime = Date.now();
const {method, url} = ctx.request;
console.log(`收到请求:${method} ${url}`);
try {
await next();
const endTime = Date.now();
const responseTime = endTime - startTime;
const statusCode = ctx.status;
console.log(`响应完成:${method} ${url} | 状态:${statusCode} | 耗时:${responseTime}ms`);
} catch (error) {
const endTime = Date.now();
const responseTime = endTime - startTime;
console.error(`请求出错:${method} ${url} | 耗时:${responseTime}ms | 错误:${error.message}`);
throw error;
}
};
app.use(loggerMiddleware);
404中间件
方式一
需要将 404 处理中间件放在所有路由之后。
javascript
import fs from "fs";
import Koa from "koa";
import koaBody from "koa-body";
import KoaRouter from "koa-router";
import path from "path";
import {fileURLToPath} from "url";
const app = new Koa();
const router = new KoaRouter();
app.use(router.routes()).use(router.allowedMethods());
// 404处理
app.use((ctx, next) => {
if (ctx.status === 404) {
ctx.body = {
err: "接口不存在"
};
}
return next();
});
const PORT = 3001;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
方式二
需要将 404 处理中间件放在所有路由之前。
javascript
const errorMiddleware = async (ctx, next) => {
try {
await next();
// 处理404
if (ctx.status === 404) {
ctx.status = 404;
ctx.body = {
success: false,
err: "请求的资源不存在"
};
}
} catch (error) {
// 捕获所有错误
console.error("全局错误捕获:", {
message: error.message,
stack: error.stack,
path: ctx.request.url,
method: ctx.request.method,
});
const statusCode = ctx.status || 500;
const err = error.message || "服务器错误";
ctx.status = statusCode;
ctx.body = {
success: false,
err
};
}
};
app.use(errorMiddleware);
全局错误处理中间件
javascript
import Koa from "koa";
import koaBody from "koa-body";
import KoaRouter from "koa-router";
const app = new Koa();
const router = new KoaRouter();
router.get("/api/users", async (ctx) => {
ctx.body = [{name: "小白", age: 18}, {name: "小黑", age: 28}];
throw Error("发生异常了");
});
const errorMiddleware = async (ctx, next) => {
try {
await next();
} catch (error) {
// 捕获所有错误
console.error("全局错误捕获:", {
message: error.message,
stack: error.stack,
path: ctx.request.url,
method: ctx.request.method,
});
const statusCode = ctx.status || 500;
const err = error.message || "服务器错误";
ctx.status = statusCode;
ctx.body = {
success: false,
err
};
}
};
app.use(errorMiddleware);
app.use(router.routes()).use(router.allowedMethods());
const PORT = 3001;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});