Express 入门:用 Node.js 后端框架搭建你的第一个服务器并理解请求生命周期
2026/9/16 4:38:45 网站建设 项目流程

Express 入门:用 Node.js 后端框架搭建你的第一个服务器并理解请求生命周期

【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum

本指南是nodeJS/express课程的第一课(对应仓库文档 introduction_to_express.md),核心任务是带你从零搭建一个可运行的 Express 服务器,并彻底理解一次 HTTP 请求从进入服务器到返回响应所经历的完整生命周期。学完本课,你将掌握 Express 的初始化与启动、路由与中间件的底层工作方式,以及如何让服务器在文件变更时自动重启——这些知识将直接支撑后续的 MVC 全栈应用与 REST API 开发(详见 routing.md、controllers.md 等进阶课程)。

为什么在学会 Node 之后还需要 Express

在上一阶段(getting_started.md),你已经通过 Node 原生的http模块、fs文件系统模块和URL类实现了一个可以读取文件、服务多页面网站的基础服务器。你会发现这套写法相当繁琐:每次都要手动解析请求、路由分发、设置响应头。当业务复杂到需要管理表单、数据库、会话、错误处理时,纯 Node 的样板代码会迅速失控。

Express 正是在这个节点登场。它是一个**刻意保持极简(barebones)且不固执己见(unopinionated)**的后端框架:它把请求解析、路由匹配、响应发送、静态资源服务等大量实现细节封装起来,让你只关心业务逻辑;同时它不做任何强制的架构约定,你可以用自己想要的方式组织代码,并且只按需引入需要的功能模块。这种设计带来的自由度很高,但在多个可行方案之间做选择时也会有点难度,这正是本课程后续各课要逐一解决的问题。

在本课程中,Express 将被用于两大方向:

  • 使用MVC(Model-View-Controller)模式构建全栈应用(参见 controllers.md 与 views.md);
  • 构建REST API——就像你在 Weather App、React Shopping Cart 等项目中使用过的那些 API 一样(参见 apis/api_basics.md)。

设置 Express:创建你的第一个服务器

初始化项目并安装依赖

在一个新建的目录中,首先运行npm init -y生成一个package.json,然后安装 Express 依赖:

npm install express

编写 app.js

创建一个app.js文件作为 Express 服务器的入口。文件名可以随意(比如index.jsmain.js),这里沿用 Express 官方文档的习惯命名。内容如下:

