前端技术(五)—— 使用Node.js编写简单的项目

一、 初始化项目

1. 创建项目

⑴ 新建 api_kjzt_server 文件夹作为项目根目录,并初始化包管理配置文件


并在项目根目录中运行如下的命令,初始化包管理配置文件:

bash 复制代码
npm init -y

⑵ 运行如下的命令,安装特定版本的 express

bash 复制代码
npm i express@4.17.1

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

bash 复制代码
// 导入 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')
})

2. 配置 cors 跨域

⑴ 运行如下的命令,安装 cors 中间件:

bash 复制代码
npm i cors@2.8.5

⑵ 在 app.js 中导入并配置 cors 中间件:

bash 复制代码
// 导入 cors 中间件
const cors = require('cors')
// 将 cors 注册为全局中间件
app.use(cors())

3. 配置解析表单数据的中间件

通过如下的代码,配置解析 application/x-www-form-urlencoded 格式的表单数据的中间

件:

bash 复制代码
//配置解析 application/x-www-form-urlencoded 格式的表单数据的中间件
app.use(express.urlencoded({ extended: false }))

4. 初始化路由相关的文件夹

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

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

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

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

5. 初始化用户路由模块

⑴ 在 router 文件夹中,新建 user.js 文件,作为用户的路由模块,并初始化代码如下:

bash 复制代码
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 中,导入并使用 用户路由模块 :

bash 复制代码
// 导入并注册用户路由模块
const userRouter = require('./router/user')
app.use('/api', userRouter)

⑶ 启动服务测试一下项目的功能

请求:http://127.0.0.1:3007/api/reguser 测试注册的功能

请求http://127.0.0.1:3007/api/login测试登录功能

6. 抽离用户路由模块中的处理函数

目的:为了保证路由模块的纯粹性,所有的 路由处理函数 ,必须抽离到对应的 路由处理函数 模块 中

⑴ 在 /router_handler/user.js 中,使用exports对象,分别向外共享如下两个 路由处理函数 :

bash 复制代码
/**
* 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
*/
// 注册用户的处理函数
exports.regUser = (req, res) => {
    res.send('reguser OK')
}
// 登录的处理函数
exports.login = (req, res) => {
    res.send('login OK')
}

⑵ 将 /router/user.js 中的代码修改为如下结构:

bash 复制代码
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

⑶ 启动服务测试一下项目的功能


请求:http://127.0.0.1:3007/api/reguser 测试注册的功能

请求http://127.0.0.1:3007/api/login测试登录功能

二、注册

1. 新建my_db_01数据库 和 ev_users表


在 my_db_01 数据库中,新建 ev_users 表如下:

脚本如下

