Koa
安装
npm i koa
开始
import Koa from 'koa';
const app = new koa();
app.use((ctx, next) => {
console.log(1);
ctx.body = "hello";
await next();
});
app.use((ctx, next) => {
console.log(2);
ctx.body += " world";
});
app.listen(8080);
中间件
import Koa from 'koa';
const app = new koa();
app.use((ctx, next) => {
// console.log(ctx);
// ctx.state.name = 'jack';
// ctx.throw(404, '出错了', {a: 1})
console.log(ctx.request);
await next();
});
app.use((ctx, next) => {
// throw new Error();
// console.log(ctx.state.name);
ctx.response.body = { name: "jack" };
});
app.on("error", (err) => {
console.log(err);
});
app.listen(8080);
错误处理中间件
// 自动抛出错误并返回
// ctx.throw(404, "页面不存在");
// 错误处理中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
message: err.message,
};
ctx.app.emit("error", err, ctx); // 可以选择性地触发应用级别的错误事件
}
});
// ...
// 监听 error 事件
app.on('error', (err, ctx) => {
console.error('服务器错误:', err.message);
});
在 Koa 中,错误处理中间件 通常需要放在最上面。这是因为 Koa 的中间件是按照「洋葱模型」执行的,即中间件是按顺序层层嵌套执行的:
- 每个中间件在执行
await next()
后,才会执行下一个中间件。 - 如果在某个中间件中发生错误,错误会向上传递,直到被捕获或到达最外层。
因此,如果你希望全局捕获所有中间件中抛出的错误,错误处理中间件必须放在所有其他中间件之前。这样一旦发生错误,它能及时捕获。
自定义404中间件
// 自定义 404 中间件
app.use(async (ctx, next) => {
await next();
if (ctx.status === 404) {
ctx.body = "请求的地址不存在,请检查请求地址";
}
});
注意:
koa中的中间件应该使用
await next()
,而不是直接使用next()
,如果没有await
,请求的异步流程可能会被打断,导致请求提前结束,出现404错误。
koa-static
npm i koa-static
import Koa from 'koa';
import serve from 'koa-static';
import path from 'path';
import { fileURLToPath } from "url";
// 在ES模块(ECMAScript Modules,ESM)中,
// __dirname 和 __filename 这两个在CommonJS模块中常用的全局变量不可直接使用
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = new Koa();
// 将项目目录下的 public 文件夹设置为静态资源目录
// 例如存在 /public/index.html 文件,则客户端访问 /index.html
app.use(serve(path.join(__dirname, 'public')));
app.listen(3002, () => {
console.log('Server running on http://localhost:3002');
});
koa-static-cache
npm i koa-static-cache
import Koa from "koa";
import koaStaticCache from "koa-static-cache";
import path from 'path';
import { fileURLToPath } from "url";
// 在ES模块(ECMAScript Modules,ESM)中,__dirname 和 __filename 这两个在CommonJS模块中常用的全局变量是不可直接使用的。
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = new Koa();
app.use(
koaStaticCache(path.join(__dirname, "static"), {
prefix: "/public",
})
);
app.use(
koaStaticCache(path.join(__dirname, "static/img"), {
prefix: "/img",
})
);
app.use((ctx, next) => {
ctx.body = "hello";
});
app.listen(8088);
注意:
koa-static-cache
是一种缓存静态文件的中间件,它会将文件加载到内存中,并且会缓存这些文件的路径信息。
因此,如果在服务器启动后有新的文件被动态上传,由于缓存的原因,koa-static-cache
可能无法处理这些新上传的文件,导致前端访问不到这些文件。
解决方案
使用
koa-static
代替koa-static-cache
:koa-static
不会缓存文件路径信息,适合动态上传和访问的场景。它会每次请求时直接访问文件系统,确保可以读取到新上传的文件。javascriptimport serve from 'koa-static'; app.use(serve(path.join(__dirname, 'public'), { // 可以添加一些其他配置,比如缓存时间等 }));
动态刷新缓存(不推荐):虽然可以使用一些技巧手动刷新
koa-static-cache
的缓存,但这样会增加复杂性和维护成本,且可能引发性能问题。重新配置
koa-static-cache
(若必须使用缓存):如果一定要用koa-static-cache
,可以设置dynamic
为true
以允许对新文件的处理。不过,这样的设置可能会使缓存失去意义。javascriptimport koaStaticCache from "koa-static-cache"; app.use( koaStaticCache(path.join(__dirname, "/public"), { prefix: "/", dynamic: true, // 允许动态加载新文件 maxAge: 0, // 可根据需要设置缓存时间,0表示不缓存 }) );
推荐使用 koa-static
koa-static
:实时读取文件系统,不会有缓存问题,适合动态文件访问。- 动态上传文件:确保文件保存到指定的公开目录中,并返回正确的 URL。
这样,你可以保证文件上传后能够立即被前端访问到。
koa-router
npm i koa-router
koa-router
和@koa/router
是同一个插件
import koa from "koa";
import koaRouter from "koa-router";
const app = new koa();
const router = new koaRouter();
app.use(async (ctx, next) => {
console.log(ctx.URL);
await next();
})
router.get("/", (ctx, next) => {
ctx.body = "首页";
});
router.get('/about', (ctx, next) => {
ctx.body = '关于';
})
// 路由嵌套 方式一
const userRouter = new koaRouter();
userRouter.get("/", (ctx, next) => {
ctx.body = "user-get";
});
userRouter.get("/info", (ctx, next) => {
ctx.body = "user-info";
});
router.use("/user", userRouter.routes());
// 路由嵌套 方式二
const apiRouter = new koaRouter({
prefix: "/api",
});
// get请求 /api/getUsers
apiRouter.get("/getUsers", (ctx, next) => {
ctx.body = ["Jack", "Tom"];
});
// post请求 /api/addUser
apiRouter.post("/addUser", (ctx, next) => {
ctx.body = "add user";
});
// 动态路由
const dynamicRouter = new koaRouter();
dynamicRouter.get("/dynamic/:id", (ctx, next) => {
ctx.body = `dynamic - ${ctx.params.id}`;
});
app.use(router.routes());
app.use(indexRouter.routes());
app.use(dynamicRouter.routes());
app.listen(8088, "localhost");
koa-body
npm i koa-body
import Koa from "koa";
import { koaBody } from "koa-body";
const app = new Koa();
app.use(koaBody());
app.use((ctx) => {
ctx.body = `Request Body: ${JSON.stringify(ctx.request.body)}`;
});
app.listen(3000);
使用 koa-body 上传文件
自定义存放文件夹、文件名称与支持多文件上传
// npm i koa koa-router koa-body koa-static cors fs-extra dayjs
import Koa from "koa";
import Router from "koa-router";
import { koaBody } from "koa-body";
import koaStatic from "koa-static";
import cors from "@koa/cors";
import fs from "fs-extra";
import dayjs from "dayjs";
import path from "path";
import { exec } from "child_process";
// esmodule中的 __dirname
const __dirname = path.resolve();
const app = new Koa();
const router = new Router();
// 使用 koa-static 中间件,将静态资源托管到 public 目录下
app.use(koaStatic(path.join(__dirname, "public")));
// 错误处理中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
console.log(err.message);
if (err.status === 413) {
ctx.status = 200;
ctx.body = { code: 1, msg: err.message };
} else {
ctx.status = 500;
ctx.body = { code: 1, msg: err.message };
}
}
});
router.post(
"/upload",
koaBody({
encoding: "utf-8", // 设置传入表单字段的编码,默认utf-8
multipart: true, // 解析多部分主体,默认false
formidable: {
// maxFields: 1000, // 限制查询字符串解析器将解码的字段数量,默认1000
// maxFieldsSize: 2 * 1024 * 1024, // 限制所有字段(文件除外)可以分配的内存量(以字节为单位)。如果超过此值,则会发出“error”事件,默认2mb (2 * 1024 * 1024)
uploadDir: path.join(__dirname, "uploads"), // 设置文件上传目录,默认os.tmpDir()
keepExtensions: true, // 写入的文件 uploadDir 将包含原始文件的扩展名,默认false
// hashAlgorithm: false, // 如果要计算传入文件的校验和,请将其设置为'sha1'或'md5',默认false
// multiples: true, // 多文件上传或不上传,默认true
maxFileSize: 50 * 1024 * 1024, // 限制文件大小 5MB
filter: ({ name, originalFilename, mimetype }) => {
// 限制文件类型
// const allowedTypes = ["image/jpeg", "image/png", "image/gif", "application/pdf", "video/mp4"];
// return allowedTypes.includes(mimetype);
// 只允许上传图片
// return mimetype && mimetype.includes("image");
return true;
},
// 处理每个文件时调用此函数,可用于在将文件保存到磁盘之前重命名文件
onFileBegin: (formName, file) => {
const dateFolder = dayjs().format("YYYY-MM-DD");
const uploadPath = path.join(__dirname, "uploads", dateFolder);
fs.ensureDirSync(uploadPath);
// 文件名称去掉特殊字符但保留原始文件名称
let newFileName = file.originalFilename
.replaceAll(" ", "_")
.replace(/[`~!@#$%^&*()|\-=?;:'",<>\{\}\\\/]/gi, "_");
const ext = path.extname(newFileName); // 获取原始扩展名
const baseName = path.basename(newFileName, ext); // 去除扩展名的文件名
const timestamp = Date.now(); // 时间戳
newFileName = `${baseName}-${timestamp}${ext}`; // 自定义文件名
file.newFilename = newFileName;
file.filepath = path.join(uploadPath, newFileName);
},
},
onError: (error, ctx) => {
console.log(error.message);
if (error.httpCode === 413) {
ctx.throw(413, "文件大小超出限制");
} else {
ctx.throw(400, error.message);
}
},
}),
async (ctx) => {
try {
const files = ctx.request.files;
if (!files || Object.keys(files).length === 0) {
ctx.body = { code: 1, msg: "未上传文件或上传文件格式有误" };
return;
}
let data = [];
for (let fileKey in files) {
const file = files[fileKey];
data.push({
name: file.originalFilename,
newFilename: file.newFilename,
// path: file.filepath,
size: file.size,
type: file.mimetype,
});
}
ctx.body = { code: 0, msg: "上传完毕", data };
} catch (error) {
ctx.status = 400;
ctx.body = { code: 400, msg: "上传失败" + error.message };
}
}
);
app.use(cors());
app.use(koaBody());
app.use(router.routes()).use(router.allowedMethods());
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
koa-bodyparser
npm i koa-bodyparser
import koa from "koa";
import Router from "koa-router";
import bodyParser from "koa-bodyparser";
const app = new koa();
app.use(bodyParser());
const router = new Router();
router.get("/", async (ctx) => {
ctx.body = "hello";
});
router.post("/change", (ctx) => {
let { id, name, price, number } = ctx.request.body;
console.log({ id, name, price, number });
});
app.use(router.routes());
app.listen(8088);
koa-bodyparser 和 koa-body 的区别
koa-bodyparser
和 koa-body
是用于处理 Koa 框架中的请求体(body)的两个中间件库。它们主要用于解析 HTTP 请求中传递的 JSON、URL-encoded、和 multipart/form-data 数据,但它们有一些不同之处。
1. koa-bodyparser
koa-bodyparser
是一个轻量级的 Koa 中间件,专注于解析 JSON 和 URL-encoded 请求体。它不支持 multipart/form-data(例如文件上传),适合处理较小且简单的请求体。
特点:
- 解析类型: 支持 JSON 和 URL-encoded 数据。
- 轻量且快速: 只提供基础的请求体解析功能。
- 简单易用: 配置简单,开箱即用。
使用方法:
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
const app = new Koa();
// 使用 koa-bodyparser 解析请求体
app.use(bodyParser());
app.use(async (ctx) => {
ctx.body = ctx.request.body; // 获取解析后的请求体
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
配置选项:
enableTypes
: 可以设置解析的请求类型,默认为['json', 'form']
。jsonLimit
: 设置 JSON 请求体的最大长度,默认为1mb
。formLimit
: 设置 URL-encoded 请求体的最大长度,默认为56kb
。
2. koa-body
koa-body
是一个功能更强大的中间件,支持 JSON、URL-encoded、和 multipart/form-data 数据的解析。它非常适合需要处理文件上传的场景。
特点:
- 解析类型: 支持 JSON、URL-encoded 和 multipart/form-data 数据。
- 文件上传: 支持文件上传和解析。
- 功能全面: 提供了更多的配置选项以适应复杂的需求。
使用方法:
import Koa from 'koa';
import koaBody from 'koa-body';
const app = new Koa();
// 使用 koa-body 解析请求体
app.use(koaBody({
multipart: true, // 允许处理 multipart/form-data
formidable: {
uploadDir: './uploads', // 文件上传的目录
keepExtensions: true, // 保留文件的扩展名
},
}));
app.use(async (ctx) => {
ctx.body = ctx.request.body; // 获取解析后的请求体
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
配置选项:
multipart
: 设置为true
以支持 multipart/form-data。jsonLimit
、formLimit
: 用于限制请求体大小。formidable
: 相关文件上传的配置,支持文件路径、扩展名保留等设置。
总结
koa-bodyparser
更适合轻量应用,不支持文件上传。koa-body
更强大,适合需要处理文件的场景。
根据项目需求选择合适的中间件即可。
koa-swig
npm i koa-swig co
import koa from "koa";
import koaRouter from "koa-router";
import Swig from "koa-swig";
import co from "co";
const render = Swig({
root: __dirname + "/view", // 模板存放目录
autoescape: true, // 是否自动escape编码
cache: false, // 是否启用缓存,开发时可以不启用,线上使用 'memory'
ext: ".html", // 模板后缀
});
const app = new koa();
const router = new koaRouter();
app.context.render = co.wrap(render);
let users = [{ username: "张三" }, { username: "李四" }, { username: "王五" }];
router.get("/", (ctx, next) => {
ctx.body = "home";
});
router.get("/user", (ctx, next) => {
ctx.body = `<!DOCTYPE html>
<html lang="en">
<head>
<title>模板文件</title>
</head>
<body>
<h1>koa swig</h1>
</body>
</html>`;
});
router.get("/list", async (ctx, next) => {
ctx.body = await ctx.render("1.html", { users });
});
app.use(router.routes());
app.listen(8088);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>koa swig</h1>
<p>list-{{Math.random()}}</p>
<ul>
{% for user in users %}
<li>{{user.username}}</li>
{% endfor %}
</ul>
</body>
</html>
mysql2
npm i mysql2
建立连接
普通连接
(async () => {
const mysql = require("mysql2/promise");
const connection = await mysql.createConnection({
host: "localhost",
user: "root",
database: "test",
});
const [rows, fields] = await connection.query("SELECT * FROM todolist");
console.log(rows);
})();
使用连接池
const mysql = require("mysql2/promise");
const { validateInteger } = require("./validate");
const DBCONFIG = {
host: "127.0.0.1",
user: "root",
password: "123456",
database: "test",
};
// 创建连接池,设置连接池的参数
const pool = mysql.createPool({
...DBCONFIG,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
使用预处理语句
// insert
// const [result] = await connection.query(`INSERT INTO mytable (name, age) VALUES(${name}, ${age})`);
// update
const [result] = await pool.query("UPDATE `todolist` SET `name` = ? , `age` = ? WHERE `id` = ? ", [name,age,id]);
// delete
const [result] = await connection.query(`DELETE FROM todolist WHERE id = ?`, id);
if (result.affectedRows > 0) {
// 操作成功
} else {
// 操作失败
}
参数化查询
参数化查询能够有效防止 SQL 注入攻击。可以通过 ?
占位符将参数传递给查询语句。
const userId = 1;
const [rows, fields] = await connection.execute('SELECT * FROM users WHERE id = ?', [userId]);
console.log(rows);
插入数据
const [result] = await connection.execute('INSERT INTO users (name, age) VALUES (?, ?)', ['Alice', 25]);
console.log('插入的ID:', result.insertId);
更新数据
const [result] = await connection.execute('UPDATE users SET age = ? WHERE name = ?', [30, 'Alice']);
console.log('受影响的行数:', result.affectedRows);
删除数据
const [result] = await connection.execute('DELETE FROM users WHERE name = ?', ['Alice']);
console.log('删除的行数:', result.affectedRows);
错误处理
mysql2
提供了多种错误处理机制。例如,如果连接失败或查询执行失败,你可以通过捕获异常来处理这些错误。
try {
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'test_db'
});
const [rows, fields] = await connection.execute('SELECT * FROM non_existent_table');
} catch (error) {
console.error('数据库错误:', error.message);
}
可以使用 mysql.format 查看实际的sql语句
// 使用 mysql.format 格式化 SQL 查询
const formattedSql = mysql.format(sql, params);
console.log("Generated SQL:", formattedSql);
关闭连接
在使用完数据库连接后,一定要记得关闭连接,否则会导致连接泄漏,影响性能。对于普通连接使用 .end()
,对于连接池使用 .release()
或在查询完成后自动回收。
普通连接
connection.end();
连接池
pool.end();
事务处理
mysql2
支持事务。通过 beginTransaction()
开始事务,执行多条 SQL 语句后,使用 commit()
提交事务或 rollback()
回滚事务。
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
await connection.execute('UPDATE users SET age = age + 1 WHERE id = ?', [1]);
await connection.execute('UPDATE users SET age = age - 1 WHERE id = ?', [2]);
await connection.commit();
console.log('事务提交成功');
} catch (err) {
await connection.rollback();
console.error('事务回滚:', err.message);
} finally {
connection.release();
}
使用execute和query执行sql脚本有什么区别
在
mysql2
中,execute()
和query()
都可以用来执行 SQL 脚本,但它们有一些重要的区别:参数化查询
execute()
:自动支持参数化查询,能够有效防止 SQL 注入攻击。通常用于需要传递参数的场景,如动态查询或更新操作。
query()
:也支持参数化查询,但更多用于简单的 SQL 查询,它不会对传入的参数进行深度处理。示例:
使用
execute()
:jsconst [rows, fields] = await connection.execute('SELECT * FROM users WHERE id = ?', [1]);
使用
query()
:jsconst [rows, fields] = await connection.query('SELECT * FROM users WHERE id = 1');
适用场景
execute()
:适合用于带有参数的动态 SQL 查询。通常在涉及参数时,优先使用execute()
。
query()
:更适合执行不带参数的静态 SQL 查询,比如简单的 SELECT 语句。执行效率
execute()
:预编译 SQL 语句并执行,可以提高执行效率,尤其是在多次执行类似 SQL 语句但参数不同的情况下。由于是预编译的,它适合需要更高性能的应用场景。query()
:每次执行时都解析整个 SQL 语句,效率相对稍低。返回结果
两者在返回结果时并没有明显区别,都返回一个包含查询结果的数组,第一个元素是查询结果,第二个元素是查询的字段元数据(字段的描述信息)。
例如:
execute()
返回:jsconst [rows, fields] = await connection.execute('SELECT * FROM users'); console.log(rows); // 查询结果 console.log(fields); // 字段元数据
query()
返回jsconst [rows, fields] = await connection.query('SELECT * FROM users'); console.log(rows); // 查询结果 console.log(fields); // 字段元数据
预处理语句
execute()
:它实际上是预处理语句(Prepared Statements),所以每次传入参数的 SQL 语句会预编译成固定的 SQL 模板,之后传递参数时直接填充。这样不仅更高效,还能防止 SQL 注入。
query()
:并不会预处理 SQL 语句,而是直接执行 SQL,因此 SQL 本身每次都要重新解析。总结:
execute()
:更适合复杂的、带参数的 SQL 语句,使用参数化查询能够提高安全性和执行效率,特别是动态 SQL 的场景。
query()
:适合执行简单的、不带参数的 SQL 语句。
直接使用createPool创建的pool连接池执行查询,和使用pool.getConnection()的连接进行查询的区别
在 mysql2
中,使用 createPool()
创建的连接池进行查询操作有两种主要方式:
- 直接使用
pool.query()
或pool.execute()
。 - 使用
pool.getConnection()
获取连接后再执行查询。
这两种方式都有各自的适用场景和差异,主要区别在于连接管理方式、灵活性和手动控制。
直接使用 pool.query()
或 pool.execute()
这种方式简洁且方便,适合大多数简单的查询操作场景。
jsconst mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: 'localhost', user: 'root', password: 'password', database: 'test_db' }); async function main() { const [rows] = await pool.query('SELECT * FROM users'); console.log(rows); } main();
优点:
- 自动管理连接:
pool.query()
和pool.execute()
自动从连接池获取连接,执行完查询后会自动释放连接。开发者不需要手动管理连接的获取和释放,非常方便。- 简单高效:适用于大多数情况下的单次查询,简洁代码减少了手动操作的复杂性。
- 错误处理简单:开发者无需担心连接释放的问题,连接池内部会自动处理。
缺点:
- 灵活性不足:当你需要在多个查询之间共享同一个数据库连接(如事务操作)时,这种方式就不适用了,因为每次查询都会获取一个新的连接。
- 无法处理复杂操作:如果你需要对数据库连接进行高级操作(如设置事务、锁定连接等),这种方式无法满足需求。
使用 pool.getConnection()
获取连接后再执行查询
这种方式适合需要在多次查询之间共享同一个连接,或者需要手动管理连接生命周期的场景。
jsconst mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: 'localhost', user: 'root', password: 'password', database: 'test_db' }); async function main() { const connection = await pool.getConnection(); // 手动获取连接 try { const [rows] = await connection.query('SELECT * FROM users'); console.log(rows); } finally { connection.release(); // 手动释放连接 } } main();
优点:
控制力更强:你可以精确控制什么时候获取连接,什么时候释放连接。在复杂操作(如事务管理、多次查询共享连接)中很有用。
适用于事务操作:当你需要执行多条 SQL 操作,并希望在同一个事务中执行时,这种方式是必不可少的。通过
pool.getConnection()
,你可以获取一个连接,并在该连接上执行多条查询。事务操作示例:
jsconst connection = await pool.getConnection(); try { await connection.beginTransaction(); await connection.query('INSERT INTO users (name, age) VALUES (?, ?)', ['Alice', 25]); await connection.query('UPDATE users SET age = ? WHERE name = ?', [30, 'Alice']); await connection.commit(); // 提交事务 } catch (err) { await connection.rollback(); // 发生错误时回滚事务 throw err; } finally { connection.release(); // 释放连接 }
缺点:
- 需要手动管理连接:你必须手动获取和释放连接,这增加了一些代码复杂度。忘记释放连接可能会导致连接池中的连接耗尽,影响应用的性能。
- 复杂度高:当你只需要简单的查询时,手动管理连接显得繁琐,增加了代码复杂度。
何时使用 pool.query()
/ pool.execute()
vs pool.getConnection()
适合使用
pool.query()
/pool.execute()
的场景:
- 简单查询或执行单条 SQL:例如只执行一个
SELECT
查询或INSERT
操作。- 无需手动管理连接:当你不需要精确控制数据库连接的生命周期,或不需要在多次查询之间共享连接。
- 代码简洁:适合简化代码编写,减少连接管理的复杂性。
适合使用
pool.getConnection()
的场景:
- 事务操作:你需要在一个事务中执行多条 SQL 语句,并确保它们要么全部成功,要么全部失败时。
- 需要多个查询共享同一个连接:例如在某些场景下,多个查询依赖于同一个连接(如锁定某些资源时)。
- 手动管理连接生命周期:你需要精确控制何时获取和释放连接,确保某些查询之间保持连接。
总结:
pool.query()
/pool.execute()
:简单快捷,适合大部分查询场景,自动管理连接,不需要手动释放。
pool.getConnection()
:提供更大的控制权,适合复杂操作、事务管理和多个查询之间共享连接的场景,但需要手动管理连接的释放。
注意:表名与列名不能使用占位符!
参数标记只能用于数据值应该出现的地方,不能用于 SQL 关键字、标识符等。
Parameter markers can be used only where data values should appear, not for SQL keywords, identifiers, and so forth.
MySQL 中的某些对象,包括数据库、表、索引、列、别名、视图、存储过程、分区、表空间、资源组和其他对象名称,称为标识符。
Certain objects within MySQL, including database, table, index, column, alias, view, stored procedure, partition, tablespace, resource group and other object names are known as identifiers.
log4js
安装
npm i log4js
基本使用
const log4js = require('log4js');
// 配置 log4js
log4js.configure({
appenders: {
out: { type: 'console' } // 定义控制台输出
},
categories: {
default: { appenders: ['out'], level: 'debug' } // 将控制台输出设置为默认输出,并且日志级别是 debug
}
});
// 获取 logger 实例
const logger = log4js.getLogger();
// 输出日志
logger.trace("This is a trace message"); // 追踪
logger.debug("This is a debug message"); // 调试
logger.info("This is an info message"); // 信息
logger.warn("This is a warn message"); // 警告
logger.error("This is an error message"); // 错误
logger.fatal("This is a fatal message"); // 严重错误
配置 appenders 和 categories
Appenders 是负责将日志输出到某个目标(控制台、文件、数据库等)的对象。
Categories 负责定义哪些日志级别被记录,通常包括 trace
、debug
、info
、warn
、error
和 fatal
。
常用的 appender
类型:
console
: 输出到控制台。file
: 输出到文件。dateFile
: 按日期切分输出日志文件。
示例:将日志同时输出到控制台和文件
log4js.configure({
appenders: {
console: { type: 'console' }, // 控制台输出
file: { type: 'file', filename: 'logs/app.log' } // 文件输出,文件名为 logs/app.log
},
categories: {
default: { appenders: ['console', 'file'], level: 'info' } // 将日志输出到 console 和 file
}
});
const logger = log4js.getLogger();
logger.info('This will be logged in both console and file');
按日期生成日志文件
如果希望按日期来切割日志文件,可以使用 dateFile
类型的 appender
:
log4js.configure({
appenders: {
dateFile: {
type: 'dateFile',
filename: 'logs/app.log',
pattern: '.yyyy-MM-dd', // 每天生成一个新文件
compress: true // 是否压缩旧的日志文件
}
},
categories: {
default: { appenders: ['dateFile'], level: 'info' }
}
});
const logger = log4js.getLogger();
logger.info('This log will be written into a file named logs/app.log.<date>');
日志分级
日志等级决定了输出的详细程度,从低到高依次是:
trace
: 最详细的日志级别,常用于开发和调试。debug
: 开发过程中调试信息。info
: 一般信息,比如应用启动成功。warn
: 警告信息,表示某些小问题但不影响程序运行。error
: 错误信息,但应用可以继续运行。fatal
: 致命错误,通常会导致程序终止。
异步日志
log4js
提供了异步日志记录方式,可以减少日志写入时对程序性能的影响。比如:
log4js.configure({
appenders: {
file: { type: 'file', filename: 'logs/app.log' },
asyncFile: { type: 'file', filename: 'logs/async.log', mode: 'async' } // 异步写入日志
},
categories: {
default: { appenders: ['asyncFile'], level: 'info' }
}
});
关闭日志
在开发环境中,可能不需要记录某些级别的日志。可以通过调整 categories
中的日志级别来控制输出。
例如,如果只需要记录 warn
及以上级别的日志:
log4js.configure({
appenders: { console: { type: 'console' } },
categories: { default: { appenders: ['console'], level: 'warn' } }
});
const logger = log4js.getLogger();
logger.info('This log will not be shown'); // 不会输出
logger.warn('This log will be shown'); // 会输出
使用不同的 Logger
可以根据不同的日志需求,创建多个 logger 实例。例如:
log4js.configure({
appenders: {
file: { type: 'file', filename: 'logs/app.log' },
errorFile: { type: 'file', filename: 'logs/errors.log' }
},
categories: {
default: { appenders: ['file'], level: 'info' },
errorLogger: { appenders: ['errorFile'], level: 'error' } // 专门记录错误日志
}
});
const logger = log4js.getLogger();
const errorLogger = log4js.getLogger('errorLogger');
logger.info('This log goes to app.log');
errorLogger.error('This error goes to errors.log');
自定义日志格式
通过 layout
可以自定义日志的输出格式。常用的 layout
类型有:
basic
: 基本格式。colored
: 带颜色的格式(适合控制台)。pattern
: 自定义日志格式的模式。
例如,使用自定义模式:
log4js.configure({
appenders: {
out: {
type: 'console',
layout: {
type: 'pattern',
pattern: '%d{yyyy-MM-dd hh:mm:ss} [%p] %c - %m' // 定义输出格式
}
}
},
categories: { default: { appenders: ['out'], level: 'info' } }
});
const logger = log4js.getLogger();
logger.info('This log will have a custom format');
JWT用户登录认证
在 Koa 项目中,可以通过 jsonwebtoken
库实现基于 JWT(JSON Web Token)的用户登录认证。
1. 安装必要的依赖
npm install koa koa-router koa-bodyparser jsonwebtoken @koa/cors
2. 项目实现步骤
配置 JWT 密钥
在项目的配置文件或环境变量中定义一个密钥,用于签发和验证 JWT。
const jwtSecret = 'your-secret-key'; // 替换为自己的密钥
中间件实现
- 登录接口:
- 验证用户身份(例如通过数据库查询)。
- 如果验证成功,生成 JWT 并返回给客户端。
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const jwt = require('jsonwebtoken');
const cors = require('@koa/cors');
const app = new Koa();
const router = new Router();
const jwtSecret = 'your-secret-key';
// 模拟的用户数据
const users = [
{ id: 1, username: 'test', password: '123456' },
];
// 登录接口
router.post('/login', async (ctx) => {
const { username, password } = ctx.request.body;
// 验证用户身份
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
ctx.status = 401;
ctx.body = { message: '用户名或密码错误' };
return;
}
// 生成 JWT
const token = jwt.sign({ id: user.id, username: user.username }, jwtSecret, { expiresIn: '2h' });
ctx.body = { token };
});
- 验证中间件:
- 解析和验证客户端提供的 JWT。
- 如果验证通过,将用户信息附加到
ctx.state
。
// JWT 验证中间件
const jwtAuth = async (ctx, next) => {
const authHeader = ctx.headers.authorization;
if (!authHeader) {
ctx.status = 401;
ctx.body = { message: '缺少授权令牌' };
return;
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, jwtSecret);
ctx.state.user = payload; // 保存用户信息到上下文状态
await next();
} catch (err) {
ctx.status = 401;
ctx.body = { message: '无效的令牌' };
}
};
- 受保护的接口:
- 使用
jwtAuth
中间件保护路由。
- 使用
// 受保护接口
router.get('/protected', jwtAuth, async (ctx) => {
ctx.body = { message: '访问成功', user: ctx.state.user };
});
整合并启动应用
app.use(cors());
app.use(bodyParser());
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
3. 客户端示例
登录请求
fetch('http://localhost:3000/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'test', password: '123456' }),
})
.then(res => res.json())
.then(data => console.log('Token:', data.token));
受保护接口请求
fetch('http://localhost:3000/protected', {
headers: { Authorization: `Bearer your-jwt-token` },
})
.then(res => res.json())
.then(data => console.log(data));
注意事项
- Token 过期处理:可以在生成
JWT
时通过expiresIn
设置过期时间,客户端需要监测过期并重新登录。 - 安全性:请确保密钥安全,建议使用环境变量管理密钥。
- HTTPS:生产环境中应使用 HTTPS 保护数据传输安全。
用户注册模块
用户注册的一般流程:
- 接收注册信息:如用户名、密码、邮箱等。
- 检查用户名是否已存在。
- 加密密码:为了安全性,密码需要加密存储。
- 将用户信息存储到数据库中。
- 返回注册结果。
安装依赖
npm install mysql2 bcrypt
用户注册接口
const bcrypt = require('bcrypt');
// 用户注册接口
router.post('/register', async (ctx) => {
const { email, password } = ctx.request.body;
// 检查用户名是否已存在
const existingUser = await getUserByEmail(email);
if (existingUser) {
ctx.status = 400;
ctx.body = { message: '用户邮箱已存在' };
return;
}
// 加密密码
const hashedPassword = await bcrypt.hash(password, 10);
// 保存用户到数据库
try {
await addUser({ email, password: hashedPassword });
ctx.status = 201;
ctx.body = { message: '注册成功' };
} catch (error) {
ctx.status = 500;
ctx.body = { message: '注册失败', error };
}
});
用户登录接口
router.post('/login', async (ctx) => {
const { email, password } = ctx.request.body;
// 查找用户
const user = await getUserByEmail(email);
if (!user) {
ctx.status = 401;
ctx.body = { message: '用户不存在' };
return;
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
ctx.status = 401;
ctx.body = { message: '密码错误' };
return;
}
// 生成 JWT
const token = jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: '2h' });
ctx.body = { token };
});
数据库结构和设计
字段名 | 数据类型 | 说明 |
---|---|---|
id | 自增主键 | 唯一标识 |
email | 字符串 | 邮箱,唯一 |
password | 字符串 | 加密后的密码 |
createdAt | 日期 | 注册时间(自动) |
updatedAt | 日期 | 更新时间(自动) |
koa项目推荐的目录结构
project-root/
├── config/ # 配置文件
│ ├── db.js # 数据库配置
│ ├── jwt.js # JWT相关配置
│ └── index.js # 其他配置文件
├── controllers/ # 控制器层(业务逻辑)
│ ├── authController.js # 认证相关逻辑
│ └── userController.js # 用户相关逻辑
├── middlewares/ # 中间件
│ ├── jwtAuth.js # JWT验证中间件
│ └── errorHandler.js # 全局错误处理中间件
├── models/ # 数据模型
│ └── user.js # 用户模型
├── routes/ # 路由层
│ ├── authRoutes.js # 认证相关路由
│ └── userRoutes.js # 用户相关路由
├── utils/ # 工具函数
│ ├── logger.js # 日志工具
│ └── helper.js # 通用辅助函数
├── app.js # 应用主入口
├── package.json # 项目依赖和脚本
└── README.md # 项目说明