Skip to content

上一章,我们实现了添加博客文章功能,是不是很简单。

这一章,我们来实现查询、修改、删除博客文章功能。为什么放到一起呢?因为删除很简单,修改跟添加基本雷同,只有查询有较大区别,所以放到一章就能讲完了。

查询博客文章

首先,从查询展示博客文章开始。

下图就是查询博客文章的接口,文件名是 articleSelect.js

与添加功能类似,其实也是四部曲。

  1. 获取参数。
  2. 验证参数。
  3. 接口逻辑。
  4. 返回数据。
js
// /apis/articleSelect.js 文件
import { mysqlPool } from "../mysql.js";
export default async (req) => {
    try {
        // 参数去掉前后空格
        const page = req.body.page; // 第几页
        const limit = req.body.limit; // 每页多少条
        const author = req.body.author; // 查谁的
        // ------------------------------------------------------
        // 验证标题参数
        // 验证作者参数,必须为非0数字开头的整数
        if (/[1-9]\d*/.test(page) === false) {
            return {
                code: 1,
                msg: "当前页码必须为非0正整数",
            };
        }

        // ------------------------------------------------------
        // 验证内容参数
        // 验证作者参数,必须为非0数字开头的整数
        if (/[1-9]\d*/.test(limit) === false) {
            return {
                code: 1,
                msg: "每页条数必须为非0正整数",
            };
        }
        // ------------------------------------------------------
        // 验证作者参数,必须为非0数字开头的整数
        if (/[1-9]\d*/.test(author) === false) {
            return {
                code: 1,
                msg: "文章作者必须传作者的数字ID",
            };
        }
        // ------------------------------------------------------
        // 从连接池中获取一个数据库连接
        const db = await mysqlPool.getConnection();
        // 查询数据库是否有对应的用户数据
        const [rows] = await db.query({
            sql: "SELECT * FROM `article` WHERE `author` = :author LIMIT :offset,:limit",
            values: {
                offset: (Number(page) - 1) * Number(limit),
                limit: Number(limit),
                author: Number(author),
            },
        });
        // 释放数据库连接
        db.release();
        // 返回成功信息
        return {
            code: 0,
            msg: "查询文章成功",
            data: rows || [],
        };
    } catch (err) {
        console.log("🚀 ~ err:", err);
        return {
            code: 1,
            msg: "未知错误",
        };
    }
};

这里,我们接收 3 个参数,分别是:

  1. page (当前查询的是第几页?)。
  2. limit (每一页查询多少条?)。
  3. author (查询谁的数据?)。

举个例子,我们要查询第 2 页数据,每一页 10 条。

那么请求的 page=2,limit=10,接口收到后,分页查询的 SQL 部分就是 LIMIT (2 - 1) * 10 = 10,10。也就是 LIMIT 10,10。

什么意思呢,我稍微讲解一下:

第一页是 1-10 (第 1 条到第 10 条)。

第二页是 10-20 (第 10 条到第 20 条)。

第三页是 20-30 (第 20 条到第 30 条)。

LIMIT 的语法是 LIMIT n,m。

n 表示从第几条开始,m 是查询多少条。

如果我们接收到 page=2 后,不减 1 的话,那么我们的分页查询是 LIMIT 20,10,从第 20 条开始,查询 10 条,那就是 (20-30),属于第 3 页了,而我们要查询的是第二页 (10-20)。

所以,我们才需要把 page 减去 1,那么,为什么 page 不从 0 开始呢?第 0 页?这是典型的编程思维,程序是给人用的,从第 1 页开始,更加符合人为习惯。

picture 0

查询接口实现后,便能把数据查询并显示到网页上了。

