Skip to content

MVC 模型与 Service 层

在使用 MVC(Model-View-Controller)模型的应用中,Express 路由和你提供的示例代码可以与 MVC 模式结合来组织代码结构,使其更加模块化和可维护。MVC 模式将应用程序的主要逻辑分成三个核心部分:Model(模型)、View(视图)、Controller(控制器),每个部分有不同的职责。

1. MVC 模式的作用和分工

  1. Model(模型)

    • 代表应用程序的数据和业务逻辑。
    • 与数据库或其他数据源进行交互,获取或修改数据。
    • 不直接处理 UI 或用户输入。
  2. View(视图)

    • 负责呈现数据(通常是 HTML、JSON 等格式)。
    • 将从模型获取到的数据展示给用户。
    • 在 Express 中,视图通常由模板引擎(如 EJS、Pug 等)生成 HTML 页面,也可以是 JSON 格式的数据响应。
  3. Controller(控制器)

    • 负责处理用户输入,调用模型并更新视图。
    • 负责路由逻辑的组织。路由会将请求发送到控制器,控制器再与模型交互并返回结果。

2. Express 中如何应用 MVC 模式?

1. Model(模型)

模型负责与数据源(如数据库)交互,获取、更新或删除数据。

例如,我们可以创建一个 UserModel 来处理用户相关的数据:

javascript
// models/User.js
const users = [
  { id: "1", name: "Alice" },
  { id: "2", name: "Bob" },
];

function getUserById(id) {
  return users.find((user) => user.id === id);
}

module.exports = { getUserById };

在这个模型中,getUserById 函数负责根据传入的 id 返回一个用户对象。

2. Controller(控制器)

控制器是路由的处理函数,负责接收用户请求,调用模型的相关方法,处理数据,并决定返回的视图或数据格式。

javascript
// controllers/userController.js
const UserModel = require("../models/User");

function getUser(req, res) {
  const userId = req.params.id;
  const user = UserModel.getUserById(userId);

  if (user) {
    res.json(user); // 返回 JSON 格式的用户数据
  } else {
    res.status(404).send("User not found");
  }
}

module.exports = { getUser };

在这个控制器中,getUser 函数会接收请求的路径参数(req.params.id),并调用 UserModel.getUserById 来获取用户数据。之后它决定是返回用户的 JSON 数据,还是返回 404 错误。

3. View(视图)

视图是用户看到的页面,在 Express 中,视图通常是通过模板引擎(如 EJS、Pug)来渲染的,或者可以直接返回 JSON 格式的数据。

如果我们使用 EJS 来渲染视图,那么我们可以创建一个视图文件,比如 user.ejs,用来展示用户的信息:

html
<!-- views/user.ejs -->
<h1>User Information</h1>
<p>ID: <%= user.id %></p>
<p>Name: <%= user.name %></p>

然后在控制器中渲染这个视图:

javascript
// controllers/userController.js
const UserModel = require("../models/User");

function getUser(req, res) {
  const userId = req.params.id;
  const user = UserModel.getUserById(userId);

  if (user) {
    res.render("user", { user }); // 渲染视图并传递用户数据
  } else {
    res.status(404).send("User not found");
  }
}

module.exports = { getUser };

在这个例子中,res.render('user', { user }) 会渲染 user.ejs 视图并传递 user 数据。

4. Router(路由器)

在 Express 中,路由会连接到控制器并通过 HTTP 请求触发相应的控制器方法。

javascript
// routes/userRoutes.js
const express = require("express");
const router = express.Router();
const userController = require("../controllers/userController");

router.get("/users/:id", userController.getUser); // 绑定路由和控制器方法

module.exports = router;

路由文件将请求传递给控制器中的适当方法。例如,GET /users/:id 请求会调用 userController.getUser 方法。

5. 应用程序入口

在应用程序的入口文件(如 app.jsserver.js)中,我们需要设置路由、控制器和视图引擎等。

javascript
// app.js
const express = require("express");
const app = express();
const userRoutes = require("./routes/userRoutes");

app.set("view engine", "ejs"); // 设置视图引擎
app.use(express.json()); // 解析 JSON 请求体

// 使用路由
app.use(userRoutes);

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

3. 整个流程

  1. 请求: 用户请求 GET /users/:id,此时 Express 路由会匹配到 /users/:id 路径,调用相应的控制器方法(userController.getUser)。

  2. 控制器: 控制器中的方法 getUser 从请求中提取路径参数 id,然后通过模型 (UserModel.getUserById(id)) 获取数据。

  3. 模型UserModel.getUserById(id) 查询数据(比如数据库查询),返回用户信息。

  4. 视图: 如果成功找到用户,控制器可以通过 res.render() 渲染视图,或者直接通过 res.json() 返回 JSON 数据。

  5. 响应: 控制器将数据发送给客户端,客户端接收并处理响应(例如渲染页面或显示 JSON 数据)。

