Express.js 入门构建 MVC 示例
前言
延续上一篇 Express.js 入门文章:Express.js 入门造个简单的增删查改待办事项 中 Express 提供便捷优雅的 API 让我们接收请求经过处理后回应,但随着规模增大,路径、商业逻辑、数据……等代码都塞在一个 app.js
实在不是好做法,本文介绍使用 MVC 架构进行代码切割,方便维护。
什么是 MVC
MVC(Model-View-Controller)是一种广泛应用的开发架构模式,将应用程序的逻辑分为三个主要部分:
- Model(模型):负责处理与数据相关逻辑。管理应用程序数据业务逻辑,并且和数据库进行交互。
- View(视图):负责显示数据,管理数据展示,通常不会进行任何业务逻辑处理。它的作用是将从 Model 获取的数据以适当的格式呈现出来。
- Controller(控制器):负责处理用户的请求并且决定如何回应。管理 Model 和 View,接收来自 View 的请求,并通过 Model 进行处理,然后再将结果返回给 View。简单来说,Controller 控制了数据的流动,并负责应用程序的商业逻辑。
通过这种方式,MVC 架构能够实现代码的清晰分离,使得不同部分的功能更加专注且可独立维护。这样一来,开发人员可以更轻松地进行开发、测试和维护,尤其是在应用程序规模变大时。
了解现有问题
举例现有的 app.js
当中:「根据 ID 编辑 Todo 代码片段」,会发现所有逻辑是参杂在一起的,像是未来与路径(Route)、商业逻辑(Controller)、DB 沟通(Model)……等,我们是否能依照 MVC 模式为参考拆解出不同职责的代码方便未来管理呢?
// Edit {id} Todo app.put("/api/todos/:id", (req, res) => { const targetId = req.params.id const targetIndex = todos.findIndex(todo => todo.id === targetId); const isTargetTodoExist = targetIndex !== -1
if (!isTargetTodoExist) { return res.status(404).json({ error: "Todo not found" }); }
const updatedTodo = { ...todos[targetIndex], title: req.body.title ?? todos[targetIndex].title, isCompleted: req.body.isCompleted ?? todos[targetIndex].isCompleted }
const newTodos = [ ...todos.slice(0, targetIndex), updatedTodo, ...todos.slice(targetIndex + 1), ];
todos = newTodos res.json(updatedTodo) })
一、划分不同职责代码档案
举例一个待办事项的 API 个别用资料夹分隔管理不同职责的代码,命名没有一定,也有的人会命名成 todos.model.js
、todos.controller.js
的模式,方便区分用途即可:
.├── routes/│ └── todos.js├── models/│ └── todos.js├── controllers/│ └── todos.js└── app.js
二、Routes
import todoRouter from './routes/todos.js';
app.use('/api/todos', todoRouter);
并且在最初 app.js
注册 routes/todos.js
相关的路径 Middleware,具体来说使用 express.Router,如此一来所有 todoRouter
都会前缀 /api/todos
路径。
import todoController from '../controllers/todos.js';
const router = express.Router();
// Edit {id} Todorouter.put('/:id', todoController.editTodo); // /api/todos/:id
export default router;
三、Controllers
简单输出一个包含 editTodo
方法的对象可被调用:
import todoModel from '../models/todos.js';
export default { editTodo: (req, res) => { const targetId = req.params.id; const updatedTodo = todoModel.edit(targetId, { title: req.body.title, isCompleted: req.body.isCompleted, }); if (!updatedTodo) { return res.status(404).json({ error: 'Todo not found' }); } return res.status(200).json(updatedTodo); },};
四、Models
简单输出一个包含 edit
方法的对象可被调用,并且主要与数据库进行互动(此范例没有用到数据库,以程序执行内变量为例):
let todos = [];
export default { edit: (id, newTodo) => { const targetIndex = todos.findIndex((todo) => todo.id === id); const isTargetTodoExist = targetIndex !== -1; if (!isTargetTodoExist) return null; const updatedTodo = { ...todos[targetIndex], title: newTodo.title ?? todos[targetIndex].title, isCompleted: newTodo.isCompleted ?? todos[targetIndex].isCompleted, }; const newTodos = [...todos.slice(0, targetIndex), updatedTodo, ...todos.slice(targetIndex + 1)]; todos = newTodos; return updatedTodo; },};
总结
通过本文范例,我们将一个集中式的 Express 应用程序重构为遵循 MVC 架构的模块化设计,清晰地划分了数据层(Model)、业务逻辑层(Controller)、与路由层(Route)之间的责任。这样的设计不仅提升了代码的可读性和可维护性,还为团队协作和项目扩展奠定了良好的基础。
- 单一职责:每个模块专注于完成其特定的功能,避免了代码混杂问题。
- 易于测试:每层模块都可以被单独测试,降低了测试的复杂性。
- 模块化开发:团队成员可以独立开发 Model、Controller 或 Route,提升开发效率。
- 良好的扩展性:随着应用程序规模增长,MVC 结构使得新增功能和优化更加直观和可控。
虽然这是适合初学者的简化范例,但在实际项目中,还可能需考虑数据验证、安全性、错误处理以及多层架构(如服务层)的设计。
延伸阅读
- 27 分钟示范 MVC 架构|六角学院| 2023 铁人赛 #3 - 六角学院 Hexschool
- Project structure for an Express REST API when there is no “standard way” - Corey Cleary