html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>文章管理</title>
        <script src="./template-web.js"></script>
    </head>
    <body>
        <!-- 用于防止浏览器自动填充密码 -->
        <input
            type="password"
            clearable
            hidden
            autocomplete="new-password"
            style="display: none"
        />
        <div class="form">
            <div class="group">
                <div class="label">标题</div>
                <div class="value">
                    <input
                        class="title"
                        type="input"
                        placeholder="请输入标题"
                    />
                </div>
            </div>
            <div class="group">
                <div class="label">内容</div>
                <div class="value">
                    <input
                        class="content"
                        type="input"
                        placeholder="请输入内容"
                    />
                </div>
            </div>
            <div class="button insert">添加</div>
            <div class="button update">更新</div>
        </div>

        <!-- 数据面板 -->
        <div class="panel"></div>

        <!-- 文章列表模板 -->
        <script
            type="text/html"
            id="article-lists"
        >
            <div class="table">
                <div class="th">
                    <div class="td id">ID</div>
                    <div class="td author">作者</div>
                    <div class="td title">标题</div>
                    <div class="td content">内容</div>
                    <div class="td created_at">创建时间</div>
                    <div class="td updated_at">更新时间</div>
                    <div class="td action">操作</div>
                </div>
                {{each data item key}}
                <div class="tr">
                    <div class="td id">{{ item.id }}</div>
                    <div class="td author">{{ item.author }}</div>
                    <div class="td title">{{ item.title }}</div>
                    <div class="td content">{{ item.content }}</div>
                    <div class="td created_at">{{ item.created_at2 }}</div>
                    <div class="td updated_at">{{ item.updated_at2 }}</div>
                    <div class="td action">
                        <span
                            class="upd"
                            data-id="{{item.id}}"
                        >
                            更新
                        </span>
                        <span
                            class="del"
                            data-id="{{item.id}}"
                        >
                            删除
                        </span>
                    </div>
                </div>
                {{/each}}
            </div>
        </script>

        <script>
            // 时间戳转年月日
            const timestampToYMD = (timestamp) => {
                if (!timestamp) return "";
                const date = new Date(timestamp);
                const year = date.getFullYear();
                const month = (date.getMonth() + 1).toString().padStart(2, "0"); // 月份是从0开始的,所以需要+1
                const day = date.getDate().toString().padStart(2, "0");
                return `${year}-${month}-${day}`;
            };

            // 查询文章列表
            const apiArticleSelect = async () => {
                // 获取登录时保存到本地的用户数据,并解析成对象结构
                const user = JSON.parse(localStorage.getItem("user"));
                // 请求查询接口,拿到文章列表
                const result = await fetch("/api/articleSelect", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json;charset=utf-8",
                    },
                    body: JSON.stringify({
                        page: 1,
                        limit: 10,
                        author: user.id,
                    }),
                });

                // 把拿到的数据变成JSON结构
                const { code, data, msg } = await result.json();

                // 判断 code 是否等于0,为0则表示拿到了正常的接口数据
                if (code === 0) {
                    // 处理日期,把时间戳转为年月日,并用新字段保存
                    const data2 = data.map((item) => {
                        item.created_at2 = timestampToYMD(item.created_at);
                        item.updated_at2 = timestampToYMD(item.updated_at);
                        return item;
                    });

                    // 把数据用模板渲染
                    const html = template("article-lists", {
                        data: data2,
                    });
                    // 把渲染后的数据放到页面中
                    document.querySelector(".panel").innerHTML = html;
                } else {
                    // 否则在控制台显示接口异常提示
                    console.log(msg);
                }
            };

            // 直接运行文章查询接口
            apiArticleSelect();
        </script>
    </body>
</html>

则是页面代码。

需要注意的是,我们不用任何现代化框架,而是引入了一个 HTML 模板引擎,art-template

HTML 模板引擎,是前端现代化框架 Vue,React 出来之前非常流行的网页开发方式,上手非常简单,一共只有 3 步:

  1. 写 HTML 模板语法。
  2. 将数据与模板进行渲染,得到 HTML 字符。
  3. 最后,把 HTML 字符插入到文档中即可。
html
<!-- 文章列表模板 -->
<script
    type="text/html"
    id="article-lists"
>
    <div class="table">
        <div class="th">
            <div class="td id">ID</div>
            <div class="td author">作者</div>
            <div class="td title">标题</div>
            <div class="td content">内容</div>
            <div class="td created_at">创建时间</div>
            <div class="td updated_at">更新时间</div>
            <div class="td action">操作</div>
        </div>
        {{each data item key}}
        <div class="tr">
            <div class="td id">{{ item.id }}</div>
            <div class="td author">{{ item.author }}</div>
            <div class="td title">{{ item.title }}</div>
            <div class="td content">{{ item.content }}</div>
            <div class="td created_at">{{ item.created_at2 }}</div>
            <div class="td updated_at">{{ item.updated_at2 }}</div>
            <div class="td action">
                <span
                    class="upd"
                    data-id="{{item.id}}"
                >
                    更新
                </span>
                <span
                    class="del"
                    data-id="{{item.id}}"
                >
                    删除
                </span>
            </div>
        </div>
        {{/each}}
    </div>
</script>

