纯后端套模板:Node.js 实现 MVC 架构的简易用户列表服务

43 阅读8分钟

在前端框架大行其道的今天,我们不妨回溯一下 Web 开发的“本源”——纯后端渲染(Server-Side Rendering, SSR) 。本文将带你用 Node.js 原生 http 模块实现一个符合 MVC 架构 的简单用户列表页面,并深入解析其中的关键知识点,如 CommonJS 与 ESM、数据驱动、HTTP 响应头、状态码等。


一、什么是 MVC?

MVC(Model-View-Controller)是一种经典的软件架构模式,用于分离关注点:

  • Model(模型) :负责数据逻辑,比如数据库操作。
  • View(视图) :负责展示界面,通常是 HTML 模板。
  • Controller(控制器) :协调 Model 和 View,处理业务逻辑。

在纯后端渲染中,服务器直接生成完整的 HTML 页面返回给浏览器,无需前端 JavaScript 渲染。


二、代码实现:从零搭建一个 MVC 风格的用户列表服务

💡 为契合掘金排版习惯,以下代码均标注语言类型,并采用模块化讲解。

1. 目录结构(理想化 MVC 分离)

虽然示例中所有代码写在一个文件里(便于演示),但在真实项目中应分目录:

text
编辑
project/
├── models/
│   └── User.js          // Model
├── views/
│   └── users.html       // View(模板)
├── controllers/
│   └── userController.js // Controller
└── server.js            // HTTP 伺服入口

但为简化,我们先看单文件版本

2. 完整代码(server.js

javascript
编辑
// server.js - 使用 CommonJS 模块系统(Node.js 默认)
const http = require('http');
const url = require('url');

// ===== Model: 模拟数据库数据 =====
const users = [
  { id: 1, name: '舒老板', email: 'shubo@163.com' },
  { id: 2, name: '陈老板', email: '6666666' },
  { id: 3, name: '王老板', email: 'wang@.com' },
];

// ===== View: 模板函数(数据驱动 HTML 生成)=====
function generateUsersHtml(users) {
  const userRows = users.map(user => `
    <tr>
      <td>${user.id}</td>
      <td>${user.name}</td>
      <td>${user.email}</td>
    </tr>
  `).join('');

  return `
    
    
    
        
        
        User List
        
            table { width: 100%; border-collapse: collapse; margin-top: 20px; }
            th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
            th { background-color: #f4f4f4; }
        
    
    
        <h1>Users</h1>
        <table>
            <thead>
                <tr><th>ID</th><th>Name</th><th>Email</th></tr>
            </thead>
            <tbody>
                ${userRows}
            </tbody>
        </table>
    
    
  `;
}

// ===== Controller: 处理路由与业务逻辑 =====
function handleRequest(req, res) {
  const parsedUrl = url.parse(req.url, true);

  if (parsedUrl.pathname === '/' || parsedUrl.pathname === '/users') {
    // 设置响应头:状态码 + 内容类型
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html; charset=utf-8');

    // 调用 Model 获取数据,传入 View 渲染
    const html = generateUsersHtml(users);
    res.end(html); // 发送完整 HTML 响应
  } else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.end('Not Found');
  }
}

// ===== HTTP 伺服器启动 =====
const server = http.createServer(handleRequest);
server.listen(1234, () => {
  console.log('✅ Server is listening at http://localhost:1234');
});

运行命令:

bash
编辑
node server.js

访问 http://localhost:1234 即可看到用户表格。


三、核心知识点详解

1. 服务器回调函数:http.createServer()

http.createServer() 接收一个回调函数,每当客户端发起 HTTP 请求时,该函数就会被调用。其签名如下:

