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.js或main.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}!`); });逐行拆解这段代码:
- 导入并初始化:
require("express")引入框架,调用它得到app对象——这就是你的整个服务器实例。 - 定义路由:以
app.get开头的那一行是一条route(路由),它告诉 Express「当收到针对/路径的 GET 请求时,执行后面的回调函数」。路由的详细机制我们稍后再展开。 - 监听端口:
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 阶段还会遇到
PUT、DELETE; - 路径参数既可以是字符串,也可以是正则表达式。字符串路径还支持
{}让字符可选(如"/message{s}"同时匹配/message与/messages),支持*通配符(splat,必须后跟名字,如"/{*splat}"可作自定义 404 的兜底路由)。
路由参数与查询参数
- 路由参数:以
:开头的路径段,例如app.get("/:username/messages", ...)。Express 会把实际值自动填充到req.params对象中(GET /odin/messages时req.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')的说明)。
参数名只是惯例,你可以随意命名(request、response等)。一个中间件函数可以:
- 修改请求或响应对象(很多包借此在
req上添加属性,或设置模板渲染用的res.locals); - 执行附加代码(如验证请求、鉴权等);
- 调用
next()移交控制权; - 结束请求-响应周期(一旦结束,链条中剩余的中间件即使存在也不会再执行)。
应用级与路由级中间件
- 应用级中间件:通过
app.use或app.METHOD绑定到整个 Express 实例,对匹配路径的每个请求执行。不指定路径时默认匹配/(即所有请求)。通常放在应用代码最顶部,保证最先执行。Express 内置了几个常用中间件,例如:express.json、express.urlencoded:解析请求体到req.body(表单处理时app.use(express.urlencoded({ extended: true }))是标准配置,见 project_mini_message_board.md);express.static('public'):以指定目录为根服务静态文件(见 views.md 的静态资源部分);
- 路由级中间件:通过
router.use或router.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 文件的修改不会自动生效,除非你手动中断进程并重新运行。有两个方案可以避免这种手动操作:
- Node 内置的 watch 模式(推荐):加上
--watch标志即可,例如node --watch app.js。Node 会监视app.js以及它最终依赖的所有文件,检测到变更后自动重启服务器——这和使用 Webpack、Vite 开发服务器的体验一致。这也是本课程推荐的最简方案。 - Nodemon:一个高度可配置、专门监听变更并自动重启的包。由于 Node 在早期并没有稳定的内置 watch 模式,你很可能在各类项目里见到 Nodemon 的身影;但既然内置方案已经可用,直接用
--watch标志更省事。
实战:用 Express 重写 Basic Informational Site
原文档的作业要求你回到此前用纯 Node 完成的 Basic Informational Site 项目(包含index.html、about.html、contact-me.html、404.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),仅供参考