这是 art-template 写的 HTML 模板语法,可以看到跟 Vue 风格很类似。

js
// 把拿到的数据变成JSON结构
const { code, data, msg } = await result.json();

// 判断 code 是否等于0,为0则表示拿到了正常的接口数据
if (code === 0) {
    // 处理日期,把时间戳转为年月日,并用新字段保存
    const data2 = data.map((item) => {
        item.created_at2 = timestampToYMD(item.created_at);
        item.updated_at2 = timestampToYMD(item.updated_at);
        return item;
    });

    // 把数据用模板渲染
    const html = template("article-lists", {
        data: data2,
    });
    // 把渲染后的数据放到页面中
    document.querySelector(".panel").innerHTML = html;
} else {
    // 否则在控制台显示接口异常提示
    console.log(msg);
}

这是将模板用数据填充,得到渲染后的 HTML 字符,最后插入到文档中。

最后就得到了我们博客文章查询与展示的页面效果。

删除博客文章

js
import { mysqlPool } from "../mysql.js";
export default async (req) => {
    try {
        // 接收参数
        const id = req.body.id;
        const author = req.body.author;
        // ------------------------------------------------------
        // 验证ID参数
        if (/[1-9]\d*/.test(id) === false) {
            return {
                code: 1,
                msg: "文章ID必须为非0正整数",
            };
        }

        // ------------------------------------------------------
        // 验证作者参数,必须为非0数字开头的整数
        if (/[1-9]\d*/.test(author) === false) {
            return {
                code: 1,
                msg: "文章作者必须传作者的数字ID",
            };
        }
        // ------------------------------------------------------
        // 从连接池中获取一个数据库连接
        const db = await mysqlPool.getConnection();
        // 查询数据库是否有对应的用户数据
        const [result] = await db.query({
            sql: "DELETE FROM `article` WHERE `id` = :id AND `author` = :author",
            values: {
                id: Number(id),
                author: Number(author),
            },
        });
        console.log("🚀 ~ result:", result);
        // 释放数据库连接
        db.release();
        // 返回成功信息
        return {
            code: 0,
            msg: "删除文章成功",
            data: {},
        };
    } catch (err) {
        console.log("🚀 ~ err:", err);
        return {
            code: 1,
            msg: "未知错误",
        };
    }
};

按照惯例,先把删除接口写好。

需要注意的是,我们同时接收 文章ID用户ID 2 个参数,这是为了确保用户删除文章的时候,只能删除他自己的文章,不能随便传个 ID,把其他用户的文章也删除了。

html
<!-- 文章列表模板 -->
<script
    type="text/html"
    id="article-lists"
>
    <div class="table">
        <div class="th">
            <div class="td id">ID</div>
            <div class="td author">作者</div>
            <div class="td title">标题</div>
            <div class="td content">内容</div>
            <div class="td created_at">创建时间</div>
            <div class="td updated_at">更新时间</div>
            <div class="td action">操作</div>
        </div>
        {{each data item key}}
        <div class="tr">
            <div class="td id">{{ item.id }}</div>
            <div class="td author">{{ item.author }}</div>
            <div class="td title">{{ item.title }}</div>
            <div class="td content">{{ item.content }}</div>
            <div class="td created_at">{{ item.created_at2 }}</div>
            <div class="td updated_at">{{ item.updated_at2 }}</div>
            <div class="td action">
                <span class="upd">更新</span>
                <span class="del">删除</span>
                <span
                    class="upd"
                    data-id="{{item.id}}"
                >
                    更新
                </span>
                <span
                    class="del"
                    data-id="{{item.id}}"
                >
                    删除
                </span>
            </div>
        </div>
        {{/each}}
    </div>
</script>

HTML 模板代码有一点改动,为了获得文章的 ID,我们把 ID 放到点击元素的 data-xxx 属性。

js
// 点击删除文章按钮
document.querySelector(".panel").addEventListener("click", async (evt) => {
    if (evt.target.className === "del") {
        const user = JSON.parse(localStorage.getItem("user"));
        const id = evt.target.dataset.id;
        const result = await fetch("/api/articleDelete", {
            method: "POST",
            headers: {
                "Content-Type": "application/json;charset=utf-8",
            },
            body: JSON.stringify({
                id: id,
                author: user.id,
            }),
        });
        // 把拿到的数据变成JSON结构
        const { code, data, msg } = await result.json();
        if (code === 0) {
            apiArticleSelect();
        } else {
            console.log(msg);
        }
    }
});

