Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月7日

发布于:2026年9月2日

如何在 JavaScript 中读取 JSON 文件

“在 JavaScript 中读取 JSON 文件”根据代码的运行环境不同,有三种不同的含义,且这些方法在不同环境间无法直接移植。 在 Node.js 中,是从磁盘读取;在浏览器中,则是通过网络获取或接受用户选择的文件。而在这两种情况下,现在都有一种标准的导入语法,但大多数人尚未采用。 本指南将涵盖所有这些情况,并介绍如何通过错误处理,将令人困惑的错误转化为显而易见的错误。

关于代理公司为何撰写此文的简要说明:我们是 Geonode,而客户反馈的最常见 JSON 错误是,他们获取的数据中出现了 Unexpected token '<'。这意味着响应内容是 HTML——可能是错误页面、登录重定向或屏蔽页面——而解析器准确地报告了未收到 JSON 数据。 在修改任何代码之前,请记录接收到的数据的前 200 个字符。本文中的其他内容几乎都假设该文件确实是 JSON,而这一假设往往正是导致问题最常见的原因。

在 Node 中:从磁盘读取

有三种方法,其中一种是现代解决方案。

** ``fs/promises`

`,标准做法:**

import { readFile } from "node:fs/promises";

const raw = await readFile("./data.json", "utf8");
const data = JSON.parse(raw);

Node 文档 对 encoding 参数有明确说明,且该参数至关重要:若不指定编码,readFile

“将返回一个 Promise,该 Promise 解析后会得到一个包含文件内容的 ``<Buffer>`

对象”;若指定了编码,则 “解析后会得到一个 ``<string>

”。JSON.parse`

会通过强制转换将 Buffer 转换为字符串,因此省略编码通常也能正常工作,且进行额外转换毫无必要。请传入 "utf8"

。

它还通过 signal

选项支持 AbortSignal

,"允许你中止正在进行的 readFile

操作"——当读取操作属于可能被取消的请求时,此功能非常有用。

同步模式,适用于启动代码:

import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("./config.json", "utf8"));

在服务器开始提供服务之前,阻塞操作是可以接受的。但在请求处理程序内部则不可行,因为这会导致其他所有连接的事件循环被阻塞。这一区别正是规则的核心所在。

**require

,仅限 CommonJS:**

const data = require("./data.json");

这种写法简洁,且具备两个常被忽略的特性。它具有缓存功能,因此对同一路径的第二次 require

调用将返回相同的对象,而无需重新读取文件——这意味着运行时修改文件不会产生影响。此外,该功能在 ES 模块中不可用。

导入属性:标准方法

这是大多数人尚未采用的语法,也是在新代码中应采用的语法。

import data from "./data.json" with { type: "json" };

或者动态导入:

const data = await import("./data.json", { with: { type: "json" } });

MDN 将此记录为 2025 年基线,自 2025 年 4 月起在最新浏览器中可用,而 Node 和 Deno 等非浏览器运行时也将遵循浏览器对 JSON 模块的语义规范。

type: "json"

属性并非装饰性属性。MDN解释称,该属性“用于验证模块是否以application/json

MIME类型提供”,并且如果文件“以application/json

以外的任何媒体类型提供,则导入将失败”。

其安全依据值得引用,因为它解释了为何该属性是强制性的而非可选的:

如果由于某种原因(例如服务器被劫持或伪造),服务器响应中的媒体类型被设置为 text/javascript

(表示 JavaScript 源代码),那么该文件将被解析并作为代码执行。 如果该“JSON”文件实际上包含恶意代码,那么import

声明将无意中执行外部代码,从而构成严重威胁。

关于迁移的一点说明:早期的提案曾使用 assert

关键字,而非 with

。MDN 将此标记为破坏性变更——使用 assert

的实现“不再受支持”。如果您在旧代码或教程中发现 assert { type: "json" }

,则需要进行更新。

在浏览器中:获取 JSON

这是最常见的情况,但也暗藏陷阱。

const res = await fetch("/data.json");
if (!res.ok) throw new Error(`HTTP ${res.status} from ${res.url}`);
const data = await res.json();

