上一章,我们实现了添加博客文章功能,是不是很简单。
这一章,我们来实现查询、修改、删除博客文章功能。为什么放到一起呢?因为删除很简单,修改跟添加基本雷同,只有查询有较大区别,所以放到一章就能讲完了。
查询博客文章
首先,从查询展示博客文章开始。
下图就是查询博客文章的接口,文件名是 articleSelect.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 个参数,分别是:
page
(当前查询的是第几页?)。limit
(每一页查询多少条?)。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 页开始,更加符合人为习惯。
查询接口实现后,便能把数据查询并显示到网页上了。
<!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 步:
- 写 HTML 模板语法。
- 将数据与模板进行渲染,得到 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 风格很类似。
// 把拿到的数据变成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 字符,最后插入到文档中。
最后就得到了我们博客文章查询与展示的页面效果。
删除博客文章
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,把其他用户的文章也删除了。
<!-- 文章列表模板 -->
<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 属性。
// 点击删除文章按钮
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 个地方要注意:
- 因为我们的数据列表是查询后动态插入的,所以不能直接监听
.del
元素,只能通过监听其父级,监听的父级不能是动态插入的 HTML 中的类名,所以我们用.panel
。 - 点击元素后,要判断类名是否是
.del
,来确定我们点击的是删除操作。 - 文章的 id,通过点击元素的
dataset.id
来获取,这是原生 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,表示要修改的是哪一篇文章。
为了方便操作,我们添加了一个 更新按钮
。
// 保存当前操作的文章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);
}
});
前端代码如下,分成了两个步骤:
- 点击更新操作,把当前行的标题和内容放到输入框中。
- 点击更新按钮,调用更新接口,提交更新内容。
最后,来演示一个完整的博客文章的增删改查操作。