一.初始化项目
(1)服务器准备
1.1初始化包管理配置文件
项目根目录: api_server 在项目根目录中打开终端运行以下代码来初始化包管理配置文件:
就会得到package.json配置文件
1.2安装express框架
npm i express@4.17.1
1.3创建项目入口文件并配置服务器
将app.js作为本项目的入口文件
//导入express
const express = require("express")
//创建服务器实例对象
const app = express()
//启动服务器
app.listen(3007,()=>{
console.log("api server running at http://127.0.0.1:3007")
})
(2)中间件准备
2.1配置cors跨域中间件
在app.js中导入并配置cors中间件:
npm i cors@2.8.5
//导入cors中间件
const cors = require("cors")
//注册cors为全局中间件
app.use(cors())
2.2配置解析表单数据的中间件
通过如下代码 配置解析application/x-www-form-urlencoded 格式的表单数据的中间件:
app.use(express.urlencoded({extended:false}))
(3)路由准备
3.1初始化路由相关的文件夹
在项目的根目录中 新建router文件夹 用来存放所有的路由模块(路由模块中 只存放客户端的请求与处理函数之间的映射关系)
在项目根目录中 新建router_handler文件夹,用来存放所有的路由处理函数模块(路由处理函数模块中 专门负责存放每个路由对应的处理函数)
3.2初始化用户路由模块
在router文件夹中 新建user.js文件 作为用户的路由模块 初始化代码如下:
const express = require("express")
//创建路由对象
const router = express.Router()
//注册新用户
router.post("/reguser",(req,res)=>{
res.send("reguser OK)
})
//登录
router.post("/login",(req,res)=>{
res.send("login OK")
})
//将路由对象共享出去
module.exports = router
在app.js中导入并使用路由模块
//导入并使用路由模块
const userRouter = require("./router/user")
app.use("/api",userRouter)
3.3抽离用户路由模块中的处理函数(模块化开发)
目的:为了保证路由模块的纯粹性 所有路由处理函数 必须抽离到对应的路由处理函数模块中
在router_handler文件夹中创建user.js文件表示用户路由处理函数模块 使用exports对象共享以下两个路由处理函数:
//在router_handler文件夹下的user.js文件中共享以下两个用户路由处理函数
exports.reguser = (req,res)=>{
res.send("reguser OK")
}
exports.login = (req,res)=>{
res.send("login OK")
}
接下来,修改在router文件夹下的user.js文件内使用了这些用户路由模块处理函数的地方:
const express = require("express")
//创建路由对象
const router = express.Router()
//导入并使用路由处理函数模块
const userHandler = require("../router_handler/user")
//注册新用户
router.post("/reguser",userHandler.reguser)
//登录
router.post("/login",userHandler.login)
//将路由对象共享出去
module.exports = router
二.注册用户的API开发
(1)数据库准备
1.1创建用户信息表
在数据库my_db_01中出个那就表ev_users 使用的软件是MySql workbench:
1.2安装并配置mysql模块(用于连接和操作MySQL数据库)
执行以下命令安装mysql模块:
npm i mysql@2.18.1
再在项目根目录中创建db/index.js 文件 在此自定义模块中创建数据库的连接对象:
//导入mysql模块 用于连接和操作数据库
const mysql = require("mysql")
//创建数据库连接池对象
const db = mysql.createPool({
host:"127.0.0.1",
user: "root",
password: "admin123",
database: "my_db_01"
})
//向外共享db数据库连接池对象
module.exports = db
(2)注册用户功能的实现(补充reguser处理函数的业务逻辑)
2.1表单信息合法性验证
将下面的代码补充到router_handler的user.js文件中的reguser路由处理函数中:
//判断用户名和密码是否为空
//接受表单数据
const userinfo = req.body //关键
//判断数据是否合法
if(!userinfo.username || !userinfo.password){
return res.send({
status:1,
message:"用户名或密码不能为空"
})
}
2.2检测用户名是否被占用
同样的将导入数据库连接池对象的代码补充到router_handler的user.js文件中:
//导入数据库连接池对象 用于操作数据库
const db = require("../db/index")
用户名查重逻辑如下:
//定义SQL查重语句
const sql = "select * from ev_users where username = ?"
//执行sql语句进行查重
db.query(sql,userinfo.username,(err,results)=>{
//sql语句执行失败
if(err){
return res.send({
status: 1,
message: err.message
})
}
//sql语句执行成功,但是用户名重复
if(results.length > 0){
return res.send({
status: 1,
message: "用户名重复 请更换用户名!"
})
//TODO: 用户名可用
res.send("用户名可用!")
}
}
})
2.3 对密码进行加密处理
为了保证密码的安全性 不建议在数据库以明文的形式保存用户密码 推荐密码进行加密存储
这里推荐使用bcryptjs对用户密码加密:
npm i bcryptjs@2.4.3
然后,在router_handler中的user.js里面导入该模块,对可用的用户的密码进行加密:
//导入加密模块
const bcrypt = require("bcryptjs")
//TODO:用户名可用
console.log(userinfo) //查看没加密之前的信息
//调用bcrpy模块的hashSync方法加密 参数1: 要加密的密码 参数2: 随机盐的长度(散列(哈希)运算时添加的一段随机数据 用于增加密码的安全性)
userinfo.password = bcrypt.hashSync(userinfo.password,10)
console.log(userinfo) //查看加密之后的信息
2.4 插入新用户(注册成功的结果)
前面加密的工作完成后,既然注册了新用户 就要向数据库插入一行新的数据:
下面是插入成功后的结果,可以看到数据库多了一条数据
三.代码优化
(1)优化res.send()代码
在处理函数中,需要多次调用res.send()向客户端响应处理失贩的结果,为了简化代码,可以手动封装一个res.cc()函数
在app.js中,所有路由之前,声明一个全局中间件,为res 对象挂载一个res.cc()函数:
//响应数据的中间件
app.use(function (req,res,next)(
/ status =8为成功status = 1 为失败:默认将status的值设置为 1,方便处理失败的情况
res.cc = function (err,status = 1) (
res.send((
1/ 状态
status,
// 状态描述,判断err是错误对象还是字符串
message:err instanceof Error?err.message:err,
next()
(2)优化表单数据处理
如果自己书写表单数据验证规则(if else逻辑)会过于繁琐与复杂 准确性也不强,为了提高效率 增强代码的健壮性 建议使用第三方包对表单数据进行验证,这里推荐joi和@escook/express-joi两个包:
const express = require('express')
const app = express()
//导入joi来定义验证规则
const Joi = require("joi")
//1.导入@escook/express-joi
const expressJoi = require("@escook/express-joi")
//解析x-www-form-urlencoded格式的表单数据
app.use(express.urlencoded({ extended: false }))
//2.定义验证规则
const userSchema={
//2.1校验req.body中的数据
body:{
//用户名必须是字符串 且只能是[a-z A-Z 0-9]最短3最长12字符 用户名为必填
username: Joi.string().alphanum().min(3).max(12).required(),
password: Joi.string()
.pattern(/^[\S]{6,15}$/)
.required(),
repassword: Joi.ref('password') //确认密码的校验规则
}
//2.2校验req.query中的数据
query:{
name: Joi.string().alphanum().min(3).required(),
age: Joi.number().min(1).max(100).required()
}
//2.3校验req.params中的数据
params:{
id:Joi.number().integer().min(0).required()
}
}
//3. 在路由中通过expressJoi(userSchema)的方式
//调用中间件进行参数验证 即在路由函数里面传递中间件函数来使用中间件
app.post("/adduser/:id",expressJoi(userSchema),function(req,res){
const body = req.body
res.send(body)
})
//4错误级别中间件
app.use(function(err,req,res,next){
//4.1Joi参数验证失败
if(err instance of Joi.ValidationError){
return res.cc(err)
}
//4.2未知错误
res.cc(err)
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(3001, function () {
console.log('Express server running at http://127.0.0.1:3001')
})
那么怎么将这两个包应用到我们的项目中呢? 首先创建schema文件夹 并在下面创建user.js文件
user.js初始化代码如下:
//在schema/user.js文件下 我们定义用户相关的验证规则
const joi = require("@hapi/joi")
//用户名的验证规则
const username = joi.string().alphanum().min(1).max(10).required()
//密码验证规则
const password = joi.string().pattern(/^[\s]{6,12}$/).required()
//共享注册和登录表单的验证规则对象
exports.regLoginShema = {
//需要对req.body中的数据进行验证
body:{
username,
password
}
}
既然已经定义好并且共享了注册和登录表单的验证规则,那么我们怎么将其应用到路由中呢?
在我们之前创建的router/user.js 用户路由模块下添加如下内容:
//1.导入验证表单数据的中间件
const expressJoi = require("@escook/express-joi")
//2.导入需要的验证规则对象
const {regLoginSchema}=require("../schema/user")
//3.注册新用户
//在注册新用户的路由中 声明局部中间件 对当前请求中携带的数据进行验证
//3.1数据验证通过后,会把这次请求流转给后面的路由处理函数
//3.2数据验证没有通过 终止后续代码执行 会抛出一个全局的Error错误 进入全局错误级别中间件进行处理
router.post("/reguser",expressJoi(regLoginSchema),userHandler.regUser)
在app.js的全局错误级别中间件中 捕获验证失败的错误,并且把验证失败的结果响应给客户端
//4错误级别中间件
app.use(function(err,req,res,next){
//4.1Joi参数验证失败
if(err instance of Joi.ValidationError){
return res.cc(err)
}
//4.2未知错误
res.cc(err)
})
注意:一定要解构出regLoginSchema这个数据验证规则对象 如果直接像我下面这样导入:
const regLoginSchema = require("../schema/user")
相当于regLoginSchema对象是下面这种结构(外面多了一层花括号 我要的是里面的body对象)
regLoginSchema = {
body:{
username,
password
}
}
四.登录用户的API开发
将router/user.js 文件加入以下代码:
//登录的路由 expressJoi是自动完成表单数据验证的模块 regLoginSchema是通过Joi模块生成的验证规则
router.post("/login",expressJoi(regLoginSchema),userHandler.login)
(1)根据用户名查询用户的数据
1.1接受表单数据:
const userinfo = req.body //在类似postman这样的模拟接口请求的软件中输入请求体的内容
1.2 定义SQL语句(用于查询数据库中是否存在该用户名)
const sql = "select*from ev_users where username = ?" //sql中?为占位符
1.3执行SQL语句 查询用户的数据
db.query(sql,userinfo.username,(err,results)=>{
//SQL语句执行失败
if(err) return res.cc(err)
//SQL语句执行成功 但是在数据库中没有查询到该用户
if(results.length !==1) res.cc("该用户不存在 请重新登录!")
//SQL语句执行成功 且找到了该用户 TODO(接下来要做的)判断用户输入的密码是否和数据库的一致
})
(2)判断用户输入的密码是否正确
实现思路: 需要调用bcrypt.compareSync(用户提交的密码,数据库中的密码) 比较密码是否一致
返回的值是布尔值(true一致 false不一致)
具体实现代码如下:
//拿到用户提交的密码和数据库的密码比较
const compareResult = bcrypt.compareSync(userinfo.password,results[0].password)
//判断两个密码是否一致
if(!compareResult){
return res.cc("用户输入的密码不正确,请重试!")
}
//TODO: 登陆成功 服务器端生成一个TOken字符串返回给客户端
(3)生成JWT的Token字符串
核心注意点: 在生成Token字符串的时候 一定要剔除密码和头像的值(敏感信息)
1.通过ES6的高级语法 快速剔除密码和头像的值:
const user = {...results[0],password:"",user_pic:""}
2.运行如下命令 安装生成Token字符串的包:
npm i jsonwebtoken@8.5.1
3.在/router_handler/user.js模块的头部区域 导入jsonwebtoken包:
const jwt = require("jsonwebtoken")
4.创建config.js文件 并且向外共享加密和还原Token的jwtSecretKey字符串:
module.exports = {
jwtSecretKey: "Zero Two And Hiro",
expiresIn: "48h"
}
5.向客户端返回Token字符串
const config = require("../config")
const tokenStr = jwt.sign(user,config.jwtSecretKey,{
expiresIn: config.expiresIn
})
6.将生成的Token返回给客户端
res.send({
status:0,
message:"登陆成功!",
token:"Bearer "+tokenStr
})
(5)配置解析Token的中间件
1.运行如下命令 安装解析Token的中间件:
npm i express-jwt@5.3.3
2.在app.js中注册路由之前 配置解析Token的中间件:
const config = require("./config")
const expressJwt = require("express-jwt")
app.use(expressJwt({secret:config.jwtSecretKey}).unless({path:[/^\/api/]}))
3.在app.js中的错误级别中间件里面 捕获并处理Token认证失败后的错误:
app.use((err,req,res,next)=>{
//表单数据验证失败导致的错误
if(err instanceof joi.ValidationError) return res.cc(err)
if(err.name == "UnauthorizedError") return res.cc("身份认证失败!")
//未知错误
res.cc(err)
})
五.用户信息API
(1)获取用户基本信息
.1.1初始化路由模块
1.在router下新建userinfo.js 初始化代码如下:
//用户路由模块
const express = require("express")//导入express模块
const router = express.Router() //创建路由对象router
//获取用户信息接口
router.get("/userinfo",(req,res)=>{
console.log("ok")
})
//导出用户路由模块
module.exports = router
2.在app.js中导入并使用个人中心的路由模块:
const userInfoRouter = require("./router/userinfo")
app.use("/my",userInfoRouter)
1.2初始化路由处理函数模块
跟之前处理router/user.js一样 在router_handler下创建userinfo.js用于存放用户个人中心模块路由的处理函数,并初始化如下代码:
exports.getUserInfo = (req,res)=>{
console.log("ok")
}
然后再在router/userinfo.js文件中导入该路由处理函数
const userInfoHandler = require("../router_handler/userinfo")
router.get("/userinfo",userInfoHandler.getUserInfo)
1.3数据库获取用户数据
在router_handler/userinfo.js中引入数据库连接池对象 操作数据库:
const db = require("../schema/index")
exports.getUserInfo = (req,res)=>{
//为了防止密码泄露 获取用户信息时应该不包括密码
const sql = "select id,username,nickname,email,user_pic from ev_users where id = ?"
db.query(sql,req.user.id,(err,results){
if(err) return res.cc(err)
if(results.length !== 1) return res.cc("获取用户信息失败!")
res.send({
status:0,
message:"获取用户信息成功!".
data: results[0]
})
})
}
(2)更新用户的基本信息
简要描述: 更新用户的基本信息 请求的URL: /my/userinfo 请求方式: POST
2.1 定义路由和处理函数
1.在/router_handler/userinfo.js 模块中 新增更新用户基本信息的路由处理函数:
//更新用户基本信息的处理函数
exports.updateUserInfo = (req,res)=>{
res.send("ok")
}
2.在/router/userinfo.js模块中 使用先前定义的路由处理函数
router.post("/userinfo",userInfoHander.updateUserInfo)
2.2 验证表单数据
1.在/schema/user.js 验证规则模块中 定义id,nickname,email的验证规则如下:
//之前定义了username和password的验证规则 这里定义id nickname emial的验证规则
const id = joi.number().integer().min(1).required()
const nickname = joi.string().required()
const email = joi.string().email().required()
2.并且使用exports向外共享验证所更新的数据的验证规则对象:
//验证规制对象 - 更新用户基本信息的规则对象
exports.updatUserInfoSchema = {
body:{
id,
nickname,
email
}
}
3.在/router/userinfo.js模块中 导入验证数据合法性的中间件:
//导入验证数据合法性的中间件
const expressJoi = require("@escook/express-joi")
4.在/router/userinfo.js模块中 导入需要的验证规则对象
//导入需要的验证规则对象
const {updateUserInfoSchema} = require("../schema/user")
5.在/router/userinfo.js模块中 修改更新用户的基本信息的路由:
router.post("/userinfo",expressJoi(updateUserInfoSchema),userInfoHandler.updateUserInfo)
2.3实现更新用户基本信息的功能
1.定义待执行的SQL语句
const sql = "update ev_users set ? where id=?"
2.调用db.query()执行SQL语句并传参
db.query(sql,[req.body,req.body.id],(err,results)=>{
if(err) return res.cc(err)
if(results.affectedRows !==1) return res.cc("修改用户基本信息失败!")
return res.cc("修改用户基本信息成功!",0)
})
(3)重置用户密码
3.1定义路由和处理函数
1.在/router/userinfo.js模块中 新增重置密码的路由:
router.post("/updatepwd",userInfoHander.updatePassword)
2.在router_handler/userinfo.js模块中配置重置密码的路由处理函数
exports.updatePassword = (req,res)=>{
console.log("ok")
}
3.2验证新旧密码规则
1.在schema/user.js 文件里面设置密码验证规则 并且新旧密码不能一致!
exports.updatePasswordSchema = {
body:{
oldPwd: password,
//joi.ref("oldPwd") 表示新密码必须和旧密码一致 是作为参数传给not()表示 新旧密码不能一致
// concat(password) 表示新密码还必须符合密码的校验规则
newPwd: joi.not(joi.ref("oldPwd")).concat(password)
}
}
2.在/router/userinfo.js模块中 导入需要的验证规则对象
const {updateUserInfoSchema,updatePasswordSchema} = require("../shema/user")
router.post("/updatepwd",expressJoi(updatePasswordSchema),userInfoHandler.updatePassword)
(4)更换用户头像
请求URL: /my/update/avatar 请求方式: post 请求头: 提供Header认证字段
4.1定义路由和处理函数
1.在/router/userinfo.js模块中 新增更新用户头像的路由:
//更新用户头像的路由
router.post("/update/avatar",userInfoHandler.updateAvator)
2.在/router_handler/userinfo.jg 模块中 定义并向外共享更新用户头像的路由处理函数
//更新用户头像的处理函数
exports.updateAvatar = (req,res)=>{
res.send("OK")
}
4.2验证表单数据
1.在/shema/user.js验证规则模块中 定义avatar的验证规则如下:
//dataUrl()指的是如下格式的字符串数据:
//data:image/png:base64,VE9PTUFOWVNFQ1JFVFM=
const avatar = joi.stirng().dataUrl().required()
2.并使用exports向外共享如下的验证规制对象:
//验证规则对象-更新头像
exports.updateAvatarSchema={
body:{
avatar,
}
}
3.在/router/userinfo.js 模块中 导入需要的验证规则对象:
const {updateAvatarSchema} = require("../schema/user")
4.3实现更新用户头像的功能
1.定义更新用户头像的SQL语句:
const sql = "update ev_users set user_pic=? where id=?"
2.调用db.query()执行该SQL语句 更新对应用户的头像
db.query(sql,[req.body.avator,req.user.id],(err,results)=>{
if(err) return res.cc(err)
if(results.affectedRows !== 1) return res.cc("更新头像失败!")
res.cc("更新头像成功!",0)
})
六.文章分类管理
准备工作: 先创建ev_article_cate 表:(alias是别名字段)
(1)获取文章分类列表API
功能描述: 获取文章分类列表 URL: /my/article/cates 请求方式: GET 返回示例如下图
后面的操作可以参考前面我书写的内容 其实都是类似的 这里只把关键步骤写出来(文章表的创建)
(2)新增文章分类API
功能描述: 新增文章分类 URL: /my/article/addcates 请求方式: POST 返回示例如下图
关键操作1: 要新增文章分类,必须先定义好验证新增文章分类的规则对象
//导入定义数据验证规则的第三方库Joi
const joi = require("joi")
const name = joi.string().required()
const alias = joi.string.alphanum().required()
exports.addCateSchema = {
body:{
name,
alias
}
}
关键操作2: 要新增文章分类 必须先保证原先的数据库里面不存在一样的类名和别名
关键操作3: 新增文章类别SQL语句的定义和执行
const addSql = "insert into ev_article_cate set ?"
db.query(addSql,req.body,(err,results)=>{
})
这里再介绍以下SQL语句插入数据的几种语法:
//1.插入完整行数据
insert into table_name (column1,column2,column3,...) values (value1,value2,value3,...)
//2.省略列名插入数据 当你要插入的数据顺序和表中列的顺序一致,并且要为所有列提供值时,可以省略列名
insert into table_name (value1,value2,value3,...)
//3.插入多行数据
insert into table_name(column1,column2,column3) values(value1_row1),(value1_row2)
//4.从其他表插入数据 还可以从一个表中选取数据并插入到另一个表中,语法如下:
insert into table_name(column1,column2,column3,...) select column1,column2,column3,... from
another_table where condition
(3)删除文章分类API
该API的简要介绍如下:
关键操作1: 定义删除文章规则对象(要对传递的路径参数id进行校验)
const id = joi.number().integer().min(1).required()
exports.deleteCateSchema = {
params:{
id //需要对路径参数的id进行规范
}
}
关键操作2: 标记删除法(为了保证数据的安全性和完整性)
(4)更新文章分类API
关键操作1: 更新文章分类就是更新文章分类名称(对应name字段) 文章分类别名(对应alias字段)
更新文章分类规制对象如下:
关键操作2: 在更新文章之前 要判断所更新的内容否重复(即name和alias的值是否已经被占用)
代码逻辑如下(不仅要弄清是否被占用 还有弄清楚是名称和别名被占用还是都被占用了):
关键操作3: 是在请求体body中传入id(要更新的文章类别和别名对应的id) name,alias 进行更新