ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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有值data为undefined。当文件读取成功时err为nulldata是文件内容。初学者最容易犯的错是拿到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文件用来管理项目依赖和脚本命令。然后安装 Expressnpm install express安装完成后package.json的dependencies中能看到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 data3. 手写代码之前先看懂 fs.readFile3.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 直接输出文本就必须指定编码。最简单的写法是传入utf8Node.js 读取完成后会自动做一次字符解码。但在处理图片、音频、压缩包等二进制文件时就不要指定 UTF-8 了。本需求是读取用户信息文本文件所以必须设置。3.3 相对路径为什么会踩坑初学者最容易踩的坑是把路径写成了相对当前命令执行目录而不是相对当前代码文件。比如你现在项目的根目录执行node app.js如果app.js和data目录都在项目根目录下执行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: zhangsanexample.com, city: 北京 }, { id: 2, name: 李四, email: lisiexample.com, city: 上海 }, { id: 3, name: 王五, email: wangwuexample.com, city: 广州 } ]这里把用户设计成 JSON 数组。每个用户对象包含id、name、email、city。字段可以根据业务随意扩展但要注意 JSON 不能写注释逗号也必须规范。如果手写容易漏逗号可以用 VS Code 的 JSON 格式化能力自动整理。4.2 创建基础的 Express 服务在项目根目录创建app.jsconst 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}); });代码说明usersFilePath保存绝对路径放在路由外部避免每次请求都重新拼接。fs.readFile是异步读取读取完成前不会阻塞其他请求。err分支必须处理因为文件一旦被意外删除readFile 回调里会收到错误对象。JSON.parse放在 try-catch 中是因为即使文件存在内容也可能因为人为改动而不再是合法 JSON。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: zhangsanexample.com, city: 北京 }, { id: 2, name: 李四, email: lisiexample.com, city: 上海 }, { id: 3, name: 王五, email: wangwuexample.com, city: 广州 } ]访问单个用户curl http://localhost:3000/users/2预期输出{ id: 2, name: 李四, email: lisiexample.com, city: 上海 }访问不存在的 IDcurl http://localhost:3000/users/999预期输出{ message: 用户不存在 }5. 不只回调readFile 的异步演进与现代写法5.1 为什么越来越多的项目改用 fs.promisesNode.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 编码给第二参传入utf8JSON.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 去打开不存在的文件。执行顺序如下在app.js中打印usersFilePath。到打印出来的路径下确认users.json是否真的存在。如果路径最后是.../express-readfile-demo/data/users.json但目录data下文件名为user.json则命名不一致。Windows 下注意路径分隔符path.join会自动处理不要手动拼接\\或/。确认你编辑的 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 读取路由层基本不用动。如果项目继续膨胀可以继续拆分controllers、services、models层。但即使不拆那么细至少做到“一个函数只做一件事”代码的可读性会高很多。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 层的过程比单纯看文章要快得多。
返回列表