bash 复制代码
/*
 Navicat Premium Data Transfer

 Source Server         : localhost_3306
 Source Server Type    : MySQL
 Source Server Version : 80026
 Source Host           : localhost:3306
 Source Schema         : my_db_01

 Target Server Type    : MySQL
 Target Server Version : 80026
 File Encoding         : 65001

 Date: 24/08/2024 22:22:04
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for ev_users
-- ----------------------------
DROP TABLE IF EXISTS `ev_users`;
CREATE TABLE `ev_users`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `user_pic` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

2. 安装并配置 mysql 模块

在 API 接口项目中,需要安装并配置 mysql 这个第三方模块,来连接和操作 MySQL 数据库

⑴ 运行如下命令,安装 mysql 模块:

bash 复制代码
npm i mysql@2.18.1

⑵ 在项目根目录中新建 /db/index.js 文件,在此自定义模块中创建数据库的连接对象:

bash 复制代码
// 导入 mysql 模块
const mysql = require('mysql')
// 创建数据库连接对象
const db = mysql.createPool({
    host: '127.0.0.1',
    user: 'root',
    password: '123456',
    database: 'my_db_01',
})
// 向外共享 db 数据库连接对象
module.exports = db

3. 注册

⑴ 实现步骤

  1. 检测表单数据是否合法
  2. 检测用户名是否被占用
  3. 对密码进行加密处理
  4. 插入新用户

⑵ 检测表单数据是否合法

1) 在router_handle 中的user.js中编写判断用户名和密码是否为空的代码
bash 复制代码
// 接收表单数据
const userinfo = req.body
// 判断数据是否合法
if (!userinfo.username || !userinfo.password) {
    return res.send({ status: 1, message: '用户名或密码不能为空!' })
}
2) 启动服务测试一下项目的功能


请求:http://127.0.0.1:3007/api/reguser 测试注册的功能

⑶ 检测用户名是否被占用

1) 导入数据库操作模块:
bash 复制代码
//导入数据库操作模块:
const db = require('../db/index')
2) 定义 SQL 语句:
bash 复制代码
//定义 SQL 语句
const sql = `select * from ev_users where username=?`
3) 执行 SQL 语句并根据结果判断用户名是否被占用:
bash 复制代码
//执行 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: '用户名被占用,请更换其他用户名!' })
    }
    // TODO: 用户名可用,继续后续流程...
})
4) 启动服务测试一下项目的功能


请求:http://127.0.0.1:3007/api/reguser 测试注册的功能


⑷ 对密码进行加密处理

为了保证密码的安全性,不建议在数据库以 明文 的形式保存用户密码,推荐对密码进行 加密 存储

在当前项目中,使用 bcryptjs 对用户密码进行加密,优点:

● 加密之后的密码,无法被逆向破解

● 同一明文密码多次加密,得到的加密结果各不相同,保证了安全性

1) 运行如下命令,安装指定版本的 bcryptjs :
bash 复制代码
npm i bcryptjs@2.4.3
2) 在 /router_handler/user.js 中,导入 bcryptjs :
bash 复制代码
//导入bcryptjs模块 
const bcrypt = require('bcryptjs')
3) 在注册用户的处理函数中,确认用户名可用之后,调用 bcrypt.hashSync(明文密码, 随机盐的长度) 方法,对用户的密码进行加密处理:
bash 复制代码
// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
userinfo.password = bcrypt.hashSync(userinfo.password, 10)

完整代码如下:

bash 复制代码
//导入数据库操作模块:
const db = require('../db/index')
//导入bcryptjs模块 
const bcrypt = require('bcryptjs')
/**
* 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
*/
// 注册用户的处理函数
exports.regUser = (req, res) => {
    // 接收表单数据
    const userinfo = req.body
    // 判断数据是否合法
    if (!userinfo.username || !userinfo.password) {
        return res.send({ status: 1, message: '用户名或密码不能为空!' })
    }

    //定义 SQL 语句
    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: '用户名被占用,请更换其他用户名!' })
        }
        // 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
        console.log('userinfo.password:'+userinfo.password)
        userinfo.password = bcrypt.hashSync(userinfo.password,10)
        console.log('userinfo.password:'+userinfo.password)
        // TODO: 用户名可用,继续后续流程...
    })
}
// 登录的处理函数
exports.login = (req, res) => {
    res.send('login OK')
}
4) 启动服务测试一下项目的功能

*请求:http://127.0.0.1:3007/api/reguser 测试注册的功能

⑸ 插入新用户

1) 定义插入用户的 SQL 语句:
bash 复制代码
const sql = 'insert into ev_users set ?'
2) 调用 db.query() 执行 SQL 语句,插入新用户:
bash 复制代码
//调用 db.query() 执行 SQL 语句,插入新用户
db.query(sql, { username: userinfo.username, password: userinfo.password }, function (err, results) {
     // 执行 SQL 语句失败
     if (err) return res.send({ status: 1, message: err.message })
     // SQL 语句执行成功,但影响行数不为 1
     if (results.affectedRows !== 1) {
         return res.send({ status: 1, message: '注册用户失败,请稍后再试!' })
     }
     // 注册成功
     res.send({ status: 0, message: '注册成功!' })
 })


完整代码如下:

bash 复制代码
//导入数据库操作模块:
const db = require('../db/index')
//导入bcryptjs模块 
const bcrypt = require('bcryptjs')
/**
* 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
*/
// 注册用户的处理函数
exports.regUser = (req, res) => {
    // 接收表单数据
    const userinfo = req.body
    // 判断数据是否合法
    if (!userinfo.username || !userinfo.password) {
        return res.send({ status: 1, message: '用户名或密码不能为空!' })
    }

    //定义 SQL 语句
    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: '用户名被占用,请更换其他用户名!' })
        }
        // 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
        console.log('userinfo.password:' + userinfo.password)
        userinfo.password = bcrypt.hashSync(userinfo.password, 10)
        console.log('userinfo.password:' + userinfo.password)
        //如果用户名没有被占用,则将注册完的用户插入数据库中
        //定义插入用户的SQL
        const sql = 'insert into ev_users set ?'
        //调用 db.query() 执行 SQL 语句,插入新用户
        db.query(sql, { username: userinfo.username, password: userinfo.password }, function (err, results) {
            // 执行 SQL 语句失败
            if (err) return res.send({ status: 1, message: err.message })
            // SQL 语句执行成功,但影响行数不为 1
            if (results.affectedRows !== 1) {
                return res.send({ status: 1, message: '注册用户失败,请稍后再试!' })
            }
            // 注册成功
            res.send({ status: 0, message: '注册成功!' })
        })
    })
}
// 登录的处理函数
exports.login = (req, res) => {
    res.send('login OK')
}
3) 启动服务测试一下项目的功能

