先记住这个答案
原生 ESM 导入 JSON 的写法是 import data from "./data.json" with { type: "json" },with 后面这段叫导入属性。它的作用是让宿主(浏览器或 Node.js)按 JSON 来处理这个文件,并且校验对方返回的内容类型,不是 JSON 就报错。导入后 data 就是解析好的对象,JSON 模块只有默认导出。早期提案写的是 assert { type: "json" },后来改成了 with,新代码只写 with。
- JSON 模块导入需用
with声明类型 - 这是安全校验:防止服务端返回 JS 被当成代码执行
- JSON 模块只有默认导出,不能按字段具名导入
- 旧
assert写法应迁移到with
写法和导入结果
在 Node.js 里建一个 data.json,内容是 {"name":"demo","n":1},然后在 .mjs 文件里导入它。
// main.mjs
import config from './data.json' with { type: 'json' }
console.log(config.name) // demo
// 动态导入时,属性放在第二个参数的 with 里
const mod = await import('./data.json', { with: { type: 'json' } })
console.log(Object.keys(mod)) // [ 'default' ]
console.log(mod.default === config) // true,同一个模块只解析一次
// 不写 with 会报错:ERR_IMPORT_ATTRIBUTE_MISSING
// import bad from './data.json'动态导入拿到的是模块命名空间对象,JSON 内容挂在 default 上。两次导入拿到的是同一个对象,因为模块只会加载和解析一次。所以如果某处代码改了 config.name,其它导入方也会看到改动,拿 JSON 当配置时不要随手改它。
为什么非要多写这一段
浏览器决定怎么处理一个模块,看的是服务器返回的 MIME 类型,而不是文件扩展名。设想你从第三方 CDN 导入一个 data.json,如果对方被攻破,返回的内容类型是 text/javascript,没有任何校验的话浏览器就会把它当脚本执行,一个本该只是数据的导入变成了任意代码执行。
加上 type: "json" 以后,意图写在了导入语句里。宿主拿到响应先核对类型,不是 JSON 就拒绝,最多只会得到一个加载错误。所以导入属性是安全机制,不是给打包器看的注释。
反过来也成立:给一个普通 JS 模块写 with { type: "json" },Node.js 会报 ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE,类型对不上两边都拦。
assert为什么被换成了with- 最早的提案叫 import assertions,语义是「只断言,不影响加载方式」。后来发现宿主确实需要根据类型改变加载行为,比如浏览器请求 JSON 模块时要带不同的请求头,「断言」这个说法就不准确了,于是改名为 import attributes,关键字换成
with。我在 Node 22 上实测,assert写法已经是语法错误。老项目升级 Node 后如果报Unexpected identifier 'assert',就是这个原因。
容易答错的地方
- 以为可以按字段具名导入
import { name } from "./data.json" with { type: "json" }会报错,提示模块没有名为name的导出。JSON 模块只有一个默认导出,要先拿到整个对象再解构。在 Webpack 里具名导入 JSON 字段可能能用,那是打包器的扩展,换到原生环境就不行了。- 以为
with只是给 TypeScript 或打包器看的 - 它是 ECMAScript 语法,运行时真的会校验。TypeScript 只负责类型检查,是否允许导入 JSON 还要看
resolveJsonModule等编译选项。代码最终在浏览器或 Node.js 里跑时,缺少或写错导入属性都是运行时错误。 - 把 JSON 模块当成每次读取的新数据
- JSON 模块和 JS 模块一样被缓存。服务器上的文件后来变了,当前页面拿到的还是第一次加载的内容。需要实时数据时用
fetch请求接口,不要用模块导入。
面试官还会怎么问?
除了 JSON,导入属性还能写什么类型?
规范本身只定义了 with 语法,具体支持哪些类型由宿主决定。JSON 模块是目前最普遍的;浏览器还有 CSS 模块脚本,写 with { type: "css" } 会得到一个 CSSStyleSheet 对象,但各浏览器支持程度不一,用之前要查兼容性。
Node.js 里用 CommonJS 怎么读 JSON?
CommonJS 直接 require("./data.json") 就行,不需要任何属性,结果同样会被缓存。在 ESM 里如果想用这种方式,可以用 createRequire(import.meta.url) 造一个 require 出来。
打包项目里需要写 with 吗?
Webpack、Vite 都能直接导入 JSON,不写也能用。但如果代码会被原生 ESM 直接加载,比如发布到 npm 的库、Node.js 脚本、<script type="module"> 直接引用,就必须写,否则运行时报错。写上也不影响打包器,所以库作者通常会写。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。