点击删除操作后,通过 dataset.xxx 获取到文章的 ID,有 4 个地方要注意:

  1. 因为我们的数据列表是查询后动态插入的,所以不能直接监听 .del 元素,只能通过监听其父级,监听的父级不能是动态插入的 HTML 中的类名,所以我们用 .panel
  2. 点击元素后,要判断类名是否是 .del,来确定我们点击的是删除操作。
  3. 文章的 id,通过点击元素的 dataset.id 来获取,这是原生 JS 的知识。
  4. 当删除文章后,要重新查询一遍文章列表并渲染数据。

picture 1

这是添加和删除操作的演示动图。

更新博客文章

js
// /apis/articleUpdate.js 文件
import { mysqlPool } from "../mysql.js";
export default async (req) => {
    try {
        // 参数去掉前后空格
        const id = req.body.id;
        const title = req.body.title.trim();
        const content = req.body.content.trim();
        const author = req.body.author;
        // ------------------------------------------------------
        // 验证ID参数
        if (/[1-9]\d*/.test(id) === false) {
            return {
                code: 1,
                msg: "文章ID必须为非0正整数",
            };
        }

        // ------------------------------------------------------
        // 验证标题参数
        if (title.length < 1 || title.length > 100) {
            return {
                code: 1,
                msg: "文章标题长度不能小于1,不能大于100",
            };
        }

        // ------------------------------------------------------
        // 验证内容参数
        if (content.length < 1 || content.length > 60000) {
            return {
                code: 1,
                msg: "文章内容长度不能小于1,不能大于6000",
            };
        }

        // ------------------------------------------------------
        // 验证作者参数,必须为非0数字开头的整数
        if (/[1-9]\d*/.test(author) === false) {
            return {
                code: 1,
                msg: "文章作者必须传作者的数字ID",
            };
        }

        // ------------------------------------------------------
        // 从连接池中获取一个数据库连接
        const db = await mysqlPool.getConnection();
        // 查询数据库是否有对应的用户数据
        const [result] = await db.query({
            sql: "UPDATE `article` SET `title` = :title,`content` = :content,`updated_at` = :updated_at WHERE `id` = :id AND `author` = :author",
            values: {
                id: id,
                title: title,
                content: content,
                author: Number(author),
                updated_at: Date.now(),
            },
        });
        // 释放数据库连接
        db.release();
        // 返回成功信息
        return {
            code: 0,
            msg: "修改文章成功",
        };
    } catch (err) {
        console.log("🚀 ~ err:", err);
        return {
            code: 1,
            msg: "未知错误",
        };
    }
};

更新文章与添加文章没有多少区别,接口传参在添加的基础上,多了一个文章 ID,表示要修改的是哪一篇文章。

picture 2

为了方便操作,我们添加了一个 更新按钮

js
// 保存当前操作的文章ID
let updateId = 0;
// 点击更新文章按钮,把内容放到输入框中
document.querySelector(".panel").addEventListener("click", async (evt) => {
    if (evt.target.className === "upd") {
        updateId = evt.target.dataset.id;
        const tr = evt.target.closest(".tr"); // 当前行
        const title = tr.querySelector(".title").innerText; // 当前行标题
        const content = tr.querySelector(".content").innerText; // 当前行内容
        document.querySelector(".form .title").value = title;
        document.querySelector(".form .content").value = content;
    }
});
// 点击更新文章按钮
document.querySelector(".update").addEventListener("click", async () => {
    const title = document.querySelector(".form .title").value;
    const content = document.querySelector(".form .content").value;
    // 获取登录时保存到本地的用户数据,并解析成对象结构
    const user = JSON.parse(localStorage.getItem("user"));
    const result = await fetch("/api/articleUpdate", {
        method: "POST",
        headers: {
            "Content-Type": "application/json;charset=utf-8",
        },
        body: JSON.stringify({
            title: title,
            content: content,
            author: user.id,
            id: updateId,
        }),
    });
    // 把拿到的数据变成JSON结构
    const { code, data, msg } = await result.json();
    if (code === 0) {
        apiArticleSelect();
    } else {
        console.log(msg);
    }
});

前端代码如下,分成了两个步骤:

  1. 点击更新操作,把当前行的标题和内容放到输入框中。
  2. 点击更新按钮,调用更新接口,提交更新内容。

picture 4

最后,来演示一个完整的博客文章的增删改查操作。

何以解忧,唯有代码。不忘初心,方得始终。