请求:http://127.0.0.1:3007/api/reguser 测试注册的功能



⑹ 优化 res.send() 代码

在处理函数中,需要多次调用res.send()向客户端响应 处理失败 的结果,为了简化代码,可以手动封装一个 res.cc() 函数

在 app.js 中,所有路由之前,声明一个全局中间件,为 res 对象挂载一个 res.cc() 函数 :

bash 复制代码
// 响应数据的中间件,一定要在路由封装之前,封装res.cc函数
app.use((req,res,next)=>{
    //status 默认值为1,表示失败的情况, status = 0 为成功
    //err的值,可能是一个错误的对象,也可能是一个错误的描述字符串
    res.cc = function(err,status = 1){
        res.send({
            // 状态
            status,
            // 状态描述,判断 err 是 错误对象 还是 字符串
            message:err instanceof Error ? err.message : err
        })
    }
    next()
})

完整代码如下:

bash 复制代码
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// 导入 cors 中间件
const cors = require('cors')
// 将 cors 注册为全局中间件
app.use(cors())
//配置解析 application/x-www-form-urlencoded 格式的表单数据的中间件
app.use(express.urlencoded({ extended: false }))
// 响应数据的中间件,一定要在路由封装之前,封装res.cc函数
app.use((req,res,next)=>{
    //status 默认值为1,表示失败的情况, status = 0 为成功
    //err的值,可能是一个错误的对象,也可能是一个错误的描述字符串
    res.cc = function(err,status = 1){
        res.send({
            // 状态
            status,
            // 状态描述,判断 err 是 错误对象 还是 字符串
            message:err instanceof Error ? err.message : err
        })
    }
    next()
})
// 导入并注册用户路由模块
const userRouter = require('./router/user')
app.use('/api', userRouter)
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(3007, function () {
    console.log('api server running at http://127.0.0.1:3007')
})

将 /router_handler/user.js 中响应的send()方法都用中间件中的res.cc()替换

bash 复制代码
//导入数据库操作模块:
const db = require('../db/index')
//导入bcryptjs模块 
const bcrypt = require('bcryptjs')
/**
* 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
*/
// 注册用户的处理函数
exports.regUser = (req, res) => {
    // 接收表单数据
    const userinfo = req.body
    // 判断数据是否合法
    if (!userinfo.username || !userinfo.password) {
        // return res.send({ status: 1, message: '用户名或密码不能为空!' })
        return res.cc('用户名或密码不能为空!');
    }

    //定义 SQL 语句
    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 })
            return res.cc(err);
        }
        // 用户名被占用
        if (results.length > 0) {
            // return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' })
            return res.cc('用户名被占用,请更换其他用户名!');
        }
        // 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
        console.log('userinfo.password:' + userinfo.password)
        userinfo.password = bcrypt.hashSync(userinfo.password, 10)
        console.log('userinfo.password:' + userinfo.password)
        //如果用户名没有被占用,则将注册完的用户插入数据库中
        //定义插入用户的SQL
        const sql = 'insert into ev_users set ?'
        //调用 db.query() 执行 SQL 语句,插入新用户
        db.query(sql, { username: userinfo.username, password: userinfo.password }, function (err, results) {
            // 执行 SQL 语句失败
            // if (err) return res.send({ status: 1, message: err.message })
            if (err) return res.cc(err)
            // SQL 语句执行成功,但影响行数不为 1
            if (results.affectedRows !== 1) {
                // return res.send({ status: 1, message: '注册用户失败,请稍后再试!' })
                return res.cc('注册用户失败,请稍后再试!');
            }
            // 注册成功
            // res.send({ status: 0, message: '注册成功!' })
            res.cc(0, '注册成功!');
        })
    })
}
// 登录的处理函数
exports.login = (req, res) => {
    res.send('login OK')
}

启动服务测试一下项目的功能

请求:http://127.0.0.1:3007/api/reguser 测试注册的功能