4. 总结

  • Model:负责数据的处理和存取(数据库查询、业务逻辑等)。
  • View:负责展示数据(通常是 HTML 或 JSON)。
  • Controller:协调 Model 和 View,处理用户请求,执行业务逻辑,返回数据或渲染视图。

通过这种方式,MVC 模型将应用的逻辑清晰地分离,使得代码更加模块化、可维护和易于扩展。在实际开发中,应用 MVC 模式有助于让不同的开发人员专注于各自的职责,例如前端开发专注于视图和用户交互,后端开发专注于业务逻辑和数据库交互。

5. Service 服务层

MVC 模式之外,Service(服务) 通常被作为一个独立的层,用于处理更复杂的业务逻辑、跨多个模型的操作、或提供给多个控制器共享的功能。它通常位于 ControllerModel 之间,但它并不直接属于 MVC 模式的三大核心部分(Model、View、Controller),而是作为一种 服务层 来组织和抽象业务逻辑。

典型的应用场景:

  • Controller:接收用户请求,验证数据(如果需要),并调用 Service 层的方法来处理业务逻辑,最终返回结果。
  • Service:执行实际的业务逻辑,可能涉及到一个或多个 Model,并处理跨模型的数据整合、外部 API 调用等。
  • Model:负责与数据库的交互,处理数据的存取操作。

Service 层的优点

  1. 简化 Controller
    • 将复杂的业务逻辑提取到 Service 层之后,Controller 只需要处理请求和响应的部分,业务逻辑可以独立测试和管理。
  2. 提高代码可维护性
    • Service 层可以集中管理与业务相关的逻辑,避免多个控制器重复代码,增强系统的可维护性。
  3. 可复用性
    • Service 层的逻辑可以被多个控制器复用,无需在每个控制器中都写一遍相同的代码。
  4. 解耦外部依赖
    • 服务层可以帮助将外部依赖(如第三方 API 调用、支付网关等)与业务逻辑解耦,减少 Controller 中的复杂性。
  5. 更易于测试
    • Service 层的业务逻辑可以独立于网络请求和响应进行单元测试。这样可以避免测试中对网络、数据库等外部依赖的强耦合。

举个例子:用户注册

假设我们有一个用户注册的业务逻辑,在不使用 Service 层的情况下,所有的业务逻辑都会写在 Controller 中。

没有 Service 层的代码(较为简单):

javascript
// controllers/userController.js
const UserModel = require("../models/User");

function registerUser(req, res) {
  const { username, password, email } = req.body;

  // 这里写的很多验证和处理逻辑本应放在 Service 层
  if (!username || !password || !email) {
    return res.status(400).send("Missing required fields");
  }

  // 假设我们通过 UserModel 来插入数据库
  UserModel.create({ username, password, email })
    .then((user) => {
      res.status(201).send(user);
    })
    .catch((err) => {
      res.status(500).send("Internal server error");
    });
}

这个代码中,所有的验证、数据处理和数据库交互都在 Controller 中实现。当业务变得复杂时,这种做法会导致 Controller 代码冗长且难以维护。

使用 Service 层的代码(更具结构化):

javascript
// services/userService.js
const UserModel = require("../models/User");

function validateUserData(username, password, email) {
  if (!username || !password || !email) {
    throw new Error("Missing required fields");
  }
}

function createUser(username, password, email) {
  return UserModel.create({ username, password, email });
}

module.exports = { validateUserData, createUser };
javascript
// controllers/userController.js
const userService = require("../services/userService");

function registerUser(req, res) {
  const { username, password, email } = req.body;

  try {
    userService.validateUserData(username, password, email);
    userService
      .createUser(username, password, email)
      .then((user) => {
        res.status(201).send(user);
      })
      .catch((err) => {
        res.status(500).send("Internal server error");
      });
  } catch (error) {
    res.status(400).send(error.message);
  }
}

在这种情况下,Controller 中只处理请求和响应,业务逻辑被移到了 Service 层。

Service 层 并不是 MVC 模式中的标准组成部分,但它通常被引入来解耦和简化业务逻辑。在 MVC 模式中,Controller 负责处理用户输入和输出,而 Service 层则负责处理和封装核心业务逻辑。通过使用 Service 层,可以使代码更加模块化、可维护,避免 Controller 中业务逻辑的臃肿。

6. MC 是什么?

如果我们不用 view,是不是可以直接把 Data 数据返回给前端,让前端渲染,那后端的逻辑是不是只有 MVC 中的 MC 了??

如果你不使用 View(视图)层,而是将数据直接返回给前端,让前端进行渲染,那么后端的逻辑确实可以简化为 Model 和 Controller,即 MC(Model-Controller)结构。在这种情况下,后端的主要职责是处理数据(Model)和接收请求、返回响应(Controller)。

常见的后端 API 结构(MC)

这种结构特别适用于现代的前后端分离应用,其中:

  • 后端:提供 RESTful API 或 GraphQL API,处理数据逻辑和与数据库的交互。
  • 前端:通过 API 获取数据,并使用前端框架进行渲染。