多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

浏览器原生JSON模块导入:告别fetch与构建工具,前端模块化新篇章

浏览器原生JSON模块导入:告别fetch与构建工具,前端模块化新篇章 大概从 2015 年 ES Module 正式成为 JavaScript 语言规范的一部分开始前端模块化终于迎来了“官方认证”。但很多人没有注意到一个细节ES Module 规范里只定义了 JavaScript 代码怎么导入JSON、CSS 这类非 JavaScript 资源始终不在浏览器的原生支持范围内。于是过去这些年你想在前端项目里导入一个 JSON 配置文件只能走fetch或者借助 Webpack、Vite、Rollup 这些构建工具。我们早就习惯了这种“间接”的做法甚至不觉得有什么问题。但浏览器厂商可没打算永远停留在这一步。现在现代主流浏览器已经在稳定版本中原生支持 JSON 模块导入了。这意味着你可以直接在原生 JavaScript 里写出下面这行代码import data from ./data.json with { type: json };不用fetch不用构建器不用任何第三方库浏览器自己就把 JSON 文件当成一个真正的 ES Module 加载进来了。对前端开发者来说这是一次“终于等到你”的体验升级。这篇文章会把这件“喜大普奔”的事讲透。我会先说明 JSON 模块导入解决了什么问题再解释它底层是怎么工作的然后给出完整的可运行示例最后聊一聊工程上应该怎么用它、有哪些坑要避开。无论你是刚接触 ES Module 的新手还是在生产环境维护大型前端项目的开发者这篇文章都能给你一个相对完整的判断。1. 为什么 JSON 模块导入值得你关注先看一个真实场景。假设你有一个网页项目需要展示一份国家列表、配置一组功能开关或者读取一份静态的行政区划数据。在没有 JSON 模块导入之前你有两条路方案一用 fetch 运行时请求const response await fetch(/config.json); const config await response.json();这段代码写得多了你会遇到几个问题请求时机要自己控制数据是异步的组件渲染前还得先等它加载错误处理和超时逻辑要自己写如果你想在模块顶层同步用这份配置根本做不到。方案二用构建工具导入import config from ./config.json;在 Webpack 或 Vite 项目里这行代码很常见。构建工具会把 JSON 文件转换成 JavaScript 模块你在代码里拿到的config就是一个普通对象。但你注意到没有这其实是“构建工具替你做了浏览器原本没做的事”。离开构建工具这行代码在原生环境就是语法错误。JSON 模块导入要解决的核心问题就是把“JSON 作为模块导入”这个能力下沉到浏览器底层。它带来三个直观收益第一原生 JavaScript 项目可以直接用。你不再需要一个 Node.js 工具链来“翻译”JSON 文件。第二依赖关系可以被静态分析。浏览器在解析模块图的时候就能知道某个模块依赖了哪个 JSON 文件这和普通 JS 模块的处理方式完全一致缓存、预加载、依赖顺序都由浏览器统一管理。第三模块语义更干净。JSON 本身就是标准的、安全性高的数据格式把它作为模块导入天然适合那些“只需要读取、不需要执行逻辑”的静态数据。所以我的判断是JSON 模块导入不是一个小语法糖而是模块系统补上了一块长期缺失的拼图。它让“数据”和“代码”在模块层面实现了统一。2. 浏览器原生 JSON 模块导入的核心原理要理解 JSON 模块导入得先明白 ES Module 的加载流程。当你写下import xxx from ./xxx.js时浏览器会做这几件事解析模块 URL找到文件。下载文件内容。根据 MIME 类型判断文件类型。解析模块语法构建模块记录。实例化并求值。对普通的.js文件第 3 步的 MIME 类型是text/javascript浏览器知道这是一个 JavaScript 模块。但对.json文件它原来的 MIME 类型是application/json浏览器不知道该拿它怎么办——它既不是可执行脚本也不符合模块解析规则。JSON 模块导入机制做的事就是在语法层面告诉浏览器“别把这个文件当成 JavaScript 解析把它当成 JSON 数据结构来加载。”这里就要说到两个关键的历史名词了。这个提案在 TC39 讨论的时候最早叫 Import Assertions语法是这样的import data from ./data.json assert { type: json };后来标准委员会意识到assert这个词有误导性因为类型声明不只是“断言”还会真正影响模块的加载行为。于是提案改名成了 Import Attributes语法也改成了withimport data from ./data.json with { type: json };从实际效果看这行代码的意思是“我导入./data.json这个文件并用{ type: json }这个属性告诉浏览器请按照 JSON 模块的规则去解析它。”还有个容易混淆的点是JSON 模块导入和JSON.parse不是一回事。JSON.parse是你主动把一段字符串解析成对象而 JSON 模块导入是让模块系统在加载阶段就完成解析。导入完成后data已经是一个真正的 JavaScript 对象或数组了不需要你再手动处理。另外浏览器对 JSON 模块导入后的数据是只读缓存的。每个 JSON 模块只会被解析一次后续任何地方导入它拿到的都是同一个解析结果。这和 ES Module 的缓存机制保持一致也是它比fetch更省事的原因之一。3. 浏览器支持情况与环境准备看到这里你可能会担心浏览器兼容性。我先说结论现代主流浏览器已经在稳定版本中支持了 JSON 模块导入但需要注意语法差异。从各浏览器引擎的实现进度来看Chromium 系浏览器和 Firefox、Safari 都在新版本中加入了这一特性。早期一些浏览器只支持assert语法随着标准演进现在新版本普遍支持with语法。如果你是在企业内部项目中使用建议先确认目标浏览器内核版本。为了演示我们准备一个最简单的环境项目要求操作系统Windows / macOS / Linux 均可浏览器Chrome / Edge / Firefox / Safari 最新稳定版Web 服务器Python 3、Node.js、VS Code Live Server 任一编辑器VS Code 或其他任意编辑器为什么需要 Web 服务器因为 ES Module 受 CORS 策略限制直接双击 HTML 文件用file://协议打开浏览器会报跨域错误。所以我们必须通过http://协议访问页面。这里顺手就给一个命令。如果你有 Python 3在项目目录下执行python3 -m http.server 8000如果你有 Node.js也可以用npx servenpx serve .启动后浏览器访问http://localhost:8000就可以。4. 核心流程拆解从零跑通 JSON 模块导入现在我们来搭一个最小示例把整个过程走一遍。4.1 创建项目结构首先创建一个目录命名为json-import-demo。然后在里面创建三个文件json-import-demo/ ├── index.html ├── main.js └── data.json4.2 准备 JSON 数据文件创建一个data.json内容如下{ title: 浏览器原生 JSON 模块导入, author: 前端实践者, tags: [ES Module, JSON, 浏览器], published: true, likeCount: 1024 }这里的数据结构包含了对象、字符串、数组、布尔值和数字基本上覆盖了 JSON 的常见数据类型。你看这就是一个标准的 JSON 文件不需要任何特殊标记。4.3 编写原生模块导入代码主文件是main.js写法和普通 ES Module 几乎一样只是多了一个with { type: json }// 文件路径json-import-demo/main.js import data from ./data.json with { type: json }; console.log(JSON 模块导入成功, data); console.log(标题, data.title); console.log(标签列表, data.tags.join(, ));如果你想把整个 JSON 模块导入并获取它的属性也可以给模块指定一个命名空间import * as jsonModule from ./data.json with { type: json };这时候jsonModule.default就是 JSON 里的对象数据。4.4 创建 HTML 入口页面在index.html里用script typemodule引入main.js!DOCTYPE html html langzh-CN head meta charsetUTF-8 title浏览器原生 JSON 模块导入/title /head body h1打开开发者工具查看控制台输出/h1 script typemodule src./main.js/script /body /html注意typemodule是关键它告诉浏览器这是 ES Module 入口模块内部的模块导入语法才会被正确识别。4.5 启动本地服务器并访问在项目目录执行python3 -m http.server 8000然后打开浏览器访问http://localhost:8000。按 F12 打开开发者工具切到 Console 面板。如果一切正常你应该看到类似下面的输出JSON 模块导入成功 {title: 浏览器原生 JSON 模块导入, author: 前端实践者, tags: Array(3), published: true, likeCount: 1024} 标题 浏览器原生 JSON 模块导入 标签列表 ES Module, JSON, 浏览器到这里一个最小的 JSON 模块导入就已经跑通了。看起来确实简单对吧但越简单的东西越值得琢磨它为什么能这么简单。5. 完整示例静态导入、动态导入与场景对比5.1 静态导入 JSON 模块上面的例子就是静态导入。静态导入最大的特点是浏览器在解析模块图的时候就能确定依赖关系数据会随着主模块一起被预加载。如果某个 JSON 是页面首屏就必须用到的配置静态导入是最合适的方式。5.2 动态导入 JSON 模块如果 JSON 文件比较大或者你希望等用户触发某个操作后再加载可以用动态导入// 文件路径json-import-demo/dynamic-import.js const button document.getElementById(loadConfigBtn); button.addEventListener(click, async () { const module await import(./data.json, { with: { type: json } }); const data module.default; console.log(动态导入 JSON 模块成功, data); // 在这里更新页面 UI document.getElementById(output).textContent data.title; });动态导入会返回一个 Promise这样你可以完全控制加载时机按需加载避免把不需要的数据塞进首屏体积里。5.3 在模块之间传递 JSON 数据JSON 模块也可以作为中间数据源被多个 JavaScript 模块引用。例如你有一个config.js模块它导入 JSON 后再导出一些经过处理的结果// 文件路径json-import-demo/config.js import settings from ./data.json with { type: json }; export const siteTitle settings.title; export const tagList exports settings.tags;这样其他模块只需要import { siteTitle } from ./config.js数据的来源仍然是 JSON 文件但使用方不需要关心 JSON 的存在。这种模式很适合做配置中心。5.4 与 fetch 方案做一次对比为了帮助你做技术选型我整理了一个对比表格对比维度fetch JSON.parse构建工具导入浏览器原生 JSON 模块导入加载时机运行时异步构建时打包模块加载阶段解析是否支持顶层同步使用否是是依赖构建工具否是否静态依赖分析否是是缓存机制HTTP 缓存构建缓存模块缓存适用场景动态接口、用户数据大型项目、需要打包优化原生模块、配置数据、静态数据这里真正容易踩坑的地方是很多开发者会把 JSON 模块导入当成fetch的替代品这其实是不准确的。fetch适合请求动态数据比如用户信息、接口返回、实时变化的内容JSON 模块导入适合那些在开发和构建阶段就固定的静态数据。如果你要请求的数据是动态生成的那就该用fetch。6. 运行结果与效果验证跑通示例只是第一步我们还需要知道怎么验证“它确实是在浏览器层面原生工作的”。打开开发者工具切到 Network 面板刷新页面。你会看到一个有意思的现象浏览器发出的请求除了index.html和main.js还会自动多一个data.json的请求。而且这个请求的 Initiator发起者列显示的是main.js也就是由模块导入触发的。再切到 Sources 面板你会在模块列表里看到data.json这个文件。点开它浏览器会显示已经解析好的 JSON 对象。这说明浏览器把 JSON 文件当成一个真正的模块加载进来了。还有一个细节值得注意在 Network 面板里data.json请求的响应是没有被 JavaScript 执行过的。它返回的就是纯 JSON 文本只是浏览器的模块加载器在内部完成了解析。这个行为和我们平时用fetch请求 JSON 非常相似但加载入口不同。如果页面控制台没有输出或者报错了优先级最高的排查路径是确认你是不是通过http://访问的。用file://打开是绝大多数新手遇到的第一个问题。确认浏览器版本是否支持with { type: json }语法。如果浏览器只支持早期assert语法with会报unexpected token错误。确认服务器返回的 Content-Type 是application/json。大多数本地静态服务器默认就是无需额外配置。7. 常见问题与排查方法问题现象可能原因排查方式解决方案Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of application/json服务器没有正确识别 JSON 模块或者浏览器不支持该语法查看 Network 面板中 JSON 请求响应头确认浏览器最新版确认服务器对.json返回application/jsonSyntaxError: Unexpected token with浏览器版本过旧不支持 Import Attributes 语法检查浏览器版本升级浏览器或临时改用动态导入写法测试SyntaxError: Unexpected string使用了assert关键字但浏览器只支持with查看控制台错误行号改用with { type: json }打开 HTML 文件直接空白控制台报 CORS 错误使用了file://协议访问模块脚本查看控制台错误信息启动本地 HTTP 服务器JSON 文件存在但请求 404路径写错检查 Network 面板请求 URL根据项目目录修正相对路径导入的 JSON 是数组时数据访问方式不对忘记数组是default的一部分打印data.default查看数组 JSON 模块同样通过default导出这里面有两个高频坑我想单独提醒一下。第一个坑默认导出的问题。JSON 模块导入后数据是模块的default导出。所以无论你是import data from ./data.json还是const module await import(./data.json)最终要拿到的数据分别是data本身或者module.default。有些同学在动态导入时写了const data await import(./data.json)然后直接访问data.title结果拿到undefined。这就是因为忘了.default。第二个坑JSON 文件必须是完整的 JSON。JSON 模块导入不像fetch那样可以解析任意 JSONP 或带注释的文本它要求文件必须是严格合法的 JSON 格式。如果你手滑在末尾多写了一个逗号模块加载阶段就会直接报解析错误。这一点和构建工具的行为可能不太一样Webpack 在某些配置下会容忍 JSON 注释但浏览器原生语法是很严格的。8. 最佳实践与工程建议说完了能跑通的示例我想结合工程经验聊一聊什么时候该用、以及怎么用更稳。8.1 适合用 JSON 模块导入的场景静态配置文件项目里会用到一组管理员不希望随意改动的启动配置例如站点名称、功能开关、主题参数。用 JSON 模块导入后配置和业务代码分离清晰又安全。常量数据表例如城市列表、国家编码、错误码映射、权限标识列表。这些数据在版本发布时是固定的用 JSON 模块导入非常合适。纯前端原型工具如果你在写一个不需要 Node 工具链的前端小工具直接用 JSON 模块导入会显得非常轻量。8.2 现阶段不建议使用的场景需要动态请求的数据用户状态、服务端返回的商品列表这些必须用fetch或 WebSocket不能用模块导入。超大 JSON 文件例如几十 MB 的地理数据。模块导入会把整个文件解析进内存缓存起来。这种场景建议按需加载、分片处理而不是一次性导入。需要兼容旧浏览器的生产项目如果项目还要支持三年前的移动端 WebView建议继续走构建工具方案或者做特性检测降级。8.3 与构建工具共存的处理思路很多开发者会问一个问题“如果我在 Vite 里写了import data from ./data.json with { type: json }会发生什么”从目前各构建工具的兼容策略看它们大多已经支持这种写法并且会按照构建工具自己的规则去处理。有的构建工具会忽略with { type: json }直接通过打包逻辑把 JSON 转换成 JS 模块有的则会完整保留这个语法让浏览器在运行时处理。这个差异在跨环境时尤其需要注意。我个人的建议是在大型工程化项目里你仍然可以让构建工具去处理 JSON因为构建工具能带来 tree-shaking、压缩、缓存 hash 等额外收益而在纯原生环境、轻量原型、无构建链路的场景中浏览器原生 JSON 模块导入是更好的选择。8.4 关于 Import Attributes 规范的学习建议JSON 模块导入背后其实是 Import Attributes 这个更大的提案。它不只支持 JSON将来还会支持 CSS Module、HTML Module 等更多资源类型。也就是说你今天学会的with { type: json }这行语法未来可能可以推广到with { type: css }、with { type: html }上。理解这个方向比单纯背一个语法更重要。8.5 安全与权限提醒最后提醒一点浏览器原生模块加载依然遵循同源策略和 CORS 规则。如果 JSON 文件在 CDN 上你需要确保 CDN 返回了正确的Access-Control-Allow-Origin头。如果你的页面和 JSON 文件不在同一个域跨域导入 JSON 模块可能会被浏览器拦截。这一点在生产环境中是不容忽视的。8.6 搭配 HTTP 缓存JSON 模块与普通 JS 模块一样会参与浏览器的 HTTP 缓存。如果 JSON 配置文件有可能在发版时更新建议你配置好缓存策略比如通过文件 hash 命名避免浏览器长时间使用旧配置。如果你用本地静态服务器做测试记得在修改 JSON 后强制刷新页面CtrlShiftR排除缓存干扰。9. 总结与后续学习方向这篇文章主要讲清楚了三件事第一浏览器原生 JSON 模块导入解决了原生 JavaScript 环境里“JSON 只能靠 fetch 或构建工具加载”的尴尬第二它的核心是 Import Attributes 提案里的with { type: json }语法第三它适合静态配置文件、常量数据等场景不适合动态数据和超大文件。你可以从今天这个最小示例开始动手。先创建一个本地 HTTP 服务跑通静态导入再试着改成动态导入最后在浏览器 Network 面板里观察模块请求的变化。这个流程二十分钟以内肯定能完成。下一步值得继续深入的方向有两个一个是关注 Import Attributes 提案的进展尤其是对 CSS Module 和 HTML Module 的支持规划另一个是研究你的前端工具链对with语法的处理方式这能帮助你在工程化项目里平滑地引入这个特性。等到浏览器原生模块系统真正把你日常要用的资源类型都覆盖了前端构建工具的一部分职责就会被浏览器接管。到那时候再回头看今天这行似曾相识的导入语句你会觉得它是水到渠成的一步。
返回列表