Contents
14. 项目后端API开发¶
本章将开发上一章介绍的项目,并且会逐一实现所有的后端功能。
另外,本章将使用之前介绍过的Postman进行接口API的请求测试。
本章涉及的知识点如下:
开发一个完整的小型项目后端;
Node.js与MySQL及Redis的连接和使用;
在项目中编写完成需求的接口。
14.1. 1.开发前的准备工作¶
本节使用Express新建项目,并且配置好数据库连接,为之后的API开发做好准备。
注意:在使用Express开发时,除非使用第三方工具,否则Express不会自动根据更改进行热更新,而需要手动重启服务器才能看到更改后的内容。
14.2. 2.初始化项目¶
首先保证本机已经安装了Express,使用如下命令生成一个新的项目:
$ express --no-view server
本项目中所有的后端内容都使用接口方式,不需要任何的模板引擎,因此使用–no- view参数初始化一个不需要模板引擎的项目。
根据提示进入项目文件夹,使用如下命令安装完整的项目依赖包并尝试启动,效果如图
$ cd server
$ cnpm install
$ SET DEBUG=server:* & npm start
14.3. 3.连接数据库¶
使用如下命令安装Redis依赖包,Redis的依赖包会自动添加到Express项目中,并且可以在Express中引用。
$ npm install redis -save
注意:如果使用的是Express 5.0及以上版本,部分写法可能出现变动,根据官方文档进行微调即可。在Node.js中,Redis包被编写为支持异步的形式,该包提供了基本的数据库操作,这里需要在该项目文件中进行统一处理。首先在项目文件夹中新建config文件夹,用来存放所有的配置文件,然后进行以下配置操作:
(1)在config文件夹中新建JavaScript文件,命名为db.js,用于存放Redis数据库的配置。代码如下:
exports.redisConfig = {host: '192.168.1.107', port: '6379', password: "123456",ttl: 5 * 60 * 1000}
说明:配置文件不一定必须要使用JavaScript文件形式,也可以使用专门用于存储配置的其他格式,引用时注意编写文件格式相应的解析。
(2)在项目文件的根目录下新建util文件夹,放置所有工具的JavaScript方法,数据库连接方法也存放在该文件夹中。
(3)新建一个redisDB.js文件,在该文件中连接数据库,并且对数据库提供的方法进行一些改写和封装。需要注意的是,对于Redis,我们尽可能只使用两个相关的操作方法,一个是set,用于数据的存储和改变,另一个是get,用于数据的获取。redisDB.js文件的代码如下:
let redis = require("redis");
//获取到数据库的配置
const {redisConfig} = require("../config/db")
//获取redis连接
const redis_client = redis.createClient(redisConfig);
//连接成功
redis_client.on("connect", () => {
console.log("连接成功")
})
//错误处理
redis_client.on("error", (err) => {
console.log(err);
});
redis = {};
//根据模式获得全部键
keys = async (cursor, re, count) => {
let getTempKeys = await new Promise((resolve) => {
//从连接中获得到该值,并且返回
redis_client.scan([cursor, "MATCH", re, "COUNT", count], (err, res) => {
console.log(err)
return resolve(res);
});
});
return getTempKeys;
}
redis.scan = async (re, cursor = 0, count = 100) => {
return await keys(cursor, re, count)
}
//set该值进入数据库
redis.set = (key, value) => {
// 将所有对象转换为Json字符串进行保存
// 需要注意的是如果该字符串过大,可能会导致性能下降
value = JSON.stringify(value);
return redis_client.set(key, value, (err) => {
if (err) {
console.log(err);
}
});
};
// 获得text,在get时可以使用then调用
text = async (key) => {
let getTempValue = await new Promise((resolve) => {
//从连接中获得到该值,并且返回
redis_client.get(key, (err, res) => {
return resolve(res);
});
});
//将该值转化为本身的对象,并且返回
getTempValue = JSON.parse(getTempValue)
return getTempValue;
}
//返回获得的值
redis.get = async (key) => {
return await text(key);
}
//设置key的过期时间
redis.expire = (key, ttl) => {
redis_client.expire(key, parseInt(ttl))
}
//获取自增id
id = async (key) => {
console.log("查找" + key)
let id = await new Promise((resolve => {
redis_client.incr(key, (err, res) => {
console.log(res)
return resolve(res)
})
}))
console.log(id)
return id
}
redis.incr = async (key) => {
return await id(key)
}
//有序集合
//新增有序集合(键名,成员和分值)
redis.zadd = (key, member, num) => {
member = JSON.stringify(member)
redis_client.zadd(key, num, member, (err) => {
if (err) {
console.log(err)
}
})
}
//获取一定范围内的元素
tempData = async (key, min, max) => {
let tData = await new Promise((resolve => {
redis_client.zrevrange([key, min, max, 'WITHSCORES'], (err, res) => {
return resolve(res)
})
}))
//同时获得了分值,所以需要进行转化为对象
let oData = []
//构造
for (let i = 0; i < tData.length; i = i + 2) {
console.log(tData[i])
oData.push({member: JSON.parse(tData[i]), score: tData[i + 1]})
}
return oData
}
redis.zrevrange = async (key, min = 0, max = -1) => {
return tempData(key, min, max)
}
//有序集合的自增操作
redis.zincrby = (key, member, NUM = 1) => {
member = JSON.stringify(member)
redis_client.zincrby(key, NUM, member, (err) => {
if (err) console.log(err)
})
}
//有序集合通过member获取其score值
tempZscore = async (key, member) => {
member = JSON.stringify(member)
return await new Promise((resolve => {
redis_client.zscore(key, member, (err, res) => {
console.log(res)
return resolve(res)
})
}))
}
redis.zscore = async (key, member) => {
return tempZscore(key, member)
}
module.exports = redis;
需要对用户的Token进行时间控制,不能让其一直有效,否则只要获取了该Token的人都可以模拟用户进行操作。
//设置key的过期时间
redis.expire = (key, ttl) => {
redis_client.expire(key, parseInt(ttl))
}
一些基本的ID应当考虑使用自增变量,这里封装一个Redis的自增ID获取方法,代码如下:
//获取自增id
id = async (key) => {
console.log("查找" + key)
let id = await new Promise((resolve => {
redis_client.incr(key, (err, res) => {
console.log(res)
return resolve(res)
})
}))
console.log(id)
return id
}
redis.incr = async (key) => {
return await id(key)
}
虽然只使用k-v形式的JSON字符串,但是对于需要排序的内容,k-v形式过于烦琐,因此需要使用Redis中的有序序列进行一些数据的存储(类似于阅读量和热点文章等)。
在某些情况下会使用到Redis中的有序集合这个结构,例如在文章的阅读数量和热点获取时需要排序。
有序集合结构基于k-v基础,v中有一个member对象,对应一个score(分值),通过score可实现排序。如果读者不理解该结构,可以查阅有关资料。有序集合代码如下:
//有序集合
//新增有序集合(键名,成员和分值)
redis.zadd = (key, member, num) => {
member = JSON.stringify(member)
redis_client.zadd(key, num, member, (err) => {
if (err) {
console.log(err)
}
})
}
//获取一定范围内的元素
tempData = async (key, min, max) => {
let tData = await new Promise((resolve => {
redis_client.zrevrange([key, min, max, 'WITHSCORES'], (err, res) => {
return resolve(res)
})
}))
//同时获得了分值,所以需要进行转化为对象
let oData = []
//构造
for (let i = 0; i < tData.length; i = i + 2) {
console.log(tData[i])
oData.push({member: JSON.parse(tData[i]), score: tData[i + 1]})
}
return oData
}
redis.zrevrange = async (key, min = 0, max = -1) => {
return tempData(key, min, max)
}
//有序集合的自增操作
redis.zincrby = (key, member, NUM = 1) => {
member = JSON.stringify(member)
redis_client.zincrby(key, NUM, member, (err) => {
if (err) console.log(err)
})
}
//有序集合通过member获取其score值
tempZscore = async (key, member) => {
member = JSON.stringify(member)
return await new Promise((resolve => {
redis_client.zscore(key, member, (err, res) => {
console.log(res)
return resolve(res)
})
}))
}
redis.zscore = async (key, member) => {
return tempZscore(key, member)
}
module.exports = redis;
注意:为了方便读者理解,对数据库的操作基本没有采用非JSON格式,但在真正的项目中,频繁地在代码中修改JSON对象并不适宜,采用Redis提供的散列或队列等结构效果会更好。
同时,为了使代码和数据逻辑更加清晰和简单,程序中没有采用事务等形式,而全部采用Redis的基本命令进行组合。在实际项目中,例如增加文章,同时需要对类型、标签和排序进行修改,这些数据库的操作都应当在同一个事务中,如果执行任意一个操作失败,将导致整个操作失败。
14.4. 4.配置服务应用列表¶
(1)配置访问列表。在config文件夹中新建app.js文件,配置允许访问的应用名称,代码如下:
exports.ALLOW_APP = ['book']
exports.NAME = 'server'
(2)在前端传递一个代表该应用的参数,该参数存在于路径或post参数中,这样路径会显得有点“难看”,所以传递时可以将该参数附带在请求的头部。
在传递时,将该参数命名为fapp,也就是说,当请求头中的fapp字段为book字符串时,符合要求。在Express中,通过如下代码获取该参数:
// 获取所有的header参数
console.log(req.headers)
// 获取应用传递的参数
req.headers.fapp
(3)编写用户状态判定中间件。
所有的路由控制前都应当有用户是否处于登录状态的判断和区分,Express的中间件非常适合完成在访问路由时进行统一的用户状态判定。
中间件可以理解为一个独立于主要功能逻辑的代码块,用于实现一些附加的功能,可以在主要逻辑处理之前或处理之后进行访问,类似于Vue.js中的“守卫”。
在util文件夹中编写middleware.js文件,用于存放用户状态判定。该中间件实现的功能是对所有的用户请求进行头部判定,如果符合条件,则继续执行,如果不符合条件,则通过res.json返回一个错误。middleware.js文件的代码如下:
const {ALLOW_APP} = require('../config/app')
const util = require('./common')
exports.checkAPP = (req, res, next) => {
console.log(req.headers)
if (!ALLOW_APP.includes(req.headers.fapp)) {
res.json(util.getReturnData(500, "来源不正确"))
} else {
next()
}
}
中间件可以使用next()对象进行下一步操作,此时的项目需求应当是在所有的路由头部执行该中间件,因此只有条件通过next()之后,才会执行主要的业务逻辑。
上述代码使用了util.js中的一个创建JSON格式化串的方法,可以在util文件夹中新建common.js文件,用于存放一些通用的方法或验证内容。代码如下:
let util = {}
util.getReturnData = (code, message = '', data = []) => {
//保证数据格式
if (!data) {
data = []
}
return {code: code, message: message, data: data}
}
//转化为格式化时间
util.getLocalDate = (t) => {
let date = new Date(parseInt(t))
return date.getFullYear() + "-" + (parseInt(date.getMonth()) + 1) + "-" + date.getDate() + " " + date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
}
module.exports = util
注意:箭头函数不需要花括号及显式的return,但为了统一格式,本书使用了显式的return。
(4)引入中间件,因为所有对用户的请求都需要该中间件验证,所以直接在app.js中引入并使用。更改后的app.js代码如下:
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
const {checkAPP} = require("./util/middleware");
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
//
app.use('/', checkAPP,indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
(5)编写一个测试路由,修改router文件夹中的index.js文件,修改后的代码如下:
var express = require('express');
var router = express.Router();
const util = require('../util/common')
// 获取footer显示内容
router.get('/getFooter', function (req,res,next) {
res.json(util.getReturnData(0,'success'))
});
也就是说,当访问该路由http://localhost:3000/getFooter时,首先进行请求头的验证,只有验证成功了,才能接着执行路由对应的业务逻辑。
(5)使用Postman进行测试,如果没有增加任何请求头,则会返回一个错误信息提示
Postman可以在请求下方的Headers选项卡中填写任意的头部信息,该信息会同时发送给服务器端。
例如,在Headers中增加一个fapp=book,并且确定其处于勾选状态.请求之后,请求成功。