# 评论系统(服务端项目)从设计、实践开发到云端部署
TIP
从本节开始,我们正式完善和开发评论系统的服务端项目。之前熟悉了什么是服务端,相关的前置技术点 和 工具。已实现了部分评论系统的功能,但还还不够系统。
通过本项目把此前所有的知识点串联起来,将知识点变为项目输出。
- 需求分析
- 接口(路由)设计
- 技术选型,架构设计,环境搭建
- 数据库设计
- 业务逻辑开发
- 单元测试 和 集成测试
- 部署和上线
- 项目总结
# 一、项目开发过程
TIP
当下主流的项目开发基本都是 前后端分离开发。
日常项目开发过程如下
注:
- 一起做需求评审,做接口设计(前后端人员共同参与 和 确定)
- 技术选型,架构设计,环境搭建
- 前后端分离开发(前端 和 后端开发人员各自负责自己的部分)
- 自测
- 前后端联调
# 1、实战项目的重要性
TIP
- 实战项目是对所学知识点的真实反馈,反馈很重要
- 实战项目增加自己的成就感
- 实战项目是企业面试时非常看重的
# 2、重视业务 和 需求
TIP
- 工作了,老板给你发工资不是因为你会写代码,而是做出来功能
- 代码只是一个工具,产出决定价值
- 要积参与其中,不要排斥,且要力争深刻理解(要将代码、需求 和 业务也结合起来)
# 3、重视技术方案设计
TIP
- 包括:接口设计,数据库模型设计等
- 设计、决定了以后怎么开发,设计比开发难
- 越难的越有价值,越要重视
# 4、不要讨厌 “重复”
TIP
- 学习就是一个重复的过程,课程中也是同样
- 大量重复之后,由量变到质变,再进行下一轮重复
- 切记:“一直重复,从未质变”
# 二、需求分析
TIP
在实际工作中,需求很重要。一定要搞清楚项目的需求是什么,需求的背景、流程是什么,一定要搞清楚才能进入后边的流程
明确项目的需求,包括业务需求、用户需求以及技术需求等。确保充分理解和定义系统的功能和模块。
# 1、需求的重要性
TIP
- 需求:描述项目最终效果的文字 和 图(正常情况下会有 原型图 和 文字描述)
- 产品经理写需求,然后要经过各个项目角色的评审通过
- 需求一般包括:原型图,功能描述
# 2、评论系统 - 原型图
TIP
以评论系统项目为例,作为需求原型
- 注册页
- 登录页
- 主页
常见画原型图的工具
# 2.1、原型图 - 注册页
TIP
注册页面的原型图,用户注册时需要填写以下信息
- 用户名
- 密码
- 年龄
- 城市
- 性别
如已经注册过了,直接点击登录进入登录页面,进行用户登录
# 2.2、原型图 - 登录页
TIP
登录页面的原型图,用户登录时只需要填写用户名 和 密码即可。如为注册过,点击登录链接进入注册页注册新用户即可
# 2.3、原型图 - 主页
TIP
用户注册登录成功后,即可进入主页
- 主页中有评论输入框,提交评论按钮
- 评论列表,包含(用户头像、用户名、评论时间、评论内容,编辑、删除按钮)
- 列表上边有评论列表的筛选条件:看全部(默认) 和 只看自己
# 3、功能描述 - 注册 和 登录
TIP
- 用户名唯一,不能重复注册
- 用户名 和 密码匹配,即可登录
- 登录成功后,跳转到首页
# 4、功能描述 - 首页
TIP
- 非登录用户不能进入首页,登录用户可发布评论
- 可查看全部评论 或 查看自己的评论
- 只能编辑 和 删除自己的评论信息,无权操作他人的评论
目前只是简单的描述,实际工作中,会有完整的 word 文档 或 在线的协作文档等(图文并茂)文档一定要出现一些关键信息。
# 三、接口(路由)设计
TIP
按照需求的顺序,分析了每个页面需要哪些功能 和 接口,经过需求分析之后就需要做接口设计。正常情况下接口设计是需要 和 前端一起来完成。
当然,这个一起完成并非要 前端 和后端开发人员坐在一起办公(可以是由任意一方来完成接口的设计,设计完成后双方的人员在一起开会讨论最终确定下来)
假定,本次设计是由我们后端开发来完成接口设计,最后与前端人员讨论确定。在设计接口之前,我们先回顾一下 RESTful API 的设计范式
# 1、RESTful API
TIP
- 一种新的 API 设计方法(早已推广使用,目前又有新的 API 设计方式,如:GraphQL AP)
- 传统 API 设计:把每个 url 当做一个功能
- RESTfulAPI 设计:把每个 url 当做一个唯一的资源
GraphQL API:GraphQL 是一种用于 API 的查询语言,它允许客户端精确地指定它们需要的数据,而不是依赖于预定义的端点。这使得 API 调用更加灵活和高效,能够减少多次请求的网络开销,提高响应速度。
# 2、如何设计成一个资源
TIP
- 尽量不用 url 参数
- 用 method 表示操作类型
# 2.1、不使用 url 参数
API 设计 | URL | 描述 |
---|---|---|
传统 API 设计 | /api/list?pageNum=1&pageSize=10 | 典型的功能描述型,从 URL 中的参数可以看出,查询第一页的数据,每页显示 10 条记录。无法表示 文件 和 文件夹的层次关系 |
RESTful API 设计 | /api/list/1/10 | 可以很好的表示文件夹 和 文件的层次关系,是一种资源 |
# 2.2、两种 API 设计对比
传统 API 设计
请求方式 | URL(路由) | 描述 |
---|---|---|
POST | /api/create-comment | 创建评论 |
POST | /api/update-comment?id=1001 | 根据 id 修改评论 |
GET | /api/get-comment?id=1001 | 根据 id 获取评论 |
GET | /api/del-comment?id=1001 | 根据 id 删除评论 |
以上 URL 中,都带有明确的功能性描述
create
、update
、get
、del
对比 RESTful API 设计
请求方式 | URL(路由) | 描述 |
---|---|---|
POST | /api/comment | 创建评论 |
PATCH | /api/comment/1001 | 根据 id 修改评论 |
GET | /api/comment/1001 | 根据 id 获取评论 |
DELETE | /api/comment/1001 | 根据 id 删除评论 |
注:
- 通过 RESTful API 设计风格,以上同样的路由地址,只需要通过不同的请求方式来得到不同的响应结果。
- 而传统的 API 设计,在 URL 路由中(
create
、update
、get
、del
)是有明确的功能描述的 - 在 RESTful API 中是将功能描述放在了 method 中(POST、PATCH、GET、DELETE ... 等),但 URL 是单纯的资源标识,它表示的永远是一个资源。
- 可以理解成 文件夹 和 文件的层次关系 ,同样的 URL 会根据不同的请求来区分对应的功能
因此,RESTful API 几乎不会用 URL 参数(类似 ?id=1001
的形式),它会用 method 来表示操作类型(功能描述)
即:RESTful API 的设计方式中 URL 表示是一个资源标识,传统的 API 设计方式中 URL 表示功能描述。
# 3、接口设计作用
TIP
- 前后端交互的桥梁。前后端一起设计,一起评审
- 前后端分离开发,都要按照该设计,不得违约
- 如发现问题,及时反馈,及时修改,及时通知,及时更新接口设计文档
# 4、接口设计按功能模块分类
TIP
按照前面需求分析来看,可将 评论系统项目 接口设计分为两类
- 用户接口设计
- 评论接口设计
接下来就需要按照功能模块来编写接口设计文档
# 5、项目接口(路由)设计步骤
TIP
项目接口(路由)设计步骤有:需求分析、接口定义、参数与响应设计、权限与认证、接口文档编写、接口实现与测试
# 5.1、需求分析
TIP
- 深入理解项目需求,明确各个接口的功能和目的。
- 与产品经理、前端开发人员等相关人员沟通,确保对接口需求有准确的理解。
# 5.2、接口定义
TIP
为每个接口定义唯一的 URL 路径,例如:
- 注册接口:
POST /users/register
- 登录接口:
POST /users/login
- 修改密码接口:
PATCH /users
- 获取评论列表接口:
GET /comments
- 创建评论接口:
POST /comments
(使用 POST 方法) - 更新评论接口:
PUT /comments/:commentId
(使用 PUT 或 PATCH 方法) - 删除评论接口:
DELETE /comments/:commentId
(使用 DELETE 方法)
确定每个接口的请求方法(GET、POST、PUT、PATCH、DELETE 等)
# 5.3、参数与响应设计
TIP
- 为每个接口定义请求参数,包括参数名称、数据类型、是否必填、格式要求等。
- 定义接口的响应格式,包括成功和失败的情况,以及对应的 HTTP 状态码和数据结构。
# 5.4、权限与认证
TIP
- 根据项目需求,为每个接口设计合适的权限控制机制。
- 确定是否需要认证,如使用 JWT(JSON Web Tokens)进行用户身份验证。
# 5.5、接口文档编写
TIP
- 编写详细的接口文档,包括接口名称、路径、请求方法、请求参数、响应格式等信息。
- 使用易于理解的语言和格式,确保团队成员能够准确理解和实现接口功能。
# 5.6、接口实现与测试
TIP
- 根据接口设计文档,在后端实现相应的接口逻辑。
- 编写测试用例,对接口进行功能测试和性能测试,确保接口的正确性和稳定性。
# 6、接口设计文档
TIP
编写注册、登录、获取评论列表、创建评论、更新评论、删除评论接口设计相关文档
# 6.1、注册接口
TIP
- URL:
/users/register
- 请求方法:
POST
- 请求参数:
username
:用户名(必填,字符串,唯一)password
:密码(必填,字符串,加密存储)email
:邮箱(必填,字符串,格式验证)age
:年龄(非必填,数字)city
:所在城市(非必填,字符串)gender
:性别(非必填,数字, 0 - 保密<默认值>,1 - 男,2 - 女)phone
:手机号(非必填,字符串,验证手机号格式,必须是 11 位数字)email
:邮箱(非必填,字符串,验证邮箱格式)role
:角色(非必填,数字)限制 role 字段只能为 0 - 普通用户(默认) 或 1 - 管理员createdAt
:创建时间(时间戳,系统自动添加)updatedAt
:更新时间(时间戳,系统自动添加)
- 响应:
注册成功响应结果
{
"code": 0,
"message": "用户注册成功",
"data": {
"id": "新创建的用户ID",
"username": "arry老师"
}
}
注册失败响应结果
{
"code": "10003",
"message": "用户注册失败",
"data": ""
}
# 6.2、登录接口
TIP
- URL:
/users/login
- 请求方法:
POST
- 请求参数:
username
:用户名(必填,字符串)password
:密码(必填,字符串)
- 响应:
登录成功响应结果
{
"code": 0,
"message": "用户登录成功",
"data": {
"token": "登录令牌"
}
}
登录失败响应结果
{
"code": "10006",
"message": "登录失败",
"data": ""
}
# 6.3、创建评论接口
TIP
- URL:
/comments
- 请求方法:
POST
- 权限要求:用户登录
- 请求参数:
content
:评论内容(必填,字符串)username
:用户名(必填,字符串)createdAt
:创建时间(时间戳,系统自动添加)updatedAt
:更新时间(时间戳,系统自动添加)
- 响应:
创建评论成功响应结果
{
"code": 0,
"message": "创建评论成功",
"data": {
"id": "新创建的评论ID"
}
}
创建评论失败响应结果
{
"code": "10201",
"message": "创建评论失败",
"data": ""
}
未登录
{
"code": -2,
"message": "用户未登录,请登录后再试",
"data": {}
}
# 6.4、获取评论列表接口
TIP
- URL:
/comments
- 请求方法:
GET
- 权限要求:用户登录
- 请求参数:
page
:页码(可选,默认值为 1)size
:每页评论数(可选,默认值为 10)sort
:排序字段和顺序(可选,例如createTime:desc
表示按创建时间降序排序)
- 响应:
获取评论列表成功响应结果
{
"code": 0,
"message": "获取评论列表成功",
"data": {
"total": "评论总数",
"list": [
{
"commentId": "评论ID",
"content": "评论内容",
"userId": "用户ID",
"createTime": "创建时间"
}
// ...其他评论
]
}
}
获取评论列表失败响应结果
{
"code": "10202",
"message": "获取评论列表失败",
"data": ""
}
未登录
{
"code": -2,
"message": "用户未登录,请登录后再试",
"data": {}
}
# 6.5、更新评论接口
TIP
- URL:
/comments/:commentId
- 请求方法:
PUT
或PATCH
- 权限要求:用户登录(且通常是评论的创建者或具有编辑权限的用户)
- 请求参数:
content
:新的评论内容(可选)- 其他需要更新的字段(可选)
- 响应:
更新评论成功响应结果
{
"code": 0,
"message": "更新评论成功",
"data": {}
}
更新评论失败响应结果
{
"code": "10205",
"message": "编辑评论失败",
"data": ""
}
未登录或无权限
{
"code": -2,
"message": "用户未登录或无权限更新该评论",
"data": {}
}
# 6.6、删除评论接口
TIP
- URL:
/comments/:commentId
- 请求方法:
DELETE
- 权限要求:用户登录(且通常是评论的创建者或具有删除权限的用户)
- 请求参数:无
- 响应:
删除评论成功响应结果
{
"code": 0,
"message": "删除评论成功",
"data": {}
}
删除评论失败响应结果
{
"code": "10204",
"message": "删除评论失败",
"data": ""
}
未登录或无权限:
{
"code": -2,
"message": "用户未登录 或 无权限删除该评论",
"data": {}
}
评论不存在
{
"code": "10203",
"message": "该评论不存在,无效的ID",
"data": ""
}
注意事项:
- 在删除评论之前,建议检查评论的关联关系,例如是否有其他用户对该评论进行了回复或点赞,如果有,则可能需要先处理这些关联数据。
- 删除评论时,还需要考虑是否保留评论的历史记录,这取决于项目的具体需求。如果需要保留历史记录,可以将评论标记为已删除而不是物理删除。
- 在删除评论的操作中,要确保删除操作的原子性,避免因为操作失败而导致数据不一致的问题。
请根据项目实际情况和需求,对删除评论接口设计文档进行适当调整和补充。同时,也请确保在整个接口设计过程中,遵循项目的统一规范,确保接口的一致性和可维护性。
# 四、数据库设计
TIP
数据库设计目的:根据需求分析系统需要存储哪些数据,数据应该怎样存储
在整个后端项目开发过程中,数据库设计是一个关键环节,它直接影响到系统的性能和稳定性。因此,在进行数据库设计时,需要充分考虑数据的完整性、安全性以及可维护性等因素。同时,随着项目的进展和需求的变化,可能需要对数据库设计进行迭代和优化,以适应新的业务场景和需求。
数据库设计步骤
需求分析:准确了解和分析用户对数据库的需求,包括数据的存储、检索和处理需求。这是数据库设计的基础,对后续设计过程有决定性影响。
概念结构设计:根据需求分析的结果,通过抽象和归纳,形成独立于具体 DBMS 的概念模型。通常使用 E-R 图(实体-关系图)来描述数据与数据之间的关系。
逻辑结构设计:将概念模型转换为具体 DBMS 支持的数据模型,并对其进行优化。这通常涉及到表的设计、关系的定义以及约束的添加等。
物理结构设计:为逻辑数据模型选择适合的存储结构和存取方法,确保数据的存储和检索效率。
数据库实施:根据逻辑设计和物理设计的结果,使用 DBMS 提供的数据语言、工具及宿主语言建立数据库,编制与调试应用程序,组织数据入库,并进行试运行。
编写数据库设计文档:数据库设计文档是项目开发过程中的一个重要组成部分,它详细描述了数据库的结构、表的设计、关系定义、约束条件、索引策略、数据完整性和安全性、变更记录 等,为开发团队提供了清晰、准确的数据库设计指南。
# 1、MongoDB 创建数据库 和 集合
TIP
- 创建数据库
icoding-comment-system
- 创建集合
comments
,存储评论数据 - 创建集合
users
,存储用户数据
以及建立每个集合中根据实际需求对应的字段信息
# 2、Mongoose 定义 Schema 和 Model
TIP
- 使用 Mongoose 连接 和 操作 MongoDB 数据库
- 定义 Schema
- 定义 Model
在 src/model/user.model.js
中定义用户 Schema 和 模型
// 数据模型(规范数据格式)
const { Schema } = require("mongoose");
// 导入数据库连接对象
const mongoose = require("../db/db");
// 定义 Schema
const userSchema = new Schema(
{
username: {
type: String,
required: true, // 必填
unique: true, // 唯一,不重复
},
password: String,
age: Number,
city: String,
// 性别
gender: {
type: Number,
// 默认值
default: 0, // 0 - 保密,1 - 男,2 - 女
},
phone: {
type: String,
required: false,
validate: {
validator: function (v) {
// 这里使用正则表达式来验证手机号格式
// 假设你的手机号格式是11位数字
return /^\d{11}$/.test(v);
},
message: "手机号格式不正确,请输入11位数字",
},
},
email: {
type: String,
required: false,
validate: {
validator: function (v) {
// 这里使用正则表达式来验证邮箱格式
return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v);
},
message: "邮箱格式不正确",
},
},
// 角色
role: {
type: Number,
enum: [0, 1], // 枚举类型,限制 role字段只能为 0 - 普通用户(默认) 或 1 - 管理员
default: 0, // 默认值为 0,即普通用户
},
},
{
timestamps: true, // 时间戳,自动添加文档的创建时间和更新时间
}
);
// 创建模型
const User = mongoose.model("user", userSchema);
// console.log(User)
// 导出 User 对象
module.exports = User;
在 src/model/comment.model.js
中定义评论信息 Schema 和 模型
// 数据模型(规范数据格式)
const { Schema } = require("mongoose");
// 导入数据库连接对象
const mongoose = require("../db/db");
// 定义 Schema
const commentSchema = new Schema(
{
// 评论内容
content: {
type: String,
required: true, // 必填
},
username: String, // 用户名
},
{
timestamps: true, // 时间戳,自动添加文档的创建时间和更新时间
}
);
// 创建 comment 模型
const Comment = mongoose.model("comment", commentSchema);
// console.log(Comment)
// 导出 Comment 对象
module.exports = Comment;
# 五、开发环境准备
TIP
经过了需求分析、数据库设计、接口(路由)设计,复杂的大型项目还有架构的设计(架构设计分为四个部分:业务架构设计、应用架构设计、数据架构设计、技术架构设计)
# 1、初始化开发环境
注:
上图清楚了描述了本项目的目录 和 层级结构,我们任何的项目设计阶段都要能够清晰的画出来,能对项目有一览众山小的全局观。
- 路由:定义 URL 模式 与 处理程序(如控制器中的方法)之间的映射关系
- controller:在 Web 应用或 MVC(模型 - 视图 - 控制器)架构中,控制器(Controller)负责处理用户输入(如 HTTP 请求),并基于这些输入与模型(Model)和视图(View)进行交互。控制器决定使用哪个模型来处理请求,以及选择哪个视图来显示结果。
- Service:Service 是一种可以在后台执行长时间运行操作而没有用户界面的应用组件。它通常用于处理网络事务、执行文件 I/O 或 与内容提供程序交互等任务。Service 可以由其他应用组件启动,并在后台持续运行,即使启动它的组件已经销毁。简单说即:业务逻辑,操作数据库
- db:连接数据库
- 中间件:中间件是一种独立的系统软件服务程序,在项目用到 2 次 或 2 次以上的功能函数都可以拆分成中间件,实现可复用
- Model:在 MVC 架构中,模型(Model)代表应用的数据和业务逻辑。它通常与数据库交互,负责数据的存取和验证等操作。模型不关心数据的展示方式,它只关心数据本身和业务规则。当前项目中 Model 即 数据库字段对应的模型
# 2、规范目录 和 层级
观察下图中的箭头
注:
- 箭头指向从 路由 -> controller -> service -> db -> 数据库
- 发送 HTTP 请求,先经过路由处理 ,路由会将逻辑分发给 controller 层,再到 service 层完成业务逻辑处理,再到 db 连接数据库完成数据库的操作
comment-system
├─ .gitignore
├─ package-lock.json
├─ package.json
├─ README.md
└─ src
├─ app
│ ├─ errHandler.js
│ └─ index.js
├─ config
│ └─ config.default.js
├─ constant
│ └─ err.type.js
├─ controller
│ └─ user.controller.js
├─ db
│ └─ db.js
├─ main.js
├─ middleware
│ ├─ auth.middleware.js
│ └─ user.middleware.js
├─ model
│ ├─ comment.model.js
│ └─ user.model.js
├─ router
│ └─ user.route.js
└─ service
└─ user.service.js
# 3、为什么要分层设计
TIP
- 高内聚低耦合:分层设计使得每一层都专注于自己的职责,层与层之间的依赖关系清晰明了。每一层都尽可能独立,减少对其他层的依赖,从而实现了高内聚低耦合的目标。这样的设计有助于减少代码的复杂性,提高代码的可读性和可维护性。
- 便于开发和管理:分层设计使得项目的开发和管理更加有序。不同的开发团队可以专注于不同的层次进行开发,降低了团队协作的难度。同时,每一层都有明确的职责和接口,方便进行单元测试和功能验证。
- 易于扩展和维护:随着项目的不断发展和变化,可能需要添加新的功能或修改现有的功能。分层设计使得这些变化可以在特定的层次中进行,而不会影响到其他层次。这使得项目的扩展和维护变得更加容易。
- 提高系统性能:通过分层设计,我们可以根据每一层的特性进行性能优化。例如,对于数据处理层,我们可以使用高效的算法和数据库操作来提高数据处理的速度;对于展示层,我们可以优化界面渲染和交互逻辑,提高用户体验。
- 安全性考虑:在某些项目中,安全性是一个非常重要的考虑因素。通过分层设计,我们可以将敏感的操作和数据访问限制在特定的层次中,并通过加密、权限控制等手段提高系统的安全性。
因此,分层设计是一种有效的项目结构设计方法,它有助于提高代码质量、降低开发难度、便于扩展和维护、提高系统性能以及增强安全性。
# 4、连接数据库
# 六、评论系统项目开发实践
TIP
- 用户注册,密码修改
- 用户登录,用户认证
- 创建评论信息
- 获取评论列表
- 删除评论信息
- 更新评论信息
关于用户注册、登录 和 相关功能前面已经完成,接下来只需要完成评论相关功能即可
# 1、创建评论信息
TIP
开发过程可以按照下图的箭头顺序来书写
- 定义创建评论信息路由
- Controller 控制器
- 权限认证中间件(校验必须登录才能创建评论)
- Service 操作数据库
- postman 测试创建评论信息接口
创建 src/router/comment.route.js
评论路由相关文件
// 评论功能的路由
// 导入 koa-router
const Router = require("koa-router");
// 导入登录验证中间件
const { auth } = require("../middleware/auth.middleware");
// 导入 createComment 方法
const { create } = require("../controller/comment.controller");
// 实例化 router 路由对象
// 给路由设置一个统一的前缀
const router = new Router({ prefix: "/comments" });
// 创建评论
// auth 登录验证中间件
// create 创建评论的 Controller
// 方式一
// router.post('/create', auth, create)
// 或 方式二
router.post("/", auth, create);
// 导出 Router 对象,这样其他模块就可以引入并使用这个路由对象了
module.exports = router;
在 src/app/index.js
中,注册评论路由
const Koa = require("koa");
const { koaBody } = require("koa-body");
const errHandler = require("./errHandler");
const userRouter = require("../router/user.route");
// 导入评论路由模块
const commentRouter = require("../router/comment.route");
const app = new Koa();
app.use(koaBody());
app.use(userRouter.routes());
// 注册 评论路由
app.use(commentRouter.routes());
app.on("error", errHandler);
module.exports = app;
创建 src/controller/comment.controller.js
评论控制器
/**
* 评论信息 Controller
* @author arry老师
* @version V1.0
*/
class CommentController {
// 创建评论
async create(ctx, next) {
// 1、获取数据
// 获取 auth 中间件中将验证后的用户数据,其中的 username
const username = ctx.state.user.username;
// 评论数据
const { content } = ctx.request.body;
console.log(content);
console.log("username:" + username);
// 2、操作数据库
// 3、返回结果
ctx.body = {
status: 0,
message: "创建评论成功",
data: {},
};
}
}
// 导出 CommentController 实例(其他模块可以通过 require 导入这个实例,并调用其方法)
module.exports = new CommentController();
启动运行
npm run dev
先登录,然后在 postman 中创建评论(方式一)
或(方式二)
VSCode 控制台打印输出 获取到的数据
创建 src/service/comment.service.js
用于编写评论相关操作数据库逻辑
// 导入 Comment 模型
const Comment = require("../model/comment.model");
/**
* 评论信息 Service
* @author arry老师
* @version V1.0
*/
class CommentService {
/**
* 创建评论,操作数据库
* @param { String } username 用户名
* @param { String } content 评论内容
* @returns 评论信息
*/
async createComment(username, content) {
return await Comment.create({ username, content });
}
}
module.exports = new CommentService();
在 src/controller/comment.controller.js
中,调用 service
中的 createComment
方法
// 导入 Service 中的方法
const { createComment } = require("../service/comment.service");
// 导入统一返回错误信息
const { createCommentError } = require("../constant/err.type");
/**
* 评论信息 Controller
* @author arry老师
* @version V1.0
*/
class CommentController {
// 创建评论
async create(ctx, next) {
// 1、获取数据
// 获取 auth 中间件中将验证后的用户数据,其中的 username
const username = ctx.state.user.username;
// 评论数据
const { content } = ctx.request.body;
// console.log(content)
// console.log("username:" + username)
// 2、操作数据库
try {
const newComment = await createComment(username, content);
// 3、返回结果
ctx.body = {
code: 0,
message: "创建评论成功",
data: newComment,
};
} catch (err) {
console.error("创建评论失败", err);
ctx.app.emit("error", createCommentError, ctx);
}
}
}
// 导出 CommentController 实例(其他模块可以通过 require 导入这个实例,并调用其方法)
module.exports = new CommentController();
改写 src/constant/err.type.js
,新增 createCommentError
统一错误处理
createCommentError: {
code: '10201',
message: '创建评论失败',
data: ''
}
在 postman 中测试(方式一)
或(方式二)
# 2、获取评论列表
TIP
- 定义获取评论列表路由
- Controller 控制器
- 权限认证中间件(校验必须登录才能获取评论列表)
- Service 操作数据库
- postman 测试分页查询接口
在 src/router/comment.route.js
中,定义获取评论列表路由
// 导入 createComment,findList 方法
const { create, findList } = require("../controller/comment.controller");
// 获取评论列表
// auth 登录验证中间件
// findList 获取评论列表的 Controller
// 方式一(http://localhost:8000/comments/list?pageNum=1&pageSize=2):
// router.get('/list', auth, findList)
// 方式二(http://localhost:8000/comments/1/2):
router.get("/:pageNum/:pageSize", auth, findList);
注:
路由地址的定义,不仅仅要遵守 RESTful API 的风格,更要考虑 SEO 搜索引擎优化。相比方式一,方式二更有利于 SEO 优化。
在 src/controller/comment.controller.js
Controller 控制器 中 定义获取评论列表的方法 findList
// 获取评论列表(分页查询)
async findList(ctx, next) {
// 1、获取数据(方式一)
// 解析 pageNum 和 pageSize
// const { pageNum = 1, pageSize = 10 } = ctx.request.query
// console.log("pageNum:" + pageNum, "pageSize:" + pageSize)
// 1、获取数据(方式二)
// parseInt 函数用于将一个字符串转换为整数。这个函数接受两个参数:要转换的字符串和基数(或称为进位制),10 表示将字符串视为一个十进制数来解析
const pageNum = parseInt(ctx.params.pageNum, 10)
const pageSize = parseInt(ctx.params.pageSize, 10)
console.log("pageNum:" + pageNum, "pageSize:" + pageSize)
// 2、操作数据库
// 3、返回结果
ctx.body = {
code: 0,
message: '获取评论列表成功',
data: {}
}
}
启动运行
npm run dev
先登录,然后在 postman 中访问路由(方式一)
postman 中访问路由(方式二)
VSCode 控制台打印输出 获取到的请求参数
在 src/service/comment.service.js
中,编写评论列表相关操作数据库逻辑
/**
* 获取评论列表(分页查询)
* @param { Number } pageNum 请求的页码,默认值 1
* @param { Number } pageSize 每页显示的文档数量, 默认值 10
* @returns 评论列表信息
*/
async getCommentList(pageNum, pageSize) {
// 计算需要跳过的文档数量
const skip = (pageNum - 1) * pageSize;
// 执行查询并获取数据
// sort({ createdAt: -1 }) 按创建时间降序排列
return await Comment.find().sort({ createdAt: -1 }).skip(skip).limit(pageSize).exec();
}
在 src/controller/comment.controller.js
中,调用 service
中的 getCommentList
方法
// 导入 Service 中的方法
const { createComment, getCommentList } = require('../service/comment.service')
// 导入统一返回错误信息
const { createCommentError, getCommentListError } = require('../constant/err.type')
// 获取评论列表(分页查询)
async findList(ctx, next) {
// 1、获取数据(方式一)
// 解析 pageNum 和 pageSize
// const { pageNum = 1, pageSize = 10 } = ctx.request.query
// console.log("pageNum:" + pageNum, "pageSize:" + pageSize)
try {
// 1、获取数据(方式二)
// parseInt 函数用于将一个字符串转换为整数。这个函数接受两个参数:要转换的字符串和基数(或称为进位制),10 表示将字符串视为一个十进制数来解析
const pageNum = parseInt(ctx.params.pageNum, 10)
const pageSize = parseInt(ctx.params.pageSize, 10)
console.log("pageNum:" + pageNum, "pageSize:" + pageSize)
// 2、操作数据库
const commentList = await getCommentList(pageNum, pageSize)
// 3、返回结果
ctx.body = {
code: 0,
message: '获取评论列表成功',
data: commentList
}
} catch (err) {
console.error('获取评论列表失败', err)
ctx.app.emit('error', getCommentListError, ctx)
}
}
改写 src/constant/err.type.js
,新增 getCommentListError
统一错误处理
getCommentListError: {
code: '10202',
message: '获取评论列表失败',
data: ''
}
postman 测试获取评论列表(方式一)
URL 请求地址:
http://localhost:8000/comments/list?pageNum=1&pageSize=2
postman 测试获取评论列表(方式二)
URL 请求地址:
http://localhost:8000/comments/1/2
# 3、删除评论信息
TIP
需求:只能删除自己的评论信息,无权操作他人的评论
- 定义删除评论列表路由
- Controller 控制器
- 权限认证中间件(校验必须登录才能删除评论)
- Service 操作数据库
- postman 测试根据 id 删除评论接口
在 src/router/comment.route.js
中,定义删除评论列表路由
// 导入 createComment,findList,remove 方法
const {
create,
findList,
remove,
} = require("../controller/comment.controller");
// 删除评论
// auth 登录验证中间件
// remove 删除评论的 Controller
// 方式一
// router.post('/del', auth, remove)
// 方式二
router.delete("/:commentId", auth, remove);
注:
删除评论路由,也可以使用 delete
请求方法,即:router.delete('/del', auth, remove)
在 src/controller/comment.controller.js
Controller 控制器 中 定义删除评论的方法 remove
// 删除评论
async remove(ctx, next) {
// 1、获取数据
// 解析 _id,根据 id 删除用户评论
// 方式一
// const { _id } = ctx.request.body
// 方式二
// 获取 URL 中的 commentId
const _id = ctx.params.commentId
// 获取 auth 中间件中将验证后的用户数据,其中的 username
const username = ctx.state.user.username
console.log("_id:" + _id, "username:" + username)
// 2、操作数据库
// 3、返回结果
ctx.body = {
code: 0,
message: '删除评论成功',
data: {}
}
}
启动运行
npm run dev
先登录,然后在 postman 中访问删除路由(同步 DELETE
请求),方式一
方式二
VSCode 控制台打印输出 获取到的请求参数
在 src/service/comment.service.js
中,编写删除评论相关操作数据库逻辑
/**
* 根据 id 删除评论信息,且 只能删除自己的评论信息,无权操作他人的评论
* @param { String } _id 评论信息 id
* @param { String } username 用户名
* @returns 删除影响的记录数,大于 0 为 true,小于 0 为 false
*/
async removeCommentById(_id, username) {
// deleteOne方法用于删除与指定查询条件匹配的第一个文档
// 删除当前登录用户 对应 id 的评论信息(即:只能删除自己的评论信息,无权操作他人的评论)
// _id 和 username 两个条件同时满足时,才能删除该评论信息(就保证了只能删除自己的评论)
const res = await Comment.deleteOne({ _id, username })
// 返回值为代码改动记录数
return res.deletedCount > 0 ? true : false;
}
在 src/controller/comment.controller.js
中,调用 service
中的 removeCommentById
方法
// 导入 Service 中的方法
const {
createComment,
getCommentList,
removeCommentById } = require('../service/comment.service')
// 导入统一返回错误信息
const {
createCommentError,
getCommentListError,
invalidCommentID,
deleteCommentError } = require('../constant/err.type')
// 删除评论
async remove(ctx, next) {
// 1、获取数据
// 解析 _id,根据 id 删除用户评论
// 方式一
// const { _id } = ctx.request.body
// 方式二
// 获取 URL 中的 commentId
const _id = ctx.params.commentId
// 获取 auth 中间件中将验证后的用户数据,其中的 username
const username = ctx.state.user.username
// console.log("_id:" + _id, "username:" + username)
try {
// 2、操作数据库
const res = await removeCommentById(_id, username)
if (res) {
// 3、返回结果
ctx.body = {
code: 0,
message: '删除评论成功',
data: {}
}
} else {
console.error('该评论不存在,无效的 ID')
return ctx.app.emit('error', invalidCommentID, ctx)
}
} catch (err) {
console.error('删除评论失败', err)
return ctx.app.emit('error', deleteCommentError, ctx)
}
}
先登录,然后在 postman 中访问删除路由。根据当前登录用户 对应的 评论信息 id 可成功删除(即:能删除自己的评论信息)
方式一
删除非当前登录用户 对应的 评论信息(即:不能删除别人的评论信息)
方式一
注:
方式二,严格按 RESTful API 风格,评论 id 在 URL 地址栏中,无需 body 传递参数,请求方式为 DELETE
# 4、编辑评论信息
TIP
- 定义编辑评论路由
- Controller 控制器
- 权限认证中间件(校验必须登录才能编辑评论)
- Service 操作数据库
- postman 测试编辑评论接口
在 src/router/comment.route.js
中,定义编辑评论路由
// 导入 createComment,findList,remove,update 方法
const {
create,
findList,
remove,
update,
} = require("../controller/comment.controller");
// 编辑评论
// auth 登录验证中间件
// update 编辑评论的 Controller
router.put("/", auth, update);
在 src/controller/comment.controller.js
Controller 控制器 中 定义编辑评论的方法 update
// 编辑评论
async update(ctx, next) {
// 1、获取数据
// 解析 _id,根据 id 编辑评论信息
// content 编辑后的评论信息
const { _id, content } = ctx.request.body
// 同时,只能编辑自己的评论信息,无权编辑别人的评论
// 获取 auth 中间件中将验证后的用户数据,其中的 username
const username = ctx.state.user.username
console.log("_id:" + _id, "username:" + username, "content:" + content)
// 2、操作数据库
// 3、返回结果
ctx.body = {
code: 0,
message: '编辑评论成功',
data: {}
}
}
启动运行
npm run dev
先登录,然后在 postman 中访问编辑评论路由
VSCode 控制台打印输出 获取到的请求参数
在 src/service/comment.service.js
中,编写编辑评论相关操作数据库逻辑
/**
* 根据 id 修改评论信息,且 只能修改自己的评论信息,无权操作他人的评论
* @param { String } _id 评论 id
* @param { String } username 当前登录的用户名
* @param { String } content 修改后的评论内容
* @returns newData 返回更新后的评论
*/
async updateCommentById(_id, username, content) {
// 修改当前登录用户 对应 id 的评论信息(即:只能修改自己的评论信息,无权操作他人的评论)
const newData = await Comment.findOneAndUpdate(
{ _id, username }, // 修改的条件(只能修改自己的评论信息,无权操作他人的评论)
{ content }, // 修改的内容
{ new: true } // 返回更新之后的最新评论
)
// 返回更新后的评论
return newData
}
在 src/controller/comment.controller.js
中,调用 service
中的 updateCommentById
方法
// 导入 Service 中的方法
const {
createComment,
getCommentList,
removeCommentById,
updateCommentById } = require('../service/comment.service')
// 导入统一返回错误信息
const {
createCommentError,
getCommentListError,
invalidCommentID,
deleteCommentError,
updateCommentError,
invalidCommentIdAndAuth } = require('../constant/err.type')
// 编辑评论
async update(ctx, next) {
// 1、获取数据
// 解析 _id,根据 id 编辑评论信息
// content 编辑后的评论信息
const { _id, content } = ctx.request.body
// 同时,只能编辑自己的评论信息,无权编辑别人的评论
// 获取 auth 中间件中将验证后的用户数据,其中的 username
const username = ctx.state.user.username
// console.log("_id:" + _id, "username:" + username, "content:" + content)
try {
// 2、操作数据库
const newData = await updateCommentById(_id, username, content)
if (newData) {
// 3、返回结果
ctx.body = {
code: 0,
message: '编辑评论成功',
data: newData
}
} else {
console.error('该评论不存在 或 无权编辑')
return ctx.app.emit('error', invalidCommentIdAndAuth, ctx)
}
} catch (err) {
console.error('编辑评论失败', err)
return ctx.app.emit('error', updateCommentError, ctx)
}
}
先登录,然后在 postman 中访问修改路由。根据当前登录用户 对应的 评论信息 id 可成功完成修改(即:能修改自己的评论信息)
修改非当前登录用户 对应的 评论信息(即:不能修改别人的评论信息)
注:
按 RESTful API 风格,编辑评论直接使用 PUT
请求,URL 地址使用 http://localhost:8000/comments
即可
postman 中相关 API 接口测试
# 七、服务端项目部署 与 实践
TIP
深入浅出 Vue 前后端分离项目(API 接口)云服务器部署 与 实践、Vue 前端项目的云服务器部署、性能优化 与 最佳实践。
# 1、API 接口服务端项目的云部署
TIP
- 云服务器购买 与 配置
- Linux 下 Node 后端服务部署
- 后端服务 Nginx 端口转发与部署
- 使用 PM2 管理进程
- Nginx 配置 HTTPS 加密协议
详细查阅 服务端项目部署 与 实践 (opens new window),相关图文教程
# 2、Vue 前端项目的云部署
TIP
- 云服务器 与 Nginx 基础配置
- Linux 下 Vue 前端项目部署
- 阿里云域名注册 与 解析
- Nginx 部署 Vue 项目
- Nginx 配置 HTTPS 加密协议
- Nginx 性能优化
- 多平台、多系统部署
详细查阅 Vue 前端项目的云服务器部署、性能优化 与 最佳实践 (opens new window),相关图文教程
# 八、前后端联调
TIP
深入浅出前后端联调的步骤 和 注意事项,联调的过程,遇到 Bug 后排查过程等。
# 1、联调时间节点
下图为前后端分离的开发过程
注:
前后端分离开发的过程,需求评审、接口设计、前端和后端各自开发(两个团队同步进行),各自开发结束后完成自测,最后进入前后端联调。
# 2、开始联调
TIP
- 启动前端项目服务
- 启动后端项目服务,保证前后端都能运行成功
- 前端 baseURL 指向后端 或 切换至测试环境、线上测试环境等
# 3、联调步骤
TIP
- 测试主要的功能
- 看各个 API 的返回,是否符合预期
- 浏览器控制台是否报错、服务端控制台是否报错
# 4、注意事项
TIP
- 联调不是测试,能跑通主要流程即可,不可陷入细节
- 联调的目的是为了打通前后端,不可能像专业的测试人员一样,将每个细节都测试的非常完善(抓住主要矛盾即可,测试有专业的测试人员负责)
- 遇到 Bug 不要慌,顺藤摸瓜找原因即可
- 如自己找不到原因,可以找 前端/后端 开发人员一起排查(多方协同)
大厂最新技术学习分享群
微信扫一扫进群,获取资料
X