news 2026/9/3 2:07:14

浏览器原生JSON模块:从fetch到import的依赖管理进化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
浏览器原生JSON模块:从fetch到import的依赖管理进化

以前做前端的时候,我经常遇到一个尴尬情况:页面需要一份 JSON 配置,数据是静态的,又不想为它专门启动一个接口。最简单的办法是fetch('./config.json'),然后在一堆thencatch里处理状态。代码能跑,但总觉得哪里不对。这份配置明明是项目的一部分,它被哪些页面用了、什么时候加载、能不能提前缓存,在代码里完全没有体现。

另一条路是把 JSON 交给打包器,像import config from './config.json'这样写。可这样它就会被打进 bundle,改一个开关变量都要重新构建整个资源。直到浏览器开始原生支持 JSON 模块导入,我才觉得这条路终于走通了。

这个功能的价值,不是让你少写几个 fetch,而是让“一段静态数据”正式进入浏览器的模块系统。数据文件不再只是网络请求,而是一等公民,能被静态分析、被预加载、被缓存,也能被明确地放进依赖图里。

接下来我想从语法、实现、边界和真实场景几个角度,把这个能力讲透。

1. 先搞清楚这个能力解决的,其实是“数据依赖”问题

1.1 过去实现 JSON 读取,常用的三条路径

在原生 JSON 模块出现之前,前端读取 JSON 的方式基本可以归为三类。

第一类是运行时请求,也就是fetch('./data.json')。这种方式最直接,但也最“无脑”。数据的加载时机由代码决定,浏览器不会提前知道你要这个文件。如果这个文件被多个模块依赖,你还需要自行设计复用机制,否则容易出现重复请求。

第二类是构建工具转换。在 Webpack、Vite 等工具里,import data from './data.json'早就被支持了。但这不是浏览器认识 JSON,而是打包器把 JSON 翻译成了一个 JS 对象,然后塞进了最终的 JavaScript bundle。它解决的只是“源码里写起来方便”,并没有让浏览器真正理解 JSON 模块。

第三类是 TypeScript 的resolveJsonModule。这本质上是编译层面的类型支持,它解决的是类型检查问题,不是运行时问题。最终运行到浏览器里的,依然是打包器转换后的 JS 代码。

这三条路径都有一个共同点:JSON 文件本身没有成为“可被浏览器识别的模块”。

1.2 为什么这个问题一直不容易被解开

要理解浏览器原生 JSON 模块的重要性,得先理解一个基础约束:浏览器里的<script type="module">,默认只认 JavaScript。

哪怕你写的是:

import config from './config.json';

如果浏览器没有 JSON 模块支持,它也会按照 JS 模块去解析config.json。JSON 的语法和 JavaScript 并不一样,比如 JSON 里的"key"键名这种写法虽然看起来像对象字面量,但在模块解析流程里很容易触发语法错误,或者更糟——被浏览器当成一段无意义的 JavaScript 执行。

所以在模块系统里,每增加一种新资源类型,都需要浏览器明确支持“模块类型声明”。过去没有这种机制,所以 JSON 文件只能靠其他方式绕过。

这就是原生 JSON 模块出现的历史背景:先让模块系统具备声明资源类型的能力,再让浏览器原生解析 JSON。

1.3 原生 JSON 模块带来的关键变化

当浏览器原生支持 JSON 模块后,最直观的变化是:.json文件可以被直接import,而且由浏览器直接完成解析。

这意味着很多原本由构建工具承担的“JSON 转 JS”工作,可以交还给浏览器。你的数据文件可以独立于 JavaScript bundle 存在,它有自己的缓存、自己的 URL、自己的依赖关系。

更重要的是,数据依赖关系变得可见了。

以往用 fetch,代码里看不到这个页面依赖哪些配置;现在你在源码里写import siteConfig from './site-config.json',静态分析工具、浏览器预加载器都能顺着这段代码发现这个依赖。它是模块图的一部分。

所以这个功能的核心价值不在“快”,而在“结构”。

2. 从 assert 到 with,再到默认导入:语法演进与正确姿势

2.1 为什么 JSON 文件需要被“点名”

早期设计者面临一个很实际的问题:浏览器支持了模块类型声明后,import config from './config.json'该如何让浏览器知道这是 JSON 而不是 JS?

答案是在导入时显式声明资源类型。这就是最开始assert { type: 'json' }的来源。

写法长这样:

import config from './config.json' assert { type: 'json' };

用的词是assert,中文意思是“断言”。

2.2 import assertions 与 import attributes 的差别

后来这个语法被调整过,关键词从assert改成了with

import config from './config.json' with { type: 'json' };

