import attributes(with { type: 'json' })解决什么问题?和旧的 assert 有什么区别?
import attributes 在 import 语句后用 with 写明模块类型,比如 with { type: 'json' }。浏览器按服务器返回的 MIME 类型解析模块,写明类型后,类型对不上就直接失败,不会把被篡改的“JSON”当脚本执行。旧提案用 assert 关键字,现在已不是标准写法。
简答
import attributes 是在
import语句后面加一段with { ... },告诉运行时这个模块应该是什么类型。最常见的写法是import data from './data.json' with { type: 'json' },把一个 JSON 文件直接当模块导入。之所以要写明类型,是因为浏览器不看文件扩展名,只按服务器返回的 MIME 类型决定怎么解析模块。如果服务器被篡改,把一个“JSON”文件返回成
text/javascript,里面的代码就会被执行。写了type: 'json',返回的类型不是application/json时导入直接失败。除了 JSON,浏览器还支持
type: 'css',导入得到一个CSSStyleSheet对象。动态导入写成import('./data.json', { with: { type: 'json' } })。要注意,早期提案用的是
assert关键字,现在这种写法已经不是标准,新代码应该用with。另外 JSON 模块只有默认导出,不能按字段做具名导入。
写法长什么样
静态导入和重新导出都把属性写在模块路径后面:
import config from './config.json' with { type: 'json' };
export { default as theme } from './theme.json' with { type: 'json' };
with 后面是一个对象。键可以是标识符或字符串,必须是运行时认识的键,值必须是字符串字面量。静态导入写了运行时不认识的键会报 SyntaxError,动态导入里写了不认识的键会报 TypeError。
动态导入时,属性放在第二个参数的 with 字段里:
const { default: config } = await import('./config.json', {
with: { type: 'json' },
});
拿到的是模块命名空间对象,JSON 内容在 default 上。因为 JSON 模块只有一个默认导出,import { name } from './config.json' with { type: 'json' } 这种具名导入是不行的。
为什么不能看扩展名就决定类型
很多人会问:文件叫 config.json,浏览器为什么还要我再声明一遍类型?
原因是浏览器加载模块时只看服务器响应里的媒体类型(MIME 类型),URL 里的扩展名不起作用。假设服务器出了问题,把这个文件的类型返回成 text/javascript,而文件内容又被换成了恶意代码,没有声明类型时它会被当成脚本执行。导入方本来只想读一份数据,结果执行了外部代码。
写了 with { type: 'json' } 以后,运行时会核对类型。服务器返回的不是 application/json,导入就失败。浏览器发请求时还会带上 Accept: application/json。所以这个属性改变的是怎样获取和校验模块,不会改变 JSON 本身怎样解析。
反过来,不写 type 直接导入 JSON,浏览器会报错,提示期望的是 JavaScript 模块,服务器却返回了 application/json。MDN 也说明 Node 和 Deno 跟浏览器保持一致,导入 JSON 模块同样要求写 type。
CSS 模块怎样用
浏览器还支持 type: 'css',导入的结果是一个 CSSStyleSheet 对象,可以直接加到文档或 Shadow DOM 上:
import sheet from './card.css' with { type: 'css' };
document.adoptedStyleSheets.push(sheet);
同样的规则也适用:服务器返回的不是 text/css,导入就失败。CSSStyleSheet 只在 window 环境里提供,所以 Worker 里通常不能导入 CSS 模块。
和旧的 assert 有什么区别
这个特性早期叫 import assertions,写法是 import data from './data.json' assert { type: 'json' }。后来提案改成了 with 关键字,MDN 明确说 assert 现在是非标准写法。
所以老教程、老项目里看到 assert,迁移时改成 with 即可,花括号里的内容不用变。新代码不要再写 assert。
常见追问
同一个路径写不同的属性,会是同一个模块吗?
不一定。MDN 说明属性是模块说明符的一部分,同样的路径带上不同的属性,可能加载出完全不同的模块。
导入的 JSON 后来在服务器上改了,页面能读到新内容吗?
读不到。动态导入的模块在整个页面或 Worker 的生命周期里都会被缓存,同一个模块只加载一次。数据会变化的话,应该用
fetch去请求,而不是用 import。
ESM 和 CommonJS 的其他区别呢?
加载时机、导出的是值还是绑定这些差别,见《ES Modules 和 CommonJS 有哪些关键区别?》。