Api接口开发(Express + MySQL)
基于 Express + MySQL 实现 Api 接口的开发
# 1. 初始化项目
蓝奏云下载地址:初始化项目完整代码 (opens new window)
在初始化项目中,需要安装的 依赖包 如下:
npm install express@4.18.1
npm install cors@2.8.5
npm install mysql@2.18.1
# 1-1. 创建项目
(1). 新建 api_server 文件夹作为项目根目录,并在项目根目录中运行如下的命令,初始化包管理配置文件:
npm init -y
(2). 在 package.json 文件中,添加 "type": "module" 启用 ES6 模块化:
{
"type": "module",
"name": "api_server",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
2
3
4
5
6
7
(2). 运行如下的命令,安装特定版本的 express:
npm install express@4.18.1
(3). 在项目根目录中新建 app.js 作为整个项目的入口文件,并初始化如下的代码:
// 导入 express 模块
import express from "express"
// 创建 express 的服务器实例
const app = express()
// write your code here...
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(3007, function () {
console.log('api server running at http://127.0.0.1:3007')
})
2
3
4
5
6
7
8
9
10
11
# 1-2. 配置 cors 跨域
(1). 运行如下的命令,安装 cors 中间件:
npm install cors@2.8.5
(2). 在 app.js 中导入并配置 cors 中间件:
// 导入 cors 中间件
import cors from "cors"
// 将 cors 注册为全局中间件
app.use(cors())
2
3
4
# 1-3. 配置解析表单数据的中间件
(1). 通过如下的代码,配置解析 application/x-www-form-urlencoded 格式的表单数据的中间件:
app.use(express.urlencoded({ extended: false }))
# 1-4. 初始化路由相关的文件夹
(1). 在项目根目录中,新建 router 文件夹,用来存放所有的路由模块
注意:路由模块中,只存放客户端的请求与处理函数之间的映射关系
(2). 在项目根目录中,新建 controller 文件夹,用来存放所有的 路由处理函数模块
注意:路由处理函数模块中,专门负责存放每个路由对应的处理函数
# 1-5. 初始化用户路由模块
(1). 在 router 文件夹中,新建 user.js 文件,作为用户的路由模块,并初始化代码如下:
import express from "express"
// 创建路由对象
const router = express.Router()
// 注册新用户
router.post('/reguser', (req, res) => {
res.send('reguser OK')
})
// 登录
router.post('/login', (req, res) => {
res.send('login OK')
})
// 将路由对象共享出去
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(2). 在 app.js 中,导入并使用 用户路由模块 :
// 导入并注册用户路由模块
import userRouter from "./router/user.js"
app.use('/api', userRouter)
2
3
# 1-6. 抽离用户路由模块中的处理函数
目的:为了保证 路由模块 的纯粹性,所有的 路由处理函数,必须抽离到对应的 路由处理函数模块 中
(1). 在 /controller/user.js 中,使用 exports 对象,分别向外共享如下两个 路由处理函数 :
// 封装用户相关的路由处理函数,供 /router/user.js 模块进行调用
const user = {
// 用户注册的处理函数
reguser(req, res) {
res.send("reguser OK")
},
// 用户登录的处理函数
login(req, res) {
res.send("login OK")
}
}
export default user
2
3
4
5
6
7
8
9
10
11
12
(2). 将 /router/user.js 中的代码修改为如下结构:
import express from "express"
const router = express.Router()
// 导入自封装的模块
import user from "../controller/user.js" // 用户相关的路由处理函数
// 用户注册
router.post("/reguser", user.reguser)
// 用户登录
router.post("/login", user.login)
export default router
2
3
4
5
6
7
8
9
10
11
12
# 1-7. 安装并配置 mysql 模块
在 API 接口项目中,需要安装并配置 mysql 这个第三方模块,来连接和操作 MySQL 数据库
(1). 创建一个名为 my_db_01 的数据库,并新建 ev_users 表,如下:
(2). 项目中执行如下命令,安装 mysql 模块
npm install mysql@2.18.1
(3). 在项目根目录中新建 /db/index.js 文件,在此自定义模块中创建数据库的连接对象:
// 导入 mysql 模块
import mysql from "mysql"
// 创建数据库连接对象
const db = mysql.createPool({
host: "127.0.0.1", // 数据库地址
user: "root", // 数据库用户名(登录数据库的账号)
password: "root", // 数据库密码(登录数据库的密码)
database: "my_db_01", // 数据库名
prot: 3306, // 数据库端口
})
// 向外共享 db 数据库连接对象
export default db
2
3
4
5
6
7
8
9
10
11
12
13
14
(4). 在 /controller/user.js 中导入数据库操作模块,来对数据库进行操作
// 导入数据库操作模块
import db from "../db/index.js"
const user = {
// 用户注册
reguser(req, res) {
// 这里写 sql 语句,对数据库进行操作
// db.query("select * from ev_users", (err, results) => {
// if (err) return console.log(err.message)
// // select 查询语句:返回的是一个数组,里面包含用户的对象信息
// console.log(results);
// })
res.send("reguser OK")
},
// 用户登录
login(req, res) {
res.send("login OK")
}
}
export default user
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2. 用户注册
用户注册:需要安装的 依赖包 如下:
npm install bcryptjs@2.4.3
npm install joi@17.6.0
npm install @escook/express-joi@1.1.1
# 2-1. 检测表单数据是否合法
(1). 判断用户名和密码是否合法
const user = {
reguser(req, res) {
// 检测表单数据是否合法(判断用户名和密码是否合法)
const userinfo = req.body
if (!userinfo.username || !userinfo.password) {
return res.send({ status: 1, message: '用户名或密码不能为空!' })
}
},
}
2
3
4
5
6
7
8
9
# 2-2. 检测用户名是否被占用
(1). 导入数据库操作模块执行 sql 语句,来判断用户名是否被占用
// 导入数据库操作模块
import db from "../db/index.js"
const user = {
reguser(req, res) {
// ...
// 定义 SQL 语句
const sql = `select * from ev_users where username=?`
// 判断用户名是否被占用
db.query(sql, [userinfo.username], function (err, results) {
// 执行 SQL 语句失败
if (err) {
return res.send({ status: 1, message: err.message })
}
// 判断用户名是否被占用
if (results.length > 0) {
return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' })
}
// TODO: 用户名可用,继续后续流程...
})
},
}
export default user
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2-3. 对密码进行加密处理
注意:为了保证密码的安全性,不建议在数据库以 明文 的形式保存用户密码,推荐对密码进行 加密存储
在当前项目中,使用 bcryptjs 对用户密码进行加密,优点:
(1). 加密之后的密码,无法被逆向破解
(2). 同一明文密码多次加密,得到的加密结果各不相同,保证了安全性
(1). 运行如下命令,安装指定版本的 bcryptjs
npm i bcryptjs@2.4.3
(2). 在 /controller/user.js 中,导入 bcryptjs :
import bcrypt from "bcryptjs"
(3). 在注册用户的处理函数中,确认用户名可用之后,对用户的密码进行加密处理:
// 格式:bcrypt.hashSync(明文密码, 随机盐的长度)
// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
userinfo.password = bcrypt.hashSync(userinfo.password, 10)
2
3
# 2-4. 添加新用户(用户注册)
const user = {
reguser(req, res) {
db.query(sql, [userinfo.username], function (err, results) {
// ...
userinfo.password = bcrypt.hashSync(userinfo.password, 10)
// 注册用户(添加新用户)
const adduser = 'insert into ev_users set ?'
db.query(adduser, { username: userinfo.username, password: userinfo.password }, (err, results) => {
if (err) return res.dend({ status: 1, message: err.message })
if (results.affectedRows !== 1) return res.send({ status: 1, message: "注册用户失败,请稍后再试" })
res.send({ status: 0, message: "注册成功" })
})
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2-5. 优化 res.send() 代码
// 这是未优化前的代码
const user = {
// 用户注册
reguser(req, res) {
// 2-1. 检测表单数据是否合法(判断用户名和密码是否合法)
const userinfo = req.body
if (!userinfo.username || !userinfo.password) {
return res.send({ status: 1, message: '用户名或密码不能为空!' })
}
// 2-2. 检测用户名是否被占用
const sql = `select * from ev_users where username=?` // 定义 SQL 语句
db.query(sql, [userinfo.username], function (err, results) {
// 执行 SQL 语句失败
if (err) {
return res.send({ status: 1, message: err.message })
}
// 判断用户名是否被占用
if (results.length > 0) {
return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' })
}
// 2-3. 调用 bcrypt.hashSync() 对,密码进行加密(返回值是加密后的字符串)
userinfo.password = bcrypt.hashSync(userinfo.password, 10)
// 2-4. 注册用户(添加新用户)
const adduser = 'insert into ev_users set ?'
db.query(adduser, { username: userinfo.username, password: userinfo.password }, (err, results) => {
if (err) return res.dend({ status: 1, message: err.message })
if (results.affectedRows !== 1) return res.send({ status: 1, message: "注册用户失败,请稍后再试" })
res.send({ status: 0, message: "注册成功" })
})
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
我们发现,在 reguser 处理函数中,多次调用了 res.send() 向客户端响应处理的结果,为了简化代码,可以手动封装一个 res.cc() 函数
(1). 在 app.js 中注册一个 res.cc() 的全局中间件(所有路由之前),用于简化处理函数中的 res.send() 的代码
// 响应数据的中间件:用于简化"处理函数"中的 res.send() 的代码
app.use((req, res, next) => {
// 为 res 对象挂载一个 cc() 函数
res.cc = function (err, status = 1) { // status 默认为 1 (表示失败); 0 表示成功
res.send({
status, // 状态
message: err instanceof Error ? err.message : err, // 状态描述,判断 err 是 错误对象 还是 字符串
})
}
next()
})
// 用户相关的路由模块
app.use('/api', userRouter)
2
3
4
5
6
7
8
9
10
11
12
13
14
(2). 简化 /controller/user.js 处理函数中的 res.send() 的代码
// 这是优化后的代码
const user = {
// 用户注册
reguser(req, res) {
// 2-1. 检测表单数据是否合法(判断用户名和密码是否合法)
const userinfo = req.body
if (!userinfo.username || !userinfo.password) {
return res.cc("用户名或密码不能为空!")
}
// 2-2. 检测用户名是否被占用
const sql = `select * from ev_users where username=?` // 定义 SQL 语句
db.query(sql, [userinfo.username], function (err, results) {
// 执行 SQL 语句失败
if (err) {
return res.cc(err)
}
// 判断用户名是否被占用
if (results.length > 0) {
return res.cc("用户名被占用,请更换其他用户名!")
}
// 2-3. 调用 bcrypt.hashSync() 对,密码进行加密(返回值是加密后的字符串)
userinfo.password = bcrypt.hashSync(userinfo.password, 10)
// 2-4. 注册用户(添加新用户)
const adduser = 'insert into ev_users set ?'
db.query(adduser, { username: userinfo.username, password: userinfo.password }, (err, results) => {
if (err) return res.cc(err)
if (results.affectedRows !== 1) return res.cc("注册用户失败,请稍后再试")
res.cc(0, "注册成功")
})
})
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 2-6. 优化表单数据验证(表单验证规则)
表单验证的原则:前端验证为辅,后端验证为主,后端永远不要相信前端提交过来的任何内容
在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且,后端做为数据合法性验证的最后一个关口,在拦截非法数据方面,起到了至关重要的作用
(1). 安装 joi 包,为表单中携带的每个数据项,定义验证规则:
npm install joi@17.6.0
(2). 安装 @escook/express-joi 中间件,来实现自动对表单数据进行验证的功能:
npm i @escook/express-joi@1.1.1
(3). 新建 /rule/user.js 用户信息验证规则模块,并初始化代码如下:
import joi from "joi"
/**
* string() 值必须是字符串
* alphanum() 值只能是包含 a-zA-Z0-9 的字符串
* min(length) 最小长度
* max(length) 最大长度
* required() 值是必填项,不能为 undefined
* pattern(正则表达式) 值必须符合正则表达式的规则
*/
// 用户名的验证规则
const username = joi.string().alphanum().min(1).max(10).required()
// 密码的验证规则
const password = joi.string().pattern(/^[\S]{6,12}$/).required()
// 注册和登录表单的验证规则对象
const reg_login_rule = {
body: {
username,
password
}
}
export default reg_login_rule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(4). 修改 /router/user.js 中的代码如下:
import express from "express"
const router = express.Router()
// 导入自封装的模块
import user from "../controller/user.js" // 用户相关的路由处理函数
// 1. 导入验证表单数据的中间件
import expressJoi from "@escook/express-joi"
// 2. 导入需要的验证规则对象
import reg_login_rule from "../rule/user.js"
// 用户注册
// 3.1 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证
// 3.2 数据验证通过后,会把这次请求流转给后面的路由处理函数
// 3.3 数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行处理
router.post("/reguser", expressJoi(reg_login_rule), user.reguser)
// 用户登录
router.post("/login", user.login)
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(5). 在 app.js 的全局错误级别中间件中,捕获验证失败的错误,并把验证失败的结果响应给客户端:
import joi from "joi"
// 错误中间件(定义在路由的后面)
app.use((err, req, res, next) => {
// 数据验证失败
if (err instanceof joi.ValidationError) return res.cc(err)
// 未知错误
res.cc(err)
})
2
3
4
5
6
7
8
9
# 3. 用户登录
用户登录:需要安装的 依赖包 如下:
npm install jsonwebtoken@8.5.1
npm install express-jwt@5.3.3
# 3-1. 检测登录表单的数据是否合法
(1). 将 /router/user.js 中 "登录"的路由代码修改如下:
// 用户登录
router.post("/login", expressJoi(reg_login_rule), user.login)
2
(2). 在 controller/user.js "登录"路由函数中,执行 sql 语句(根据用户名查询用户的数据)
const user = {
// 用户注册...
// 用户登录
login(req, res) {
// 3-1. 判断用户名是否存在(根据用户名查询用户的数据)
const userinfo = req.body
const sql = `select * from ev_users where username=?`
db.query(sql, userinfo.username, function (err, results) {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,但是查询到数据条数不等于 1
if (results.length !== 1) return res.cc('用户不存在')
// TODO:判断用户输入的登录密码是否和数据库中的密码一致
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3-2. 判断用户输入的密码是否正确
核心实现思路:调用 bcrypt.compareSync(用户提交的密码, 数据库中的密码) 方法,比较密码是否一致,返回值是布尔值
const user = {
// 用户注册...
// 用户登录
login(req, res) {
// 3-1. 判断用户名是否存在(根据用户名查询用户的数据)
const userinfo = req.body
const sql = `select * from ev_users where username=?`
db.query(sql, userinfo.username, function (err, results) {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,但是查询到数据条数不等于 1
if (results.length !== 1) return res.cc('用户不存在')
// 3-2. 判断密码是否正确:用户输入的密码 和 数据库中存储的密码,进行比较(返回值是布尔值)
const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)
if (!compareResult) {
return res.cc('密码错误')
}
// TODO:登录成功,生成 Token 字符串
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3-3. 生成 JWT 的 Token 字符串
核心注意点:在生成 Token 字符串的时候,一定要剔除 密码 和 头像 的值
(1). 运行如下的命令,安装生成 Token 字符串的包:
npm i jsonwebtoken@8.5.1
(2). 在 /controller/user.js 模块的头部区域,导入 jsonwebtoken 包:
import jwt from "jsonwebtoken"
(3). 使用 jwt.sing() 方法,对用户信息对象进行加密,生成一个加密后的 token 字符串,并响应给客户端
const user = { ...results[0], password: '', avatar: '' } // 剔除密码和头像的值
const secretKey = "Hello World ^_^" // 密钥
const tokenStr = jwt.sign(user, secretKey, { expiresIn: '24h', }) // token 有效期为24个小时
res.send({
status: 0,
message: '登录成功!',
token: 'Bearer ' + tokenStr, // 将加密后的 Token 字符串响应给客户端
})
2
3
4
5
6
7
8
# 3-4. 配置解析 Token 的中间件
(1). 运行如下的命令,安装解析 Token 的中间件:
npm i express-jwt@5.3.3
(2). 在 app.js 中用户相关的路由模块之前,配置解析 Token 的中间件:
import expressJWT from "express-jwt"
// 解析 Token 的中间件:expressJWT() 用于把加密后的用户信息,解密出来并挂载到 req.user 属性上
// 使用 .unless({ path: [/^\/api\//] }) 指定哪些接口不需要进行 Token 的身份认证
app.use(expressJWT({ secret: "Hello World ^_^" }).unless({ path: [/^\/api\//] }))
2
3
4
5
(3). 在 app.js 中的 错误级别中间件 里面,捕获并处理 Token 认证失败后的错误:
// 错误中间件:捕获登录/注册表单时验证失败的错误,并把验证失败的结果响应给客户端
app.use((err, req, res, next) => {
// 数据验证失败...
// 捕获 jwt 解析失败后产生的错误(Token 字符串不合法/过期)
if (err.name === 'UnauthorizedError') return res.cc('token不合法或已过期')
// 未知错误...
})
2
3
4
5
6
7
8
9