你可能会觉得这只是在换名字,但其实背后语义有变化。

assert更多表达“我确信它是 JSON,你要检查一下”;with表达的则是“我要以 JSON 模块的方式加载它”。后者更像是给模块系统提供导入属性,而不是在断言一件事。

这也是为什么with后续被叫做 Import Attributes,而不是 Import Assertions。方案演进过程中,整个提案的定位从“验证”转向了“指令”。

如果在非必要的情况下,我建议不要继续用assert写法。它属于已经被淘汰的语法方向,长期维护成本更高。

2.3 默认导入与动态导入示例

随着浏览器支持的推进,JSON 模块也出现了一种更简洁的用法:

import config from './config.json';

这种写法听起来不够“华丽”,但对开发者最友好。浏览器会根据文件扩展名和 MIME 类型,直接按 JSON 模块解析。

不过在大部分生产环境里,我仍然建议先确认你熟用的浏览器内核是否支持这种默认推断。如果支持面不够,就继续使用:

import config from './config.json' with { type: 'json' };

除了静态导入,你也可以用动态导入:

const { default: config } = await import('./config.json', { with: { type: 'json' } }); console.log(config);

需要注意,动态导入返回的是一个模块命名空间对象,真正的 JSON 内容被放在default属性里。

2.4 关键语义:默认导出、冻结对象、MIME 类型

JSON 模块有一些和普通 JS 模块不同的语义,这里特别值得留意。

第一,一个 JSON 模块只能提供一个默认导出。JSON 本身不是 JavaScript 的程序结构,没有“命名空间”的概念,所以规范约定把解析后的整个 JSON 对象作为默认导出。你不能写import { title } from './config.json'

第二,默认导出的对象是冻结的。也就是说,你不能在运行时修改这个对象的属性。

import config from './config.json'; config.title = 'new title'; // TypeError: Cannot assign to read only property

模块作用域默认是严格模式,所以一旦尝试修改,控制台会直接报错。这个设计是对的。配置数据如果可以被任意模块修改,很容易出现多模块互相污染的状态问题。只读,反而更安全。

第三,服务器返回的 MIME 类型必须是application/json。如果服务器把.json文件当成了text/plain,浏览器依然可能拒绝加载,或者继续按错误的模块类型解析。

3. 落地实操:一个最小页面跑通 JSON 模块

3.1 目录结构与最小示例

先不看复杂的工程化项目,我们从一个最普通的静态页面开始。

假设有这样一个目录:

json-modules-demo/ index.html main.mjs data/ config.json

index.html里只需要一个模块入口:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>浏览器原生 JSON 模块</title> </head> <body> <h1 id="title">读取中...</h1> <script type="module" src="./main.mjs"></script> </body> </html>

data/config.json内容:

{ "title": "浏览器原生 JSON 模块示例", "version": "1.2.0", "darkMode": true, "menu": ["首页", "文档", "关于"] }

然后在main.mjs里直接导入:

import config from './data/config.json'; document.getElementById('title').textContent = config.title; console.log('JSON module loaded:', config);

如果你的浏览器支持 JSON 模块,打开页面后标题会被替换成 JSON 里的内容,控制台里能看到完整的配置对象。

3.2 本地服务器、MIME 与 CORS 三个前置条件

上面的示例虽然简单,但有一个很重要的前提:你不能直接双击index.htmlfile://协议下打开。

ES Module 默认受 CORS 限制,file://下模块加载会失败。你需要启动一个本地静态服务。

如果你有 Python,可以这样:

python3 -m http.server 8080

然后访问:

http://localhost:8080/index.html

这时候还有两个容易被忽略的点。

第一,静态服务要为.json文件返回正确的 MIME 类型。绝大多数现代静态服务器默认没问题,但如果你用了某些精简服务器,或者 CDN 配置不当,就可能会返回text/plain。这时候打开 DevTools 的 Network 面板,检查 JSON 请求的Content-Type是一个好习惯。

第二,如果你把 JSON 模块放在另一个域名下,比如https://cdn.example.com/site-config.json,那么服务端必须返回Access-Control-Allow-Origin头。模块导入的跨域策略比<script>严格,它不会像普通脚本那样绕过 CORS。

3.3 支持检测与兼容回退

由于不同浏览器内核的支持进度不一样,落地前最好先做一个快速检测。

你可以用动态导入探一下当前浏览器的支持情况:

const probeUrl = new URL('./data/probe.json', import.meta.url).href; try { const module = await import(probeUrl, { with: { type: 'json' } }); console.log('JSON modules supported', module.default); } catch (err) { console.warn('JSON modules not supported', err); }

注意,这里依赖import.meta.url,所以代码必须放在模块文件里,不能直接放在普通<script>里。

