前后端交互—开发一个完整的服务器

代码下载

初始化

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

npm init -y

运行如下的命令,安装 express、cors:

npm i express cors

在项目根目录中新建 app.js 作为整个项目的入口文件,配置 cors 跨域中间件、解析表单数据的中间件,并启动服务器:

// 导入 express 模块
const express = require('express');
// 创建 express 的服务器实例
const app = express();

// 配置跨域中间件
const cors = require('cors');
app.use(cors());

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

// 指定端口号并启动web服务器
app.listen('80', function() {
    console.log('server running at http://127.0.0.1:80');
});

因为在处理函数中,需要多次调用 res.send() 向客户端响应 处理失败 的结果,为了简化代码, 可以手动封装一个 res.cc() 函数。在 app.js 中所有路由之前,声明一个全局中间件,为 res 对象挂载一个 函数 :

// 在 路由 之前自定义中间件,处理错误的响应数据
app.use(function(req, res, next) {
    // status 默认值1,表示失败,0表示成功
    // err 可能是错误对象,也可能是错误描述字符串
    res.cc = function(err, status = 1) {
        res.send({
            status: status,
            message: err instanceof Error ? err.message : err
        });
    };

    next();
});
  1. 在项目根目录中,新建 router 文件夹,用来存放所有的 路由 模块,路由模块中,只存放客户端的请求与处理函数之间的映射关系
  2. 在项目根目录中,新建 routerHandler 文件夹,用来存放所有的 路由处理函数模块 路由处理函数模块中,专门负责存放每个路由对应的处理函数
  3. 在项目根目录中,新建 db 文件夹,用来存放所有的 数据库 模块
  4. 在项目根目录中,新建 middleware 文件夹,用来存放所有的 自定义中间件 模块

登录注册

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

安装并配置 mysql 模块,需要安装并配置 mysql 这个第三方模块,来连接和操作 MySQL 数据库,运行如下命令,安装 mysql 模块:

npm i mysql

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

const mysql = require('mysql');
const db = mysql.createPool({
    host: 'localhost',
    port: '3306',
    user: 'root',
    password: 'admin123',
    database: 'my_db_01'
});

module.exports = db;

初始化用户路由模块

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

const express = require('express') // 创建路由对象
const router = express.Router()
// 注册新用户
router.post('/signin', (req, res) => {
  res.send('signin OK')
})
// 登录
router.post('/login', (req, res) => {
  res.send('login OK')
})
// 将路由对象共享出去 module.exports = router

在 app.js 中,导入并使用 用户路由模块 :

// 配置路由
const user = require('./router/user');
app.use('/api', user);

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

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

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

/**
* 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用 */

// 注册用户的处理函数 
exports.signin = (req, res) => {
  res.send('signin OK')
}

// 登录的处理函数
exports.login = (req, res) => {
  res.send('login OK')
}

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

const express = require('express');
const router = express.Router();
const handler = require('../routerHandler/user');

// 注册
router.post('/signin', handler.signin);

// 登录
router.post('/login', handler.login);

module.exports = router;

joi 表单数据验证

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

在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且, 后端做为数据合法性验证的最后 一个关口 ,在拦截非法数据方面,起到了至关重要的作用。

单纯的使用 if...else... 的形式对数据合法性进行验证,效率低下、出错率高、维护性差。因此, 推荐使用 第三方数据验证模块 ,来降低出错率、提高验证的效率与可维护性, 让后端程序员把更多的精力放在核心业务逻辑的处理上。

安装 joi 包,为表单中携带的每个数据项,定义验证规则:

npm install joi

在 /middleware/expressJoi.js 中,使用 joi 对象,定义并向外共享表单数据验证中间件:

const joi = require('joi');

