当前位置: 首页 > news >正文

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

代码下载

初始化

新建 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 是必选参数!');}
}
http://www.lryc.cn/news/261445.html

相关文章:

  • 前端框架的虚拟DOM(Virtual DOM)
  • 什么是http状态码?
  • linux/CentOS 7安装Nginx
  • 软件工程期末复习+数据仓库ETL
  • 学习C语言——体会计算机中的0和1
  • PyTorch官网demo解读——第一个神经网络(1)
  • 升华 RabbitMQ:解锁一致性哈希交换机的奥秘【RabbitMQ 十】
  • vue3 element-plus 日期选择器 el-date-picker 汉化
  • 剑指 Offer(第2版)面试题 35:复杂链表的复制
  • 自定义指令Custom Directives
  • 预测性维护对制造企业设备管理的作用
  • 华为、新华三、锐捷常用命令总结
  • 链路追踪详解(四):分布式链路追踪的事实标准 OpenTelemetry 概述
  • Node.js 工作线程与子进程:应该使用哪一个
  • python matplotlib 三维图形添加文字且不随图形变动而变动
  • Ubuntu设置kubelet启动脚本关闭swap分区
  • MySQL数据库存储
  • verilog语法进阶,时钟原语
  • 案例069:基于微信小程序的计算机实验室排课与查询系统
  • C语言:将三个数从大到小输出
  • 基于Hadoop的铁路货运大数据平台设计与应用
  • Java基础题2:类和对象
  • 冒泡排序学习
  • LeetCode(65)LRU 缓存【链表】【中等】
  • 网站提示“不安全”
  • 【Linux】驱动
  • Java研学-HTML
  • SpringBoot之响应的详细解析
  • redis:四、双写一致性的原理和解决方案(延时双删、分布式锁、异步通知MQ/canal)、面试回答模板
  • 智能优化算法应用:基于动物迁徙算法3D无线传感器网络(WSN)覆盖优化 - 附代码