res.ok 的检查并非可选,跳过这一步正是本文引言中出现错误的直接原因。fetch 不会因 HTTP 错误状态而拒绝请求——无论是 403、404 还是 500,都会正常解析。 随后调用 .json() 会尝试解析错误页面,结果会出现关于 < 字符的语法错误,而这与您的 JSON 内容毫无关系。

以下是一个能提供有用反馈的失败版本:

async function fetchJson(url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
  const type = res.headers.get("content-type") ?? "";
  if (!type.includes("application/json")) {
    const body = await res.text();
    throw new Error(`Expected JSON, got ${type}: ${body.slice(0, 200)}`);
  }
  return res.json();
}

仅需两行检查代码,就能将晦涩难懂的解析错误转换为明确指出状态码、内容类型以及实际接收内容的错误信息。

另请注意,Response.json() 不接受 reviver 函数。如果您需要使用 reviver 函数(例如用于日期转换或处理大整数),请使用 res.text(),随后再使用 JSON.parse。我们在 JSON.parse 指南中详细介绍了 reviver 函数及精度问题。

在浏览器中:用户选择的文件

对于从用户计算机上选中的文件,请使用 File API。

<input type="file" id="picker" accept="application/json">
document.getElementById("picker").addEventListener("change", async e => {
  const file = e.target.files[0];
  if (!file) return;
  try {
    const data = JSON.parse(await file.text());
    console.log(data);
  } catch (err) {
    console.error(`Could not parse ${file.name}: ${err.message}`);
  }
});

File.text() 会返回一个 Promise,其解析结果为字符串格式的文件内容,这比旧版 FileReader (需要使用事件处理程序)要简洁得多。如果需要对非常大的文件进行进度事件监控,FileReader 仍然是你的首选。

有两点需要记住。浏览器无法读取任意的本地路径——用户必须选择文件,这是一项有意设置的安全边界,而非可以绕过的限制。此外,.json 扩展名并不能保证文件内容,因此 try /catch 才是真正执行实际操作的代码。

拖放操作使用相同的 File 对象,这些对象通过 event.dataTransfer.files 获取。

能提供有价值信息的错误处理

这个习惯,能让解决一个问题的时间从五分钟缩短到一小时。

function parseJson(text, source) {
  try {
    return JSON.parse(text);
  } catch (err) {
    throw new Error(
      `Failed to parse JSON from ${source}: ${err.message}. ` +
      `First 200 chars: ${text.slice(0, 200)}`
    );
  }
}

关键在于切片。JSON.parse 错误会指出位置和字符;而实际的输入则能告诉你原因。 三种签名可覆盖大多数情况:

Unexpected token '<' — 内容是 HTML。可能是错误页面、登录重定向或目录列表。

Unexpected end of JSON input — 输入为空或被截断。可能是 204 响应、未完全写入的文件,或是你忘记等待的 fetch 请求。

Unexpected token '}' 位于合理位置 —— 真正的 JSON 格式错误,通常是尾随逗号。尽管 JavaScript 允许尾随逗号,但 JSON 禁止其使用。

对于文件读取,需区分读取失败与解析失败。ENOENT 表示文件不存在,这与内容无效是不同的问题,应显示不同的错误信息。

验证所读取的内容

解析成功。这仅表明语法正确,但完全无法说明数据是否符合代码的预期格式——而这两者之间的差距,正是导致生产环境故障的意外原因所在。

解析和验证是两个独立的步骤。 JSON.parse

会毫无问题地返回包含拼写错误的键 {"user": {"nmae": "Ada"}}

,或者返回一个本应为数字却包含字符串 "n/a"

的 price

字段。随后,你的代码会在下游某个位置(距离实际问题有好几个函数之遥)发生错误,而报错信息往往只描述了症状而非根本原因。

对于任何超出你控制范围的内容,请根据模式进行验证。 许多库都能很好地处理这一点,无论你选择哪个,模式都是一样的:

const Config = z.object({
  port: z.number().int().min(1).max(65535),
  host: z.string(),
  retries: z.number().int().default(3),
  features: z.array(z.string()).optional(),
});

const config = Config.parse(JSON.parse(await readFile("./config.json", "utf8")));

现在,错误会明确指出字段名称、预期类型以及实际找到的内容——这正是五分钟就能修复的问题与耗费整个下午的问题之间的区别。

对于简单情况,添加几个断言毫无成本:

const data = JSON.parse(raw);
if (!Array.isArray(data.items)) throw new Error("items must be an array");
if (data.items.length === 0) throw new Error("items is empty — check the source");

第二个检查的价值远超表面所见。空数组在 JSON 中是有效的,解析时也没有问题,但它通常只是症状而非合理结果——例如,API 因过滤器错误而返回空值,或者抓取成功但页面内容已发生变化。

对非你生成的数值要保持怀疑态度。 JSON 中的数值会被转换为 JavaScript 双精度浮点数,因此大于 Number.MAX_SAFE_INTEGER

的整数会悄无声息地丢失精度,且在任何环节都不会报错。 标识符通常是受影响的首要对象:两个不同的数据库记录在解析后可能得到相同的值。如果某个字段是标识符而非数量,它应在 JSON 中以字符串形式出现——如果你无法控制数据生成方,那么解析器(reviver)的 ``context.source`

` 参数会为你提供原始的数字值。

**并在边界处进行一次验证。**在数据进入程序时检查其结构,意味着下游的所有处理都可以假设数据是正确的。如果在二十个地方进行防御性检查,就意味着有二十个地方需要更新,且没有一个明确记录契约的单一点。

大文件 `

JSON.parse

` 是同步操作,且需要将整个文档加载到内存中。随着文件大小的增加,这两点都会成为问题。

大致指导原则。 小于 1 兆字节时,无需考虑。在 1 到 10 兆字节之间,需进行性能测试——特别是在浏览器的主线程中,因为解析会阻塞渲染并导致明显的卡顿。 超过 10 兆字节,或者超过可用内存的大约十分之一时,请采取其他措施。

将其移出主线程。 在浏览器中,Web Worker 可以在不冻结界面的情况下进行解析。在 Node 中,工作线程对事件循环也能起到同样的作用。

使用换行符分隔的 JSON。 这是一种结构性解决方案,而非权宜之计。每行一个 JSON 文档意味着你可以以恒定内存占用处理任意大小的文件,每行独立解析,且即使文件被截断,仍能获取所有完整的记录:

import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";

const rl = createInterface({ input: createReadStream("./data.jsonl") });
for await (const line of rl) {
  if (line.trim()) handle(JSON.parse(line));
}

如果你能控制格式,对于任何随时间推移逐步追加的数据,这都是更好的设计方案——这也是为何崩溃的数据收集任务会留下可用的文件,而非无法解析的文件。

当格式是一个无法更改的单一大型数组时,请使用流式解析器。 有多种库会在数据到达时立即输出值,而非先构建整个数据树。

或者减少请求量。 分页、字段选择、更窄的端点。这几乎总是正确的答案,却也几乎总是被忽略,因为这需要与 API 所有者沟通。

写回 JSON

简要说明一下相反的情况,因为这通常是接下来会问的问题。

import { writeFile } from "node:fs/promises";
await writeFile("./out.json", JSON.stringify(data, null, 2), "utf8");

null, 2

参数会生成带缩进的输出,这一点比看起来更重要:任何可能被人类阅读或在版本控制中进行差异比较的文件都应进行格式化,而用于传输的文件则不应进行格式化。

有三类内容在 JSON.stringify

处理后会丢失,且不会引发错误,而是导致数据无声丢失。undefined

值和函数会从对象中完全移除,并在数组中变成 null

。Date

对象会转换为 ISO 字符串,因此若不使用复活器,它们无法还原为日期。而 BigInt

会直接抛出异常——标准做法是将大整数序列化为字符串。

对于追加操作,请编写以换行符分隔的 JSON,而不是重写数组:

import { appendFile } from "node:fs/promises";
await appendFile("./log.jsonl", JSON.stringify(record) + "\n", "utf8");

向 JSON 数组追加内容需要读取数组、解析数组、将数据推入数组,并重写整个文件——这不仅开销很大,而且如果写入过程中进程意外终止,还会导致文件损坏。

大家还问

如何在 Node.js 中读取 JSON 文件?

const data = JSON.parse(await readFile("./data.json", "utf8")) 使用 node:fs/promises。或者使用标准的导入语法:import data from "./data.json" with { type: "json" },该语法自 2025 年起已成为基线标准,并在 Node 和浏览器中均可正常工作。

JavaScript 能在浏览器中读取本地文件吗?

不能通过路径直接读取。浏览器无法打开任意的本地文件,这是出于安全考虑的刻意限制。用户必须通过 <input type="file"> 或拖放操作选择文件,之后 File.text() 会返回文件内容。

import ... with { type: "json" } 起什么作用?

它将 JSON 文件作为模块导入,同时验证服务器是否以 application/json MIME 类型提供该文件。如果没有此检查,以 text/javascript 形式提供的文件会被解析并作为代码执行——而该属性正是为了防止这种安全问题而存在的。

为什么读取 JSON 时会出现“Unexpected token '<'”错误?

因为内容以 < 开头,这意味着您接收的是 HTML 而不是 JSON——通常是错误页面或登录重定向页面。对于 fetch,原因几乎总是缺少 res.ok 检查,因为 fetch 不会因 HTTP 错误状态而拒绝请求。

应该使用 require 还是 import 来加载 JSON?

在新代码中应使用 import ... with { type: "json" },因为这是标准做法,且在 ES 模块中有效。require 仅适用于 CommonJS,并且会缓存结果,因此运行时编辑的文件将不会被重新读取。对于在程序运行过程中会发生变化的文件,这两种方式都不合适——对于此类文件,请使用 readFile。

如何读取一个非常大的 JSON 文件?

使用 worker 将解析任务移出主线程,或者将数据重构为以换行符分隔的 JSON,以便每行都能在固定内存中独立解析。对于不可变的大型数组,请使用流式解析器。同时请考虑是否可以从源头减少请求的数据量。

JSON.parse 和 response.json() 有什么区别?

Response.json() 会读取请求正文并一步完成解析,且不接受 reviver 函数。JSON.parse 则针对你已有的字符串进行处理,并且支持 reviver 函数。如果你需要 reviver 函数(例如处理日期或大整数),请先使用 res.text(),然后使用 JSON.parse。

如何处理可能不存在的 JSON 文件?

请将读取错误与解析错误分开处理。在 Node 中,错误代码 ENOENT 表示文件不存在,这种情况通常应返回默认值而非报错;而错误代码 SyntaxError 则表示文件存在但内容有误。

总结

具体方法取决于环境,而如今的标准比以往更加统一。import data from "./data.json" with { type: "json" } 既适用于 Node,也适用于浏览器,截至 2025 年已被列为基准标准,并且包含一项 MIME 类型检查——该检查的存在是出于真正的安全考虑,而非走过场。

对于在程序运行过程中会发生变化的文件,请显式读取它们——在 Node 中使用 readFile 并采用 "utf8" 编码;在浏览器中使用 fetch 并进行 res.ok 检查;对于用户选择的文件,则使用 File.text()。其中 res.ok 这一检查是本文中价值最高的一行代码,因为跳过它正是导致最常见 JSON 错误的直接原因。

当出现故障时,在触碰任何代码之前,先记录输入的前 200 个字符。Unexpected token '<' 表示 HTML,Unexpected end of JSON input 表示输入为空或被截断,这两种情况的判断都应基于实际接收到的内容,而非通过推测解析器的行为。

此外,如果文件体积不断增大,从结构上解决问题的办法是采用以换行符分隔的 JSON,而不是升级更强大的机器。每行一个文档的格式可以在恒定内存下流式读取,安全地追加数据,并且在写入中断时,每个完整的记录都能完好无损地保留下来。