如果浏览器不支持with语法,这段动态导入可能会在解析阶段就报错,但错误会被catch捕获。也就是说,你至少能知道这个能力不可用,然后决定是否需要走回退方案。

回退方案也很直接:继续用 fetch。

let config; try { const module = await import('./config.json', { with: { type: 'json' } }); config = module.default; } catch { const response = await fetch('./config.json'); config = await response.json(); }

这属于一种渐进增强策略:支持原生 JSON 模块的环境用它,不支持的回到请求处理。

3.4 常见报错和排查链路

我用过一段时间后,整理过一条排查路径,遇到问题时按顺序走,基本都能找到方向。

先从现象说起。

如果页面加载后控制台报模块加载失败,第一反应不要怀疑语法,先看 Network 面板里.json文件的请求状态。

  • 如果是 404,大概率是路径写错了,检查import里的相对路径是否正确。
  • 如果状态码是 200,但控制台报 unexpected token 或类似语法错误,说明 JSON 文件被当成了 JavaScript 模块来解析。此时检查有没有写with { type: 'json' },以及当前浏览器内核是否支持 JSON 模块。
  • 如果控制台提示 CORS 问题,说明是跨域请求,看服务端有没有返回正确的 CORS 头。
  • 如果加载成功了,但运行时报Cannot assign to read only property,说明你试图修改数据结构里的某个字段,这不是加载问题,是模块语义导致的限制。
  • 如果双击本地 HTML 文件打开后报错,解决办法是先启动一个本地 HTTP 服务,而不是继续在file://下折腾。

这里最容易误判的情况是“浏览器没有报语法错误,但数据始终没有显示”。这种情况十有八九是模块入口在file://下无法加载,或者静态服务器没有正确处理.json的 MIME 类型。

4. 场景判断:它适合什么,不适合什么

4.1 适合的场景:静态配置、语言包、测试数据

原生 JSON 模块最适合的数据,是那些“基本不随用户变化、运行时不需要修改、但希望独立缓存”的内容。

最典型的场景是国际化语言包。以前语言包通常被打进 bundle,或者用 fetch 按需拉取。现在你可以让每个语言文件成为一个独立 JSON 模块,依赖关系由源码决定,浏览器能自动处理加载。

另一个场景是站点静态配置,比如功能开关、公告内容、版本信息。如果这些数据是从后端生成的,也可以由构建流程写成一个 JSON 文件,然后被前端模块直接导入。这样变更配置时不需要提交新的 JavaScript bundle。

测试代码中固定使用的 fixture 数据也很适合。测试环境里不需要复杂请求,直接用 JSON 模块导入,能省掉一套 mock 机制。

4.2 不适合的场景:动态数据、用户权限数据、JSONC

但原生 JSON 模块并不是万能的。

第一类不适合的场景是动态数据。比如实时变化的股票行情、用户行为统计、服务端实时计算的结果。这类数据通过接口获取仍然更合理,因为你可以控制请求频率、超时、错误重试,还能按用户维度定制。

第二类不适合的场景是包含用户权限的数据。浏览器端能访问到的任何数据,在安全性上都应该视为公开数据。JSON 模块也不例外。你不能因为加载方式更优雅,就把需要鉴权的配置直接暴露在前端。

第三类不适合的场景是 JSONC、JSON5 这类带有注释和尾逗号的配置文件。原生 JSON 模块要求文件必须是严格 JSON 格式。如果你维护的是带注释的开发配置,那就需要构建时预处理,或者干脆继续用打包器方案。

4.3 和 fetch 的取舍对照

很多人会问:有了 JSON 模块,fetch 是不是就没用了?

这两者其实解决的是不同问题。我做过一个对比:

对比维度fetch()原生 JSON 模块
加载时机脚本执行后发起模块解析阶段声明
依赖可见性代码里需要维护引用关系静态依赖图可见
缓存策略需要手动设计模块缓存自动复用
数据是否可修改默认可变默认冻结
请求可控性支持超时、取消、自定义 header受模块加载机制约束
响应格式支持任意格式仅支持严格 JSON
适用场景动态接口、实时数据静态配置、确定性数据

如果你要请求的是后端接口,显然应该用 fetch。如果你要读取的是项目静态资源,JSON 模块明显更贴合。

4.4 从单页面到正式工程,还差哪些拼图

在小页面里跑通之后,如果想把它放进正式工程,有几个现实问题要先确认。

第一是构建工具的配合。像 Vite、Webpack 这类打包器,默认会把源码里的 JSON 导入当作“非浏览器模块”处理,最后打进 bundle。如果你希望浏览器保留原生 JSON 模块行为,就需要让构建链路把.json当作外部资源,而不是转换为 JS 对象。