⑺ 优化表单数据验证

表单验证的原则:前端验证为辅,后端验证为主,后端永远不要相信前端提交过来的任何内容

1) 安装 @escook/express-joi 中间件,来实现自动对表单数据进行验证的功能:
bash 复制代码
npm i @escook/express-joi
2) 安装 @hapi/joi 包,为表单中携带的每个数据项,定义验证规则:
bash 复制代码
npm install @hapi/joi@17.1.0
3) 新建 /schema/user.js 用户信息验证规则模块,并初始化代码如下:
bash 复制代码
const joi = require('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()
// 注册和登录表单的验证规则对象
exports.reg_login_schema = {
    // 表示需要对 req.body 中的数据进行验证
    body: {
        username,
        password,
    },
}
4) 修改 /router/user.js 中的代码如下:
bash 复制代码
const express = require('express')
// 创建路由对象
const router = express.Router()
// 导入用户路由处理函数模块
const userHandler = require('../router_handler/user')

// 1. 导入验证表单数据的中间件
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',userHandler.login)
// 将路由对象共享出去
module.exports = router
5) 在app.js的全局错误级别中间件中,捕获验证失败的错误,并把验证失败的结果响应给客户端:
bash 复制代码
const joi = require('@hapi/joi')
// 错误中间件
app.use(function (err, req, res, next) {
    // 数据验证失败
    if (err instanceof joi.ValidationError) return res.cc(err)
    // 未知错误
    res.cc(err)
})
6) 启动服务测试一下项目的功能

请求:http://127.0.0.1:3007/api/reguser 测试注册的功能

点击下载完整代码

三、 登录

1. 实现步骤

  1. 检测表单数据是否合法
  2. 根据用户名查询用户的数据
  3. 判断用户输入的密码是否正确
  4. 生成 JWT 的 Token 字符串

2. 检测登录表单的数据是否合法

将 /router/user.js 中 登录 的路由代码修改如下:

bash 复制代码
// 登录的路由
router.post('/login', expressJoi(reg_login_schema), userHandler.login)

3. 根据用户名查询用户的数据

⑴ 接收表单数据:

bash 复制代码
const userinfo = req.body

⑵ 定义 SQL 语句:

bash 复制代码
const sql = `select * from ev_users where username=?`

⑶ 执行 SQL 语句,查询用户的数据:

bash 复制代码
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:判断用户输入的登录密码是否和数据库中的密码一致
})

⑷ 启动服务测试一下项目的功能

请求:http://127.0.0.1:3007/api/login 测试注册的功能


4. 判断用户输入的密码是否正确

核心实现思路:调用 bcrypt.compareSync(用户提交的密码, 数据库中的密码) 方法比较密码是否一致
返回值是布尔值(true 一致、false 不一致)

具体的实现代码如下:

bash 复制代码
// 拿着用户输入的密码,和数据库中存储的密码进行对比
const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)
// 如果对比的结果等于 false, 则证明用户输入的密码错误
if (!compareResult) {
    console.log('登录失败');
    return res.cc( '登录失败!');
}

5. 生成 JWT 的 Token 字符串

核心注意点:在生成 Token 字符串的时候,一定要剔除 密码 和 头像 的值

⑴ 通过 ES6 的高级语法,快速剔除 密码 和 头像 的值:

bash 复制代码
// 剔除完毕之后,user 中只保留了用户的 id, username, nickname, email 这四个属性的值
const user = { ...results[0], password: '', user_pic: '' }

⑵ 运行如下的命令,安装生成 Token 字符串的包:

bash 复制代码
npm i jsonwebtoken@8.5.1

⑶ 在 /router_handler/user.js 模块的头部区域,导入 jsonwebtoken 包:

bash 复制代码
// 用这个包来生成 Token 字符串
const jwt = require('jsonwebtoken')

⑷ 创建 config.js 文件,并向外共享 加密 和 还原 Token 的 jwtSecretKey 字符串:

bash 复制代码
//这是全局的配置文件
module.exports = {
    //设置加密和解密的token密钥
    jwtSecretKey: 'kejizhentan No1. ^_^',
}

⑸ 在 /router_handler/user.js 中将用户信息对象加密成 Token 字符串:

bash 复制代码
// 导入全局的配置文件
const config = require('../config')
// 生成 Token 字符串
const tokenStr = jwt.sign(user, config.jwtSecretKey, {
     expiresIn: '10h', // token 有效期为 10 个小时
 })

