赞
踩
1.新建 api_server
文件夹作为项目根目录,并在项目根目录中运行如下的命令,初始化包管理配置文件:
npm init -y
2.运行如下的命令,安装特定版本的 express
:
npm i express@4.17.1
3.在项目根目录中新建 app.js
作为整个项目的入口文件,并初始化如下的代码:
- // 导入 express 模块
- const express = require('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')
- })
1.运行如下的命令,安装 cors
中间件:
npm i cors@2.8.5
- // 导入 cors 中间件
- const cors = require('cors')
- // 将 cors 注册为全局中间件
- app.use(cors())
通过如下的代码,配置解析 application/x-www-form-urlencoded 格式的表单数据的中间件:
app.use(express.urlencoded({ extended: false }))
1.在项目根目录中,新建 router 文件夹,用来存放所有的路由模块
路由模块中,只存放客户端的请求与处理函数之间的映射关系
2.在项目根目录中,新建router_handler 文件夹,用来存放所有的 路由处理函数模块
路由处理函数模块中,专门负责存放每个路由对应的处理函数
3.在项目根目录中,新建 schema 文件夹,用来存放表单验证规则模块
表单验证规则模块中,只存放各个路由的表单数据校验代码
在项目根目录下创建 config.js ,用来存放全局变量
- module.exports = {
- jwtSecretKey: 'itheima No1. ^_^', // 加密字符串
- expiration_time: '10h' // token 有效期为 10 个小时
- }
在项目根目录下创建 db 文件夹,里面创建index.js 文件
- // 导入 mysql 模块 npm i mysql@2.18.1
- const mysql = require('mysql')
-
- // 创建数据库连接对象
- const db = mysql.createPool({
- host: 'localhost',
- user: 'root', // 本地
- // user: 'my_db_01', // 线上
- password: '207080',
- database: 'my_db_01',
- })
- // 检测 mysql 模块能否正常工作
- // 调用 db.query() 函数,指定要执行的 SQL 语句,通过回调函数拿到执行的结果:
- db.query('select 1', (err, results) => {
- if (err) return console.log(err.message);
- // 只要能打印出 [ RowDataPacket { '1': 1 } ]的结果,就能证明数据库链接正常
- console.log(results);
- })
-
- // 向外共享 db 数据库连接对象
- module.exports = db
1.在 router 文件夹中,新建 user.js 文件,作为用户的路由模块,并初始化代码如下:
- // 用户登录/注册的路由模块
- const express = require('express')
- // 创建路由对象
- const router = express.Router()
-
- // 导入用户路由处理函数模块
- const userHandler = require('../router_handler/user')
-
- // 1. 导入验证表单数据的中间件(自动验证) npm i @escook/express-joi
- const expressJoi = require('@escook/express-joi')
-
- // 2. 导入需要的验证规则对象
- const { reg_login_schema } = require('../schema/user')
-
- // 注册新用户
- // 3. 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证
- // 3.1 数据验证通过后,会把这次请求流转给后面的路由处理函数
- // 3.2 数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行处理
- router.post('/reguser', expressJoi(reg_login_schema), userHandler.regUser)
-
- // 登录
- router.post('/login', expressJoi(reg_login_schema), userHandler.login)
-
- // 将路由对象共享出去
- module.exports = router
2.在 /router_handler/user.js 中,使用 exports 对象,分别向外共享如下两个 路由处理函数 :
- /**
- * 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
- */
- // 导入 mySql 数据库的链接模块
- const db = require('../db/index.js')
-
- // 导入对密码进行加密处理的第三方中间件 npm i bcryptjs@2.4.3
- const bcrypt = require('bcryptjs')
-
- // 用这个包来生成 Token 字符串
- const jwt = require('jsonwebtoken')
-
- // 导入配置文件
- const config = require('../config')
-
-
- // 注册用户的处理函数
- exports.regUser = (req, res) => {
- // 1.判断用户名和密码是否为空
- // 接收表单数据
- const userinfo = req.body
-
- // 2.检测用户名是否被占用
- // 定义 SQL 语句:
- const sql = `select * from ev_users where username=?`
- // 执行 SQL 语句并根据结果判断用户名是否被占用:
- db.query(sql, [userinfo.username], function (err, results) {
- // 执行 SQL 语句失败
- if (err) {
- return res.cc(err.message)
- }
- // 用户名被占用
- if (results.length > 0) {
- return res.cc('用户名被占用,请更换其他用户名!')
- }
- // TODO: 用户名可用,继续后续流程...
- // 调用 bcrypt.hashSync(明文密码, 随机盐的长度) 方法,对用户的密码进行加密处理:
- // 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
- userinfo.password = bcrypt.hashSync(userinfo.password, 10)
-
- // 向 my_db_01 数据库的 ev_users 表插入新用户
- const user = { username: userinfo.username, password: userinfo.password }
- const sql = 'insert into ev_users set ?'
- db.query(sql, user, (err, results) => {
- // 执行 SQL 语句失败
- if (err) {
- return res.cc(err.message)
- }
- // SQL 语句执行成功,但影响的行数不为 1
- if (results.affectedRows !== 1) {
- return res.cc('注册新用户失败,请稍后重试!')
- }
- return res.cc('注册成功!', 0, 201)
- })
- })
-
- }
-
- // 登录的处理函数
- exports.login = (req, res) => {
- // 1.接收表单数据:
- const userinfo = req.body
- // 2.定义 SQL 语句
- const sql = `select * from ev_users where username=?`
- // 3.执行 SQL 语句,查询用户的数据:
- 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:判断用户输入的登录密码是否和数据库中的密码一致
- // 核心实现思路:调用 bcrypt.compareSync(用户提交的密码, 数据库中的密码) 方法比较密码是否一致
- // 返回值是布尔值(true 一致、false 不一致)
- // 拿着用户输入的密码,和数据库中存储的密码进行对比
- const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)
- // const compareResult = userinfo.password == results[0].password
- // 如果对比的结果等于 false, 则证明用户输入的密码错误
- if (!compareResult) {
- return res.cc('登录失败,密码错误!')
- }
-
- // TODO:登录成功,生成 Token 字符串
- // 核心注意点:在生成 Token 字符串的时候,一定要剔除 密码 和 头像 的值
- // 剔除完毕之后,user 中只保留了用户的 id, username, nickname, email 这四个属性的值
- const user = { ...results[0], password: '', user_pic: '' }
- // 运行如下的命令,安装生成 Token 字符串的包:
- // npm i jsonwebtoken@8.5.1
- // 生成 Token 字符串
- // 参数1:用户的信息对象
- // 参数2:加密的秘钥
- // 参数3:配置对象,可以配置当前 token 的有效期
- // 记住:千万不要把密码加密到 token 字符中
- const tokenStr = jwt.sign(user, config.jwtSecretKey, {
- expiresIn: config.expiration_time, // token 有效期
- })
- res.send({
- status: 0,
- msg: '登录成功!',
- // 为了方便客户端使用 Token,在服务器端直接拼接上 Bearer 的前缀
- token: 'Bearer ' + tokenStr,
- })
- })
- }
3.在/schema/user.js 中,创建表单验证规则
- // 用户信息验证模块
-
- // 使用第三方的中间件验证表单数据
- /*
- 安装 @hapi/joi 包,为表单中携带的每个数据项,定义验证规则:
- npm install @hapi/joi@17.1.0 // @hapi/joi 版本已弃用,
- npm install joi
- 安装 @escook/express-joi 中间件,来实现自动对表单数据进行验证的功能:
- npm i @escook/express-joi
- */
- const joi = require('joi')
-
- /**
- * string() 值必须是字符串 默认情况不支持空字符串 Joi.string().allow('') 支持空字符串
- * alphanum() 值只能是包含 a-zA-Z0-9 的字符串
- * min(length) 最小长度
- * max(length) 最大长度
- * required() 值是必填项,不能为 undefined
- * pattern(正则表达式) 值必须符合正则表达式的规则
- * number()值必须是一个数字
- * email()值必须是邮箱格式
- */
-
- // 用户名的验证规则
- const username = joi.string().alphanum().min(1).max(10).required()
-
- // 密码的验证规则 6~12位 [\S]表示,非空白就匹配 [\s] 只要出现空白就匹配
- // /^[\S]{6,12}$/ = 以非空白的元素开头和结尾数量6-12
- const password = joi
- .string()
- .pattern(/^[\S]{6,12}$/)
- .required()
-
- // 注册和登录表单的验证规则对象
- exports.reg_login_schema = {
- // 表示需要对 req.body 中的数据进行验证
- body: {
- username,
- password,
- },
- }
-
- // 定义 id, nickname, emial 的验证规则
- const id = joi.number().integer().min(1).required()
- const nickname = joi.string().required()
- const email = joi.string().email().required()
-
-
- // 验证规则对象 - 更新用户基本信息
- exports.update_userinfo_schema = {
- body: {
- id,
- nickname,
- email,
- },
- }
-
- // 验证规则对象 - 重置密码
- exports.update_password_schema = {
- body: {
- // 使用 password 这个规则,验证 req.body.oldPwd 的值
- oldPwd: password,
- // 使用 joi.not(joi.ref('oldPwd')).concat(password) 规则,验证 req.body.newPwd 的值
- // 解读:
- // 1. joi.ref('oldPwd') 表示 newPwd 的值必须和 oldPwd 的值保持一致
- // 2. joi.not(joi.ref('oldPwd')) 表示 newPwd 的值不能等于 oldPwd 的值
- // 3. .concat() 用于合并 joi.not(joi.ref('oldPwd')) 和 password 这两条验证规则
- newPwd: joi.not(joi.ref('oldPwd')).concat(password),
- },
- }
-
- // 头像的验证规则就不需要了,因为在router/userinfo内使用 multer 来解析数据自带验证
- // 头像图片的验证规则
- // dataUri() 指的是如下格式的字符串数据:
- // data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
- // const avatar = joi.string().dataUri().required()
-
- // 验证规则对象 - 更新头像
- // exports.update_avatar_schema = {
- // body: {
- // avatar,
- // },
- // }
-
-
1.在 router 文件夹中,新建 userinfo.js 文件,作为用户的路由模块,并初始化代码如下:
- // 用户信息的路由模块
- // 导入 express
- const express = require('express')
- const router = express.Router()
-
- // 导入用户信息的处理函数模块
- const userinfo_handler = require('../router_handler/userinfo')
-
- // 导入验证数据合法性的中间件
- const expressJoi = require('@escook/express-joi')
-
- // 导入解析 formdata 格式表单数据的包
- const multer = require("multer")
-
- // 导入处理路径的核心模块
- const path = require('path')
-
- // 导入需要的验证规则对象
- const { update_userinfo_schema, update_password_schema } = require('../schema/user')
-
- // 获取用户的基本信息
- router.get('/userinfo', userinfo_handler.getUserInfo)
-
- // 更新用户的基本信息
- router.put('/userinfo', expressJoi(update_userinfo_schema), userinfo_handler.updateUserInfo)
-
- // 重置密码的路由
- router.patch('/updatepwd', expressJoi(update_password_schema), userinfo_handler.updatePassword)
-
- // 更新用户头像的路由
- // 注意:使用 express.urlencoded() 中间件无法解析 multipart/form-data 格式的请求体数据。
- // npm i multer@1.4.2
-
- // 创建 multer 的实例对象,通过 dest 属性指定文件的存放路径
- const upload = multer({ dest: path.join(__dirname, '../public/image/head_portrait') })
- // upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据
- // 将文件类型的数据(avatar),解析并挂载到 req.files 属性中 files是数组
- // 将其他文本类型的数据,解析并挂载到 req.body 属性中
- // array 有两个参数 表单name属性值(前端的文件对象名要和这里的相同) 最多上传文件的个数
- router.post('/update/avatar', upload.array('avatar', 1), userinfo_handler.updateAvatar)
- // 或
- // 将文件类型的数据,解析并挂载到 req.file 属性中 file是对象
- // 将其他文本类型的数据,解析并挂载到 req.body 属性中
- // router.post('/update/avatar', upload.single('avatar'), userinfo_handler.updateAvatar)
-
- module.exports = router
-
2.在 /router_handler/userinfo.js 中,使用 exports 对象,向外共享路由处理函数 :
- /**
- * 在这里定义和用户信息相关的路由处理函数,供 /router/userinfo.js 模块进行调用
- */
-
- // 导入数据库操作模块
- const db = require('../db/index')
-
- // 导入文件操作模块
- const fs = require('fs')
- // 导入处理路径的 path 核心模块
- const path = require('path')
-
- // 导入对密码进行加密处理的第三方中间件 npm i bcryptjs@2.4.3
- const bcrypt = require('bcryptjs')
-
- // 获取用户基本信息的处理函数
- exports.getUserInfo = (req, res) => {
- // 根据用户的 id,查询用户的基本信息
- // 注意:为了防止用户的密码泄露,需要排除 password 字段
- const sql = `select id, username, nickname, email, user_pic,role_id,role_name,is_enabled from ev_users where id=?`
- // 调用 db.query() 执行 SQL 语句:
- // 注意:req 对象上的 user 属性,是 Token 解析成功,express-jwt 中间件帮我们挂载上去的
- db.query(sql, req.user.id, (err, results) => {
- // 1. 执行 SQL 语句失败
- if (err) return res.cc(err)
-
- // 2. 执行 SQL 语句成功,但是查询到的数据条数不等于 1
- if (results.length !== 1) return res.cc('获取用户信息失败!')
-
- // 3. 将用户信息响应给客户端
- res.send({
- status: 0,
- msg: '获取用户基本信息成功!',
- data: results[0],
- })
- })
- }
-
- // 更新用户基本信息的处理函数
- exports.updateUserInfo = (req, res) => {
- const sql = `update ev_users set ? where id=?`
- db.query(sql, [req.body, req.body.id], (err, results) => {
- // 执行 SQL 语句失败
- if (err) return res.cc(err)
-
- // 执行 SQL 语句成功,但影响行数不为 1
- if (results.affectedRows !== 1) return res.cc('修改用户基本信息失败!')
-
- // 修改用户信息成功
- return res.cc('修改用户基本信息成功!', 0)
- })
- }
-
- // 重置密码的处理函数
- exports.updatePassword = (req, res) => {
- // 定义根据 id 查询用户数据的 SQL 语句
- const sql = `select * from ev_users where id=?`
-
- // 执行 SQL 语句查询用户是否存在
- db.query(sql, req.user.id, (err, results) => {
- // 执行 SQL 语句失败
- if (err) return res.cc(err)
-
- // 检查指定 id 的用户是否存在
- if (results.length !== 1) return res.cc('用户不存在!')
-
- // TODO:判断提交的旧密码是否正确
- // 在头部区域导入 bcryptjs 后,(因为数据库中的密码是加密过的)
- // 即可使用 bcrypt.compareSync(提交的密码,数据库中的密码) 方法验证密码是否正确
- // compareSync() 函数的返回值为布尔值,true 表示密码正确,false 表示密码错误
-
- // 判断提交的旧密码是否正确
- const compareResult = bcrypt.compareSync(req.body.oldPwd, results[0].password)
- if (!compareResult) return res.cc('原密码错误!')
-
- // 对新密码进行 bcrypt 加密之后,更新到数据库中:
- // 定义更新用户密码的 SQL 语句
- const sql = `update ev_users set password=? where id=?`
-
- // 对新密码进行 bcrypt 加密处理
- const newPwd = bcrypt.hashSync(req.body.newPwd, 10)
- console.log(req.body.newPwd);
- // 执行 SQL 语句,根据 id 更新用户的密码
- db.query(sql, [newPwd, req.user.id], (err, results) => {
- // SQL 语句执行失败
- if (err) return res.cc(err)
-
- // SQL 语句执行成功,但是影响行数不等于 1
- if (results.affectedRows !== 1) return res.cc('更新密码失败!')
-
- // 更新密码成功
- res.cc('更新密码成功!', 0)
- })
- })
- }
-
- // 更新用户头像的处理函数
- exports.updateAvatar = (req, res) => {
- // req.files/req.file 是上传成功文件的信息对象 (.array req.files .single('avatar') req.file)
- // req.body 上传文件时所携带的其他文本表单数据
- let file = req.files[0]
- // 手动判断是否上传了头像
- if (!file || file.fieldname !== 'avatar') return res.cc('头像是必选参数!')
- // Date.now() 返回自 1970 年 1 月 1 日 00:00:00 (UTC) 到当前时间的毫秒数。类型为 number,
- // toString() 方法返回 string 类型。
- // file.originalname 文件的名称包含了格式 如: '人è\x84¸.PNG'
- // 这里写相对路径
-
- // 定义新的文件名
- // let Url = 'public/image/head_portrait/' + Date.now().toString() + '_' + req.file.originalname
- let Url = path.join('public/image/head_portrait/', file.originalname)// 覆盖重复的文件
-
- // fs.rename():重命名文件或文件夹 同步
- // fs.renameSync(oldPath,newPath) 异步 oldPath 原路径 newPath 新路径
- fs.renameSync(path.join('./public/image/head_portrait/', file.filename), Url) // 设置文件名
- // 图片访问路径
- const imgUrl = `/${Url}`
-
- const sql = 'update ev_users set user_pic=? where id=?'
- // 调用 db.query() 执行 SQL 语句,更新对应用户的头像:
- // req.body.avatar 是一个base(贝s)64字符串,可以直接用img 标签展示
- db.query(sql, [imgUrl, req.user.id], (err, results) => {
- // 执行 SQL 语句失败
- if (err) return res.cc(err)
-
- // 执行 SQL 语句成功,但是影响行数不等于 1
- if (results.affectedRows !== 1) return res.cc('更新头像失败!')
-
- // 更新用户头像成功
- // return res.cc('更新头像成功!', 0)
- res.send({
- status: 0,
- msg: '更新头像成功',
- imgUrl: imgUrl
- })
- })
-
- }
3.在/schema/user.js 中,写入表单验证规则
表单验证已经写在上面注册与登录的文件中,他们共用 schema/user.js 文件
1.在 router 文件夹中,新建 article.js 文件,作为用户的路由模块,并初始化代码如下:
-
- // 导入 express
- const express = require('express')
- // 创建路由对象
- const router = express.Router()
-
- // 导入验证数据的中间件(自动验证)
- const expressJoi = require('@escook/express-joi')
-
- // 导入获取文章列表的验证模块
- const { getArticleList_schema, } = require('../schema/article')
-
- // 导入文章的路由处理函数模块
- const article_handler = require('../router_handler/article')
-
- // 获取-文章列表
- router.post('/list', expressJoi(getArticleList_schema), article_handler.getArticleList)
-
- module.exports = router
2.在 /router_handler/article.js 中,使用 exports 对象,向外共享路由处理函数 :
-
- // 导入数据库操作模块
- const db = require('../db/index')
-
- /**
- * 带分页和模糊查询-获取文章列表
- * */
- exports.getArticleList = (req, res) => {
- const body = req.body;
- let sql = 'select * from ev_articles where is_delete<>1 ' // 查询列表所有的数据
- let sql_b = 'select count(*) as total from ev_articles where is_delete<>1' // 查询结果总数据条数
- let content = [] // 实参
- let isMore = false //是否有多个查询参数
- let total = 0 // 总数
-
- db.query(sql_b, (err, results) => {
- if (err) return res.cc(err)
- total = results[0].total
- })
- if (body.model.cate_id) {
- /** like 用法:
- * 使用like一般都需要指定某列数据应用like查询,它的语法如下:
- * select * from 表名 where 列名 like 要查询的数据
- * select * from 表名 where 列名 like '%abc%' 查询列值包含abc的
- * select * from 表名 where 列名 like 'abc%' 查询列值以abc开头的
- * select * from 表名 where 列名 like '%abc' 查询列值以abc结尾的
- * select * from 表名 where 列名 like 'ab%c' 查询列值以ab开头,c结尾的
- * % 占位符表示0个或多个字符,可放在查询条件的任意位置
- */
- sql += 'and cate_id like ? ';
- content.push('%' + body.model.cate_id + '%') // 模糊查询 cate_id
- isMore = true;
- }
- if (body.model.state) {
- if (isMore) {//true代表有多个参数
- sql += 'and state like ? ' // and 是与两个条件都必须满足,or是或的关系
- } else {
- sql += 'and state like ? '
- }
- content.push('%' + body.model.state + '%') // 模糊查询 state
- }
- if (body.current) { //开启分页
- let current = body.current;//当前页码
- let pageSize = body.size;//一页展示多少条数据
- sql += 'limit ?,?' // ? 开始查询的索引 , ? 要查多少条数据
- // parseInt 将字符串转为整数
- content.push((current - 1) * pageSize, parseInt(pageSize))
- }
- db.query(sql, content, (err, results) => {
- // 执行 SQL 语句失败
- if (err) return res.cc(err)
- // SQL 语句执行成功,但是没有查询到任何数据
- if (results.length === 0) return res.cc('文章列表数据为空,请更筛选条件重试~', 0)
- res.json({
- status: 0,
- msg: '获取文章列表数据成功!',
- total: total,
- data: results
- })
- })
- }
3.在/schema/article.js 中,写入表单验证规则
- // 导入定义验证规则的模块
- const joi = require('joi')
-
- /**
- * string() 值必须是字符串 默认情况不支持空字符串 Joi.string().allow('') 支持空字符串
- * alphanum() 值只能是包含 a-zA-Z0-9 的字符串
- * min(length) 最小长度
- * max(length) 最大长度
- * required() 值是必填项,不能为 undefined
- * pattern(正则表达式) 值必须符合正则表达式的规则
- * number()值必须是一个数字
- * email()值必须是邮箱格式
- * integer() 整数
- * valid() 有效值
- */
-
- const current = joi.number().integer().required()
- const size = joi.number().integer().required()
- const cate_idR = joi.required()
- const stateR = joi.string().valid('已发布', '草稿', '').required()
-
- //校验规则对象 - 获取 - 文章列表
- exports.getArticleList_schema = {
- body: {
- current,
- size,
- model: {
- cate_id: cate_idR,
- state: stateR
- }
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。