javascript
编辑
http.createServer((req, res) => {
  // 处理请求
});
  • req(IncomingMessage 对象) :代表客户端的请求,包含:

    • req.url:完整请求路径(如 /users?name=xxx
    • req.method:HTTP 方法(如 GETPOST
    • req.headers:请求头信息
    • 请求体(需监听 data 事件读取,适用于 POST 等)
  • res(ServerResponse 对象) :用于构建并发送响应,常用方法包括:

    • res.statusCode = 200:设置 HTTP 状态码
    • res.setHeader(key, value):设置响应头
    • res.write(chunk):写入响应体片段(可多次调用)
    • res.end([data])必须调用,结束响应并可选发送最后一段数据

⚠️ 注意:即使你只用 res.end(html) 一次性发送内容,也不能省略 res.end() ,否则连接不会关闭,浏览器会一直等待。


2. URL 解析:url.parse(req.url, true)

Node.js 内置的 url 模块可解析 URL 字符串:

javascript
编辑
const parsedUrl = url.parse(req.url, true);
  • 第二个参数 true 表示自动解析查询字符串(query string)为对象

  • 返回对象的核心属性:

    • pathname:路径部分(不包含查询参数)。
      例如:http://localhost:1234/users?id=1 → pathname = '/users'
    • query:查询参数对象(仅当第二个参数为 true 时存在)。
      例如:?name=舒老板&age=30 → query = { name: '舒老板', age: '30' }

✅ 在路由判断中,我们通常只关心 pathname,而 query 可用于过滤、分页等动态逻辑。


3. 响应头(Content-Type)详解

Content-Type 响应头告诉浏览器如何解释响应体内容:

说明
text/html;charset=utf-8按 HTML 文档解析,使用 UTF-8 编码(解决中文乱码的关键!
text/plain;charset=utf-8按纯文本显示,适合错误提示或简单消息
application/json返回 JSON 数据,常用于 API 接口

🌐 如果不指定 charset=utf-8,中文可能显示为乱码(如 刘者),因为浏览器默认可能使用 ISO-8859-1 或其他编码。


4. res.end() 的本质:不只是“返回给浏览器”

很多人误以为 res.end() 是“把内容直接返回给浏览器”,其实这是对底层机制的误解。

✅ 正确认知:

res.end() 并不是 “直接返回给浏览器”,而是 Node.js 的 http 模块中 ServerResponse 对象的核心方法,其本质是:

结束 HTTP 响应流程,并将响应数据(若有)通过 TCP 连接传输给客户端(浏览器是最常见的客户端,但也可以是 Postman、curl、其他微服务等)。

🔧 核心逻辑拆解:响应的传输路径

res.end() 被调用后,Node.js 会完成以下步骤:

  1. 组装 HTTP 响应头

    • 若你未显式设置某些头(如 Content-Length),Node.js 会自动计算并补充。
  2. 通过 TCP 连接发送数据

    • 将完整的 HTTP 响应(响应头 + 响应体)通过已建立的 TCP 连接发送给客户端。
  3. 关闭响应流

    • 标记该响应已完成,后续不能再调用 res.write() 或 res.end(),否则会抛出错误。

💬 简单说:res.end() 是 “收尾并发送” 的动作。浏览器最终接收到的是经过 TCP/IP 协议栈传输的 HTTP 响应数据包,而不是 res.end() 直接“返回”了什么。

⚙️ res.end() 的关键特性

特性说明
必须调用HTTP 响应必须通过 res.end() 结束,否则客户端会一直等待(直到超时)
可选参数签名:res.end([data[, encoding]][, callback]) • data:要发送的响应体(字符串或 Buffer) • encoding:字符串编码(默认 utf8) • callback:发送完成后的回调
与 Express 的 res.send() 区别• 原生 Node.js 只有 res.end(),需手动设 Content-Type、编码等 • Express 的 res.send() 是封装方法,会自动推断类型、设置头、计算长度,最终仍调用 res.end() 完成发送

📌 示例:原生 Node.js 中 res.end() 的作用

javascript
编辑
const http = require('http');

const server = http.createServer((req, res) => {
  // 1. 设置响应头(可选,但推荐)
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  
  // 2. 通过 res.end() 发送数据并结束响应
  res.end('Hello 浏览器!'); // 数据会被传输到客户端(浏览器)
});

server.listen(3000, () => {
  console.log('服务运行在 http://localhost:3000');
});

此时浏览器访问 http://localhost:3000,会接收到:

  • 响应头

    text
    编辑
    Content-Type: text/plain; charset=utf-8
    Content-Length: 17  // 自动计算字节数(含中文)
    Date: ...
    Connection: keep-alive
    
  • 响应体

    text
    编辑
    Hello 浏览器!
    

🔍 注意:Content-Length 是 Node.js 自动根据 res.end() 的参数计算得出的,确保客户端知道何时接收完全部数据。

🧠 总结:res.end() 的真正角色

  • 直接作用:结束 Node.js 的 HTTP 响应流,触发数据向客户端的传输。

  • 数据接收方:可以是浏览器、curl、Postman、其他服务等任意 HTTP 客户端。

  • 传输方式:通过 TCP 连接将组装好的 HTTP 响应包发送出去,客户端(如浏览器)解析后展示内容。

  • 通俗理解

    res.end() 是 Node.js 告诉客户端:“响应已完成,这是最后一批数据,连接可以关闭了。”
    浏览器只是这个过程中最常见的接收端,而非唯一目标。


5. CommonJS vs ESM(模块系统)

特性CommonJS(Node.js 传统)ESM(ES6 标准)
导入require('module')import ... from 'module'
导出module.exports = ...export default ...
加载时机运行时同步加载编译时静态分析
Node 支持默认支持需 .mjs 后缀或 package.json 中设 &#34;type&#34;: &#34;module&#34;

建议:新项目优先使用 ESM,但理解 CommonJS 对阅读旧代码至关重要。


6. 数据驱动(Data-Driven Rendering)

  • 视图(HTML)不是写死的,而是通过模板字符串动态插入数据。
  • generateUsersHtml(users) 函数接收数据,返回完整 HTML。
  • 这是 SSR 的核心思想:数据 → 模板 → HTML 字符串 → 浏览器渲染

🔍 对比现代前端:React/Vue 是“客户端数据驱动”,而这里是“服务端数据驱动”。


7. HTTP 响应关键要素

📌 状态码(Status Code)

  • 200:成功
  • 404:资源未找到
  • 500:服务器内部错误

📌 响应头(Headers)

  • Content-Type:告诉浏览器如何解析响应体。

    • text/html:HTML 文档
    • application/json:JSON 数据
    • text/plain:纯文本

⚠️ 忘记设置 charset=utf-8 可能导致中文乱码!


8. http.createServer 与伺服状态

  • http.createServer(callback) 创建一个 HTTP 服务器实例。
  • server.listen(port, callback) 启动监听,进入“伺服状态”(即等待请求)。
  • 每次有 HTTP 请求,都会触发回调函数 (req, res) => {...}

🔄 这是一个事件驱动、非阻塞 I/O 的典型 Node.js 模型。


四、拓展思考:这种方案的优劣?

✅ 优点

  • 首屏加载快:HTML 直接返回,无需等待 JS 执行。
  • SEO 友好:搜索引擎可直接抓取完整内容。
  • 简单直接:适合小型应用或内部工具。

❌ 缺点

  • 交互性差:每次操作都要刷新页面(除非结合 AJAX)。
  • 服务器压力大:每个请求都要重新渲染 HTML。
  • 开发体验落后:相比现代前端框架,缺乏组件化、状态管理等能力。

💡 适用场景:管理后台、报表系统、低交互内容页。


五、总结要点(方便复习)

概念说明
MVCModel(数据)、View(模板)、Controller(逻辑)分离
服务器回调函数http.createServer((req, res) => {...})req 为请求对象,res 为响应对象
URL 解析url.parse(req.url, true) 获取 pathname(路径)和 query(查询参数)
Content-Type必须正确设置,如 text/html;charset=utf-8 避免中文乱码
res.end() 本质结束 HTTP 响应流,触发 TCP 传输;不是直接“返回给浏览器”
res.end() 必须调用否则客户端会一直等待,导致超时
res.end() vs res.send()原生只有 res.end();Express 的 res.send() 是高级封装,底层仍调用 res.end()
CommonJSNode.js 默认模块系统,使用 require/module.exports
数据驱动用 JavaScript 动态生成 HTML,而非静态文件
HTTP 响应必须正确设置 statusCode 和 Content-Type
伺服状态server.listen() 启动后,服务器持续监听请求
SSR 本质服务端拼接 HTML 字符串,一次性返回给浏览器