⑹ 在 /router_handler/user.js 中将生成的 Token 字符串响应给客户端:

bash 复制代码
res.send({
   status: 0,
    message: '登录成功!',
    // 为了方便客户端使用 Token,在服务器端直接拼接上 Bearer 的前缀
    token: 'Bearer ' + tokenStr,
})

⑺ /router_handler/user.js 完整代码

bash 复制代码
//导入数据库操作模块:
const db = require('../db/index')
//导入bcryptjs模块 
const bcrypt = require('bcryptjs')
// 用这个包来生成 Token 字符串
const jwt = require('jsonwebtoken')
// 导入配置文件
const config = require('../config')
/**
* 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
*/
// 注册用户的处理函数
exports.regUser = (req, res) => {
    // 接收表单数据
    const userinfo = req.body
    // 判断数据是否合法
    /**if (!userinfo.username || !userinfo.password) {
        // return res.send({ status: 1, message: '用户名或密码不能为空!' })
        return res.cc('用户名或密码不能为空!');
    }*/

    //定义 SQL 语句
    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: '用户名被占用,请更换其他用户名!' })
            return res.cc('用户名被占用,请更换其他用户名!');
        }

        // 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
        console.log('userinfo.password:' + userinfo.password)
        userinfo.password = bcrypt.hashSync(userinfo.password, 10)
        console.log('userinfo.password:' + userinfo.password)
        //如果用户名没有被占用,则将注册完的用户插入数据库中
        //定义插入用户的SQL
        const sql = 'insert into ev_users set ?'
        //调用 db.query() 执行 SQL 语句,插入新用户
        db.query(sql, { username: userinfo.username, password: userinfo.password }, function (err, results) {
            // 执行 SQL 语句失败
            // if (err) return res.send({ status: 1, message: err.message })
            if (err) return res.cc(err)
            // SQL 语句执行成功,但影响行数不为 1
            if (results.affectedRows !== 1) {
                // return res.send({ status: 1, message: '注册用户失败,请稍后再试!' })
                return res.cc('注册用户失败,请稍后再试!');
            }
            // 注册成功
            // res.send({ status: 0, message: '注册成功!' })
            res.cc(0, '注册成功!');
        })
    })
}
// 登录的处理函数
exports.login = (req, res) => {
    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('登录失败!')
        // 拿着用户输入的密码,和数据库中存储的密码进行对比
        const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)
        // 如果对比的结果等于 false, 则证明用户输入的密码错误
        if (!compareResult) {
            console.log('登录失败');
            return res.cc('登录失败!');
        }
        // 剔除完毕之后,user 中只保留了用户的 id, username, nickname, email 这四个属性的值
        const user = { ...results[0], password: '', user_pic: '' }
        // 对用户的信息进行加密,生成 Token 字符串
        const tokenStr = jwt.sign(user, config.jwtSecretKey, {
            expiresIn: '10h', // token 有效期为 10 个小时
        })
        res.send({
            status: 0,
            message: '登录成功!',
            // 为了方便客户端使用 Token,在服务器端直接拼接上 Bearer 的前缀
            token: 'Bearer ' + tokenStr,
        })
    })
}

⑻ 启动服务测试一下项目的功能


6. 配置解析 Token 的中间件

⑴ 运行如下的命令,安装解析 Token 的中间件:

bash 复制代码
npm i express-jwt@5.3.3

⑵ 在 app.js 中注册路由之前,配置解析 Token 的中间件:

