ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Express 入门指南:从路由到中间件,轻松搭建 Node.js Web 服务

2026/10/3 4:07:52 拓冰建站 浏览量
Express 入门指南:从路由到中间件,轻松搭建 Node.js Web 服务 说实话每次看到有人搜“Node.js 是干什么的”我基本上能猜到接下来的第二张搜索词里八九不离十会出现 Express。这个组合出现得如此频繁以至于我都习惯了——在 Node.js 生态里Node.js 和 Express 这两个关键词的绑定关系就像咖啡和奶精有人喜欢喝纯的但绝大多数人的第一杯是从这一口混合开始上路的。如今随便打开一个 Node.js 岗位的 JD十有八九也会看到 Express 列在技术栈清单里。这里不整高深理论只讲我从实际项目里趟出来的经验和坑。Express 是 Node.js 生态里历史最久、使用量最大的 Web 应用框架它把原生 http 模块里那些繁琐的细节收敛得整整齐齐路由分发、请求解析、响应包装、中间件编排全部一站式解决。它的定位很简单——帮你用最少的样板代码把 HTTP 服务跑起来。这篇文章适合三类人刚装好 Node.js 还不知道接下来该干嘛的新手、准备 Node.js 相关面试的求职者、以及在原生 http 模块里用 if-else 写路由写到怀疑人生的老手。不管你是哪种看完这篇都能把 Express 从“听说过”变成“能上手用”。1. Express 解决的核心问题让路由和中间件变简单1.1 原生 http 模块写服务的痛点我经常问身边的人一个问题如果不用任何框架用 Node.js 写一个带两个接口的后端服务你会怎么写大部分人第一反应是用 http.createServer然后监听 request 事件在回调里去判断请求方法和 URL。用不了几分钟代码就会长成这样const http require(http); http.createServer((req, res) { const { url, method } req; if (url / method GET) { res.writeHead(200, { Content-Type: text/plain; charsetutf-8 }); res.end(首页); } else if (url /api/users method GET) { res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify([{ id: 1, name: 张三 }])); } else { res.writeHead(404, { Content-Type: text/plain; charsetutf-8 }); res.end(找不到); } }).listen(3000);注意这才两个业务接口代码已经要用 if-else 层层判断了。如果接口有二十个呢url 长一点、参数多一点、还要区分 POST 和 GET这段代码很快就会变成一场分支大战。更要命的是日志怎么打权限校验怎么统一加跨域头怎么处理上传的文件体怎么解析静态资源怎么托管每一个问题都得自己在请求回调里手写写完这一堆“基础设施”才有空开始写真正的业务逻辑。这显然不是我们想要的开发节奏——所以 Express 出现了。1.2 中间件流水线Express 的灵魂设计Express 的核心抽象能力用一句话概括就是中间件Middleware。你可以把中间件理解成流水线上的工位每个工位只负责一件事有的工位负责给请求过个目、记一笔日志有的工位负责检查请求头有的负责把 JSON body 解析出来直到最后一个工位才轮到真正的业务逻辑去返回数据。我常用的一个生活类比是机场安检乘客是 request登机口是业务接口中间那几道查验身份、核对登机牌、扫随身行李的关卡就是中间件。如果你坐过国内航班肯定熟悉这套流程先凭身份证换登机牌相当于解析请求头再过安检门相当于鉴权最后到登机口扫码登机相当于路由匹配。整个过程里的每一个环节都是独立服务任何一个不通过都会被打回。乘客依次经过这些关卡任何一道关卡发现异常会直接把你“return”回去流程提前终止。为什么这个设计如此重要因为日志、鉴权、错误处理、跨域这类“横切关注点”都可以抽出来做成独立的中间件函数。需要时在 app.use() 里挂一个不需要就撤掉互不干扰。原生 http 模块没有这种抽象能力你只能把所有逻辑揉进一个大回调而 Express 把“通用的处理流程”和“具体的业务接口”彻底解耦了这也是它保持轻量、生态却无比丰富的原因。1.3 为什么偏偏是 Express 成了默认选择有人会问同样能做 Web 服务的框架不少为什么教程、岗位需求、开源项目里到处都是 Express我的看法是三个原因叠加的结果第一出身早。Express 在 2010 年发布吃到了 Node.js 生态早期的先发红利大量基础设施和教程都以它为入口建立起来。第二设计哲学克制。Express 核心只解决路由和中间件其余一切交给社区中间件这种“小而精”的做法让框架本身足够稳定也让第三方插件有了极大的发挥空间。第三学习曲线平缓。它没有强制你学一套复杂约定写一个接口就是一行 app.get这种直白让新手几乎不用付出额外成本就能跑起来。相比之下Koa 虽然由 Express 原班人马打造、抽象更优雅但许多生产场景下你需要自己组合更多东西普通项目反而不如 Express 省心。这一章我想额外强调一个观点理解中间件模型比背 API 重要得多。API 查文档就能用但中间件模型决定了你会怎么写、怎么排查、怎么拆代码。后面的几章所有实操都是围绕这个模型展开的。2. 从零起步装环境、建项目、跑通 Hello World2.1 选对 Node.js 版本是关键先说环境安装。很多人卡在第一步下载哪个版本打开 nodejs.org 官网会看到两个大按钮LTS 和 Current。对绝大多数人来说请无条件选 LTSLong Term Support长期维护版。LTS 版本意味着在几年内有持续的安全更新而且生态里绝大多数 npm 包都在这个版本上验证过你不会因为 Node 版本过新而踩到依赖兼容问题。真正的生产环境项目求稳永远优先于求新。这里插一段我踩过的坑。用 nvmNode Version Manager管理 Node 版本比手动安装系统级全局版本省心一万倍。macOS 和 Linux 用 nvmWindows 用户装的是 nvm-windows命令基本一致。装完之后记住这几个命令就够了nvm install --lts # 安装当前 LTS 版本 nvm list # 查看已安装的版本 nvm use 22.11.0 # 切换到指定版本有个很容易被忽视的细节如果你执行 nvm install 时指定了一个尚未正式发布的版本号比如 v24.21.0会直接报错error installing 24.21.0: node.js v24.21.0 is not yet released or is not available。这既不是你网络有问题也不是电脑配置有问题就是版本号不存在或还没到发布时间。处理方式很简单先用nvm ls-remote --lts列出当前所有已发布的 LTS 版本再挑一个真实存在的版本安装。日常开发中我建议选定一个 LTS 版本后长期使用不要频繁换版本否则容易出现“本地好好的、CI 挂了”的尴尬局面。2.2 初始化项目和安装 Express环境准备好之后开始搭项目。找一个你觉得顺眼的目录执行mkdir my-express-app cd my-express-app npm init -y npm install expressnpm init -y会生成一个默认的 package.json。这里提醒一下默认生成的 name、author、description 等都是一些占位值正式项目别偷懒至少把 name、version、description、license 填清楚后面做发布、做 CI 都会用到。npm install express会把 Express 下载到本地 node_modules 目录并把依赖记录写入 package.json 的 dependencies 字段。装完之后你会发现 node_modules 里多了一堆文件心理上不要慌那些是 Express 自身依赖的传递依赖完全正常。顺手提醒一句网上搜“Express”这种通用词时经常冒出 SQL Server Express 之类的链接那是微软家免费数据库软件跟 Node.js 生态里的 Express 框架完全是两码事别搞混。拿到初始项目后我建议你顺手改一下 package.json 里的 scripts 字段改成最简单的启动配置{ scripts: { start: node app.js, dev: node --watch app.js } }这里用的是 Node.js 20 以上版本内置的--watch参数可以让代码文件变更后自动重启服务省去手动重启的麻烦。早期版本里它是实验特性实际用下来很稳如果你还在用 Node 18用 nodemon 做同样的事也可以。2.3 Hello World 逐行拆解新建一个 app.js内容如下const express require(express); const app express(); app.get(/, (req, res) { res.send(Hello World); }); app.listen(3000, () { console.log(服务已启动: http://localhost:3000); });这段代码是 Express 的经典起手式总共五个关键点第一行引入 express 模块拿到的实际上是一个函数第二行调用这个函数创建 app 实例第四到第六行告诉 app收到根路径的 GET 请求时返回 Hello World第八行让 app 开始监听 3000 端口。整个过程没有任何多余配置这就是 Express 的直白风格。要注意的是 res.send 和原生 http 的 res.end 的区别。res.send 是 Express 包的一层糖它自动判断你传的是字符串还是对象字符串会设置成 text/html对象会自动 JSON.stringify 并设置 application/json。而且不需要手动追加 charsetutf-8Express 会自动带上 utf-8 编码中文不会再变成乱码。启动验证很简单终端运行node app.js看到“服务已启动”的输出后浏览器打开 http://localhost:3000页面显示 Hello World第一步就通了。3. 路由、中间件与框架对比实战里的高频考点3.1 路由系统从单文件到模块化当你开始写真实项目接口数量会迅速增长。如果全写在 app.js 一个文件里几百行代码堆下去后期维护会非常痛苦。好在 Express 提供了 Router 机制来做模块化拆分。先看路由参数的写法app.get(/user/:id, (req, res) { res.json({ userId: req.params.id }); }); app.get(/search, (req, res) { res.json({ keyword: req.query.keyword, page: req.query.page }); });这里注意区分两种参数来源路径参数挂在 req.params 上是 URL 里的动态段查询参数挂在 req.query 上是问号后面的键值对。面试时经常有人混淆这两个对象实际开发里也经常看到有人把资源标识符放进查询串、把过滤条件塞进路径时间久了接口设计会很难看。我的建议很简单资源定位用路径参数比如 /article/:id非必选过滤和分页用查询串比如 ?page1pageSize10。再来看路由拆分。在项目里建一个 routes/users.js 文件const express require(express); const router express.Router(); router.get(/, (req, res) { res.json({ code: 0, data: [{ id: 1, name: 张三 }] }); }); router.get(/:id, (req, res) { res.json({ code: 0, data: { id: Number(req.params.id) } }); }); module.exports router;然后在 app.js 里挂载const usersRouter require(./routes/users); app.use(/api/users, usersRouter);这样/api/users前缀下面挂了一个 RouterRouter 内部的路径会自动拼接成 /api/users 和 /api/users/:id。多人协作时每人维护一个 Router 文件冲突少、可读性高大项目里几乎都是这个套路。我自己的经验是路由文件的命名最好和资源名对齐一个资源对应一个文件找代码的时候直奔目标不用全局搜索。3.2 中间件顺序和 next 是命门中间件函数长什么样它和普通接口回调的唯一区别就是多了一个 next 参数。调用 next() 会把控制权交给下一个中间件不调用请求就会一直卡在当前位置直到浏览器超时。这一点是新手最容易踩的坑看这个错误示例app.use((req, res, next) { console.log(记录日志); // 忘记调用 next()后续流程全部卡住 }); app.get(/, (req, res) { res.send(永远不会走到这里); });记住一条最核心的规则所有中间件和路由的注册顺序就是请求的执行顺序。日志中间件必须在业务路由之前注册否则请求到达业务路由时已经不需要它了该做鉴权的中间件如果写在公开接口之后那公开接口就白白处于“裸奔”状态。日常开发中常见的中间件大概可以分成三类。第一类是应用级中间件通过 app.use 挂载对所有请求生效比如日志、鉴权、解析请求体。第二类是路由级中间件挂在某个具体 Router 或具体路径上比如只有 /api/admin 开头的请求才需要校验管理员身份。第三类是错误处理中间件函数签名必须是四个参数 (err, req, res, next)注册在所有路由之后统一处理抛出或 next(err) 传来的错误返回 500 或者自定义错误信息。三类中间件看着名字唬人核心逻辑完全一致都是“按顺序接力各干各的活”。Express 自带的内置中间件里最常用的是 express.json() 和 express.static()。前者解析 JSON 请求体并把对象挂到 req.body后者托管静态目录比如图片和前端打包产物。第三方生态里cors 处理跨域、morgan 输出日志、helmet 设置安全响应头装齐这套基本就能把一个后端服务的骨架撑起来。3.3 Express vs Fastify性能与生态的权衡Express 不是唯一选项。近几年 Fastify 声量很大面试时也常被拿来比较。我直接给一张对照表维度ExpressFastify性能中等生态成熟更高号称比 Express 快约 2-3 倍内置机制依赖中间件自由度大内置 schema 校验、插件体系、日志支持TypeScript支持一般需自己配置类型支持更好生态规模巨大资料和中间件最多快速增长但远不如 Express学习成本低中怎么选我的建议很实际如果是公司老项目团队里都是写 Express 的老手没有换框架的硬性理由那就继续用 Express架构升级和人员替换成本都不值得为一点性能去冒。如果是从零起步的个人技术项目或者团队对 TypeScript 强类型体验有很高要求可以试试 Fastify。但从学习顺序和就业角度来看先精 Express 再了解 Fastify永远是最稳妥的路径。绝大多数 Node.js 岗位要求的都是 Express社区里能搜到的解决方案也几乎都围绕它建立。框架之争聊到最后你会发现“能不能快速解决问题”比“纸面性能高多少”重要得多。4. 常见问题与排查技巧实录4.1 端口被占用EADDRINUSE几乎每个新手都会遇到这个报错Error: listen EADDRINUSE: address already in use :::3000。翻译过来就是 3000 端口已经有人在用了。排查三板斧先看是不是上次的 node 进程没退出调试时 CtrlC 按得太快会留下僵尸进程然后在 Windows 用netstat -ano | findstr 3000、macOS 或 Linux 用lsof -i :3000找到占用进程的 PID最后按 PID 杀掉进程Windows 是taskkill /PID PID /FmacOS 或 Linux 是kill -9 PID。如果总是跟别人抢端口更省心的做法是让端口从环境变量读写成const port process.env.PORT || 3000;部署到云服务器或 CI 环境时就不会因为端口写死而翻车。4.2 请求体解析为什么 req.body 是 undefined“我 POST 数据过来req.body 怎么是 undefined”这是后台开发群里出现频率极高的问题。答案通常很简单你忘了在路由之前调用 express.json()。Express 默认不会解析请求体必须先用内置中间件把它挂出来。一个更隐蔽的坑是客户端提交的 Content-Type 如果不是 application/json而是 text/plain 或者 application/x-www-form-urlencodedexpress.json() 根本不会处理。表单提交要用app.use(express.urlencoded({ extended: true }));。所以前后端联调解析不出数据时先抓包看一眼 Content-Type八成问题出在这里。4.3 静态资源 404路径和目录结构的问题托管静态目录的标准写法是const path require(path); app.use(express.static(path.join(__dirname, public)));把 HTML、CSS、图片放到 public 目录后访问对应路径就能得到文件。但新手容易踩两个坑。第一是路径名搞错public 下明明是 assets/app.js你却访问 /public/assets/app.js等于多拼了一段虚拟前缀。第二是相对路径的坑如果 Express 启动时的当前工作目录跟 public 实际位置不一致用相对路径就可能找不到文件。用 __dirname 拼绝对路径能规避大部分这类问题这也是我在所有项目里都坚持的写法。4.4 版本相关的坑Express 4 与 Express 5 的不兼容现在新装 express默认拿到的已经是 5.x 版本。Express 5 和 4 存在一些不兼容点最典型的是路由通配符的写法变了Express 4 里常见的app.get(*, ...)这类通配写法在 5 里会出问题因为底层的路径匹配库做了升级。如果升级过程中看到和 path-to-regexp 相关的报错基本就是这个原因。稳妥的做法是新项目直接用 Express 5老项目可以把 package.json 里锁成express: ^4.21.2等有大块时间再统一升级。不要在生产环境里一边写着旧语法一边升级依赖这个教训我付出过不少加班时间。4.5 面试高频题速查围绕 Express 的面试题翻来覆去都是那几个点。我把最常见的整理成一张速查表问题核心要点Express 是什么基于 Node.js 的 Web 应用框架核心是路由与中间件中间件机制如何工作每个中间件处理请求后调用 next() 进入下一层注册顺序即执行顺序express.json() 做了什么解析 JSON 请求体结果挂到 req.body如何拆分路由用 express.Router() 做模块化app.use 挂载前缀错误处理怎么写四参数中间件 (err, req, res, next)注册在所有路由之后静态文件怎么托管app.use(express.static())推荐绝对路径面试里大部分问题不会超出这个范围。比起背答案更重要的是能把中间件模型讲清楚——这是所有 Express 设计的基础也是面试官判断你是不是“真用过”的分水岭。4.6 一个最容易被忽视的开发习惯注意中间件顺序这个坑我反复踩过还是忍不住再强调一遍。很多人写代码的时候习惯把 app.use(express.static()) 放在最后结果静态资源永远 404习惯把 express.json() 放在某个路由之后结果那个路由的 req.body 永远是空的习惯把错误处理中间件放在最前面结果所有错误都不走进它。中间件顺序之所以重要是因为 Express 的请求处理本质上是线性流水线请求从第一个注册的中间件开始依次经过每一个能匹配的中间件最后才到达路由。你在前面加一个 app.use它就会在所有接口执行之前先过一遍。理解了“先注册先执行”这六个字至少能避开一半的 Express 排障工作。