“HTML读取json文件(传递给变量)”这个需求,听起来简单,实际上几乎是每个写前端页面的人都会撞上的一道坎。特别是那些刚离开教程、想自己动手做点什么的朋友,手里拿着一个静态页面模板,想把数据抽离到JSON文件里统一管理,结果一打开浏览器全是红色报错,连文件都读不出来。又或者好不容易把数据读进来了,却发现在“外面”拿不到那个变量,控制台打印出一个undefined,一脸懵。
这篇文章我把整个事情从头到尾捋一遍:怎么读、怎么存、怎么用、踩过的坑有哪些,以及最关键的——“读取”和“传参”之间那个异步的坑到底怎么填。适合正在做静态页面、前端Demo、简单的数据展示页,以及想搞清楚JS异步时序的读者,内容按现有实践来写,可以直接照着操作。
1. 内容整体设计与思路拆解
1.1 这个需求到底在解决什么问题
先拆解标题里“HTML读取json文件(传递给变量)”这半句话。它背后其实是一类非常典型的应用场景:把数据和页面结构分离。举个例子,你写一个产品展示页,产品列表可能有20条,每一条有名称、价格、图片、描述。最粗暴的写法是把这20条数据全部写死在HTML标签里,改一次数据就要翻半天代码。稍微聪明一点的做法,是把这些数据放到一个JSON文件里,页面加载的时候去读这个文件,然后把读到的数据丢给JavaScript变量,再用这个变量去拼接、渲染页面。
这套逻辑在工程上属于“数据与视图分离”,落到实际操作里就是:数据存在JSON,视图是HTML,JavaScript充当搬运工。标题里说的“传递给变量”也不是真的把文件内容“传”给变量那么玄乎,本质上是把JSON文本解析成JavaScript对象,再赋给一个变量供后续使用。
这个需求常见的出现场合有几类:一类是静态页面和活动页,没有后端接口但数据量又不想写死在DOM里;一类是前端Demo,模仿接口返回的数据结构;还有一类是配置文件,比如图表类的配置项、页面的文案配置。我见过很多做数据可视化的人,把图表的数据单独放在一个JSON文件里,用的时候读出来再传给图表的options变量,这个思路是完全成立的,而且比写死数据要干净得多。
1.2 主流实现方案与选型逻辑
在有多种方案可以选的情况下,先分清它们各自的位置,再决定用哪种。
| 方案 | 实现方式 | 适用场景 | 缺点 |
|---|---|---|---|
| Fetch API | fetch('data.json').then(res => res.json()) | 现代浏览器、标准环境 | 老浏览器不支持,本地file协议受限制 |
| XMLHttpRequest | xhr.open('GET', 'data.json') | 兼容老环境 | 写法繁琐,回调嵌套地狱 |
| 脚本标签注入 | <script src="data.js"></script>配合全局变量 | 完全没有服务器、纯本地打开 | 需要JSON改写成JS赋值语句 |
| ES模块导入 | import data from './data.json' with { type: 'json' } | 工程化项目、Vite等构建工具 | 需要构建环境支持 |
实际选择的时候,我的建议是现在写新东西一律优先Fetch API。理由很简单:现代浏览器基本都原生支持,语法是Promise风格,代码可读性好,配合async/await写起来像同步代码一样舒服。XHR是上一代的老兵,兼容性确实好,但现在很少需要专门为老掉牙的浏览器写新代码了,而且那套onreadystatechange状态判断的写法,能少碰就少碰。
脚本标签注入这个方案很特殊,它适合一种极端的场景:你的HTML文件是直接在本地双击打开的,地址栏是file://开头,这时候Fetch和XHR都会因为浏览器的安全策略直接罢工。如果不想起本地服务器,就可以把JSON文件改成一个JS文件,里面写const data = { ... },然后通过<script>标签引入,这样不涉及跨域请求,纯本地也能跑。不过它的代价是JSON文件不能再被其他工具直接读取,严格说已经不是JSON了,属于妥协方案。
ES模块导入是工程化项目里的最优解,尤其是配合现在的主流打包工具,直接import就能把JSON当作模块引入,还能利用打包工具的插桩、压缩和缓存处理。但如果只是写一个独立的HTML页面,用这个方案反而要操心构建配置,属于杀鸡用牛刀。
选型逻辑总结一句话:考虑“你在什么环境下跑”,再考虑“你手头有没有服务器”,最后考虑“你的项目需不需要工程化”。没有哪个方案绝对最好,只有哪个方案在当前场景下最省事。
2. 核心细节解析:读取文件与变量赋值的完整链条
2.1 Fetch API 的读写路径拆解
把Fetch这块讲透,因为这是当前最主力、也最容易出问题的方案。先看最基础的一段代码:
fetch('data.json') .then(function(response) { return response.json(); }) .then(function(data) { console.log(data); }) .catch(function(error) { console.error('加载失败:', error); });这段代码里每一行的作用分别是:第一行发一个网络请求,请求当前目录下的data.json;第一个.then拿到的response是一个响应对象,里面装的是原始数据流,需要调用.json()方法把响应体解析成JavaScript对象,这个方法本身也返回一个Promise;第二个.then拿到的data就是真正解析好的对象,文件里的内容到这里才算“传递给了变量”。
有个新手最容易忽略的点:.json()不是同步的,它返回的还是一个Promise,所以必须再链一个.then或者在await后面用。很多人第一次写的时候只写了一个.then,拿到的直接是Response对象,打印出来一串看不懂的字段,然后怀疑是不是JSON格式错了,其实只是少了一环解析。
用async/await改造之后,代码会更直观:
async function loadData() { try { const response = await fetch('data.json'); const data = await response.json(); return data; } catch (error) { console.error('读取失败:', error); return null; } }这里的关键是await,它会“暂停”函数执行,等返回结果落地再继续。这种写法在逻辑上和同步代码一致,阅读顺序就是执行顺序,排查问题的时候大脑负担小很多。
2.2 “传递给变量”的异步陷阱
现在说标题里另一个核心诉求。很多人写完上面的代码,想要在函数外面使用data,于是写出这样的代码:
let data = {}; function loadData() { fetch('data.json') .then(res => res.json()) .then(json => { data = json; }); } loadData(); console.log(data); // 永远都是 {}这段代码的问题在于:fetch是异步的,loadData()调用之后,程序不会等网络请求回来再执行下一行,而是立刻继续往下走,执行console.log(data)。这时候网络请求还没回来,data还是初始值{}。这不是代码写错了,而是JavaScript的运行机制决定的——它不会停下来等网络。
解决这个问题的核心思路只有一句话:所有对数据的操作,都必须放在拿到数据之后执行。所谓“传递给变量”,真正准确的表述是“在数据到达后,赋值给变量,并在同一个时机消费这个变量”。
正确写法是把消费数据的逻辑放进回调里,或者放在await之后:
async function init() { const data = await loadData(); if (data) { renderPage(data); // 在这里使用变量 } }从工程角度,这其实是一个“时序设计”问题。如果你一开始就注意到“数据什么时候到达”和“我什么时候用数据”这两件事必须对齐,后面很多报错都是可以避免的。
2.3 全局变量与局部变量的正确用法
有人会有疑问:那我把data定义成全局变量,等数据到位之后再用,行不行?行,但要小心。全局变量确实可以在浏览器开发者工具的Console里手动访问,调试方便,但它会让数据在初始化为空对象、还没填充真实值的那段时间暴露在外部,如果有其他函数在错误的时机读取了它,拿到的是空对象,容易引发一连串异常。
我的建议是:数据尽量放到模块作用域或者函数作用域中管理,只在必要的时候暴露。比如可以这样组织:
let allData = null; async function fetchData() { const res = await fetch('data.json'); allData = await res.json(); } async function init() { await fetchData(); // 到这里 allData 一定有值 renderList(allData.list); }allData放在顶层,但是所有读它的动作都在init()里完成,保证执行顺序正确。如果页面还有后续用户交互需要用到这份数据,比如点击按钮后过滤数据,只要点击发生在allData赋值之后,全局读取就没问题。这符合实际页面中“先加载后交互”的常规逻辑。
3. 实操过程与核心环节实现
3.1 完整可运行的入门示例
从头搭建一个最小可运行项目。目录结构如下:
project/ ├── index.html ├── data.json └── app.jsdata.json的内容:
{ "title": "商品列表", "items": [ { "name": "机械键盘", "price": 399, "stock": 15 }, { "name": "无线鼠标", "price": 129, "stock": 42 }, { "name": "显示器支架", "price": 259, "stock": 8 } ] }index.html的骨架:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>读取JSON示例</title> </head> <body> <h1 id="title">加载中...</h1> <ul id="list"></ul> <script src="app.js"></script> </body> </html>app.js里做整个读取和渲染:
async function loadData() { const response = await fetch('data.json'); if (!response.ok) { throw new Error(`HTTP 错误:${response.status}`); } return response.json(); } function renderPage(data) { const titleEl = document.getElementById('title'); const listEl = document.getElementById('list'); titleEl.textContent = data.title; data.items.forEach(function(item) { const li = document.createElement('li'); li.textContent = `${item.name} - ¥${item.price} - 库存${item.stock}`; listEl.appendChild(li); }); } async function init() { try { const data = await loadData(); renderPage(data); } catch (error) { document.getElementById('title').textContent = '数据加载失败'; console.error(error); } } init();运行这个例子需要注意一个前提:环境必须是HTTP服务,不能是file://协议直接打开。具体原因和处理方法在下一节详细展开。你可以先在项目目录下启动一个本地服务器,比如用Python的话在终端执行python -m http.server 8080,然后浏览器访问http://localhost:8080,就可以看到数据正常渲染。
3.2 把数据渲染进页面的两种方式对比
上面例子用的是document.createElement逐个创建列表项,这是一种安全、可控的做法。另一种更省事的方案是用innerHTML拼接字符串,直接渲染,代码量少很多:
listEl.innerHTML = data.items.map(function(item) { return `<li>${item.name} - ¥${item.price} - 库存${item.stock}</li>`; }).join('');两者本质区别在于操作方式:createElement是DOM API的标准操作,逻辑清晰、性能好(尤其在大量节点时),而且因为元素节点由代码创建,不受字符串解析规则影响,安全性更高。innerHTML是把HTML字符串丢给浏览器解析,如果拼接的字符串里混入了用户输入内容,比如商品名称里有特殊字符,甚至一段<script>标签,就可能被浏览器当作HTML执行,形成XSS漏洞。
个人建议是:初学阶段、做小Demo,用什么都可以;一旦数据可能包含外部输入,或者要做正式一点的项目,一定要用createElement或者textContent来写入文本,避免直接用innerHTML拼接不可信内容。如果非要省事,可以做一个简单的转义函数,把<、>、&这些危险字符替换成HTML实体,也能规避大部分问题。
3.3 异常处理、加载状态与超时控制
实战中不能只管顺利路径,读文件失败、数据格式错误、网络异常都是常态。至少要考虑下面这几种情况。
第一是HTTP状态检查。fetch不会主动断掉非200状态,比如404的时候,响应对象依然会到达.then,只是response.ok为false。如果不检查,后面去解析JSON只会得到一堆不友善的报错。
if (!response.ok) { throw new Error(`加载失败:${response.status} ${response.statusText}`); }第二是JSON解析异常。response.json()在数据格式不合法时会抛错,一般会被catch捕获。这种情况下建议日志要打原始文本,方便排查:
const text = await response.text(); try { return JSON.parse(text); } catch (error) { console.error('JSON解析出错,原始内容:', text); throw error; }用text()拿到字符串,再手动JSON.parse,比直接response.json()多留了一手原始数据,排查问题的时候特别管用。
第三是加载状态和超时。页面初识加载期间,至少要有一个“加载中”的视觉反馈,避免用户看到一块空白。超时控制方面,fetch本身没有原生超时机制,需要配合AbortController实现:
async function loadDataWithTimeout(url, timeout = 8000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { if (error.name === 'AbortError') { throw new Error('请求超时,请刷新重试'); } throw error; } finally { clearTimeout(timer); } }这段代码在超过8秒后主动中断请求,并给出明确提示,比让用户盯着空白页面干等强得多。
4. 常见问题与排查技巧实录
4.1 本地打开HTML时报错:跨域限制
这是刚接触这个需求的人最常撞的墙。报错信息大致是:
Access to fetch at 'file:///.../data.json' from origin 'null' has been blocked by CORS policy这里的CORS策略是浏览器为了防止网页读取本地文件而设计的安全机制。当HTML文件是以file://协议直接打开的,浏览器会认为页面的来源是null,而fetch请求一个本地JSON文件也属于跨域行为,默认是被禁止的。
解决方案有三个梯队。第一是起一个本地HTTP服务器,把项目目录作为根目录,用HTTP协议访问页面,这也是最推荐的方式。常用的有Python自带模块python -m http.server,Node环境可以用npx serve,或者直接用编辑器插件,比如常见的Live Server插件。第二是把JSON改造成JS文件,用<script src="data.js"></script>引入,挂到全局变量上,这种方式完全绕开网络请求,适合极度简单的页面。第三是修改浏览器安全策略,比如在启动命令里加参数关闭安全机制,这种方式只建议在调试时临时用,而且改动浏览器行为有风险,日常工作不推荐。
从长远来说,日常开发中养成“起服务、看页面”的习惯,不只为了解决JSON读取,还能让后续的模块加载、字体加载、接口联调都顺畅,是一本万利的投入。
4.2 数据拿到手了,控制台却打印undefined
这类问题的典型场景是:网络面板里能看到JSON文件加载成功,但是页面里的变量始终是undefined或者初始值。根子基本都出在异步时序上。
拿一个具体例子来说:
let data = null; fetch('data.json') .then(r => r.json()) .then(json => { data = json; }); console.log(data); // nullconsole.log执行的时候,fetch的响应还没回来,data还是null。解决方式前面已经说过:任何读取data的代码都要放到数据到达之后的回调、或await后面去执行。
还有另一种情况也会导致变量拿不到值:作用域搞错了。比如在某个函数内部定义了let data,然后想在函数外面读取,这当然读不到,因为let是块级作用域。检查一下你的变量声明位置是不是在读取位置的外层,这是一个容易被忽视但极其常见的问题。
4.3 JSON文件没报错,但解析出来的字段全是undefined
JSON能加载、不报语法错误,但访问属性的时候拿到undefined,这通常意味着数据结构和你预期的不一样。比如你以为data.items是一个数组,实际文件里可能是{ "data": { "items": [] } }这种嵌套结构,或者items写成了item。
排查方法是把解析结果完整打印出来,看一眼结构:
const data = await response.json(); console.log(JSON.stringify(data, null, 2));用JSON.stringify带上缩进打印,结构一目了然,再对照代码里的属性名,一般马上能发现是字段名对不上还是层级多了一层。这个坑的隐蔽性在于它不报错,只是默默返回undefined,非常容易让人怀疑是变量传递的问题,结果其实只是写错了一个字段路径。
4.4 数据修改了,但页面加载的总是旧内容
浏览器对静态资源有缓存机制,JSON文件被加载过一次之后,如果文件名不变、服务器也没返回新的缓存头,浏览器可能直接拿缓存里的旧数据,导致你改了JSON之后页面毫无变化。
解决方式分两层。临时调试层面,可以在浏览器开发者工具里勾选“禁用缓存”,或者在Network面板里强制刷新。规范一点的方案是在请求URL后追加版本参数:
const url = `data.json?v=${Date.now()}`;这样每次加载都会带一个不同的时间戳,等于告诉浏览器“这是一份新资源”,绕开缓存。如果做的是正式一点的项目,可以考虑在文件名里带版本号,比如>function getParam(paramName) { const params = new URLSearchParams(window.location.search); return params.get(paramName); } async function init() { const channel = getParam('channel') || 'default'; const url = `data-${channel}.json?v=${Date.now()}`; const data = await fetch(url).then(r => r.json()); renderPage(data); }
这里用URLSearchParams读取查询参数,拼出对应的JSON文件名。如果渠道名非法,还要做个白名单校验,比如只允许a、b、c三个值,否则直接兜底到默认文件,避免用户通过URL注入奇怪的路径。
5.2 多份JSON合并:把配置和数据分开管理
项目稍微复杂一点,就会发现所有东西塞进一个JSON文件会变得很臃肿。更好的组织方式是拆成多个文件:一份存页面配置,一份存业务数据,一份存文案。加载的时候并行请求,全部就绪后再合并。
async function loadAll() { const [settings, businessData, copyContent] = await Promise.all([ fetch('config/settings.json').then(r => r.json()), fetch('data/business.json').then(r => r.json()), fetch('lang/zh-CN.json').then(r => r.json()), ]); const finalData = { settings, businessData, copyContent, }; renderPage(finalData); }Promise.all是并行加载多个请求的关键,它接收一个Promise数组,等所有请求都完成之后统一返回结果,比串行请求一个接一个快得多。任何一行代码都挂掉的话,Promise.all会直接走catch分支,整体失败,这也是一个合理的设计——页面要么完整展示,要么提示加载失败,不做半成品状态。
5.3 安全性补充:不要盲目加载不可信的JSON
前面在渲染部分提到过XSS,这里再深入提醒一点。通过fetch加载的JSON数据,如果你直接拼进innerHTML,一旦文件内容被篡改,可能会注入恶意脚本。除了渲染时使用textContent,还要注意两点:一是不加载来源不可信的JSON文件,如果文件存放在CDN或第三方服务器,务必确认内容可信,比如服务端返回时校验签名或哈希;二是对从文件里读出来的用户生成内容做过滤,比如提前剔除<script>标签、onerror属性等危险模式。
JSON文件本身虽然是静态的,但它最终会进入页面的上下文执行,一旦中间环节被人动手脚,后果就是页面层面的安全风险。在做这类数据加载需求的时候,多留一个心眼,养成“不信任输入”的习惯,是进阶开发者应具备的意识。
6. 写在实操边上的一些经验
做这个需求前前后后也帮不少人看过代码,发现最容易出问题的永远不是“读文件”本身,而是“读完怎么处理”以及“处理的时机对不对”。我个人的习惯是:先确认运行环境是HTTP还是file协议,再决定方案,省得后面被CORS折腾;然后写代码时永远把加载函数单独拆出来,不要跟渲染逻辑混在一起,这样出问题能快速定位;最后一点是数据到了之后先打印一次结构,确认字段名和层级没问题,再往下写渲染逻辑。
还有一个小技巧分享:调试的时候,直接在浏览器开发者工具里执行await fetch('data.json').then(r => r.json()),几条交互式的命令就能当场确认数据能不能读、读出来长什么样,比反复刷新页面、打日志要省事得多。这个习惯帮我节省了大量排查时间。
如果你打算长期做前端方向的开发,这个需求虽然小,但是背后涉及的“异步时序”、“跨域策略”、“数据与视图分离”这些概念,是后续所有复杂项目的地基。把地基打稳,后面盖楼就会顺手很多。