bash 复制代码
//一定要在路由之前配置解析token的中间件
// 导入配置文件
const config = require('./config')
// 解析 token 的中间件
const expressJWT = require('express-jwt')
// 使用 .unless({ path: [/^\/api\//] }) 指定哪些接口不需要进行 Token 的身份认证
app.use(expressJWT({ secret: config.jwtSecretKey }).unless({ path: [/^\/api\//] }))

⑶ 在 app.js 中的 错误级别中间件 里面,捕获并处理 Token 认证失败后的错误:

bash 复制代码
// 错误中间件
app.use(function (err, req, res, next) {
    //省略其它代码...
    // 捕获身份认证失败的错误
    if (err.name === 'UnauthorizedError') return res.cc('身份认证失败!')
    // 未知错误...
})

完整代码如下:

bash 复制代码
// 导入 express
const express = require('express')
// 创建服务器的实例对象
const app = express()
const joi = require('@hapi/joi')

// 导入并配置 cors 中间件
const cors = require('cors')
// 将 cors 注册为全局中间件
app.use(cors())

// 配置解析表单数据的中间件,注意:这个中间件,只能解析 application/x-www-form-urlencoded 格式的表单数据
app.use(express.urlencoded({ extended: false }))

// 一定要在路由之前,封装 res.cc 函数
app.use((req, res, next) => {
  // status 默认值为 1,表示失败的情况
  // err 的值,可能是一个错误对象,也可能是一个错误的描述字符串
  res.cc = function (err, status = 1) {
    res.send({
      status,
      message: err instanceof Error ? err.message : err,
    })
  }
  next()
})

// 一定要在路由之前配置解析 Token 的中间件
const expressJWT = require('express-jwt')
const config = require('./config')

app.use(expressJWT({ secret: config.jwtSecretKey }).unless({ path: [/^\/api/] }))

// 导入并使用用户路由模块
const userRouter = require('./router/user')
app.use('/api', userRouter)

// 定义错误级别的中间件
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)
})

// 启动服务器
app.listen(3007, () => {
  console.log('api server running at http://127.0.0.1:3007')
})

⑷ 启动服务测试一下项目的功能




点击获取完整代码

四、个人中心

1.获取用户的基本信息

⑴ 实现步骤

  1. 初始化 路由 模块
  2. 初始化 路由处理函数 模块
  3. 获取用户的基本信息

⑵ 初始化路由模块

1) 创建 /router/userinfo.js 路由模块,并初始化如下的代码结构:
bash 复制代码
// 导入 express
const express = require('express')
// 创建路由对象
const router = express.Router()
// 获取用户的基本信息
router.get('/userinfo', (req, res) => {
    res.send('ok')
})
// 向外共享路由对象
module.exports = router
2)在app.js中导入并使用个人中心的路由模块:
bash 复制代码
// 导入并使用用户信息路由模块
const userinfoRouter = require('./router/userinfo')
// 注意:以 /my 开头的接口,都是有权限的接口,需要进行 Token 身份认证
app.use('/my', userinfoRouter)

⑶ 初始化路由处理函数模块

1) 创建 /router_handler/userinfo.js 路由处理函数模块,并初始化如下的代码结构:
bash 复制代码
// 获取用户基本信息的处理函数
exports.getUserInfo = (req, res) => {
    res.send('ok')
}
2) 修改 /router/userinfo.js 中的代码如下:
bash 复制代码
// 导入 express
const express = require('express')
// 创建路由对象
const router = express.Router()
// 导入用户信息的处理函数模块
const userinfo_handler = require('../router_handler/userinfo')
// 获取用户的基本信息
router.get('/userinfo',userinfo_handler.getUserInfo)
// 向外共享路由对象
module.exports = router

⑷ 获取用户的基本信息

1) 在 /router_handler/userinfo.js 头部导入数据库操作模块:
bash 复制代码
// 导入数据库操作模块
const db = require('../db/index')
2) 定义 SQL 语句:
bash 复制代码
// 根据用户的 id,查询用户的基本信息
// 注意:为了防止用户的密码泄露,需要排除 password 字段
const sql = `select id, username, nickname, email, user_pic from ev_users where id=?`
3) 调用 db.query() 执行 SQL 语句:
bash 复制代码
// 注意: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,
        message: '获取用户基本信息成功!',
        data: results[0],
    })
})
4) 启动服务测试一下项目的功能


2. 更新用户的基本信息

⑴ 实现步骤

  1. 定义路由和处理函数
  2. 验证表单数据
  3. 实现更新用户基本信息的功能

⑵ 定义路由和处理函数

1) 在/router/userinfo.js模块中,新增 更新用户基本信息 的路由:
bash 复制代码
// 更新用户的基本信息
router.post('/userinfo', userinfo_handler.updateUserInfo)
2) 在 /router_handler/userinfo.js 模块中,定义并向外共享 更新用户基本信息 的路由处理函数:
bash 复制代码
// 更新用户基本信息的处理函数
exports.updateUserInfo = (req, res) => {
    res.send('ok')
}

⑶ 验证表单数据

