news 2026/9/4 2:25:06

Express.js 用户数据读取:使用 fs.readFile 解析 JSON 的完整教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Express.js 用户数据读取:使用 fs.readFile 解析 JSON 的完整教程

刚开始接触 Express.js 开发时,最头疼的问题往往不是路由怎么写,而是“用户数据到底从哪里来”。接数据库 Mongo、MySQL 对新手太重;写死在内存数组里,服务一重启数据就失效。对小工具、Demo 或者教学项目来说,一个很实用的过渡方案是:把用户信息保存到本地 JSON 文件,用 Node.js 自带的 fs.readFile 去读取,再通过 Express 接口返回给前端。本文通过一个完整示例,把 readFile 的基础语法、Express 集成方式、异常处理和工程化建议一次讲清楚。

很适合这几类同学阅读:正在学 Node.js / Express.js 的初学者;想用文件做临时数据的个人开发者;以及准备给老项目加一个轻量数据层的后端开发。学习完你不仅能写出可运行的用户列表接口,还能理解为什么自己写的路径会报错、为什么读出来的内容不能直接用,以及遇到报错该从哪里排查。

1. 为什么在 Express 应用中用 readFile 读取用户信息

1.1 这个需求从哪里来

很多前端转后端或者刚接触 Node.js 的同学都有一个惯性思维:把数据放在 JS 文件里,运行时用变量保存。

比如说:

const users = [ { id: 1, name: '张三' }, { id: 2, name: '李四' } ];

这种方式写接口很快,但问题也很明显:

  • 数据是临时内存数据,服务重启后全部丢失。
  • 数据改动要在代码里进行,没有独立的“数据层”。
  • 无法模拟真实项目中“异步读取数据”的场景。

后来我参与几个小型项目,才慢慢意识到一个折中方案:在项目目录里放一个users.json,用 Node.js 的fs.readFile把数据异步读取出来,再由 Express 路由返回。这样做的好处是:

  • 数据从代码中分离出来,修改用户信息不需要重启接口服务。
  • 只要文件存在,数据就是持久的。
  • 不引入数据库依赖,项目结构依然很轻。
  • 能顺便练习 Node.js 的回调、异步和异常处理。

这种模式常见于个人博客的留言数据、内网小工具的用户配置、课程设计的学校管理系统,以及临时给前端提供 Mock 数据。

1.2 readFile 到底做了什么

readFile是 Node.js 内置 fs 模块提供的一个异步方法,作用是把指定路径的文件整个读到内存中。

可以把它理解成“打开一个文件,读出全部文本内容”。它接收三个参数:文件路径、编码格式、回调函数。当文件读取完成时,Node.js 会把结果交给回调函数。

再看一个表达式:

fs.readFile(path, 'utf8', callback);

其中第三参的回调如果写成(err, data) => {},那么:

  • 当文件读取失败时,err有值,dataundefined
  • 当文件读取成功时,errnulldata是文件内容。

初学者最容易犯的错是:拿到data之后直接用,忘记它是一个字符串而不是对象。尤其当文件内容是 JSON 时,必须先JSON.parse(data)才能转成数组或者对象,否则 Express 的res.json()接收到字符串后,行为会和你预期的不一致。

2. 环境准备与项目初始化

为了下面的示例能跑起来,需要先准备好 Node.js 环境。这里以 Node.js LTS 环境为例,代码统一使用 CommonJS 模块规范。如果你的项目开启了"type": "module",稍作修改也能运行,但下面的内容默认不用 ESM。

版本方面不用刻意追求最新。只要能正常执行npm install express,并且内置的require('fs')可用即可。示例里依赖的核心包只有 Express,如果你只想测试 fs.readFile,甚至可以不安装任何第三方依赖。

2.1 初始化项目

在命令行中执行:

mkdir express-readfile-demo cd express-readfile-demo npm init -y

执行之后,会生成一个package.json文件,用来管理项目依赖和脚本命令。

然后安装 Express:

npm install express

安装完成后,package.jsondependencies中能看到express字段。如果你网络环境比较特殊,安装变慢,可以考虑切换 npm 镜像源后再试。

2.2 准备项目目录结构

推荐的文件组织方式如下:

express-readfile-demo/ ├── data/ │ └── users.json ├── app.js ├── package.json └── node_modules/

data/users.json用来存放用户数据,app.js是 Express 服务入口。路由逻辑、文件读取逻辑会在app.js中完成。正式项目里可以把路由和数据访问层继续拆分,但这个 Demo 保持单文件有助于理解数据流。