const expressJoi = function (schemas, options = { strict: false }) {
  // 自定义校验选项
  // strict 自定义属性,默认不开启严格模式,会过滤掉那些未定义的参数项
  //        如果用户指定了 strict 的值为 true,则开启严格模式,此时不会过滤掉那些未定义的参数项
  if (!options.strict) {
    // allowUnknown 允许提交未定义的参数项
    // stripUnknown 过滤掉那些未定义的参数项
    options = { allowUnknown: true, stripUnknown: true, ...options }
  }

  // 从 options 配置对象中,删除自定义的 strict 属性
  delete options.strict

  // TODO: 用户指定了什么 schema,就应该校验什么样的数据
  return function (req, res, next) {
    ['body', 'query', 'params'].forEach(key => {
      // 如果当前循环的这一项 schema 没有提供,则不执行对应的校验
      if (!schemas[key]) return

      // 执行校验
      const schema = joi.object(schemas[key])
      const { error, value } = schema.validate(req[key], options)

      if (error) {
        console.log('---------------');
        // 校验失败
        throw error
      } else {
        // 校验成功,把校验的结果重新赋值到 req 对应的 key 上
        req[key] = value
      }
    })

    // 校验通过
    next()
  }
}

module.exports = expressJoi

新建 /schema/user.js 用户信息验证规则模块,并初始化代码如下:

// @hapi/joi 包,为表单中携带的每个数据项,定义验证规则
const joi = require('joi');


// * string() 值必须是字符串
// * alphanum() 值只能是包含 a-zA-Z0-9 的字符串 * min(length) 最小长度
// * max(length) 最大长度
// * required() 值是必填项,不能为 undefined
// * pattern(正则表达式) 值必须符合正则表达式的规则
const username = joi.string().alphanum().min(2).max(20).required();
const password = joi.string().required().pattern(/^[\S]{6,16}$/);

exports.schema = {
    user: {
        body: {
            username,
            password
        }
    }
}

修改 /router/user.js 中的代码如下:

// 导入验证表单数据的中间件
const expressJoi = require('../middleware/expressJoi');
// 导入需要的验证规则对象
const { schema } = require('../schema/user');

// 为 注册新用户 配置表单验证中间件
// 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证
//      数据验证通过后,会把这次请求流转给后面的路由处理函数
//      数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行 处理
router.post('/signin', expressJoi(schema.user), handler.signin);

// 登录
router.post('/login', expressJoi(schema.user), handler.login);

在 app.js 的全局错误级别中间件中,捕获验证失败的错误,并把验证失败的结果响应给客户端:

// 配置 错误处理 中间件
const joi = require('joi');
app.use(function(err, req, res, next) {
    // console.log(err);
    if (err instanceof joi.ValidationError) {
        // 处理数据校验错误
    } else {
        // 处理其他未知错误
    }
    return res.cc(err);
});

加密处理

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

  • 加密之后的密码, 无法被逆向破解
  • 同一明文密码多次加密,得到的 加密结果各不相同 ,保证了安全性

运行如下命令,安装 bcryptjs:

npm i bcryptjs

调用 bcrypt.hashSync(明文密码, 随机盐的长度) 方法,对用户的密码进行加密处理:

const bcrypt = require('bcryptjs');
// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
userinfo.password = bcrypt.hashSync(userinfo.password, 10);

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

// 拿着用户输入的密码,和数据库中存储的密码进行对比
const compareResult = bcrypt.compareSync(userinfo.password, results[0].password);

// 如果对比的结果等于 false, 则证明用户输入的密码错误 
if (!compareResult) {
    console.log('密码错误!');
}

注册

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

新建 /middleware/constValue.js 常量数据模块,并初始化代码如下:

// sql 语句
// 查找用户名
const selectUN = `select * from users where username = ?`;
// 插入用户
const insertUser = `insert into users set ?`;

module.exports = {
    selectUN,
    insertUser
}

修改 /routerHandler/user.js 中的代码,实现最终完整的登录逻辑,如下:

