这次处理一个很小但很典型的 Express.js 场景:后端没有数据库,只有一个 JSON 文件,需要用户信息时用 Node.js 自带的 readFile 把它读出来,再通过 Express 路由返回给前端。很多人刚接触 Express 时,会把所有数据塞进内存变量里,或者一上来就接 MongoDB/MySQL。实际上对学习阶段、原型验证、内部工具来说,直接用 fs.readFile 读一个本地 JSON 文件就够了,代码量少,也不依赖额外数据库服务。
这里说的 readFile,是 Node.js 内置 fs 模块的fs.readFile,不是任何第三方库。核心流程是:浏览器请求/users或/users/:id,Express 收到请求后在路由处理函数里调用fs.readFile读取data/users.json,拿到文件内容后通过JSON.parse转成对象数组,再决定返回全部用户还是只返回匹配 id 的用户。整个服务仅依赖 express 一个 npm 包,其余都用 Node.js 原生能力完成,非常适合用来理解“Express 路由 + 异步文件读取 + JSON 接口”这条链路。
本期内容是 Echo 系列“5分钟学编程 · Express.js 篇”的第 12 篇,定位是能直接上手跑通的最小示例。文章会先带你搭一个最小 Express 服务,准备一份可用的 users.json 用户数据,然后分两种风格实现 readFile 读取:一种是传统的 err-first 回调写法,另一种是fs.promises.readFile配合 async/await 的写法。最后用 curl 做接口验证,并讨论文件路径、JSON 解析、批量读取和后续扩展方向。适合正在学 Node.js/Express 接口开发的读者,也适合需要快速给前端人物 mock 用户数据但不想引入数据库的人。
1. 核心知识点速览
在开始敲代码之前,先把本期的关键信息列出来。下面的表格可以帮助你快速判断这个教程需要安装什么、能学到什么,以及适合用在什么场景。
| 项目 | 本期内容 |
|---|---|
| 教程主题 | Express.js 中使用 Node.js fs.readFile 读取用户信息 |
| 核心能力 | 通过 HTTP 接口读取本地 JSON 文件中的用户数据 |
| 主要依赖 | express,文件读取使用 Node.js 内置 fs 模块 |
| 数据存储 | 本地 JSON 文件 data/users.json,适合学习和原型验证 |
| 接口设计 | GET /users 返回全部用户,GET /users/:id 返回单个用户 |
| 阅读方式 | fs.readFile 回调风格 + fs.promises.readFile async/await 风格 |
| 前置条件 | 已安装 Node.js 和 npm,建议使用 LTS 版本 |
| 服务端口 | 示例统一使用 3000,端口冲突可自行修改 |
| 生产建议 | 文件存储只用于学习和小型实验;生产环境建议接数据库 |
如果你关注的是“这个例子里有没有数据库、要不要装 Redis、要不要写 Docker”,答案是都不需要。你只需要一个能运行 Node.js 的电脑、一个编辑器,以及能执行 npm install 的网络环境。跟着文章往下走,跑通一个只读用户接口的成本很低。
这里也要理解边界:本教程不是讲如何设计一个用户系统,也不是讲如何用 Express + ORM + 数据库做生产级 CRUD。它更像是一块“积木”,帮你把“接口请求”和“文件读取”这两个知识点拼起来。后续如果要增加 POST、PUT、DELETE、登录鉴权、字段权限,完全可以在此基础上继续延伸。
2. 适用场景与学习边界
2.1 这个 Demo 能解决什么问题
从实际使用角度看,这个 Demo 至少有四类用途。
第一,适合刚接触 Express 的初学者。你不需要理解数据库连接池、ORM 映射、事务这些概念,只需要知道“readFile 是异步的,读取完成后要处理回调或 await”,就能写一个可以运行的用户查询接口。
第二,适合项目前期的 mock 接口阶段。前端希望先拿到用户列表数据结构,后端暂时不想搭建数据库,可以直接在项目里放一个 users.json,通过 Express 暴露接口。这样前端联调时不需要等真实数据库。
第三,适合本地工具或内部管理后台。用户量不大、访问量也不高,希望用最少的依赖从文件里读取配置项或人员名单。readFile 直接从文件系统读取即可。
第四,适合想拆解 fs 模块用法的读者。你会发现文件读取成功不代表数据格式正确,格式正确不代表一定能找到目标用户,三层判断本身就是很好的练习。
2.2 不建议在哪些场景使用
文件型存储有它的适用边界。用户量变大后,每个请求都完整读取一个 JSON 文件并 JSON.parse,会造成不必要的磁盘 IO 和 CPU 开销,并且文件内数据会随着用户增长越来越大,读取时间也会变长。高并发场景下,多个请求同时读同一个文件也不是问题,因为读取不会产生写污染;但如果你未来要往文件里写用户数据,就要考虑并发写、文件锁、数据一致性问题。
如果你的应用需要做频繁增删改查,需要按用户名模糊搜索,需要给用户字段加索引,需要不同服务实例共享同一份用户数据,那就不适合继续用 JSON 文件。相对稳妥的方案是换成 SQLite、PostgreSQL 或 MySQL。SQLite 对本地单机项目非常友好,PostgreSQL 和 MySQL 则更适合正式服务端部署。
另外要提醒一点:用户信息属于敏感数据。教程示例中使用的是张三、李四这类演示数据,不涉及真实隐私;但如果你把真实姓名、手机号、身份证号放在 JSON 文件里,又没有接口权限控制,一旦服务被访问到就可能引发数据风险。生产环境使用用户数据必须做脱敏、授权、审计,并避免把静态数据目录直接暴露给浏览器。
3. 环境准备与前置条件
本教程不涉及 GPU 或模型部署,环境要求非常简单。你需要准备以下内容。
操作系统方面,Windows、macOS、Linux 都可以。Node.js 是必须的,建议安装 LTS 版本。如果不确定本机是否安装了 Node.js,可以先打开终端执行下面两条命令,正常输出版本号表示环境可用。
node -v npm -v如果命令找不到,需要先到 Node.js 官网下载对应系统的 LTS 安装包,或者使用 nvm 这类版本管理工具安装。没有 Node.js,后面的 Express 项目完全跑不起来。
编辑器方面,VS Code 或 WebStorm 都可以,能编辑 JavaScript 文件即可。终端工具方面,Windows 可使用 PowerShell 或 CMD,macOS/Linux 使用 Terminal。后续请求接口时,curl 是自带工具,不需要额外安装。
目录结构上,建议新建一个专门的项目目录,避免在系统盘随机位置创建文件。整篇文章推荐的目录是这样的:
express-readfile-demo/ ├── data/ │ └── users.json ├── src/ │ └── server.js └── package.json为什么把数据文件和代码分开?因为 data 是数据资源,src 是服务端代码。如果后续要加配置、日志、静态资源,都可以按目录继续扩展。把 users.json 放在 data 目录下,也能避免不小心把它放到 public 静态目录里导致被前端直接下载。
如果你的电脑上已经安装过 npm 镜像源或者有离线环境,安装依赖会更快。互联网正常的情况下,直接使用 npm 官方源就好,不需要额外配置。
4. 项目初始化与 Express 服务启动
4.1 初始化项目并安装 express
第一步,进入终端,执行下面一组命令。它会创建项目目录、进入目录、初始化 package.json,并安装 express 依赖。
mkdir express-readfile-demo cd express-readfile-demo npm init -y npm install expressnpm init -y会生成一个默认的 package.json,里面的 main 字段默认是 index.js。后续文章推荐把入口文件放在 src/server.js,所以可以手动修改 package.json,或者不强求保持一致。只要启动命令里的路径正确,package.json 里的入口字段不会影响手动执行node src/server.js。
安装成功后,项目里会出现 node_modules 目录,package.json 的 dependencies 中也会出现 express 版本记录。如果你在别的电脑上重新部署,只要保留 package.json,执行npm install就能安装回依赖,不需要复制 node_modules。
4.2 准备用户数据文件
接下来创建 data 目录和用户数据文件。数据内容可以根据自己的需求调整,但结构最好是有 id 的对象数组。这里用一个简单示例:
[ { "id": 1, "name": "张三", "email": "zhangsan@example.com", "city": "北京" }, { "id": 2, "name": "李四", "email": "lisi@example.com", "city": "上海" }, { "id": 3, "name": "王五", "email": "wangwu@example.com", "city": "广州" } ]注意,这里必须保证 JSON 语法正确。很多初学者明明代码逻辑没问题,但 readFile 之后 JSON.parse 一直报错,检查半天才发现是 users.json 里多了一个逗号,或者使用了注释。JSON 文件不能写 JavaScript 注释,也不能在最后一个元素后面加多余的逗号。
4.3 编写基础 Express 服务
在 src 目录下创建 server.js。先用不到完整业务逻辑,只搭一个能访问根路径的服务,确认 express 能正常启动。
const express = require('express'); const app = express(); const PORT = 3000; app.get('/', (req, res) => { res.send('Express + readFile demo'); }); app.listen(PORT, () => { console.log(`Server is running at http://localhost:${PORT}`); });启动命令如下:
node src/server.js终端显示Server is running at http://localhost:3000,就说明基础服务已经启动。此时用浏览器打开http://localhost:3000,页面会显示Express + readFile demo。
这里先别急着继续,先确认端口是否能被访问。如果终端报错Error: listen EADDRINUSE: address already in use :::3000,说明 3000 端口已经被占用,需要把代码里的 PORT 改成 3001 或其他端口,或者在启动命令里通过环境变量动态指定。
5. readFile 读取用户信息接口实现
这一章是核心内容。我们要实现两个接口:GET /users返回全部用户列表,GET /users/:id根据 id 返回单个用户。两个接口都会用到 readFile 去读取 data/users.json。
5.1 理解 fs.readFile 的异步回调机制
fs.readFile是异步读取文件的函数。它不会阻塞 Node.js 事件循环,读取完成后会执行传入的回调函数。回调函数第一个参数是错误对象 err,第二个参数是文件内容 data。如果不指定编码,data 默认是 Buffer;如果指定'utf8',data 就是字符串。
因为 Node.js 的回调风格是错误优先,所以写代码时第一个判断总是if (err)。拿到 data 之后的下一步不是直接返回给客户端,而是判断数据是否能被 JSON.parse 解析。parse 过程中也可能抛出异常,因此要包一层 try catch。
5.2 使用回调风格读取全部用户
先用一个简化的全部用户接口说明整体逻辑:
const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); const PORT = 3000; const usersFilePath = path.join(__dirname, '..', 'data', 'users.json'); app.get('/users', (req, res) => { fs.readFile(usersFilePath, 'utf8', (err, data) => { if (err) { console.error(err); return res.status(500).json({ code: 500, message: '读取用户文件失败' }); } try { const users = JSON.parse(data); res.json({ code: 0, data: users }); } catch (parseErr) { console.error(parseErr); res.status(500).json({ code: 500, message: '用户文件格式错误' }); } }); }); app.listen(PORT, () => { console.log(`Server is running at http://localhost:${PORT}`); });这个版本关注三件事:readFile 读取文件,JSON.parse 转换数据,res.json 返回 JSON。读取成功后的响应结构使用了{ code: 0, data: users },code 表示业务状态码,便于前端统一判断。
这里有一个容易忽略的点:当文件读取失败时,如果直接使用res.json而没有加 return,后续代码仍然可能继续执行,导致 Express 出现无法发送响应头的问题。所以错误分支一定要写 return,或者将 if else 逻辑写完整。上面代码中 return 的作用就是中断当前函数执行。
5.3 引入路由参数并返回单个用户
接下来在相同文件里增加/users/:id路由。这里的:id是路径参数,拿到之后通过req.params.id获取。由于 URL 里的参数是字符串,而 users.json 里的 id 是数字,直接比较会永远找不到,因此需要用Number(req.params.id)做一次转换。
app.get('/users/:id', (req, res) => { const userId = Number(req.params.id); fs.readFile(usersFilePath, 'utf8', (err, data) => { if (err) { console.error(err); return res.status(500).json({ code: 500, message: '读取用户文件失败' }); } try { const users = JSON.parse(data); const user = users.find((item) => item.id === userId); if (!user) { return res.status(404).json({ code: 404, message: `未找到 id 为 ${userId} 的用户` }); } res.json({ code: 0, data: user }); } catch (parseErr) { console.error(parseErr); res.status(500).json({ code: 500, message: '用户文件格式错误' }); } }); });整个逻辑是:读取文件 -> 解析 JSON -> 在数组中 find 目标用户 -> 找不到就返回 404,找得到就返回用户对象。这个链路覆盖了 readFile、JSON.parse、Array.find 和 Express 状态码组合使用,是本期最值得吸收的部分。
5.4 改造成 fs.promises.readFile + async/await
回调风格写两层嵌套还能接受,但一旦后续增加更多判断,回调嵌套会变得很难维护。Node.js 从较新版本开始提供了fs.promises模块,我们可以用require('fs').promises拿到 Promise 风格的 readFile,再通过 async/await 顺序写出同步代码的观感。
改造后的完整入口文件如下:
const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); const PORT = 3000; const usersFilePath = path.join(__dirname, '..', 'data', 'users.json'); const fsp = fs.promises; app.get('/users', async (req, res) => { try { const data = await fsp.readFile(usersFilePath, 'utf8'); const users = JSON.parse(data); res.json({ code: 0, data: users }); } catch (err) { console.error(err); res.status(500).json({ code: 500, message: '服务器读取用户文件失败' }); } }); app.get('/users/:id', async (req, res) => { const userId = Number(req.params.id); try { const data = await fsp.readFile(usersFilePath, 'utf8'); const users = JSON.parse(data); const user = users.find((item) => item.id === userId); if (!user) { return res.status(404).json({ code: 404, message: `未找到 id 为 ${userId} 的用户` }); } res.json({ code: 0, data: user }); } catch (err) { console.error(err); res.status(500).json({ code: 500, message: '服务器内部错误' }); } }); app.listen(PORT, () => { console.log(`Server is running at http://localhost:${PORT}`); });与回调风格相比,async/await 版本不再需要手动处理第二层回调。readFile 的异常会直接进入 catch 块,JSON.parse 抛出的异常也可以被同一个 catch 捕获。代码结构从“嵌套”变成“顺序”,更适合新手理解,也更适合后续维护。
需要区分的是:这里用 try catch 包住 await,并不会让所有文件读取都变成同步操作。await 只是让异步代码的书写方式更接近同步,底层仍然是异步执行。两个/users和/users/:id请求同时到达时,Node.js 依然能处理其他事件,不会因为一个文件读取请求而阻塞全部请求。
5.5 文件路径为什么建议使用 __dirname + path.join
文章中的代码使用了path.join(__dirname, '..', 'data', 'users.json'),而不是直接写'./data/users.json'。
原因在于:__dirname始终指向当前模块文件所在目录。在 src/server.js 里,__dirname 指向 src 目录,所以..回到项目根目录,再拼接 data/users.json 就能稳定定位到数据文件。如果直接写相对路径,它依赖的是进程启动时的当前工作目录。当你在项目根目录执行node src/server.js时没问题,但如果某天你换到其他目录启动这个脚本,相对路径就可能指向不存在的文件,readFile 就会报 ENOENT 错误。
使用 path.join 也比直接写字符串拼接__dirname + '/../data/users.json'更稳,因为 path.join 会按当前操作系统的路径分隔符处理。Windows 下不用太担心路径反斜杠和正斜杠的差异。
如果你把 users.json 和 server.js 放在同一个目录,可以简化成path.join(__dirname, 'users.json')。但既然项目分层是 src 和 data,保留..是正确做法。
6. 功能测试与效果验证
完成代码后,最重要的事情是验证接口是否符合预期。本节给出完整的启动、请求和判断方法。
6.1 启动 Express 服务
在项目根目录执行:
node src/server.js看到终端输出Server is running at http://localhost:3000就说明服务启动成功。如果修改了代码,需要先按 Ctrl + C 停掉当前进程,再重新执行启动命令。不要开着旧进程测新代码。
6.2 使用 curl 测试接口
打开一个新终端,分别执行下面三条命令:
curl http://localhost:3000/users curl http://localhost:3000/users/1 curl http://localhost:3000/users/999第一条命令预期返回全部用户数组:
{"code":0,"data":[{"id":1,"name":"张三","email":"zhangsan@example.com","city":"北京"},{"id":2,"name":"李四","email":"lisi@example.com","city":"上海"},{"id":3,"name":"王五","email":"wangwu@example.com","city":"广州"}]}第二条命令预期返回 id 为 1 的用户对象:
{"code":0,"data":{"id":1,"name":"张三","email":"zhangsan@example.com","city":"北京"}}第三条命令因为 users.json 中没有 id 为 999 的用户,预期返回 404:
{"code":404,"message":"未找到 id 为 999 的用户"}6.3 判断成功的标准
接口是否正常,可以从三个层面判断。
HTTP 状态码层面,/users 应返回 200,/users 下面不存在的路径应返回 404,文件读取失败时应返回 500。业务状态码层面,code 字段为 0 表示查询成功,code 为 404 或 500 表示失败。数据内容层面,/users 的 data 字段应该是数组,/users/:id 的 data 字段应该是用户对象,而不是套了一层多余数组。
前端在调用时,通常会先判断response.ok,再判断 body.code。如果后端没有统一返回结构,前端就要额外处理字符串、对象、错误文本等多种格式。本教程在示例中返回 code 字段也是一种工程化习惯,方便前端统一拦截错误。
6.4 验证失败时的排查方向
如果访问 /users 时终端报错,优先查看终端里是否打印了错误堆栈。常见的失败原因有三类。
第一,文件路径不对。报错信息通常是ENOENT: no such file or directory。检查 src/server.js 里 usersFilePath 是否指向了真实存在的 data/users.json。如果项目根目录没有 data 目录,需要先创建。
第二,JSON 解析失败。常见错误是Unexpected token } in JSON。用编辑器打开 users.json 检查是否存在多余逗号、中英文引号混用、文件尾部缺少右中括号等问题。JSON 没有注释,不要用//注释。
第三,路由顺序问题。/users/:id应该放在/users后面并没有严格顺序要求,因为 Express 可以使用精确路径匹配,不会把/users和/users/:id混淆。但如果你以后增加/users/me这类固定路径,一定要把它放在/users/:id之前,否则/users/me会被当成 id 为 me 的动态参数处理。
7. 接口 API 与批量任务扩展
严格来说,上一章实现的/users和/users/:id已经是一个可用的 HTTP API。前端可以直接用 fetch 调用,也可以被其他后端服务通过 curl 调用。这一节继续讨论 API 参数、统一返回和批量读取扩展。
7.1 前端如何调用这个接口
假设你的前端页面运行在 http://localhost:5173,后端接口位于 http://localhost:3000,可以使用下面这段 fetch 读取全部用户:
fetch('http://localhost:3000/users') .then((res) => res.json()) .then((body) => { if (body.code === 0) { console.log(body.data); } });读取单个用户时,把 id 放到路径里:
const userId = 1; fetch(`http://localhost:3000/users/${userId}`) .then((res) => res.json()) .then((body) => { if (body.code === 0) { console.log(body.data); } });这里需要注意跨域问题。如果你直接从浏览器里的另一个端口访问 Express 接口,浏览器会拦截跨域请求。本教程不讨论跨域,但你在真正开发前后端分离项目时,可能需要为 Express 增加 cors 中间件或手动设置响应头。这个问题常见于本地联调,建议提前了解。
7.2 统一返回格式与错误处理
上一章接口返回的格式并不完全相同。成功时返回{ code: 0, data: ... },404 时返回{ code: 404, message: ... },500 时也返回 message。如果前端希望在出错时也拿到 code,这个结构是一致的;但如果你希望错误时也必须有 data 字段,可以进一步约定:所有响应都带 code、message、data 三个字段,即使错误时 data 为 null。
建议在后续开发中封装一个统一响应函数,而不是在每个路由里重复写 status 和 json。这样能显著减少出错概率。比如抽成一个sendSuccess(res, data)和sendError(res, status, message),路由内部只关心业务逻辑。
7.3 单个用户一个文件:Promise.all 批量读取
之前的设计是一个 users.json 里放所有用户。实际工作里有时会遇到另一种设计:每个用户一个 JSON 文件,文件放在 data/users 目录下。这时候要返回全部用户,就不能只 readFile 一次,需要读取目录下的多个文件并汇总结果。
这里给出一个批量读取多个用户文件的示例。先把代码需要的目录结构调整成:
express-readfile-demo/ ├── data/ │ └── users/ │ ├── 1.json │ ├── 2.json │ └── 3.json然后修改路由逻辑,使用fs.promises.readdir读取目录,再通过Promise.all并发读取所有文件。
const usersDir = path.join(__dirname, '..', 'data', 'users'); app.get('/users', async (req, res) => { try { const files = await fsp.readdir(usersDir); const jsonFiles = files.filter((file) => file.endsWith('.json')); const readPromises = jsonFiles.map(async (file) => { const content = await fsp.readFile(path.join(usersDir, file), 'utf8'); return JSON.parse(content); }); const users = await Promise.all(readPromises); res.json({ code: 0, data: users }); } catch (err) { console.error(err); res.status(500).json({ code: 500, message: '批量读取用户文件失败' }); } });从“readFile 读取用户信息”的角度看,这个批量读取扩展依然以单个 readFile 为核心,只是通过 readdir 拿到文件列表,再用 Promise.all 并行执行多个 readFile。文件数量不多时,这种写法清晰高效。如果文件数量很大,比如到上千个,一次性并发读取所有文件可能造成文件描述符压力,需要引入分批读取或并发限制机制。
7.4 什么情况需要引入正式任务队列
如果读取单个文件本身已经足够快,不需要额外设计任务队列。但如果你在扩展场景中不只是 readFile,而是要在读取每个用户文件后做 OCR、图像处理、模型推理或第三方 API 调用,单次请求就会变成耗时任务。这种情况下,建议把任务放到异步队列中处理,而不是让 HTTP 请求一直等待所有文件处理完成。
比较好的做法是先接收任务并返回任务 id,后台通过队列逐个处理,前端再通过轮询或 WebSocket 获取处理结果。Node.js 社区常见的方案包括 BullMQ、Redis 队列和更轻量的内存任务列表。这些不是本期重点,只在遇到真实耗时场景时再做迁移。
8. readFile 性能观察与注意事项
虽然 Express + readFile 不需要 GPU 或显存,但文件读取依然有性能边界需要说清楚。
8.1 readFile、readFileSync 与 fs.promises.readFile 的差异
很多新手会混淆 Node.js 里三种读取文件的方式。这里用一张表说明。
| 读取方式 | 是否阻塞 | 代码风格 | 适用场景 |
|---|---|---|---|
| fs.readFileSync | 同步阻塞 | try/catch 同步 | 启动时加载配置、工具脚本 |
| fs.readFile | 异步不阻塞 | 回调函数 | 路由内读取请求数据 |
| fs.promises.readFile | 异步不阻塞 | async/await + try/catch | 路由内读取,推荐优先使用 |
在 Express 路由处理函数里,不建议使用 readFileSync。因为 readFileSync 会阻塞事件循环,在读写较大的文件时,其他请求会被卡住,影响服务吞吐量。启动时读取少量配置,readFileSync 是合理的;但请求处理链路中,异步读取是默认标准。
8.2 每次请求都重复读文件的问题
当前实现的/users和/users/:id会在每次请求时都调用一次 readFile。对于一个只有几条用户数据的 JSON 文件,这个成本可以忽略。但如果你把 users.json 一直增长到几万条用户,每次请求都要读取整个几 MB 文件并解析 JSON,响应延迟会明显增加,同时磁盘 IO 也浪费在“其实只需要一个用户”的场景上。
更合理的做法分几层。如果用户数据变化不频繁,可以在服务启动时读文件并缓存到内存,后续请求直接返回缓存数据;在用户修改后手动刷新缓存或设置过期时间。如果文件变得很大,可以使用数据库替换 JSON 文件,数据库天然支持按 id 索引,不需要每次全量加载。如果数据从远程接口获取,再考虑缓存层。
8.3 多实例部署时文件存储的注意点
假设同一个服务部署了两份实例,分别运行在不同的进程或机器上,它们各自读取同一份文件。如果文件在某台机器本地,另一个实例读不到同样的数据,就会出现用户信息不一致。解决思路是把数据存储到共享数据库,或者将 users.json 放到共享存储上并保证所有实例都能访问。不过对于学习项目,本地单实例跑一个 Node 进程已经足够演示 readFile 的使用方法,不需要过早考虑多实例一致性。
8.4 文件编码与中文乱码问题
读取文件时一定要指定编码'utf8',否则返回的是 Buffer。Buffer 在 JavaScript 里会被自动转成类数组对象,直接 JSON.parse 会报错。即使你稍后手动调用Buffer.toString(),如果文件本身是用 GBK 保存的,中文仍可能乱码。最好的做法是统一把 data/users.json 保存为 UTF-8,并在 readFile 的第二个参数传'utf8'。编辑器右下角一般会显示文件编码,注意确保是 UTF-8。
9. 常见问题与排查方法
Express + readFile 这类入门项目,排查难度其实不高。下面这张表把最容易遇到的问题集中列出。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动报 EADDRINUSE | 3000 端口被占用 | 终端查看报错;运行 lsof -i :3000 | 更换端口或关闭占用进程 |
| 提示 Cannot find module 'express' | 未安装 express 依赖 | 查看 package.json 是否包含 express | 执行 npm install |
| readFile 报 ENOENT | 文件路径错误 | 打印 usersFilePath 拼出的路径 | 使用 __dirname + path.join |
| JSON.parse 报 unexpected token | users.json 格式错误 | 打开文件检查逗号和引号 | 修正 JSON 文件 |
| 访问接口返回空对象或乱码 | readFile 没指定 utf8 | 检查第二个参数 | 改为 fs.readFile(file, 'utf8') |
| /users/1 一直查不到用户 | id 类型是字符串 | console.log 打印 req.params.id | 使用 Number(req.params.id) |
| 修改代码后接口不变 | 旧进程没退出 | 查看终端进程 | 重启 node 进程,或用 nodemon |
| 前端浏览器跨域 | 不同端口互相请求 | 查看浏览器 Network 报错 | 引入 cors 或设置响应头 |
| 找不到 id 时也返回 200 | 路由里没判断 user 是否存在 | 检查 find 后的 if | find 不到时 return res.status(404) |
其中端口占用和文件路径错误是出现频率最高的两个问题。端口占用可以换一个端口继续测试,比如把 PORT 改成 3001。文件路径问题可以在路由处理函数里临时执行console.log(usersFilePath),然后去系统里确认这个路径到底是否存在。
依赖安装失败也比较常见。如果 npm install 因为网络问题失败,可以优先检查 npm 源配置或使用其他镜像源,但注意不要为了安装依赖而使用非常规代理工具。安装成功后,node_modules 目录会存在,package-lock.json 也会生成。
如果你使用的是旧版本 Express,启动方式和代码略有差异。当前常规稳定版本是 Express 4.x,文章中的代码基于常见稳定写法;使用 Express 5 时,存在部分异步错误处理差异,建议遇到版本相关问题以你实际安装版本的官方文档为准。
10. 最佳实践与安全建议
到这里,核心代码已经可以跑通。为了让你在实际项目中不只是“能用”,而是“用得稳”,下面给出几条工程化建议。
10.1 路径尽量用绝对路径
始终使用path.join(__dirname, ...)解析文件路径。这个做法可以避免“启动目录变了导致文件找不到”的问题。如果要支持外部配置,可以用环境变量传入数据目录路径,而不是把路径写死散落在多个路由中。
10.2 优先选择异步方式并在路由内做好错误处理
路由处理函数中使用fs.promises.readFile搭配 async/await,能让代码顺序更清晰。每次读取文件都要处理失败分支,不要让“文件不存在”“JSON 格式错误”“目标用户不存在”三个错误都裸奔给前端。建议将三个状态分别映射为 500、500、404,避免前端把错误解析成成功。
10.3 控制返回字段范围
当前 users.json 中如果有内部字段,比如 password、phone、internalRemark,不应该原样返回给所有客户端。可以在返回前做一次字段过滤。最简单的方式是解构出需要的字段:
const safeUser = { id: user.id, name: user.name, email: user.email }; res.json({ code: 0, data: safeUser });更复杂的项目可以封装 pick 函数或使用序列化工具。原则是“最小返回字段”,不要把不必要的内部数据暴露给前端。
10.4 不要把用户 JSON 放进 public 静态目录
如果 Express 配置了express.static('public'),并且你把 users.json 误放到 public 目录,那么任何人都可以直接通过http://localhost:3000/users.json下载整个文件,绕过你的路由逻辑。用户数据文件应该放在服务端可读、但不能被静态资源服务直接访问的位置,例如 data 目录,并确保没有把 data 目录作为静态资源根目录。
10.5 真实用户数据必须做合规处理
本教程使用的用户信息完全是演示数据。如果你拿它处理真实用户信息,需要考虑数据来源的合法授权、最小化采集、脱敏展示、访问日志审计。即使只是本地测试,也不要随意下载和存储真实用户的敏感信息。涉及第三方平台用户数据时,还要遵守对应平台的使用条款和隐私合规要求。
10.6 文件存储只是过渡方案
JSON 文件存储适合在用户量小、结构简单、读取频率不高的情况下使用。当你发现需要写操作、需要并发控制、需要按字段查询、需要保证多实例数据一致时,应尽早切换到 SQLite、PostgreSQL、MySQL 等数据库。不要把 JSON 文件当成长期数据库,除非你能接受它的边界和风险。
10.7 开发阶段推荐使用 nodemon 自动重启
每次修改 server.js 都要手动重启服务,开发体验一般。可以安装 nodemon 作为开发依赖,然后使用npx nodemon src/server.js启动。这样修改代码后服务会自动重启,提升调试效率。生产环境尽量使用 node 直接启动或配合进程管理工具,不要依赖 nodemon。
11. 总结与下一步
这一篇解决的核心问题是:在 Express.js 中如何用 Node.js 的 readFile 读取用户信息并暴露为 HTTP 接口。你已经看到,不需要引入数据库,借助 fs 模块和 express 就能完成一个最小但完整的读取链路。重点知识包括:readFile 的异步回调机制、JSON.parse 的必要性、路由参数的类型转换、错误状态码的区分以及使用 __dirname + path.join 解决路径问题。
建议你先不要急着跳到数据库或大型框架。先把上面最小的服务跑通,手动执行一遍 curl,体会从“浏览器发起请求”到“Express 接收请求”,再到“readFile 读取文件”这三个环节。最容易踩的坑有两个:一个是路径没写对导致 ENOENT,另一个是 req.params.id 与 JSON id 类型不一致导致查询不到用户。这两个坑以后写 Node.js 服务时会反复遇到,尽早踩清楚更划算。
下一步可以在这个 Demo 基础上做几个方向的扩展。第一个方向是给 /users 增加 POST 接口,用 fs.writeFile 或 fs.promises.writeFile 写入新的用户;第二个方向是把用户数据从 JSON 文件迁移到 SQLite,体会文件存储与关系型数据库的差异;第三个方向是为 /users/:id 增加鉴权中间件,只有携带有效 token 的请求才允许读取用户信息。每一步都不需要替换全部代码,而是继续加深对 Express 路由和 Node.js 文件模块的理解。
如果这篇内容对你有帮助,可以先收藏备用。等真正需要写一个文件读接口时,直接翻到第 5 章的完整代码,复制后改成自己的数据目录即可。