创建data目录:

mkdir data

3. 手写代码之前,先看懂 fs.readFile

3.1 完整语法与参数说明

fs.readFile的标准形式是:

fs.readFile(path[, options], callback)

第二参options可以是一个字符串编码,也可以是一个对象。例如:

  • 'utf8':表示按 UTF-8 编码读出字符串。
  • { encoding: 'utf8', flag: 'r' }:对象形式。
  • 如果不传编码,默认返回 Buffer 对象。

回调函数是必须的:

fs.readFile('./data/users.json', 'utf8', (err, data) => { if (err) { console.error(err); return; } console.log(data); });

这样写的时候,console.log输出的data是一个字符串。如果文件内容是下面的 JSON:

[ { "id": 1, "name": "张三" } ]

那么控制台输出的是包含方括号和花括号的完整文本。

3.2 为什么要传 utf8 编码

很多新手去掉'utf8'后发现打印出来的是一堆看不懂的字节,原因是 readFile 默认输出的 Buffer 对象,本质上是二进制数据在内存中的表示。打印时虽然能看到十六进制内容,但没法直接用于业务逻辑。

比如:

fs.readFile('./data/users.json', (err, data) => { console.log(data); // 默认输出 Buffer });

输出类似:

<Buffer 5b 0a 20 20 7b 20 22 69 64 22 3a 20 31 20 7d 0a 5d>

想让 readFile 直接输出文本,就必须指定编码。最简单的写法是传入'utf8',Node.js 读取完成后会自动做一次字符解码。

但在处理图片、音频、压缩包等二进制文件时,就不要指定 UTF-8 了。本需求是读取用户信息文本文件,所以必须设置。

3.3 相对路径为什么会踩坑

初学者最容易踩的坑,是把路径写成了相对当前命令执行目录,而不是相对当前代码文件。

比如你现在项目的根目录执行:

node app.js

如果app.jsdata目录都在项目根目录下,执行node app.js时也许一切正常。但如果你在src目录或者系统计划任务中换了一种启动方式,那么相对路径可能变为src/data/users.json,导致文件不存在错误。

更稳妥的做法是借助path模块,将路径基于__dirname拼接。__dirname永远是当前 JS 文件所在的目录。

const path = require('path'); const filePath = path.join(__dirname, 'data', 'users.json');

这种方式不会因为启动目录的变化而找不到文件,后面所有示例都会使用这一写法。

4. 完整实战:用 Express + readFile 实现用户信息读取接口

4.1 准备用户数据文件

data/users.json中写入以下内容:

[ { "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 数组。每个用户对象包含idnameemailcity。字段可以根据业务随意扩展,但要注意 JSON 不能写注释,逗号也必须规范。如果手写容易漏逗号,可以用 VS Code 的 JSON 格式化能力自动整理。

4.2 创建基础的 Express 服务

在项目根目录创建app.js

const express = require('express'); const path = require('path'); const app = express(); const PORT = 3000; app.get('/', (req, res) => { res.send('用户信息服务已启动'); }); app.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); });

这里的res.send()是 Express 提供的最简单响应方式。当我们在浏览器访问http://localhost:3000时,会看到页面输出字符。

先运行一遍:

node app.js

如果控制台打印Server running at http://localhost:3000,说明 Express 环境没问题。接下来逐步加入 readFile 相关逻辑。

4.3 编写 /users 用户列表接口

在刚才的app.js基础上加入 fs 模块和 readFile 逻辑。

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({ message: '服务器内部错误' }); } try { const users = JSON.parse(data); res.json(users); } catch (parseError) { console.error('用户文件解析失败:', parseError); res.status(500).json({ message: '用户数据格式错误' }); } }); }); app.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); });

代码说明:

  1. usersFilePath保存绝对路径,放在路由外部,避免每次请求都重新拼接。
  2. fs.readFile是异步读取,读取完成前不会阻塞其他请求。
  3. err分支必须处理,因为文件一旦被意外删除,readFile 回调里会收到错误对象。
  4. JSON.parse放在 try-catch 中,是因为即使文件存在,内容也可能因为人为改动而不再是合法 JSON。
  5. res.status(500).json()表示返回 HTTP 状态码 500 和一个 JSON 对象。

这里的关键在于:readFile 的“操作结果”不是通过return返回的,而是通过回调参数传给我们的。阅读代码时,要把回调函数内部当成“文件读取完成后才执行的代码块”,不要误以为return res.status(...)能让外层函数立刻结束。

4.4 增加 /users/:id 单用户查询路由