const bcrypt = require('bcryptjs');
const constValue = require('../middleware/constValue');
const signin = (req, res) => {
    const info = req.body;

    // 数据校验交给 expressJoi 中间件处理
    // if (info.username && info.password) {
        db.query(constValue.selectUN, info.username, function(err, result) {
            if (err) {
                res.cc(err);
            } else if (result.length > 0) {
                res.cc('用户名被占用,请更换其他用户名!');
            } else {
                // 调用 bcrypt.hashSync(明文密码, 随机盐的长度) 方法,对用户的密码进行加密处理
                info.password = bcrypt.hashSync(info.password, 10);
                db.query(constValue.insertUser, {username: info.username, password: info.password}, function(err, result) {
                    if (err) {
                        res.cc(err);
                    } else if (result.affectedRows === 1) {
                        res.cc('注册成功!', 0);
                    } else {
                        res.cc('注册失败,请稍后重试!');
                    }
                });
            }
        });
    // } else {
    //     res.cc('用户名或密码不能为空!');
    // }
};

登录

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

在 /middleware/constValue.js 定义并导出查询用户数据的 SQL 语句以及 JWT 秘钥、加密算法、无权限表达式:

// 查找用户信息
const selectInfo = `select id, username, nickname, email, user_pic from users where id = ?`;
// JWT 秘钥
const jwtSecretKey = 'JWTSecretKeyabcABC123!@#JWTSecretKey';
// JWT 加密算法
const jwtAlgorithms = 'HS256';
// JWT 无需权限的接口 表达式
const jwtUnlessPath = /^\/api\//;

运行如下的命令,安装生成 Token 字符串的包、解析 Token 的中间件的包:

npm i jsonwebtoke express-jwt

核心注意点: 在生成 Token 字符串的时候,一定要剔除 密码 和 头像 的值 通过 ES6 的高级语法,快速剔除 密码 和 头像 的值,将用户信息对象加密成 Token 字符串:

// 通过 ES6 的高级语法,快速剔除用户敏感信息后的数据作为 token 的加密数据
const user = { ...result[0], password: '', user_pic: '' }
// token 有效期为 10 天
const token = jwt.sign(user, constValue.jwtSecretKey, { expiresIn: '10d' });

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

// 配置 解析 Token 的中间件
var { expressjwt } = require('express-jwt');
const constValue = require('./middleware/constValue');
app.use(expressjwt({
    secret: constValue.jwtSecretKey, 
    algorithms: [constValue.jwtAlgorithms]
}).unless({
    path: [constValue.jwtUnlessPath]
}));

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

app.use(function(err, req, res, next) {
    // console.log(err);
    if (err instanceof joi.ValidationError) {
        // 处理数据校验错误
    } else if (err.name === 'UnauthorizedError') {
        // 处理身份认证失败的错误
        return res.cc('身份验证失败!');
    } else {
        // 处理其他未知错误
    }
    return res.cc(err);
});

修改 /routerHandler/user.js 中的代码,实现最终完整的登录逻辑,如下:

const jwt = require('jsonwebtoken');
const expressJwt = require('express-jwt');
const { use } = require('../router/user');
const login = (req, res) => {
    const info = req.body;
    // 查询 username 是否存在
    db.query(constValue.selectUN, info.username, function(err, result) {
        if (err) {
            res.cc(err);
        } else if (result.length === 1) {
            // 判断密码是否正确
            if (bcrypt.compareSync(info.password, result[0].password)) {
                // 剔除用户敏感信息后的数据作为 token 的加密数据
                const user = { ...result[0], password: '', user_pic: '' }
                console.log(user);
                const token = jwt.sign(user, constValue.jwtSecretKey, { expiresIn: '10d' });
                res.send({
                    status: 0,
                    token: 'Bearer ' + token,
                    message: '登录成功!'
                });
            } else {
                res.cc('密码错误!');
            }
        } else {
            res.cc('登录失败!');
        }
    });
};

个人中心

获取用户的基本信息

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

