Appearance
❤项目优化(番外篇)
查询接口优化
查询接口条件优化(文章)
这里我们以文章模块为例,之前我们查询接口是这样子的
javascript
// 标题
if (title !== undefined && title !== '' && title !== null) {
query += ' WHERE title = ?';
params.push(title);
}
// 类型
if (articletype !== undefined && articletype !== '' && articletype !== null) {
query += params.length ? ' AND' : ' WHERE';
query += ' articletype = ?';
params.push(articletype);
}
if (pageNum !== undefined && pageSize !== '' && pageSize !== null) {
query += ' LIMIT ?, ?';
let offset = (pageNum - 1) * pageSize;
params.push(offset);
params.push(parseInt(pageSize));
}
我们写了公开函数方法进行优化
优化之后就成为了:
javascript
const {addCondition,addDateRangeCondition,addPagination} = require('./apimethods.js'); // 引入封装方法
// 构建查询条件
const params = [];
query = addCondition(query, params, 'title', title); // 标题
query = addCondition(query, params, 'articletype', articletype); // 类型
query = addPagination(query, params, pageNum, pageSize); //分页条件
新增接口优化
新增接口条件默认值优化(用户接口)
接下来我们对于用户接口部分我们进行一些优化
javascript
export function editUserStatus (n) {
return request({
url: '/api/user/status',
method: 'put',
data:n
})
}
向其中插入一个SQL语句尝试一下
javascript
INSERT INTO user (name, age,state) VALUES ('lin', 18,1)
如果前台的参数没有传,那么默认会出现一种情况
javascript
用户前面判断显示可以使用,但是用户其实真实状态是null
如何添加一个用户,然后默认这个用户角色是可以正常使用的呢?
这就需要我们改一下我们的增加用户的接口,这个接口使默认用户权限是初始化打开的
我们将 0 为初始化可以使用 1 为正常使用 2 为禁用以后
☞ 新增用户接口优化
javascript
// 准备 SQL 插入语句
const insertSql = `INSERT INTO user (name, age,state) VALUES (?, ?,1)`;
禁用和启用接口分离(用户模块)
javascript
app.put('/api/user/status', (req, res) => {
// console.log(req.body);
const { name, age } = req.body; // 从请求体中获取数据
const values = [state];
// 准备 SQL 插入语句
const insertSql = `UPDATE user SET state = ?`;
connectionpool.query(insertSql, values, (err, results) => {
// console.log(err,'err');
// console.log(results,'results');
if (err) {
console.error('Error querying database:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
res.json({
code: '200',
data: results,
});
});
});
查询接口返回条数页数优化
以我们字典类型模块的接口为例,不管有没有在添加查询参数条件的情况下查到数据,都会返回total,这种是错误的,
如果查询不到数据,total应该返回0,这样前端才能知道是否查询到数据了,所以这里我们修改一下,也就是针对已经查询完之后的查询部分进行更改
javascript
// 字典值type接口
// 查询接口
router.get('/', (req, res) => {
console.log(req.query, 'req.query');
const { dict_name, dict_type, status,page_num, page_size,begin_time,end_time} = convertKeysToSnakeCase(req.query);
let query = `SELECT * FROM sys_dict_type`;
// 构建查询条件
const params = [];
query = addCondition(query, params, 'dict_name', dict_name); // 字典名称
query = addCondition(query, params, 'dict_type', dict_type); // 字典类型
query = addCondition(query, params, 'status', status); // 状态
// query = addCondition(query, params, 'dict_label', dict_label); // 字典标签
query = addDateRangeCondition(query, params, begin_time, end_time); //查询时间
query = addPagination(query, params, page_num, page_size); //分页条件
console.log(query,'query');
connectionPool.query(query,params,(error, results, fields) => {
if (error) {
res.send({
code: 500,
message:'查询失败!',
});
res.status(500).json({ error: error.message });
} else {
let sqltotal = `SELECT COUNT(*) AS total FROM sys_dict_data;`
// 查询数据库并返回数据
connectionPool.query(sqltotal, (errtotal, rows) => {
let total = rows[0]['total'];
if (errtotal) {
console.error('Error querying database:', err);
res.status(500).json({ error: 'Internal server error' });
return;
} else {
res.send({
total: total,
code: 200,
data: convertToCamelCase(results),
message:'查询成功!',
});
}
});
}
});
});
=> 判断一下是否查询到空数组
js
if(results.length>0){
let sqltotal = `SELECT COUNT(*) AS total FROM sys_dict_data;`
// 查询数据库并返回数据
connectionPool.query(sqltotal, (errtotal, rows) => {
let total = rows[0]['total'];
if (errtotal) {
console.error('Error querying database:', err);
res.status(500).json({ error: 'Internal server error' });
return;
} else {
res.send({
total: total,
code: 200,
data: convertToCamelCase(results),
message:'查询成功!',
});
}
});
}else{
res.send({
total: 0,
code: 200,
data: [],
message:'查询成功!',
});
}
2、Node文件内置模块fs
的认识
Node.js内置模块fs
(文件系统)用于处理文件系统相关的操作,为Node.js提供了处理文件系统的能力,使Node.js能够在服务器端进行文件相关的操作,读取配置文件、处理日志、操作文件数据库等
- 读取文件:使用
fs.readFile()
或fs.readFileSync()
方法来读取文件内容。 - 写入文件:使用
fs.writeFile()
或fs.writeFileSync()
方法来将数据写入文件。 - 追加文件:使用
fs.appendFile()
方法向文件中追加数据。 - 删除文件:使用
fs.unlink()
方法可以删除文件。 - 创建目录:使用
fs.mkdir()
方法创建目录。 - 删除目录:使用
fs.rmdir()
方法删除目录。 - 重命名文件或目录:使用
fs.rename()
方法可以重命名文件或目录。 - 检查文件或目录的存在性:使用
fs.access()
方法检查文件或目录是否存在。 - 获取文件或目录的信息:使用
fs.stat()
方法获取文件或目录的信息,如大小、创建时间等。
3、文件模块化优化
在 Node.js 中,可以通过模块化的方式将接口分为不同的模块进行导入,这样子我们的模块可以进行不同的导入以及导导出,使接口模块化和易于维护
新建一个productRoutes.js
javascript
const express = require('express');
const router = express.Router();
// 定义产品相关接口
router.get('/products', (req, res) => {
// 处理获取产品列表的逻辑
});
router.post('/products', (req, res) => {
// 处理创建产品的逻辑
});
module.exports = router;
在之前的app.js根目录之中进行引入
javascript
const express = require('express');
const app = express();
const userRoutes = require('./userRoutes');
const productRoutes = require('./productRoutes');
// 将用户相关接口模块挂载到 /api/user 路径下
app.use('/api/user', userRoutes);
// 将产品相关接口模块挂载到 /api/product 路径下
app.use('/api/product', productRoutes);
// 启动服务
app.listen(8888, () => {
console.log('Server is running on port 3000');
});
接下来我们尝试访问一下
javascript
[http://localhost:8888/api/product](http://localhost:8888/api/product)
结果显示我们报错404
分析,我们使用的地址前面注意是/api/product,然后在我们的项目之中再次添加了/products ,也就是说我们正常的路径其实应该是/api/product/products
,访问地址http://localhost:8888/api/product/products
这个时候我们发现,接口已经可以正常输出了!
javascript
app.use('/api/product', productRoutes);
(1)抽离数据库模块为db.js
javascript
// 创建数据库连接池 createPool(高并发方式)
const mysql = require('mysql');
// 创建数据库连接池
const connectionPool = mysql.createPool({
host: 'localhost', // 数据库主机地址,如果是本地数据库则使用localhost
user: 'xxxx', // 数据库用户名
password: 'xxxx', // 数据库密码
database: 'xxx', // 要连接的数据库名
multipleStatements: true, // 允许执行多条语句
});
module.exports = connectionPool;
在根之中导入使用
javascript
const connectionPool = require('./server/db'); // 引入数据库连接池模块
剩下的就跟砸门之前connectionPool的正常使用一样
这里有个小插曲,就是我分离模块以后一直报500的错误,最后找了几个小时发现是单词拼写错了🤮
javascript
connectionPool 我给拼写成了connectionpool
所以要谨慎再谨慎啊
(2)抽离用户模块为 userRoutes.js
需要注意的就是路径的变化和接口的使用方式的变化
javascript
之前我们使用:/api/user
// 新增用户 POST 请求处理程序
app.post('/api/user', (req, res) => {
// console.log(req.body);
const { name, age } = req.body; // 从请求体中获取数据
const values = [name, age];
// 准备 SQL 插入语句
const insertSql = `INSERT INTO user (name, age,state) VALUES (?, ?,1)`;
connectionPool.query(insertSql, values, (err, results) => {
// console.log(err,'err');
// console.log(results,'results');
if (err) {
console.error('Error querying database:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
res.json({
code: '200',
data: results,
});
});
});
现在我们使用:/
// 新增用户 POST 请求处理程序
router.post('/', (req, res) => {
// console.log(req.body);
const { name, age } = req.body; // 从请求体中获取数据
const values = [name, age];
// 准备 SQL 插入语句
const insertSql = `INSERT INTO user (name, age,state) VALUES (?, ?,1)`;
connectionPool.query(insertSql, values, (err, results) => {
// console.log(err,'err');
// console.log(results,'results');
if (err) {
console.error('Error querying database:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
res.json({
code: '200',
data: results,
});
});
});
引入和使用用户模块部分就更改为了:
javascript
const userRoutes = require('./server/api/userRoutes'); // 引入用户路由模块
// 使用用户路由
app.use('/api/user', userRoutes);
测试一下,功能正常
(3)抽离文章模块为 articleRoutes.js
articleRoutes.js
大致如下
javascript
const express = require('express');
const router = express.Router();
const connectionPool = require('../db'); // 引入数据库连接池模块
.....
module.exports = router;
同样在我们的根目录之中进行使用:
javascript
// 引入文章模块
const articleRoutes = require('./server/api/articleRoutes');
// 使用文章接口
app.use('/api/articles', articleRoutes);
测试一下,没问题
文章数据增加
数据增加接口优化
javascript
const insertSql = `INSERT INTO articles (title, author,articlestatus) VALUES (?, ?,1)`;
const { title, author,articletype,articlestatus } = req.body; // 从请求体中获取数据
const values = [title, author,articletype,1];
const insertSql = 'INSERT INTO articles SET ?'; // 准备 SQL 插入语句
文章图片访问
图片太大了 ,存不进去,这个时候更改我们图片存储字段(为了存储base64位的图片)
改数据库
thumburl的数据类型为
LONGTEXT,以便能够存储较长的字符串。
5、返回数据规范
之前我们返回的数据都没有规范,现在我们定义一个返回接口的规范,用来规范返回数据
javascript
// 类型推断
interface ApiResponse {
code: number;
message: string;
}
import axios, { AxiosResponse } from 'axios';
cors模块跨域优化
javascript
app.options('*', cors()); // 这将在所有路由上启用 CORS 的预检请求处理
数据库监测完善
之前我们只是监测了数据库连接,现在我们监测一下数据库的查询情况,这部分我们也是放入我们的db.js数据库文件中
javascript
const connectionPool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'root',
database: 'myblog',
connectionLimit: 10, // 设置连接池的最大连接数
waitForConnections: true, // 当连接池中没有可用连接时,是否等待新的连接被创建
queueLimit: 0 // 当连接池已满且没有可用连接时,新请求是否排队等待
});
// 监测数据库连接状态
connectionPool.getConnection((err, connection) => {
if (err) {
console.error('林太白数据库连接失败😢:', err);
} else {
console.log('林太白数据库连接成功😊');
}
});
编写错误中间件
需要编写一个错误中间件,用来抛出错误,防止因为错误而造成接口崩溃
注意:错误中间件一定要放在所有路由之后
(1) 在所有路由之后放置中间件
js
app.use((err, req, res, next) => {
// 如果错误是由token解析失败导致的
if (err.name === 'UnauthorizedError') {
return res.send({
status: 401,
message: '无效的token'
})
}
// 如果是其他位置原因导致的错误
res.send({
status: 500,
message: '未知的错误'
})
next()
})
(2)我们请求本地的接口尝试
JS
http://localhost:8888/api/user/5`
最后我们输出结果为:
js
{"code":401,"message":"无效的token"}
命名优化
mysql之中比较规范的命名是蛇形命名法
js
create_time
前端之中比较友好规范的命名是驼峰命名法
js
createTime
那么我们Node之中如何将蛇形命名法转化为驼峰命名法:
js
create_time => createTime
第一种方式:(这种方式我们局部使用正好)
js
function camelToSnake(str) {
return str.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`);
}
第二种方式:(全局使用)
js
function camelToSnakeObject(obj) {
if (Array.isArray(obj)) {
return obj.map(camelToSnakeObject);
} else if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const newKey = key.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`);
acc[newKey] = camelToSnakeObject(obj[key]);
return acc;
}, {});
}
return obj;
}
//使用
// 示例
const camelCaseObj = {
noticeContent: 'Some content',
createdAt: '2024-12-27',
userInfo: {
firstName: 'John',
lastName: 'Doe'
}
};
const snakeCaseObj = camelToSnakeObject(camelCaseObj);
console.log(snakeCaseObj);
添加参数(驼峰命名法转换为蛇形命名法)
项目之中结合使用
JS
// 驼峰命名法(前端使用)=>蛇形命名法(sql数据库使用)
function camelToSnake(str) {
return str.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`);
}
function convertKeysToSnakeCase(obj) {
if (Array.isArray(obj)) {
return obj.map(convertKeysToSnakeCase);
} else if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const newKey = camelToSnake(key);
acc[newKey] = convertKeysToSnakeCase(obj[key]);
return acc;
}, {});
}
return obj;
}
module.exports = {
convertKeysToSnakeCase
};
//引入使用
const { convertKeysToSnakeCase } = require('../methods.js'); // 引入封装方法
const postData = convertKeysToSnakeCase({
noticeTitle,
noticeType,
noticeContent,
status,
createBy,
createTime,
updateBy,
updateTime,
remark
});
这里得到的postData里面的参数其实就已经是我们过滤过的数据了
查询参数(蛇形命名法转换为驼峰命名法)
同样,我们把查询出来的方法也更改为驼峰命名法
JS
// 将蛇形命名法转换为驼峰命名法
function snakeToCamel(str) {
return str.replace(/_([a-z])/g, (match, p1) => p1.toUpperCase());
}
// 递归转换对象的所有键为驼峰命名法
function convertToCamelCase(obj) {
if (Array.isArray(obj)) {
return obj.map(item => convertToCamelCase(item)); // 对数组进行递归处理
} else if (obj !== null && typeof obj === 'object') {
const newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = snakeToCamel(key);
newObj[newKey] = convertToCamelCase(obj[key]); // 对每个键的值进行递归处理
}
}
return newObj;
}
return obj; // 如果是基础数据类型,直接返回
}
删除接口优化
之前我们的删除语句是这样子的,这种删除有个缺陷就是无法执行多个删除语句,只能删除一个
JS
// 统一使用 IN 语句删除,无论是单个还是多个ID
const sqlQuery = "DELETE FROM sys_notice WHERE notice_id = ?";
现在我们统一写法为IN字句的删除,多个和单个的删除都可以复用
JS
// 统一使用 IN 语句删除,无论是单个还是多个ID
const sqlQuery = 'DELETE FROM notices WHERE id IN (?)';
时间参数
在数据新增的时候,创建时间参数和更新时间参数应该是使用的现在的时间
mysql表的设计之中时间应该是
JS
CREATE TABLE notices (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
在数据库表中,create_time 字段自动使用当前时间戳,插入时不需要显式传递。
但是每次更新时,需要手动设置 update_time 为当前时间,或者使用数据库的 ON UPDATE CURRENT_TIMESTAMP 来自动处理。
数据更新的时候,更新时间手动插入为:
JS
const updateTime = new Date();
update_time:new Date()
Node后台接口地址
Mac查看自己的ip
js
curl ifconfig.me
curl ipinfo.io/ip // 查看ip地址
ifconfig // 查看ip地址 inet
之前我们采取的后台地址都是http://localhost:8888这种方式,但是有明显的缺陷,就是只能本机访问,那么如果我们想要改成局域网可以访问的方式应该如何做呢
js
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
=> 修改为:
const PORT = 8888;
const ip = '0.0.0.0';
app.listen(PORT,ip,() => {
console.log(`Server is running on http://localhost:${PORT}`);
});
进一步封装接口模块
接下来我们抽离接口方法为一个单独的文件,方便我们后续的维护
简单抽离一下查询接口
☞ apimethods.js
JS
// 接口的公共方法部分
function addApi(queryData,sqltotal,req,res,querySearch) {
// console.log(query,sqltotal,querySearch,'查询');
let querySearchData=convertKeysToSnakeCase(querySearch);
// 查询Sql
let querySql=queryData;
// console.log(querySearchData,'querySearchData');
// 构建查询条件
let params = [];
Object.keys(querySearchData).forEach(key => {
// console.log(key, querySearchData[key]); // 输出:key 和对应的值
if(key!='page_num'&&key!='page_size'){
querySql = addCondition(querySql, params, key, querySearchData[key]);
}
});
if(querySearchData['page_num']&&querySearchData['page_size']){
querySql = addPagination(querySql,params,querySearchData.page_num,querySearchData.page_size); //分页条件
}
console.log('查询条件', querySql, params);
// 查询数据库并返回数据
connectionPool.query(querySql,params, (err, results) => {
// console.log(err,'err');
// console.log(results,'results');
if (err) {
res.send({
code: 500,
// data: results,
message:'添加失败!'
});
return ;
} else {
if(results.length>0){
// 查询数据库并返回数据
connectionPool.query(sqltotal, (errtotal, rows) => {
let total = rows[0]['total'];
if (errtotal) {
res.send({
code: 401,
// data: results,
message: '查询失败!'
});
return;
} else {
console.log('查询成功');
res.send({
total: total,
code: 200,
data:convertToCamelCase(results),
message: '查询成功!',
});
}
});
}else{
res.send({
total: 0,
code: 200,
data: [],
message:'查询成功!',
});
}
}
});
}
☞ 使用起来
这么一看确实简单整洁了不少,5行代码就实现了一个查询接口
JS
router.get('/',(req, res) => {
// console.log(req.query, 'req.query');
// console.log(req.body,'req.body');
// console.log(req.params,'req.params');
// 查询数据库列表
let query = `SELECT * FROM articles`;
// 查询数据库总数
let sqltotal = `SELECT COUNT(*) AS total FROM articles`
// 查询数据库并返回数据
const { title,type, page_num, page_size } = convertKeysToSnakeCase(req.query);
let querySearch = {title,type, page_num, page_size};
addApi(query,sqltotal,req,res,querySearch);
});
抽离增加接口
JS
// 新增接口
function addApi(insertSql, res, postData) {
connectionPool.query(insertSql, postData, (error, results, fields) => {
// console.log(results,'results');
// console.log(error,'error');
if (error) {
res.send({
code: 500,
error: error,
message: '添加失败!'
});
return;
} else {
res.send({
code: 200,
// data: results,
message: '添加成功!'
});
return;
}
});
}
使用
JS
// 新增 POST
router.post('/', (req, res) => {
const insertSql = 'INSERT INTO articles SET ?'; // 准备 SQL 插入语句
const { title, author,type,content} = convertKeysToSnakeCase(req.body);
const postData = {title,author,type,status:1,content};
addApi(insertSql,res,postData);
});
抽离详情接口
封装
JS
// 详情接口
function getApi(querySql,values,res) {
connectionPool.query(querySql, values, (err, results) => {
if (err) {
console.error('Error querying database:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
res.json({
code: 200,
data: convertToCamelCase(results[0]) || {},
});
});
}
使用
JS
router.get('/:id', (req, res) => {
// console.log(req.query,'req.query');
const { id } = req.params;
const values = [id];
let querySql = 'SELECT * FROM articles WHERE id = ?';
getApi(querySql,values,res);
});
抽离修改接口
JS
// 更新接口
function updateApi(updateSql,Search,res) {
connectionPool.query(updateSql,Search, (error, results, fields) => {
if (error) {
res.send({
code: 500,
// data: results,
error:error,
message:'修改失败!'
});
return;
} else {
res.send({
code: 200,
// data: convertToCamelCase(results),
message:'修改成功!'
});
}
});
}
使用
JS
// 更新接口
router.put('/', (req, res) => {
// console.log(req,'req');
const { title, content,id} = convertKeysToSnakeCase(req.body);
const updatevalues = {title, content};
let updateSql='UPDATE articles SET ? WHERE id = ?';
updateApi(updateSql,[updatevalues, id],res);
});
抽离删除接口
JS
// 更新接口
function delApi(delSql,delvalue,res) {
connectionPool.query(delSql, delvalue, (error, results) => {
if (error) {
console.error('Error querying database:', error);
res.status(500).json({ error: error });
return;
}
res.json({
code: 200,
// data: results,
message:'删除成功!',
});
});
}
使用
JS
// 删除数据 DELETE请求处理程序
router.delete('/:id', (req, res) => {
// const {id} = req.body; // 从请求体中获取数据
const id = req.params.id;
const delsql = "DELETE FROM articles WHERE id = ?";
delApi(delsql,[id],res);
});
抽离更改状态接口
更改状态接口抽离
JS
// 更新状态接口
function changeStatusApi(statusSql,upvalue,res) {
connectionPool.query(statusSql,upvalue, (error, results, fields) => {
if (error) {
res.json({
code: 500,
error: error,
message:'更改失败!',
});
return;
} else {
res.json({
code: 200,
// data: results,
message:'更改成功!',
});
}
});
}
使用
JS
// 禁用和使用,用于更改状态
router.put('/status/:id', (req, res) => {
const id = parseInt(req.params.id);
const newState = req.body.status;
const query = 'UPDATE articles SET status = ? WHERE id = ?';
changeStatusApi(query,[newState, id],res);
});
mysql库优化为mysql2
之前我们使用的mysql库,现在我们使用mysql2库,因为mysql2库性能更高,并且支持promise,在mysql8.0中,mysql2库支持异步操作
JS
卸载之前的mysql
npm uninstall mysql
安装mysql2
npm install mysql2
使用mysql2库
const mysql = require('mysql');
// 创建数据库连接池
const connectionPool = mysql.createPool({
host: 'xxx', // 数据库主机地址,如果是本地数据库则使用localhost
user: 'xxx', // 数据库用户名
password: 'xxx', // 数据库密码
database: 'xxx', // 要连接的数据库名
multipleStatements: true, // 允许执行多条语句
});
=> 只需要把对应模块给替换成我们的mysql2 即可
const mysql2 = require('mysql2');
// 创建数据库连接池
const connectionPool = mysql2.createPool({
host: 'xxx', // 数据库主机地址,如果是本地数据库则使用localhost
user: 'xxx', // 数据库用户名
password: 'xxx', // 数据库密码
database: 'xxx', // 要连接的数据库名
multipleStatements: true, // 允许执行多条语句
});