什么是服务器?

服务器是一个响应客户端请求并提供服务的系统。在 Web 开发中,服务器通过 请求-响应 的模式与客户端通信。

为什么需要服务器?

  • 文件访问:集中存储和管理文件。
  • 集中化:统一管理数据和业务逻辑。
  • 安全性:通过服务器控制访问权限,保护数据。

服务器如何工作?

每个服务器都绑定到计算机上的一个端口(如 80、443)。客户端通过 协议://域名:端口 的方式连接到服务器。

例如:

1
http://localhost:3000
  • localhost 表示本地计算机。
  • 3000 是服务器监听的端口。

使用本机作为服务器

实际上,每台计算机都可以运行服务器代码,只需要启动一个服务程序。例如:

1
2
3
4
5
6
7
8
9
10
const express = require("express");
const app = express();

app.get("/", (req, res) => {
res.send("Hello, Server!");
});

app.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});

运行此代码后,访问 http://localhost:3000,你将看到浏览器中显示 “Hello, Server!”。


Node.js 和 Express.js

什么是 Node.js?

Node.js 是一个运行 JavaScript 的服务器端平台,基于 Chrome 的 V8 引擎。它允许你在服务器上运行 JavaScript。

什么是 Express.js?

Express.js 是 Node.js 上的一个轻量级 Web 框架,用于快速构建服务器端应用程序。它简化了路由、中间件和请求处理。

以下是一个使用 Express.js 构建简单 API 的示例:

1
2
3
4
5
6
7
8
9
10
const express = require("express");
const app = express();

app.get("/api", (req, res) => {
res.send({ message: "Hello, API!" });
});

app.listen(3000, () => {
console.log("API server running on http://localhost:3000/api");
});

访问 /api 路径会返回 JSON 数据:

1
2
3
{
"message": "Hello, API!"
}

我们需要从特殊到不特殊地定义端点

image-20241130001917084

文件结构

image-20241130001637804

  • /client - 包含我们所有的 React 代码、组件、页面、工具等内容。(前端)

  • /server - 包含我们所有的后端代码。

  • 其他文件 - 由团队人员设置,用于配置 React 应用。如有疑问,请随时咨询相关人员。


NPM(Node 包管理器)

NPM 是 Node.js 的包管理工具,用于安装和管理依赖。

常用命令

  • npm install:安装依赖。
  • npm start:运行项目。
  • npm run <script>:运行在 package.json 中定义的脚本。
  • npm run hotloader:运行开发环境(具体任务由脚本配置)。

项目结构:

  • package.json:存储项目元信息,例如依赖项、脚本等。
  • node_modules:包含安装的依赖。

例如,以下是典型的 package.json 文件:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"express": "^4.18.2"
}
}

通过运行 npm install,NPM 会下载 express 并将其存储在 node_modules 文件夹中。

image-20241130001503527


创建 API 端点

image-20241130002058179

image-20241130002109137

image-20241130002117242

中间件(Middleware)

中间件是 Express.js 的核心概念,用于在请求和响应之间插入额外的处理逻辑。

  • 中间件是当请求到达时运行的函数

  • 可以在路由处理器之前运行

  • 路由处理器也是中间件!

中间件的作用

  • 修改请求:为请求对象添加属性。
  • 预处理:解析请求体、验证用户身份等。
  • 处理错误:捕获并处理异常。
  • 提供静态内容

中间件的定义和调用

Express.js 使用 app.use(middlewareFunction) 注册中间件。

app.use() 是 Express 提供的方法,用于注册中间件或路由模块。

  • 中间件:对请求进行预处理(如解析 JSON 数据、身份验证等)。
  • 路由模块:定义多个路由(如 GET、POST)来处理不同的路径。

错误中间件需要四个参数:errorreqresnext

  • 在接收到请求和执行端点代码之间运行的代码
  • 中间件就像流水线上的工人
    • 它们可以传递请求并修改/返回响应
  • 中间件按定义的顺序被调用

image-20241130002244445

添加中间件

通过调用app.use()来注册中间件

例子:

image-20241130003035151

基本结构和调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require("express");
const app = express();

// 中间件定义
app.use((req, res, next) => {
console.log("Request received at:", new Date().toISOString());
next(); // 调用 next() 将请求交给下一个中间件或路由
});

// 路由处理
app.get("/", (req, res) => {
res.send("Hello, Middleware!");
});

// 启动服务器
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});

next() 的作用

  • next() 是什么?

    • next() 是 Express 中间件函数提供的一个回调,用于将请求传递给下一个中间件或路由处理器。
    • 它确保中间件链能够按顺序执行。
  • 如果没有调用 next() 会怎么样?

    • 如果中间件没有调用 next(),请求会被挂起,客户端将无法接收到响应。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      app.use((req, res, next) => {
      console.log("Middleware 1");
      // 没有调用 next(),请求停在这里
      });

      app.get("/", (req, res) => {
      res.send("Hello, world!");
      });

      app.listen(3000, () => {
      console.log("Server is running on http://localhost:3000");
      });
      • 当客户端访问 / 时,Middleware 1 会打印日志,但请求不会继续执行到路由 / 的处理器,客户端最终会超时。
  • 正确使用 next() 的示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    app.use((req, res, next) => {
    console.log("Middleware 1");
    next(); // 将请求交给下一个中间件或路由
    });

    app.get("/", (req, res) => {
    res.send("Hello, world!");
    });

    app.listen(3000, () => {
    console.log("Server is running on http://localhost:3000");
    });
    • 输出顺序:
      • 服务端日志:Middleware 1
      • 客户端响应:Hello, world!