1) 在/schema/user.js验证规则模块中,定义 id , nickname , email 的验证规则如下:
bash 复制代码
// 定义 id, nickname, emial 的验证规则
const id = joi.number().integer().min(1).required()
const nickname = joi.string().required()
const email = joi.string().email().required()
2) 并使用 exports 向外共享如下的 验证规则对象 :
bash 复制代码
// 验证规则对象 - 更新用户基本信息
exports.update_userinfo_schema = {
    body: {
        id,
        nickname,
        email,
    },
}
3) 在 /router/userinfo.js 模块中,导入验证数据合法性的中间件:
bash 复制代码
// 导入验证数据合法性的中间件
const expressJoi = require('@escook/express-joi')
4) 在/router/userinfo.js 模块中,导入需要的验证规则对象:
bash 复制代码
// 导入需要的验证规则对象
const { update_userinfo_schema } = require('../schema/user')
5) 在 /router/userinfo.js 模块中,修改 更新用户的基本信息 的路由如下:
bash 复制代码
// 更新用户的基本信息
router.post('/userinfo', expressJoi(update_userinfo_schema),
    userinfo_handler.updateUserInfo)

⑷ 实现更新用户基本信息的功能

1) 定义待执行的 SQL 语句:
bash 复制代码
const sql = `update ev_users set ? where id=?`
2) 调用 db.query() 执行 SQL 语句并传参:
bash 复制代码
// 更新用户基本信息的处理函数
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)
    })
}
3) 启动服务测试一下项目的功能




3. 重置密码

⑴ 实现步骤

  1. 定义路由和处理函数
  2. 验证表单数据
  3. 实现重置密码的功能

⑵ 定义路由和处理函数

1) 在/router/userinfo.js模块中,新增 重置密码 的路由:
bash 复制代码
// 重置密码的路由
router.post('/updatepwd', userinfo_handler.updatePassword)
2) 在 /router_handler/userinfo.js 模块中,定义并向外共享 重置密码 的路由处理函数:
bash 复制代码
// 重置密码的处理函数
exports.updatePassword = (req, res) => {
    res.send('ok')
}

⑶ 验证表单数据

核心验证思路:旧密码与新密码,必须符合密码的验证规则,并且新密码不能与旧密码一致!

1) 在 /schema/user.js 模块中,使用 exports 向外共享如下的 验证规则对象 :
bash 复制代码
// 验证规则对象 - 重置密码
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),
    },
}
2) 在 /router/userinfo.js 模块中,导入需要的验证规则对象:
bash 复制代码
// 导入需要的验证规则对象
const { update_userinfo_schema, update_password_schema } = require('../schema/user')
3) 并在 重置密码的路由 中,使用 update_password_schema 规则验证表单的数据,示例代码如下:
bash 复制代码
// 重置密码的路由
router.post('/updatepwd', expressJoi(update_password_schema),
    userinfo_handler.updatePassword)

⑷ 在/router_handler/userinfo.js 模块中 实现重置密码的功能

1) 根据 id 查询用户是否存在:
bash 复制代码
// 定义根据 id 查询用户数据的 SQL 语句
const querySql = `select * from ev_users where id=?`
// 执行 SQL 语句查询用户是否存在
db.query(querySql, req.user.id, (err, results) => {
    // 执行 SQL 语句失败
    if (err) return res.cc(err)
    // 检查指定 id 的用户是否存在
    if (results.length !== 1) return res.cc('用户不存在!')
    // 判断提交的旧密码是否正确
    const compareResult = bcrypt.compareSync(req.body.oldPwd, results[0].password)
    if (!compareResult) return res.cc('原密码错误!')
})
2)判断提交的 旧密码 是否正确:
bash 复制代码
// 在头部区域导入 bcryptjs 后,
// 即可使用 bcrypt.compareSync(提交的密码,数据库中的密码) 方法验证密码是否正确
// compareSync() 函数的返回值为布尔值,true 表示密码正确,false 表示密码错误
const bcrypt = require('bcryptjs')

// 判断提交的旧密码是否正确
const compareResult = bcrypt.compareSync(req.body.oldPwd, results[0].password)
if (!compareResult) return res.cc('原密码错误!')
3)对新密码进行bcrypt加密之后,更新到数据库中:
bash 复制代码
// 定义更新用户密码的 SQL 语句
const updateSql = `update ev_users set password=? where id=?`
// 对新密码进行 bcrypt 加密处理
const newPwd = bcrypt.hashSync(req.body.newPwd, 10)
// 执行 SQL 语句,根据 id 更新用户的密码
db.query(updateSql, [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)
})

/router_handler/userinfo.js 模块中完整代码如下:

