Skip to content
本页内容

Koa

安装

bash
npm i koa

开始

js
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);

中间件

js
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);

错误处理中间件

js
// 自动抛出错误并返回
// 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 的中间件是按照「洋葱模型」执行的,即中间件是按顺序层层嵌套执行的:

  1. 每个中间件在执行 await next() 后,才会执行下一个中间件。
  2. 如果在某个中间件中发生错误,错误会向上传递,直到被捕获或到达最外层。

因此,如果你希望全局捕获所有中间件中抛出的错误,错误处理中间件必须放在所有其他中间件之前。这样一旦发生错误,它能及时捕获。

自定义404中间件

js
// 自定义 404 中间件
app.use(async (ctx, next) => {
    await next();
    if (ctx.status === 404) {
        ctx.body = "请求的地址不存在,请检查请求地址";
    }
});

注意:

koa中的中间件应该使用await next(),而不是直接使用next(),如果没有await,请求的异步流程可能会被打断,导致请求提前结束,出现404错误。

koa-static

bash
npm i koa-static
js
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

bash
npm i koa-static-cache
js
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 可能无法处理这些新上传的文件,导致前端访问不到这些文件。

解决方案

  1. 使用 koa-static 代替 koa-static-cachekoa-static 不会缓存文件路径信息,适合动态上传和访问的场景。它会每次请求时直接访问文件系统,确保可以读取到新上传的文件。

    javascript
    import serve from 'koa-static';
    
    app.use(serve(path.join(__dirname, 'public'), {
        // 可以添加一些其他配置,比如缓存时间等
    }));
  2. 动态刷新缓存(不推荐):虽然可以使用一些技巧手动刷新 koa-static-cache 的缓存,但这样会增加复杂性和维护成本,且可能引发性能问题。

  3. 重新配置 koa-static-cache(若必须使用缓存):如果一定要用 koa-static-cache,可以设置 dynamictrue 以允许对新文件的处理。不过,这样的设置可能会使缓存失去意义。

    javascript
    import 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

bash
npm i koa-router

koa-router@koa/router 是同一个插件

js
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

bash
npm i koa-body
js
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 上传文件

自定义存放文件夹、文件名称与支持多文件上传

js
// 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

bash
npm i koa-bodyparser
js
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-bodyparserkoa-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 数据。
  • 轻量且快速: 只提供基础的请求体解析功能。
  • 简单易用: 配置简单,开箱即用。

使用方法:

javascript
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 数据。
  • 文件上传: 支持文件上传和解析。
  • 功能全面: 提供了更多的配置选项以适应复杂的需求。

使用方法:

javascript
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。
  • jsonLimitformLimit: 用于限制请求体大小。
  • formidable: 相关文件上传的配置,支持文件路径、扩展名保留等设置。

总结

  • koa-bodyparser 更适合轻量应用,不支持文件上传。
  • koa-body 更强大,适合需要处理文件的场景。

根据项目需求选择合适的中间件即可。

koa-swig

bash
npm i koa-swig co
js
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);
html
<!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

bash
npm i mysql2

建立连接

普通连接

js
(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);
})();

使用连接池

js
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,
});

使用预处理语句

js
// 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 注入攻击。可以通过 ? 占位符将参数传递给查询语句。

js
const userId = 1;
const [rows, fields] = await connection.execute('SELECT * FROM users WHERE id = ?', [userId]);
console.log(rows);

插入数据

js
const [result] = await connection.execute('INSERT INTO users (name, age) VALUES (?, ?)', ['Alice', 25]);
console.log('插入的ID:', result.insertId);

更新数据

js
const [result] = await connection.execute('UPDATE users SET age = ? WHERE name = ?', [30, 'Alice']);
console.log('受影响的行数:', result.affectedRows);

删除数据

js
const [result] = await connection.execute('DELETE FROM users WHERE name = ?', ['Alice']);
console.log('删除的行数:', result.affectedRows);

错误处理

mysql2 提供了多种错误处理机制。例如,如果连接失败或查询执行失败,你可以通过捕获异常来处理这些错误。

js
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语句

js
// 使用 mysql.format 格式化 SQL 查询
const formattedSql = mysql.format(sql, params);
console.log("Generated SQL:", formattedSql);

关闭连接

在使用完数据库连接后,一定要记得关闭连接,否则会导致连接泄漏,影响性能。对于普通连接使用 .end(),对于连接池使用 .release() 或在查询完成后自动回收。

普通连接

js
connection.end();

连接池

js
pool.end();

事务处理