next() 的核心作用

  1. 传递控制权

    • 将当前请求传递给下一个符合条件的中间件或路由处理器。
    • 如果没有调用 next(),请求会被中止。
  2. 错误处理

    • 当调用 next(err) 时,可以将错误传递给错误处理中间件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    app.use((req, res, next) => {
    const error = new Error("Something went wrong");
    next(error); // 将错误传递到错误处理中间件
    });

    app.use((err, req, res, next) => {
    console.error(err.message);
    res.status(500).send("Server Error");
    });
  3. 控制执行顺序

    • 中间件是按照定义的顺序执行,next() 决定是否让请求继续向下一个中间件流转。

为特定路径定义中间件

1
2
3
4
5
6
7
8
app.use("/api", (req, res, next) => {
console.log("API middleware triggered");
next();
});

app.get("/api/test", (req, res) => {
res.send("API Test Endpoint");
});

解析

  • /api 中间件只会对 /api 开头的路径生效,如 /api/test
  • 中间件在处理路径匹配后可以继续调用 next() 将控制权交给下一个中间件或路由。

用中间件处理 JSON 数据

1
2
3
4
5
6
app.use(express.json()); // 解析 JSON 请求体

app.post("/data", (req, res) => {
console.log(req.body); // 输出解析后的 JSON 数据
res.send("Data received");
});

解析

  • express.json() 是 Express 内置的 JSON 解析中间件。
  • 用于处理请求体中的 JSON 数据,将其解析为 JavaScript 对象并赋值给 req.body

Catch All 路由和 React 集成

在使用 React 和 Express 构建全栈应用时,通常需要处理 “Catch All” 路由以支持前端的路由功能。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require("express");
const path = require("path");
const app = express();

app.get("/api/test", (req, res) => {
res.send({ message: "Wow I made my first API!" });
});

// 定义 React 静态文件路径
const reactPath = path.join(__dirname, "..", "client", "dist");
app.use(express.static(reactPath));

// 捕获所有未定义的路由并返回 React 的 index.html
app.get("*", (req, res) => {
res.sendFile(path.join(reactPath, "index.html"));
});

解释

  • require("express") 是什么?

    • require 是 Node.js 中用于引入模块的函数。
    • express 是一个流行的 Node.js 框架,用于构建 Web 应用程序和 API。
    • require("express") 导入了 Express 框架的主模块。
    • 为什么需要它?
      • Express 提供了简单的 API 来创建服务器、定义路由、中间件和处理请求/响应。
  • const path = require("path");

    • path 是 Node.js 内置的模块,用于处理文件和目录路径。

    • 通过使用 path 模块,你可以构建跨平台兼容的路径,避免手动拼接路径时出错。

      作用:这行代码使你能够使用 path 模块来处理路径相关的操作,确保路径的兼容性。

  • const app = express();

    • 这行代码调用了 express() 函数,创建了一个 Express 应用实例,并赋值给 app 变量。

    • app 是你的 Express 应用,所有路由和中间件都会在这个实例上定义和处理。

      作用:这行代码初始化了一个 Express 应用,允许你在 app 上定义路由、添加中间件等。

  • express.static(reactPath) 提供 React 的静态资源(如 JS、CSS 文件)。

    • 什么是静态文件?

      • 静态文件是指无需服务器动态处理、可以直接传送到客户端的资源文件,例如:

        • HTML 文件
        • CSS 样式表
        • JavaScript 脚本
        • 图片(如 PNG、JPG)
        • 其他如字体文件、音频文件等
      • 它们是直接存储在文件系统中的固定内容,不依赖用户输入进行动态生成。

    • 静态文件的作用

      • 提供网站的前端资源(比如布局、样式、交互等)。

      • 高效地向客户端传递文件内容,减少服务器的计算负担。

  • app.get("\*", (req, res) => { ... });

  • 这行代码定义了一个 通配符路由(catch-all route),* 表示匹配任何路径。

  • 当用户访问任何未被前面定义的路由时,都会被这个路由捕获。

  • 这里的 res.sendFile(path.join(reactPath, "index.html")); 表示服务器返回 reactPath 目录下的 index.html 文件。

    作用:捕获所有未匹配的路径,并返回 React 应用的 index.html 文件。这是单页应用(SPA)的常见做法,当用户刷新或访问其他 URL 时,都会返回同一个 index.html 文件,由 React 路由来处理页面切换。

  • React 会通过前端路由(如 React Router)处理路径并渲染对应页面。

代码总体解释

这段代码是典型的 前后端分离 项目中的服务器端设置:

  1. 后端 API:通过 /api/test 提供一个简单的 API,当用户访问时,返回一个包含消息的 JSON 响应。
  2. 托管静态文件:通过 express.static() 中间件,将构建后的 React 应用文件夹(dist)作为静态文件提供给前端。
  3. 单页应用支持:使用 * 路由捕获所有未定义的路径,返回 React 应用的 index.html,确保客户端的前端路由可以正常工作。

总结:这段代码主要实现了一个简易的后端服务,它提供了一个简单的 API 和 React 应用的静态文件托管,同时确保 React 应用能处理用户直接访问不同 URL 的情况。

运行服务器

  • 同时运行两个服务器:
    • localhost:5050:处理 React 热加载。
    • localhost:3000:Node.js 后端服务器。
  • 测试后端用 localhost:3000,查看网站用 localhost:5050