const express = require("express"); const app = express(); app.get("/", (req, res) => res.send("Hello, world!")); const PORT = 3000; app.listen(PORT, (error) => { // This is important! // Without this, any startup errors will silently fail // instead of giving you a helpful error message. if (error) { throw error; } console.log(`My first Express app - listening on port ${PORT}!`); });

逐行拆解这段代码:

  1. 导入并初始化require("express")引入框架,调用它得到app对象——这就是你的整个服务器实例。
  2. 定义路由:以app.get开头的那一行是一条route(路由),它告诉 Express「当收到针对/路径的 GET 请求时,执行后面的回调函数」。路由的详细机制我们稍后再展开。
  3. 监听端口app.listen(PORT, callback)让服务器在指定端口上监听入站请求。这里的localhost本质上就是本机自己的回环连接地址。

回到终端运行node app.js,一切正常的话你会看到:

My first Express app - listening on port 3000!

如果出现问题(比如端口已被占用),由于我们在回调里对error做了显式抛出处理,你会看到一条有用的错误信息,而不是静默失败。恭喜,你的第一个 Express 服务器已经跑起来了!

端口变量的正确写法

上面的示例为了演示把端口硬编码成了3000。实际开发中,端口号通常来自环境变量,并在环境变量不存在时提供一个回退值:

const PORT = process.env.PORT || 3000;

这样做有两个实际好处:

  • 当指定端口被占用时,可以直接修改环境变量而无需改动源码;
  • 许多托管服务(PaaS)会为你的应用配置专属端口,这个端口可能与本地硬编码的值不同(详见 deployment.md 中对实例与部署的讲解)。

一次请求的旅程:从浏览器到响应

发出一条 GET 请求

服务器已在 3000 端口运行,现在在浏览器地址栏访问http://localhost:3000/(末尾的/即使忘记输入,浏览器也会自动补上)。这个动作的本质是:浏览器向localhost:3000上监听的服务器(也就是你的 Express 应用)发送一条针对/路径的GET请求,然后把收到的响应内容显示在窗口中。

实际上,你在地址栏访问任何网址都在做同一件事。访问https://theodinproject.com/paths等价于「向theodinproject.com发送一条针对/paths路径的GET请求,并把响应渲染出来」。访问http://localhost:3000/后,页面将显示Hello, world!

请求对象、中间件链与响应对象

当 Express 收到这条GET请求时,它会把请求封装进一个request 对象req),然后让这个请求对象按顺序穿过一串我们称为「中间件函数」(middleware functions)的函数链,直到某个中间件函数指示 Express 向请求作出响应为止。

在我们的示例中,请求是「GET +/路径」,恰好匹配app.js里定义的路由:

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

这条语句翻译成人话就是:「如果一条 GET 请求到达/路径,就把该请求传入下面这串中间件函数。」这里我们只注册了一个函数。

关于路由匹配有两个关键点:

  • 如果定义了多条路由,Express 会把请求传给第一个同时匹配 HTTP 动词(如GET)与路径(如/)的路由
  • 路由的定义顺序至关重要——先定义的路由会先被匹配。

Express 把回调函数执行时所需的两个对象作为参数传入:第一个参数(惯例命名为req)是请求对象,第二个参数(惯例命名为res)是response 对象。我们的回调调用res.send("Hello, world!"),指示响应对象把字符串"Hello, world!"作为响应体发回。

回调执行完毕、函数返回后,由于 Express 已经被指示「对请求作出响应」,它会结束这次请求-响应周期(request-response cycle)。与此同时,浏览器收到响应并把它显示在屏幕上。res.send几乎可以发送任何内容,甚至可以通过res.sendFile直接发送一个文件给客户端。

路由基础:动词、路径、参数

app.get("/", handler)只是路由的冰山一角。在 routing.md 中,路由被定义为「将请求的 HTTP 动词(如GET/POST)与 URL 路径匹配到一组中间件函数(即控制器)」的机制。这里先掌握最基本的几个概念:

HTTP 动词与路径

每个 HTTP 动词都有对应的 Express 路由方法:

app.get("/", (req, res) => res.send("Hello, world!")); app.post("/messages", (req, res) => res.send("This is where you can see any messages."));
  • app.get只匹配 GET 请求,app.post只匹配 POST 请求——向/messages发 GET 请求不会命中 POST 路由;
  • 可以用app.all()让一个路由匹配所有动词;
  • 课程中主要使用GET(仅用于从服务器获取数据)和POST(用于向服务器提交数据,如表单),后续 REST API 阶段还会遇到PUTDELETE
  • 路径参数既可以是字符串,也可以是正则表达式。字符串路径还支持{}让字符可选(如"/message{s}"同时匹配/message/messages),支持*通配符(splat,必须后跟名字,如"/{*splat}"可作自定义 404 的兜底路由)。

路由参数与查询参数

  • 路由参数:以:开头的路径段,例如app.get("/:username/messages", ...)。Express 会把实际值自动填充到req.params对象中(GET /odin/messagesreq.params{ username: "odin" });
  • 查询参数:URL 中?之后以key=value形式、&分隔的部分,不属于路径本身。Express 自动解析后填充到req.query,重复的键会合并成数组(如?sort=date&sort=likes得到query.sort === ["date", "likes"])。
app.get("/:username/messages", (req, res) => { console.log("Params:", req.params); console.log("Query:", req.query); res.end(); });

提示:查询参数在表单场景中也常被用到——当表单以GET方法提交时,数据不会出现在req.body中,而是以查询字符串形式进入req.query(见 forms_and_data_handling.md 的搜索练习)。

中间件:请求-响应周期中的核心概念

中间件函数是 Express 最核心的概念。它运行在「入站请求」与「最终路由处理器」之间,通常接收三个参数:

  • req:请求对象;
  • res:响应对象;
  • next:把控制权移交给链条中下一个中间件函数的函数(可选,详见 controllers.md 对next的四种调用方式——无参、传错误、next('route')next('router')的说明)。

参数名只是惯例,你可以随意命名(requestresponse等)。一个中间件函数可以:

  • 修改请求或响应对象(很多包借此在req上添加属性,或设置模板渲染用的res.locals);
  • 执行附加代码(如验证请求、鉴权等);
  • 调用next()移交控制权;
  • 结束请求-响应周期(一旦结束,链条中剩余的中间件即使存在也不会再执行)。

应用级与路由级中间件

  • 应用级中间件:通过app.useapp.METHOD绑定到整个 Express 实例,对匹配路径的每个请求执行。不指定路径时默认匹配/(即所有请求)。通常放在应用代码最顶部,保证最先执行。Express 内置了几个常用中间件,例如:
    • express.jsonexpress.urlencoded:解析请求体到req.body(表单处理时app.use(express.urlencoded({ extended: true }))是标准配置,见 project_mini_message_board.md);
    • express.static('public'):以指定目录为根服务静态文件(见 views.md 的静态资源部分);
  • 路由级中间件:通过router.userouter.METHOD绑定到某个 Express Router 实例,只在请求匹配并穿过该路由时才执行。这让我们可以把路由分组拆到独立文件(如routes/authorRouter.js),再用app.use("/authors", authorRouter)挂载——这也正是 MVC 项目组织方式的基础。

顺序决定一切

Express按照中间件定义/注册的顺序执行。例如:

function myMiddleware(req, res, next) { console.log("Middleware function called"); req.customProperty = "Hello from myMiddleware"; next(); } app.use(myMiddleware);

此例中中间件记录日志、给请求对象添加自定义属性、然后调用next()放行。链条中后续的中间件都能读到req.customProperty。反之,如果某个修改请求对象的中间件被定义得太靠后,它之前的中间件就无法看到它的改动。所以注册顺序必须谨慎安排——这也是错误处理中间件必须放在应用代码末尾的原因。

另一个容易踩的坑:res.send等响应方法只结束请求-响应周期,不会终止函数执行。在res.send("Hello")之后再写res.send("Bye")会抛出「cannot send after sending」的错误。因此控制器在发送 404 响应后通常要显式return跳出(见 controllers.md 的getAuthorById示例)。

响应方法一览

在控制器中发送响应时有多种选择(详见 controllers.md):

方法用途与特点
res.send(body)通用响应方法,根据传入数据自动设置Content-Type;传入对象时会被序列化为 JSON 并设为application/json
res.json(data)显式发送 JSON,始终将Content-Type设为application/json,是发送 JSON 时的首选(res.json内部也调用res.send,但会强制 JSON 序列化)
res.redirect(path)将客户端重定向到其他 URL
res.render(view, locals)渲染视图模板并返回生成的 HTML(配合 EJS 模板引擎使用,见 views.md)
res.status(code)手动设置状态码,本身不会结束请求-响应周期,需链式调用(如res.status(404).send(...));省略时默认状态码为200

此外还有res.sendFile,可以响应一个文件——这正是你重写 Basic Informational Site 项目时要用的能力。

文件变更时自动重启服务器

node app.js运行服务器时,项目目录中任何 JavaScript/JSON 文件的修改不会自动生效,除非你手动中断进程并重新运行。有两个方案可以避免这种手动操作:

  1. Node 内置的 watch 模式(推荐):加上--watch标志即可,例如node --watch app.js。Node 会监视app.js以及它最终依赖的所有文件,检测到变更后自动重启服务器——这和使用 Webpack、Vite 开发服务器的体验一致。这也是本课程推荐的最简方案。
  2. Nodemon:一个高度可配置、专门监听变更并自动重启的包。由于 Node 在早期并没有稳定的内置 watch 模式,你很可能在各类项目里见到 Nodemon 的身影;但既然内置方案已经可用,直接用--watch标志更省事。

实战:用 Express 重写 Basic Informational Site

原文档的作业要求你回到此前用纯 Node 完成的 Basic Informational Site 项目(包含index.htmlabout.htmlcontact-me.html404.html四个页面,原本用http模块 + 手工 URL 判断实现),安装 Express 后用几条app.get()重写它。改造后的核心思路是:

const express = require("express"); const app = express(); const path = require("node:path"); // 为每个页面路径注册一条路由 app.get("/", (req, res) => res.sendFile(path.join(__dirname, "index.html"))); app.get("/about", (req, res) => res.sendFile(path.join(__dirname, "about.html"))); app.get("/contact-me", (req, res) => res.sendFile(path.join(__dirname, "contact-me.html"))); // 兜底 404 路由:必须放在所有具体路由之后 app.get("/{*splat}", (req, res) => res.sendFile(path.join(__dirname, "404.html"))); const PORT = process.env.PORT || 3000; app.listen(PORT, (error) => { if (error) throw error; console.log(`Server listening on port ${PORT}!`); });

注意两点:一是 404 兜底路由(/{*splat})必须定义在所有具体路由之后,否则会抢占所有请求(路由顺序问题可回顾 routing.md);二是用path.join(__dirname, ...)拼接绝对路径,避免相对路径在不同启动目录下失效。

完成本项目后,你已具备继续学习下一课 routing.md(路由进阶)、controllers.md(控制器、中间件与错误处理)、views.md(EJS 模板)的基础,紧接着的 project_mini_message_board.md 将让你用 Express + EJS + 表单 POST 完成第一个真正可交互的全栈应用,project_inventory_application.md 则要求你完整实践 MVC + CRUD。沿着这条路径,你就能一路走到 REST API 与部署上线(deployment.md)。

小结与自检

本课需要掌握的核心能力:

  • npm init -y+npm install express初始化项目,并写出可运行的app.js
  • 描述一条入站请求如何被封装为req对象、穿过中间件链、被路由匹配、最终由res.send等响应方法结束请求-响应周期;
  • 说清楚什么是中间件函数,以及注册顺序为何重要;
  • node --watch app.js(或 Nodemon)实现文件变更后的自动重启;
  • 理解端口应优先取自process.env.PORT并回退到默认值,这对本地开发与云端部署都至关重要。

练习建议:花几分钟通读 Express 官方文档的 API 部分熟悉术语,然后把上面 Basic Informational Site 的改写亲手跑通。从「纯 Node 手写路由」到「Express 声明式路由」的对比,会让你对框架的价值与请求生命周期有更直观的体会。

【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询