1、初始化

1.1创建项目

1.新建 api_server 文件夹作为项目根目录,并在项目根目录运行如下命令初始化管理配置文件

2.运行如下命令安装特定版本的 express

npm i express@4.17.1

 3.在项目根目录新建 app.js 作为整个项目的入口文件,并初始化如下代码

1.2 配置 cors 跨域

1.运行如下命令安装 cors 中间件

npm i cors@2.8.5
2.在 app.js 中导入并配置 cors 中间件
// 导入 cors 中间件
const cors = require('cors')
// 将 cors 注册全局中间件
app.use(cors())

​1.3 配置解析表单数据中间件

通过如下代码配置解析 application/xwwwformurlencoded 格式表单数据的中间件

app.use(express.urlencoded({ extended: false }))

1.4 初始化路由相关文件夹

1.在项目根目录中,新建 router 文件夹用来存放所有的路由模块

    路由模块中,只存放客户端请求处理函数之间的映射关系

2.在项目根目录中,新建router_handler 文件夹,用来存放所有的 路由处理函数模块

     路由处理函数模块中,专门负责存放每个路由对应处理函数

3.在项目根目录中,新建 schema 文件夹,用来存放表单验证规则模块

    表单验证规则模块中,只存放各个路由的表单数据校验代码

1.5 初始化 config.js 配置文件

在项目根目录创建 config.js ,用来存放全局变量

module.exports = {
  jwtSecretKey: 'itheima No1. ^_^', // 加密字符串
  expiration_time: '10h' // token 有效期为 10 个小时
}

1.6 初始化 db 数据库配置文件

在项目根目录创建 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.7 在根目录创建 public 文件夹,用来存放静态数据

最终的项目结构

2、接口api实现

 用户注册登录接口使用 jwt 做前后端身份验证

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
    }
  }
}

原文地址:https://blog.csdn.net/weixin_43263566/article/details/126991448

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_22028.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注