创建 /router/userinfo.js 路由模块,并初始化如下的代码结构:

const express = require('express');
const router = express.Router();
const handler = require('../routerHandler/userinfo');

// 获取用户信息
router.get('/userinfo', handler.userinfo);

module.exports = router;

在 app.js 中导入并使用个人中心的路由模块:

const userinfo = require('./router/userinfo');
app.use('/my', userinfo);

在 /middleware/constValue.js 定义并导出查询用户信息的 SQL 语句:

// 查找用户信息
const selectInfo = `select id, username, nickname, email, user_pic from users where id = ?`;

创建 /routerHandler/userinfo.js 文件,实现最终完整的逻辑,如下:

const db = require('../db/index');
const constValue = require('../middleware/constValue');

const userinfo = function(req, res) {
    console.log(req.auth);
    db.query(constValue.selectInfo, req.auth.id, function(err, result) {
        if (err) {
            res.cc(err);
        } else if (result.length === 1) {
            res.send({
                status: 0,
                data: result[0],
                message: '用户信息获取成功!'
            });
        } else {
            res.cc('用户信息获取失败!');
        }
    });
};

module.exports = {
    userinfo
};

更新用户的基本信息

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

具体实现见代码......

重置密码

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

在 /schema/user.js 验证规则模块中,定义 newPassword 的验证规则并使用 exports 向外共享如下的验证规则对象:

const password = joi.string().required().pattern(/^[\S]{6,16}$/);
// 1. joi.ref('oldPassword') 表示 newPassword 的值必须和 oldPassword 的值保持一致
// 2. joi.not(joi.ref('oldPwd')) 表示 newPwd 的值不能等于 oldPwd 的值
// 3. .concat() 用于合并 joi.not(joi.ref('oldPwd')) 和 password 这两条验证规则
const newPassword = joi.not(joi.ref('oldPassword')).concat(password);

exports.schema = {
    updatePassword: {
        body: {
            oldPassword: password,
            newPassword
        }
    }
}

其他具体实现见代码......

更新用户头像

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

在 /schema/user.js 验证规则模块中,定义 user_pic 的验证规则并使用 exports 向外共享如下的验证规则对象:

// dataUri() 指的是如下格式的字符串数据:
// data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
const user_pic = joi.string().dataUri().required();

exports.schema = {
    updateAvatar: {
        body: {
            user_pic
        }
    }
}

在 /router/userinfo.js 模块中,导入需要的验证规则对象, 修改 更新用户头像 的路由:

const expressJoi = require('../middleware/expressJoi');
const { schema } = require('../schema/user');
// 更换头像
router.post('/updateAvatar', expressJoi(schema.updateAvatar), handler.updateAvatar);

在 /middleware/constValue.js 定义并导出 更新用户头像 的 SQL 语句:

// 修改头像
const updateAvatar = `update users set user_pic = ? where id = ?`;

处理 /routerHandler/userinfo.js 中的代码,实现最终完整的登录逻辑,如下:

// 更换头像
const updateAvatar = (req, res) => {
    console.log('------------');
    console.log(req.body);
    db.query(constValue.updateAvatar, [req.body.user_pic, req.auth.id], (err, result) => {
        if (err) {
            res.cc(err);
        } else if (result.affectedRows === 1) {
            res.cc('更换头像成功!', 0);
        } else {
            res.cc('更换头像失败!');
        }
    })
}

文章分类管理

具体实现见代码......

文章管理

新建 articles 表如下:

发布新文章

  1. 初始化路由模块
  2. 初始化路由处理函数模块
  3. 使用 multer 解析表单数据
  4. 验证表单数据
  5. 实现发布文章的功能

创建 /routerHandler/article.js 路由处理函数模块,并初始化如下的代码结构:

// 发布新文章的处理函数 
exports.addArticle = (req, res) => {
  res.send('ok')
}

创建 /router/article.js 路由模块,并初始化如下的代码结构:

// 导入 express
const express = require('express') 
// 创建路由对象
const router = express.Router()

// 导入文章的路由处理函数模块
const handler = require('../routerHandler/article');

// 发布新文章
routor.post('/addArticle', handler.addArticle);

// 向外共享路由对象 
module.exports = router

在 app.js 中导入并使用文章的路由模块:

const article = require('./router/article');
app.use('/my', article);
使用 multer 解析表单数据

注意: 使用 express.urlencoded() 中间件无法解析 multipart/form-data 格式的请求体 数据。

推荐使用 multer 来解析 multipart/form-data 格式的表单数据。运行如下的终端命令,在项目中安装 multer :

npm i multer

在 /router/article.js 模块中导入并配置 multer :

// 导入解析 formdata 格式表单数据的包 
const multer = require('multer') 
// 导入处理路径的核心模块
const path = require('path')
// 创建 multer 的实例对象,通过 dest 属性指定文件的存放路径
const upload = multer({ dest: path.join(__dirname, '../uploads') })

// 发布新文章的路由
// upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据
// 将文件类型的数据,解析并挂载到 req.file 属性中
// 将文本类型的数据,解析并挂载到 req.body 属性中
routor.post('/addArticle', uploads.single('cover_img'), handler.addArticle);
实现发布文章的功能

通过 express-joi 自动验证 req.body 中的文本数据;通过 if 判断手动验证 req.file 中的 文件数据。

在 /schema/user.js 验证规则模块中,定义数据验证规则并使用 exports 向外共享如下的 验证规则对象:

const id = joi.number().integer().min(1).required();
const title = joi.string().required();
const content = joi.string().required().allow('');
const state = joi.string().valid('已发布', '草稿').required();

exports.schema = {
    addArticle: {
        body: {
            title,
            content,
            cate_id: id,
            state
        }
    }
}

在 /router/article.js 模块中,导入需要的验证规则对象,并在路由中使用。

在 /middleware/constValue.js 定义并导出 发布文章 的 SQL 语句:

// 插入文章
const insertArticle = `insert into articles set ?`;

修改 /routerHandler/article.js 中的代码,实现最终完整的登录逻辑,如下:

const addArticle = (req, res) => {
    console.log(req.body);
    console.log(req.file);
    if (req.file && req.file.fieldname === 'cover_img') {
        const article = {
            ...req.body,
            // 文章封面在服务器端的存放路径
            cover_img: `/uploads/${req.file.filename}`,
            pub_date: new Date(),
            author_id: req.auth.id
        };
        db.query(constValue.insertArticle, article, (err, result) => {
            if (err) {
                res.cc(err);
            } else if (result.affectedRows === 1) {
                res.cc('文章添加成功!', 0);
            } else {
                res.cc('文章添加失败!');
            }
        });
    } else {
        res.cc('cover_img 是必选参数!');
    }
}
相关推荐
爱学习的白杨树8 分钟前
什么是MVCC?
java·服务器·数据库
xserver241 分钟前
ensp 基于静态NAT发布公司网站服务器,
运维·服务器
QYpiying42 分钟前
服务器中了挖矿病毒-应急响应
运维·服务器
RealName-Wang1 小时前
联想 P368-C3 thinkstation服务器介绍
运维·服务器
sone121381 小时前
计算机网络(第8版)第四章 网络层(4.7.1~4.7.3)
服务器·网络·计算机网络
DO_Community2 小时前
DigitalOcean Droplet 云服务器:新增自动扩展池功能
服务器
安科瑞刘鸿鹏2 小时前
老旧小区用电安全保护装置#限流式防火保护器参数介绍#
运维·服务器·物联网·能源
Rain_Rong2 小时前
linux检测硬盘
linux·运维·服务器
我曾经是个程序员2 小时前
鸿蒙学习记录之http网络请求
服务器·学习·http
真真-真真3 小时前
WebXR
linux·运维·服务器