多数项目里,光有列表还不够,还需要前端传入用户 ID 查询某个用户信息。我们可以修改app.js,增加一个带动态参数的路由:

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'); // 读取并解析用户文件 function readUsersFile(callback) { fs.readFile(usersFilePath, 'utf8', (err, data) => { if (err) { callback(err); return; } try { const users = JSON.parse(data); callback(null, users); } catch (parseError) { callback(parseError); } }); } // 用户列表 app.get('/users', (req, res) => { readUsersFile((err, users) => { if (err) { console.error('读取用户数据失败:', err); return res.status(500).json({ message: '服务器内部错误' }); } res.json(users); }); }); // 根据 id 查单个用户 app.get('/users/:id', (req, res) => { const userId = Number(req.params.id); readUsersFile((err, users) => { if (err) { console.error('读取用户数据失败:', err); return res.status(500).json({ message: '服务器内部错误' }); } const user = users.find((item) => item.id === userId); if (!user) { return res.status(404).json({ message: '用户不存在' }); } res.json(user); }); }); app.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); });

这里的req.params.id是字符串类型。如果直接和 JSON 里的数字 ID 比较,可能会因为类型不一致导致永远匹配不到,所以要先用Number()做类型转换。

把读取文件的逻辑抽取成readUsersFile后,两个路由可以复用同一套文件读取和 JSON 解析逻辑。后续要换数据源,也只需要改这一个函数。

4.5 启动服务并用浏览器/curl 验证

重启服务:

node app.js

访问用户列表接口:

curl http://localhost:3000/users

预期输出:

[ { "id": 1, "name": "张三", "email": "zhangsan@example.com", "city": "北京" }, { "id": 2, "name": "李四", "email": "lisi@example.com", "city": "上海" }, { "id": 3, "name": "王五", "email": "wangwu@example.com", "city": "广州" } ]

访问单个用户:

curl http://localhost:3000/users/2

预期输出:

{ "id": 2, "name": "李四", "email": "lisi@example.com", "city": "上海" }

访问不存在的 ID:

curl http://localhost:3000/users/999

预期输出:

{ "message": "用户不存在" }

5. 不只回调:readFile 的异步演进与现代写法

5.1 为什么越来越多的项目改用 fs.promises

Node.js 的 fs 模块自带回调风格,但回调风格在链路复杂之后容易产生嵌套。上面只读一个文件还好,如果查询某个用户后还要读取他的订单文件,再读取订单里的商品文件,就会形成“回调地狱”。

Node.js 提供了 Promise 版本 API,也就是:

const fs = require('fs').promises;

也可以写为:

const fsPromises = require('fs/promises');

两者本质一样,都提供了readFile方法,但不再使用回调,而是返回 Promise 对象。这样就能结合async/await写出更接近同步逻辑的代码。

5.2 async/await 读取用户文件的改写版

可以基于上面的示例,把readUsersFile改造成 Promise 版本:

const express = require('express'); const fs = require('fs').promises; const path = require('path'); const app = express(); const PORT = 3000; const usersFilePath = path.join(__dirname, 'data', 'users.json'); async function readUsers() { const data = await fs.readFile(usersFilePath, 'utf8'); return JSON.parse(data); } app.get('/users', async (req, res) => { try { const users = await readUsers(); res.json(users); } catch (error) { console.error('读取用户数据失败:', error); res.status(500).json({ message: '服务器内部错误' }); } }); app.get('/users/:id', async (req, res) => { const userId = Number(req.params.id); try { const users = await readUsers(); const user = users.find((item) => item.id === userId); if (!user) { return res.status(404).json({ message: '用户不存在' }); } res.json(user); } catch (error) { console.error('读取用户数据失败:', error); res.status(500).json({ message: '服务器内部错误' }); } }); app.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); });

这里有个非常重要的变化:async函数中await fs.readFile()之后的代码,会等待文件读取完成再执行。如果文件读取失败,Promise 会进入 rejected 状态,并被路由函数里最近的 try-catch 捕获。

阅读这段代码时要注意:readUsers()函数中await后面的异常会向外抛出,因此调用它时必须用 try-catch 包裹,否则 Express 路由回调可能处理不了 rejected 状态,造成进程层面的 UnhandledPromiseRejection。

5.3 Express 5 / 异步路由的错误转发

如果你用的是 Express 5,异步路由抛出的错误会自动传给错误处理中间件。但为了兼容性和行为可控,我依然建议在每个路由内部显式处理 try-catch。尤其在团队协作中,这种“自己能处理的错误自己收口”的方式,比依赖框架行为更容易排查。