mysql2 支持事务。通过 beginTransaction() 开始事务,执行多条 SQL 语句后,使用 commit() 提交事务或 rollback() 回滚事务。

js
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()

js
const [rows, fields] = await connection.execute('SELECT * FROM users WHERE id = ?', [1]);

使用 query()

js
const [rows, fields] = await connection.query('SELECT * FROM users WHERE id = 1');

适用场景

execute():适合用于带有参数的动态 SQL 查询。通常在涉及参数时,优先使用 execute()

query():更适合执行不带参数的静态 SQL 查询,比如简单的 SELECT 语句。

执行效率

  • execute():预编译 SQL 语句并执行,可以提高执行效率,尤其是在多次执行类似 SQL 语句但参数不同的情况下。由于是预编译的,它适合需要更高性能的应用场景。
  • query():每次执行时都解析整个 SQL 语句,效率相对稍低。

返回结果

两者在返回结果时并没有明显区别,都返回一个包含查询结果的数组,第一个元素是查询结果,第二个元素是查询的字段元数据(字段的描述信息)。

例如:

execute() 返回:

js
const [rows, fields] = await connection.execute('SELECT * FROM users');
console.log(rows);  // 查询结果
console.log(fields);  // 字段元数据

query() 返回

js
const [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() 创建的连接池进行查询操作有两种主要方式:

  1. 直接使用 pool.query()pool.execute()
  2. 使用 pool.getConnection() 获取连接后再执行查询

这两种方式都有各自的适用场景和差异,主要区别在于连接管理方式灵活性手动控制

直接使用 pool.query()pool.execute()

这种方式简洁且方便,适合大多数简单的查询操作场景。

js
const 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() 获取连接后再执行查询

这种方式适合需要在多次查询之间共享同一个连接,或者需要手动管理连接生命周期的场景。

js
const 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(),你可以获取一个连接,并在该连接上执行多条查询。

事务操作示例

js
const 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.

13.5.1 PREPARE Statement

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.

9.2 Schema Object Names

log4js

安装

npm i log4js

基本使用

js
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 负责定义哪些日志级别被记录,通常包括 tracedebuginfowarnerrorfatal

常用的 appender 类型:

  • console: 输出到控制台。
  • file: 输出到文件。
  • dateFile: 按日期切分输出日志文件。

示例:将日志同时输出到控制台和文件

js
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

js
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 提供了异步日志记录方式,可以减少日志写入时对程序性能的影响。比如:

js
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 及以上级别的日志:

js
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 实例。例如:

js
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: 自定义日志格式的模式。

例如,使用自定义模式:

js
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. 安装必要的依赖

bash
npm install koa koa-router koa-bodyparser jsonwebtoken @koa/cors

2. 项目实现步骤

配置 JWT 密钥

在项目的配置文件或环境变量中定义一个密钥,用于签发和验证 JWT。

javascript
const jwtSecret = 'your-secret-key'; // 替换为自己的密钥

中间件实现

  1. 登录接口
    • 验证用户身份(例如通过数据库查询)。
    • 如果验证成功,生成 JWT 并返回给客户端。
javascript
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 };
});
  1. 验证中间件
    • 解析和验证客户端提供的 JWT。
    • 如果验证通过,将用户信息附加到 ctx.state
javascript
// 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: '无效的令牌' };
  }
};
  1. 受保护的接口
    • 使用 jwtAuth 中间件保护路由。
javascript
// 受保护接口
router.get('/protected', jwtAuth, async (ctx) => {
  ctx.body = { message: '访问成功', user: ctx.state.user };
});

整合并启动应用

javascript
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. 客户端示例

登录请求

javascript
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));

受保护接口请求

javascript
fetch('http://localhost:3000/protected', {
  headers: { Authorization: `Bearer your-jwt-token` },
})
  .then(res => res.json())
  .then(data => console.log(data));

注意事项

  1. Token 过期处理:可以在生成 JWT 时通过 expiresIn 设置过期时间,客户端需要监测过期并重新登录。
  2. 安全性:请确保密钥安全,建议使用环境变量管理密钥。
  3. HTTPS:生产环境中应使用 HTTPS 保护数据传输安全。

用户注册模块

用户注册的一般流程:

  1. 接收注册信息:如用户名、密码、邮箱等。
  2. 检查用户名是否已存在
  3. 加密密码:为了安全性,密码需要加密存储。
  4. 将用户信息存储到数据库中
  5. 返回注册结果

安装依赖

bash
npm install mysql2 bcrypt

用户注册接口

js
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 };
  }
});

用户登录接口

js
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             # 项目说明