首页 / 文章 / 七道Node.js与Express入门编程练习题,附解答与解析

七道Node.js与Express入门编程练习题,附解答与解析

完成七道典型的初级 Node.js 面试题目,涵盖 Express 路由与中间件、错误处理、字符串反转与去重等内容,并学会对每道题目进行讲解。

1640 词

初级 Node.js 和 Express 面试很少涉及复杂的算法。通常会给一些简单的任务,比如构建路由、编写中间件或整理数组,面试官会观察你的思考过程以及你是否能够解释代码为何能正常运行。以下七项任务涵盖了最常见的内容,每项都配有可运行的解决方案、关键代码的解析以及能体现真正理解程度的后续问题。

任务 1:构建一个基本的 GET 路由

这是一个经典的热身题:“在 / 处创建一个 GET 接口,返回 Hello World。”解决方案包括创建一个 Express 应用,为根路径注册处理函数,然后在指定端口上开始监听。

import express from "express";

const app = express();

app.get("/", (req, res) => {
    res.status(200).send("Hello World");
});

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

由于使用了 import,该项目需要在 package.json 中指定 "type": "module",或者使用 .mjs 扩展名。

逐行读取路由配置

调用 app.get() 会注册一个仅对 GET 请求作出响应的处理函数:

app.get("/", (req, res) => {

第一个参数 / 表示路径。回调函数会接收到描述传入请求的 req,以及用于构建响应的 res。

响应内容正是在这里生成的:

res.status(200).send("Hello World");

status(200) 用于设置状态码(200 是默认值,但明确写出会更清晰),而 send() 则用于写入响应内容并结束响应。

最后,服务器便开始运行:

app.listen(3000, () => {

这会将应用绑定到3000端口;当服务器接受连接时,回调函数会被触发。实际应用通常从process.env.PORT中读取端口。

如何表述

“app.get()用于为GET请求注册路由。当客户端访问/时,Express会执行对应的回调函数,该函数会返回状态码200并输出‘Hello World’。”

任务2:编写请求日志记录中间件

下一个要求是:“创建一个能够记录所有传入请求的中间件。”中间件是一个具有(req, res, next)参数签名的函数。这个中间件会打印请求方法和URL,然后将控制权传递给后续处理;app.use()会将其应用到其后注册的所有路由上。

const logger = (req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
};

app.use(logger);

在路由处理程序响应之前,每个请求都会按顺序依次经过这个中间件链:

Client
  ↓
Request
  ↓
Logger Middleware
  ↓
Route Handler
  ↓
Response

其中最重要的一行是函数内部的最后一行:

next();

next()用于告知Express当前步骤已完成,接下来应执行下一个中间件或路由处理。如果忘记调用它,请求将会挂起直到客户端超时,这是经常被问到的问题。另一种做法是通过发送响应来结束请求,就像认证中间件在检测到缺少令牌时所做的那样。

如何表述

“中间件会接收请求、响应以及next函数。它可以检查或修改请求,然后要么直接响应,要么调用next()将控制权传递给下一个处理环节。”

任务3:在路由中处理异步操作

真正的接口端点需要等待数据库和其他 API 的响应,因此经常会遇到“如何在 Express 中处理异步操作?”这样的问题。一个合理的答案是使用带有 try/catch 结构的 async 处理函数:

app.get("/users", async (req, res, next) => {
    try {
        const users = await getUsers();
        res.status(200).json(users);
    } catch (error) {
        next(error);
    }
});

关键所在在于 await 语句:

const users = await getUsers();

它会让该处理函数暂停执行,直到 getUsers() 执行完成,且不会阻塞其他请求。如果 Promise 被拒绝,则会执行 catch 块:

catch (error) {
    next(error);
}

next(error) 会将错误传递给第5题中提到的错误处理中间件。需要补充的是:Express 4 不会捕获异步处理函数中被拒绝的 Promise,因此必须使用 try/catch,而 Express 5 则会自动处理这些错误。如需更深入的了解,可参阅 在 Node.js 应用中构建生产级错误处理机制。

任务4:绘制CRUD接口草图

CRUD代表创建、读取、更新和删除,这四种基本数据操作各自对应一种HTTP方法。users资源的相关实现如下:

app.post("/users", (req, res) => {
    res.status(201).send("User created");
});

app.get("/users", (req, res) => {
    res.status(200).send("Users");
});
app.put("/users/:id", (req, res) => {
    res.status(200).send("User updated");
});
app.delete("/users/:id", (req, res) => {
    res.status(200).send("User deleted");
});

创建操作会返回201 Created状态码,而处理单个用户的路由会接收从req.params.id获取的:id参数。无需死记硬背代码行,只需理解每种方法的含义即可:

POST   → Create
GET    → Read
PUT    → Update
DELETE → Delete

需要区分PUT和PATCH的区别:按照常规,PUT会替换整个资源,而PATCH则只应用部分更改。

如何表达

“CRUD涵盖了四种基本数据操作。在REST架构中,POST用于创建数据,GET用于读取数据,PUT或PATCH用于更新数据,DELETE则用于删除数据。”

任务5:集中处理错误

错误处理通常在完成任务3之后进行。可以先创建一个会出错的路由,然后将错误信息转发出去:

app.get("/", (req, res, next) => {
    try {
        throw new Error("Something went wrong");
    } catch (error) {
        next(error);
    }
});

接着添加一个错误处理程序,用于记录日志并返回通用的500状态码:

app.use((err, req, res, next) => {
    console.error(err.message);
    res.status(500).json({
        message: "Internal Server Error"
    });
});

使这个函数与众不同的是它的参数签名:

(err, req, res, next)

Express通过其声明的四个参数来识别错误处理程序,顺序为err, req, res, next;只有当有错误被传递进来时这些处理程序才会执行。在定义路由之后注册该处理程序,并在生产环境中避免将err.message发送给客户端。执行流程如下:

Route
  ↓
Error occurs
  ↓
catch(error)
  ↓
next(error)
  ↓
Error middleware
  ↓
500 Response

错误处理程序中是否必须使用next()?

如果处理程序本身就负责发送响应,则不需要:

app.use((err, req, res, next) => {
    console.error(err.message);
    res.status(500).send("Internal Server Error");
});

next仍然需要被声明,因为当只有三个参数时,Express会将该函数视为普通中间件。

任务6:反转字符串

有些问题只需简单的 JavaScript 解决:“将字符串 hello 反转。”这种常见的单行代码通过串联三种方法来实现:

const str = "hello";
const reversed = str.split("").reverse().join("");
console.log(reversed);

它的输出结果是:

olleh

每一步的作用

使用 split 方法可以将字符串拆分为字符:

str.split("")

拆分后的结果是一个数组:

["h", "e", "l", "l", "o"]

接着在原数组上直接反转顺序:

.reverse()

这样就能得到:

["o", "l", "l", "e", "h"]

最后,将这些字符不加分隔符地拼接起来:

.join("")

这样就得到了反转后的字符串:

"olleh"

一个常见的变体是:“现在不用 reverse() 来实现。”从最后一个索引开始向下遍历到零:

const str = "hello";
let reversed = "";
for (let i = str.length - 1; i >= 0; i--) {
    reversed += str[i];
}
console.log(reversed);

这两种方法都是线性时间复杂度。需要说明的是:split("") 和索引操作是基于 UTF-16 代码单元的,因此表情符号会被拆分;而 [...str] 则是按代码点进行拆分的。

任务7:找出数组中的重复项

另一个常见题目:“返回出现次数超过一次的值。”给定:

const numbers = [1, 2, 3, 2, 4, 3, 5];

期望的结果是:

[2, 3]

使用两个Set对象,一个用于记录已出现的值,另一个用于存储确认的重复项:

const numbers = [1, 2, 3, 2, 4, 3, 5];
const seen = new Set();
const duplicates = new Set();
for (const number of numbers) {
    if (seen.has(number)) {
        duplicates.add(number);
    } else {
        seen.add(number);
    }
}
console.log([...duplicates]);

运行后会输出:

[2, 3]

为何使用两个Set有效

seen会记录所有传入的值,首次出现的值会被添加到其中:

seen.add(number);

当该值再次出现时,此检查就会通过:

if (seen.has(number))

随后该值会被放入duplicates中,由于Set会忽略重复项,因此只会被记录一次。查找操作的时间几乎为常数时间,所以该解决方案是线性复杂度而非二次嵌套循环的复杂度。若要统计出现次数,可使用Map。

在编写代码前先思考你的解题思路

最重要的习惯是在编写代码之前先说明自己的计划,因为面试官同样看重推理过程而非最终结果。对于处理重复值的任务,可以先这样开头:

“我的计划是使用一个集合来存储已经出现过的值。在循环过程中,任何已存在于该集合中的值都会被放入另一个用于存储重复值的集合中,从而确保每个值都是唯一的。”

然后再编写代码。这样既能证明你理解了原理而非死记硬背,也能让面试官尽早为你指明方向。

准备清单

在参加 Node.js 或 Express 的初级职位面试之前,务必确保自己能够熟练完成以下内容:

  • 定义路由并解释 req 和 res 的作用
  • 使用 app.use() 并编写自己的中间件
  • 解释 next() 的功能以及缺少它时会发生什么
  • 使用 async/await、try/catch 以及 next(error) 处理异步任务
  • 编写一个包含四个参数的错误处理函数,并在路由之后注册它
  • 将 CRUD 操作映射到相应的 HTTP 方法及状态码
  • 利用 JavaScript 的核心方法操作字符串和数组
  • 在需要查找或计数时选择使用 Set 还是 Map
  • 在编码过程中清晰阐述自己的思路
  • 在这个阶段,扎实的基础、易读的代码以及清晰的说明远比复杂的算法更重要。

    相关阅读

  • 将时间类型与实际日期任务匹配:七个实战示例 — 通过 JavaScript Temporal 操作七个常见的日期和时间相关任务,涵盖普通日期处理、月份溢出问题以及时区、夏令时运算和验证等内容。
  • Node.js 和 Express 面试准备:70道附详细答案的题目 — 系统性地梳理了70道 Node.js 和 Express 面试题目,内容涉及事件循环、流处理、中间件、安全性及扩展性等方面,并对常见错误答案进行了纠正。