同样,生产项目里不建议把整段 try-catch 重复写在多个路由中,可以定义一个统一的错误处理中间件,把读取数据错误集中处理,但这部分内容超出本案例核心,可以留到后续讲。

6. 常见报错与排查清单

6.1 高频错误现象对照表

问题现象常见原因解决思路
浏览器访问接口返回 500文件路径不对,readFile 找不到文件打印 usersFilePath,检查文件是否真实存在
读取出来是 Buffer 乱码没有给 readFile 传 utf8 编码给第二参传入'utf8'
JSON.parse报错JSON 文件被手写坏,缺少逗号或多余逗号用 JSON 格式化工具检查内容
接口返回 undefined没有先 JSON.parse 就使用 data确认读取结果是字符串,先解析再使用
/users/:id查不到数据req.params.id 是字符串,而数据 id 是数字用 Number() 做类型转换
服务启停后加载不出来相对路径随启动位置变化使用path.join(__dirname, ...)构建路径
回调函数里的 res.json 没有执行误把 return 写在 readFile 外层检查 readFile 回调内部是否有 return
大量请求时接口变慢每次请求都重复读盘引入缓存或读取一次后放入内存

6.2 文件找不到排查步骤

当接口返回 500,同时控制台出现类似ENOENT: no such file or directory, open ...的错误时,说明 readFile 去打开不存在的文件。

执行顺序如下:

  1. app.js中打印usersFilePath
  2. 到打印出来的路径下确认users.json是否真的存在。
  3. 如果路径最后是.../express-readfile-demo/data/users.json,但目录data下文件名为user.json,则命名不一致。
  4. Windows 下注意路径分隔符,path.join会自动处理,不要手动拼接'\\''/'
  5. 确认你编辑的 JSON 文件保存时没有额外扩展名,比如users.json.txt

6.3 JSON.parse 报错排查

JSON 解析失败的原因最常见的是手写 JSON 不严谨。比如说:

{ id: 1, name: '张三', }

在 JS 对象里能运行,但 JSON 文件里不能写单引号,也不能没有属性名的双引号。合法的 JSON 对象要保持"key": value的形式,并且最后一个属性后面不能有多余逗号。

如果你修改过users.json后接口突然 500,第一时间可以用 Node 命令行快速检查:

node -e "JSON.parse(require('fs').readFileSync('./data/users.json', 'utf8')); console.log('OK')"

能打印OK说明数据文件格式正确。

7. 工程建议与小项目落地要点

7.1 永远使用绝对路径

不管项目有多小,读文件的路径都建议用path.join(__dirname, 'data', 'users.json')。初学者最容易忽略的就是把路径写成'./data/users.json',然后在不同终端环境下启动导致路径错乱。虽然一个同学在本地 ESM 项目里也用过import.meta.url拼接,但对 CommonJS 的 Express 项目,__dirname是最稳定的答案。

7.2 不要在路由回调里堆太多逻辑

上面示例把readUsers()抽成独立函数,是一种适合继续扩展的习惯。路由只负责“接收请求、调用数据函数、返回响应”,数据函数只负责“读文件并解析”。后续如果要换成从 MySQL 读取,路由层基本不用动。

如果项目继续膨胀,可以继续拆分controllersservicesmodels层。但即使不拆那么细,至少做到“一个函数只做一件事”,代码的可读性会高很多。

7.3 利用 JSON 解析后的缓存优化

每次请求都执行fs.readFile,在小 Demo 里没有任何问题,但如果这个接口被频繁调用,重复读同一个文件会造成不必要的磁盘 IO。可以考虑做一层简单的缓存:

let cachedUsers = null; let cacheTime = 0; const CACHE_DURATION = 60 * 1000; async function readUsersWithCache() { const now = Date.now(); if (cachedUsers && now - cacheTime < CACHE_DURATION) { return cachedUsers; } const data = await fs.readFile(usersFilePath, 'utf8'); cachedUsers = JSON.parse(data); cacheTime = now; return cachedUsers; }

这样做的好处是,60 秒内多次请求不会重复读取文件。缺点是需要人工维护缓存失效时间。真实项目中如果对数据实时性要求较高,这种手动缓存需要谨慎使用。

7.4 文件更新与并发写入