不同工具配置方式不同,这个必须在动手前查清楚。我的建议是:先在一个没有打包器的原生页面里验证核心能力,再评估你的构建工具是否能完整保留这种行为。

第二是浏览器版本覆盖。如果项目需要支持老版本浏览器,原生 JSON 模块可能不在支持范围内。这时候要么用with显式声明并做兼容回退,要么继续使用打包器方案。

第三是部署策略。JSON 模块有自己的缓存,依赖 URL 作为模块标识。如果你的数据内容变化频繁,建议在文件名里带上内容哈希,避免浏览器拿到旧缓存。

5. 我的判断:不要神化它,但要重视它

5.1 模块系统的边界正在扩大

站在更宏观的视角看,原生 JSON 模块代表了一个明显的趋势:浏览器的 ES Module 体系,正在从“只能加载 JavaScript”,扩展成“可以加载多种资源”。

模块系统以后可能还会覆盖更多类型。这对前端架构的影响是深远的。过去我们习惯把所有资源交给打包器处理,觉得浏览器天然不懂这些文件类型。但原生模块能力出现后,一部分工作可以重新分配:浏览器负责解析和加载,构建工具负责组织和优化。两者不是替代关系,而是重新划清了边界。

这个边界划分对中小项目尤其有价值。一个不需要复杂构建链的静态站点,可以只依赖浏览器原生能力,把语言包、配置、数据文件作为模块直接管理。

5.2 落地时要盯住三个风险点

任何新能力都伴随风险,JSON 模块也不例外。我建议你重点盯住三点。

一是支持范围。不要默认所有浏览器都支持 JSON 模块。更好的做法是把它当作一个渐进增强能力,在支持的环境里使用,在不支持的环境里自动回退到 fetch 或打包器方案。

二是数据冻结。多人协作时,很容易有人习惯性地给导入的配置对象追加字段。一旦遇到冻结对象报错,沟通成本会额外增加。所以在团队里使用前,最好先说明这个语义差异。

三是缓存粒度。模块缓存是独立的,数据更新后如果没有新的 URL,浏览器可能不会重新拉取。发布时尽量让数据文件的 URL 随内容变化,否则容易出现“配置改了但前端看不到变化”的诡异问题。

5.3 下一步你可以怎么做

如果你想尝试这个能力,我建议从一个小实验入手。

找一个小型静态页面,把其中一个固定 JSON 文件改成模块导入,跑通后再尝试搭配 import map,看看能否把数据源映射成更短、更稳定的标识符。最后再评估你的构建链:这个 JSON 文件是继续留在 bundle 里,还是独立出来交给浏览器。

等这一步跑通,你会真正理解原生 JSON 模块的体验差别:它在很多时候并不表现为“更快”,而是表现为“更清晰”。数据文件终于有了自己的位置,不再只是代码里的一个字符串,也不是网络请求里的一个临时响应,而是模块系统里一个正式的成员。

浏览器原生 JSON 模块,不是让你改写所有项目,而是给了一个新的选择。当你的数据是静态的、确定的、只读的时候,可以试着让它回归到模块系统里来。

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

STM32与OpenMV自动泊车系统:嵌入式视觉与运动控制综合实践

简介&#xff1a;本资源是面向高校电子类专业本科生的毕业设计与课程作业参考方案&#xff0c;完整实现南京航空航天大学电赛校赛‘自动泊车’赛题功能&#xff0c;聚焦STM32嵌入式主控与OpenMV机器视觉协同开发。资源包共201个文件&#xff0c;含34个C源码&#xff08;如stm32…

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

中国量子计算技术路线与开发者实践指南

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

作者头像 李华
网站建设 2026/9/3 2:04:39

STM32模糊PID水温控制系统设计:从算法到仿真的完整实现

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

作者头像 李华
网站建设 2026/9/3 2:03:42

SpringBoot+Vue校园考勤与教学一体化系统实战

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

作者头像 李华
网站建设 2026/9/3 2:02:42

数据源备注与多数据源实践:让智能问数更准确的Spring Boot方案

背景&#xff1a;SQLBot 与智能问数为什么依赖“备注”1.1 SQLBot 到底解决什么问题SQLBot&#xff0c;简单理解就是一个“用自然语言查询数据库”的智能助手。用户不需要写 SQL&#xff0c;只需要用业务语言描述需求&#xff0c;例如“查一下最近 7 天每个支付渠道的订单金额”…

作者头像 李华
网站建设 2026/9/3 2:02:08

YOLOv12+PyQt5交通应急车辆识别实战

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

作者头像 李华