bash 复制代码
// 导入数据库操作模块
const db = require('../db/index')
// 在头部区域导入 bcryptjs 后,
// 即可使用 bcrypt.compareSync(提交的密码,数据库中的密码) 方法验证密码是否正确
// compareSync() 函数的返回值为布尔值,true 表示密码正确,false 表示密码错误
const bcrypt = require('bcryptjs')
// 获取用户基本信息的处理函数
exports.getUserInfo = (req, res) => {
    // 根据用户的 id,查询用户的基本信息
    // 注意:为了防止用户的密码泄露,需要排除 password 字段
    const sql = `select id, username, nickname, email, user_pic from ev_users where id=?`
    // 注意: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,
            message: '获取用户基本信息成功!',
            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 查询用户的信息
    const sql = `select * from ev_users where id=?`
    // 执行根据 id 查询用户的信息的 SQL 语句
    db.query(sql, req.user.id, (err, results) => {
      // 执行 SQL 语句失败
      if (err) return res.cc(err)
      // 判断结果是否存在
      if (results.length !== 1) return res.cc('用户不存在!')
  
      // 判断密码是否正确
      const compareResult = bcrypt.compareSync(req.body.oldPwd, results[0].password)
      if (!compareResult) return res.cc('旧密码错误!')
  
      // 定义更新密码的 SQL 语句
      const sql = `update ev_users set password=? where id=?`
      // 对新密码进行加密处理
      const newPwd = bcrypt.hashSync(req.body.newPwd, 10)
      // 调用 db.query() 执行 SQL 语句
      db.query(sql, [newPwd, req.user.id], (err, results) => {
        // 执行 SQL 语句失败
        if (err) return res.cc(err)
        // 判断影响的行数
        if (results.affectedRows !== 1) return res.cc('更新密码失败!')
        // 成功
        res.cc('更新密码成功', 0)
      })
    })
  }
4) 启动服务测试一下项目的功能


4. 更新用户头像

⑴ 实现步骤

  1. 定义路由和处理函数
  2. 验证表单数据
  3. 实现更新用户头像的功能

⑵ 定义路由和处理函数

1) 在/router/userinfo.js 模块中,新增 更新用户头像 的路由:
bash 复制代码
// 更新用户头像的路由
router.post('/update/avatar', userinfo_handler.updateAvatar)
2)在 /router_handler/userinfo.js 模块中,定义并向外共享 更新用户头像 的路由处理函数:
bash 复制代码
// 更新用户头像的处理函数
exports.updateAvatar = (req, res) => {
    res.send('ok')
}

⑶ 验证表单数据

1) 在 /schema/user.js 验证规则模块中,定义 avatar 的验证规则如下:
bash 复制代码
// dataUri() 指的是如下格式的字符串数据:
// data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
const avatar = joi.string().dataUri().required()
2)并使用 exports 向外共享如下的 验证规则对象 :
bash 复制代码
// 验证规则对象 - 更新头像
exports.update_avatar_schema = {
    body: {
        avatar,
    },
}
3) 在 /router/userinfo.js 模块中,导入需要的验证规则对象:
bash 复制代码
const { update_avatar_schema } = require('../schema/user')
4) 在 /router/userinfo.js 模块中,修改 更新用户头像 的路由如下:
bash 复制代码
// 更新用户头像的路由
router.post('/update/avatar', expressJoi(update_avatar_schema),
    userinfo_handler.updateAvatar)

⑷ 实现更新用户头像的功能

1) 定义更新用户头像的 SQL 语句:
bash 复制代码
const sql = 'update ev_users set user_pic=? where id=?'
2) 调用 db.query() 执行 SQL 语句,更新对应用户的头像:
bash 复制代码
// 更新用户头像的处理函数
exports.updateAvatar = (req, res) => {
  // 1. 定义更新头像的 SQL 语句
  const sql = `update ev_users set user_pic=? where id=?`
  // 2. 调用 db.query() 执行 SQL 语句
  db.query(sql, [req.body.avatar, req.user.id], (err, results) => {
    // 执行 SQL 语句失败
    if (err) return res.cc(err)
    // 影响的行数是否等于 1
    if (results.affectedRows !== 1) return res.cc('更换头像失败!')
    // 成功
    res.cc('更换头像成功!', 0)
  })
}
3) 启动服务测试一下项目的功能



相关推荐
RobinDevNotes几秒前
从 Electron 到 GPUI,9 个跨平台桌面方案该怎么选
前端·javascript·rust·electron·openresty·前端开发工具
jay神1 小时前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神1 小时前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat1 小时前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss2 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan3 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真3 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment3 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan3 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么3 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模