本文只讲了读取用户信息,没有讲写入。但需要提醒的是,如果未来使用fs.writeFile修改同一个文件,必须考虑多个请求同时写文件的并发问题。Node.js 是单线程事件循环,但异步读写之间仍可能产生“后写覆盖先写”的情况。小项目可通过异步队列或一次只允许一个写操作来控制,生产环境则建议尽快切换到数据库、对象存储或 Redis。

7.5 权限与敏感信息

users.json如果包含手机号、密码等敏感字段,把它放在能被外部直接访问的静态资源目录中会非常危险。Express 的express.static中间件默认不会托管项目根目录,但如果你配了静态目录,一定要注意不要把data目录暴露出去。更稳妥的做法是:

  • 将数据文件放在项目根目录外,或者非 public 目录。
  • app.get('/users')接口控制访问。
  • 接口层不要随意把密码返回给前端。

生产环境中的“用户信息”类数据,应按最小返回原则设计接口字段,只返回前端实际需要的字段。

7.6 日志与观察

readFile 读取文件属于异步 IO,出错时一定要输出日志。不要只写res.status(500).json(...),却不打印错误细节。否则线上出现故障时,前端只能看到“服务器内部错误”,后端却无从排查。比较简单的做法是使用console.error输出上下文:

console.error(`读取用户文件失败,路径:${usersFilePath}`, error);

后续接入日志框架时,可以把这一条换成结构化日志,方便检索。

7.7 结合前后端联调

在实际前后端联调过程中,前端同学通常不希望接口返回 500 后只有一句话。推荐的约定格式是:

{ "message": "用户不存在", "code": 40401 }

把业务状态码与 HTTP 状态码分离,前端判断起来更灵活。不过,这类设计可以等业务变复杂之后再引入,不必强行套在所有小项目上。

7.8 还能继续扩展什么

本案例展示了用 readFile 读取用户数据。反向的需求也很常见:如何利用fs.writeFile新增用户、修改用户信息,让接口从只读模式变成支持增删改的模式。在扩展过程中,你会接触到JSON.stringify、文件锁、数据校验等概念。如果继续做下去,你会发现文件型数据层很快会遇到瓶颈,这时再引入 MySQL 或 MongoDB 也就顺理成章了。

下一期可以继续写“用 writeFile 保存用户注册信息”,把 Express + 文件操作的闭环补完整。先动手把上面的/users接口跑起来,再改一改代码,体会一下 readFile 从文件系统拉取数据再交给 HTTP 层的过程,比单纯看文章要快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 2:24:33

索尼游戏生态技术解析:DRM、在线服务与二手市场变革

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 2:23:55

C语言自习室管理系统:从数据结构到文件I/O的完整项目实践

简介&#xff1a;这是一套面向计算机专业本科生与嵌入式初学者的C语言综合实践项目——自习室管理系统设计源码&#xff0c;聚焦于真实场景下的资源调度与文件化管理问题&#xff0c;适用于课程设计、毕业设计及嵌入式系统入门开发。压缩包共172个文件&#xff0c;总大小24.33M…

作者头像 李华
网站建设 2026/9/4 2:22:59

MATLAB2021a下EKF/UKF/SIR电池SOC估计算法实战

简介&#xff1a;本资源是一套面向信号处理、导航与控制系统方向的非线性状态估计算法仿真工具包&#xff0c;适用于高校研究生、算法工程师及具备MATLAB基础的进阶学习者&#xff0c;聚焦解决非线性动态系统下的实时滤波与状态估计问题。压缩包共4个文件&#xff08;3个核心算…

作者头像 李华
网站建设 2026/9/4 2:20:29

大模型评测实战:DeepSeek、Grok、Opus横向对比自建指南

最近大模型圈的新版本消息几乎是一波接一波。像 DeepSeek V4 Pro、Grok 4.6、Opus 4.8 这类名字频繁出现在开发者社区里&#xff0c;很多人关心的问题很直接&#xff1a;这些模型在真实开发任务里到底谁更能打&#xff0c;而不是只看官方发布会里精心设计的 Demo。但真要回答这…

作者头像 李华
网站建设 2026/9/4 2:19:06

大模型服务化部署实战:Ollama、vLLM与Ray Serve技术解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 2:17:58

从零构建航拍屋顶识别数据集:YOLOv8训练实战与应用解析

简介&#xff1a;本资源是面向深度学习目标检测任务的航拍屋顶识别专用数据集&#xff0c;适用于YOLO系列&#xff08;v5至v10&#xff09;、Faster R-CNN、SSD等主流模型的训练与验证&#xff0c;特别适合计算机视觉初学者及遥感图像分析方向的研究者开展屋顶定位、城市建筑密…

作者头像 李华