尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Highcharts跨域数据加载:JSON与JSONP方案全解析

Highcharts跨域数据加载:JSON与JSONP方案全解析 做前端的都清楚图表类需求只要一上量Highcharts 基本是绕不开的选型。这个库本身配置灵活、文档齐全但真正让开发头疼的往往不是图表配置而是数据怎么进来。尤其是前后端分离之后图表数据接口可能部署在另外一个域名、甚至另外一个端口上跨域问题就直接把很多人卡住了。网上关于 JSON 和 JSONP 跨域的碎片文章不少但大多只讲了一半要么只给后端配置不聊前端细节要么代码能跑但说不清原理。这篇就把 Highcharts 场景下 JSON 和 JSONP 两种跨域数据加载方案完整梳理一遍从浏览器拦截机制到前后端联动再到底层原理和踩坑记录一次性说透。先说清楚这篇文章适合谁看。如果你正在做前端可视化大屏、后台管理系统的报表页面或者接手了老项目需要对接第三方数据接口并且已经被控制台里的 CORS 报错折磨过那这篇内容正好对你有用。全文不绕弯子直接讲实操每个结论都会解释背后的逻辑保证你读完能照着落地不会看完还是一头雾水。1. 为什么图表会抽风先搞懂跨域到底卡在哪1.1 同源策略浏览器给前端立下的规矩很多人一听到跨域两个字就觉得是个 bug其实这是浏览器安全模型的一部分不是 Highcharts 故意折腾你。所谓同源指协议http/https、域名、端口三者完全一致。只要有一个不一致浏览器就会认为这是跨源请求默认情况下会拦下来。我举个例子你就明白了。你把页面部署在http://192.168.1.10:8080数据接口部署在http://192.168.1.10:9090虽然同一个 IP但端口不同这就是跨域。又或者页面在https://dashboard.example.com接口在http://api.example.com协议不同、子域名不同也算跨域。浏览器拦截的不是服务端响应而是前端 JS 代码对响应数据的读取权限。也就是说网络层面数据其实已经返回到浏览器了但浏览器按规矩把读数据这个动作挡住了。这种设计的本意是防止恶意网站窃取用户在别的网站上的登录态和数据。但对开发者来说合法场景也会被误伤——不同子系统之间共享数据本来就是常见需求。于是就有了两条出路一条是让后端明确告诉浏览器这个接口允许你读走 CORS/JSON 方案另一条是利用 script 标签天然不会被 CORS 拦截的特性走 JSONP 方案。1.2 Highcharts 数据加载的典型姿势和报错现场Highcharts 本身是个纯前端绘图库它不负责请求数据。常规操作是先用$.getJSON、fetch或axios拿到数据再通过chart.series[0].setData()或者初始化时在series.data里直接填入。理论上数据怎么来都行但跨域限制一掺和进来事情就不一样了。我见过太多次这样的报错现场——页面其他功能正常唯独图表区域一直转圈控制台里出现Access to XMLHttpRequest at http://api.example.com/data from origin http://localhost:8080 has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource.或者是 jQuery 风格的提示Failed to load resource: the server responded with a status of 404 (Not Found)第一类报错说明你用的是 JSON 方式被同源策略拦截了。第二类报错可能是 JSONP 地址拼写有误也可能是后端没按 JSONP 格式返回。下面分别展开。2. JSON 跨域方案正统思路后端要配合2.1 CORS 的原理和关键响应头JSON 方案对应的正统跨域机制叫 CORSCross-Origin Resource Sharing跨域资源共享。它的思路很简单前端正常发请求后端在响应头里附加Access-Control-Allow-Origin浏览器看到这个头之后就允许前端读取数据。后端要配的无非这几个头Access-Control-Allow-Origin指定允许访问的来源域名。可以写死一个域名也可以写*表示全部放开。Access-Control-Allow-Methods允许的 HTTP 方法比如 GET、POST、PUT、DELETE。Access-Control-Allow-Headers允许的自定义请求头比如Content-Type、Authorization。Access-Control-Allow-Credentials如果请求带 Cookie需要设为true并且Allow-Origin不能是*。我实际做 Node.js 项目时最常用的是cors这个中间件几行代码就搞定。Express 项目里这样写const express require(express); const cors require(cors); const app express(); app.use(cors({ origin: http://localhost:8080, // 只允许这个前端地址访问 methods: [GET, POST], allowedHeaders: [Content-Type, Authorization], credentials: true })); app.get(/data, (req, res) { res.json({ data: [1, 2, 3, 4, 5] }); }); app.listen(9090);这段配置的意思是只有来自http://localhost:8080的请求能读这个接口的数据。如果你暂时图省事想放开所有来源把origin改成true或*也行但生产环境我强烈不建议这样做原因后面讲安全性的时候细说。2.2 前端怎么用 fetch/axios 加载 JSON 数据后端把 CORS 头配好之后前端代码跟普通请求没有任何区别。以 fetch 为例fetch(http://api.example.com/data) .then(response response.json()) .then(json { const chart Highcharts.chart(container, { series: [{ data: json.data }] }); });axios 的写法也类似axios.get(http://api.example.com/data) .then(response { const chart Highcharts.chart(container, { series: [{ data: response.data.data }] }); });如果请求要带 Content-Type 为application/json的 POST 体或者带了Authorization头浏览器会先发一个 OPTIONS 预检请求确认后端允许这个方法和这些请求头后才发正式请求。后端如果只配了 GET 没配 POST或者没把Authorization加进 Allow-Headers前端就会看到预检失败这个坑特别隐蔽数据量大的报表项目里见了不止一次。2.3 哪种场景优先选 JSON 方案我的经验是只要后端是自己团队维护的就优先选 JSON CORS。原因有两条。第一CORS 是 W3C 标准方案支持 GET、POST、PUT、DELETE 全部 HTTP 方法也能带请求头和 Cookie功能上限高。第二返回的数据就是纯 JSON前端解析路径短不存在额外包裹层调试起来清爽得多。JSON 方案唯一的硬伤是后端不可控。比如你接的是第三方开放接口或者运维权限不在自己手里别人没给你配 CORS 头那这条路就走不通了。这时候才轮到 JSONP 登场。3. JSONP 跨域方案绕道而行专治后端不改3.1 JSONP 的核心原理script 标签不守规矩JSONPJSON with Padding能够跨域靠的是浏览器对script标签的特殊待遇。你用script标签加载一个外部 JS 文件时浏览器不会执行同源策略检查。比如你在页面里写script srchttps://cdn.example.com/lib.js/script这个跨域请求是能正常加载执行的。JSONP 的思路就是把这个特性利用起来后端返回的内容不是一个 JSON 对象而是一段可以执行的 JS 代码这段代码把数据当作参数塞进一个函数调用里。前端提前把这个函数定义好数据自然就回调回来了。具体交互流程分三步前端构造script标签src 指向后端接口并带上一个回调函数名参数比如?callbackhandleChartData。后端收到请求后不返回{data: [1,2,3]}而是返回handleChartData({data: [1,2,3]})。浏览器执行这段返回的 JS 代码相当于调用了前端定义好的handleChartData函数数据就传进来了。整个过程不需要 XMLHttpRequest也不需要 fetch自然就不受同源策略管辖。理解这个流程之后你就能明白为什么 JSONP 只能发 GET 请求——script标签加载资源本来就是 GET。3.2 借力 jQuery一行代码搞定 JSONP 请求实际项目里手动拼script标签的繁琐流程jQuery 早就替你封装好了。Highcharts 老项目里最常见的搭配就是 jQuery $.ajax的 JSONP 模式写起来非常简洁$.ajax({ url: http://api.example.com/data, dataType: jsonp, jsonp: callback, // 告诉后端回调函数名放在哪个参数里 jsonpCallback: handleData, // 自定义回调函数名可选默认自动生成 success: function(res) { const chart Highcharts.chart(container, { series: [{ data: res.data }] }); } });发出去的请求 URL 大概是这样的http://api.example.com/data?callbackhandleData_1736496000000注意最后那个_时间戳参数是 jQuery 自动加的目的是防止浏览器缓存请求结果保证每次拉到的都是最新数据。这个细节后面排查缓存问题时还会用到。如果你不想引入 jQuery手写 JSONP 也不难我后面在实操环节给出完整示例。3.3 后端怎么配才能返回 JSONP 格式后端要做的适配很小核心就一句话识别到callback参数时把 JSON 数据包一层函数调用再返回。以 Express 为例const express require(express); const app express(); app.get(/data, (req, res) { const data { data: [5, 10, 15, 20, 25] }; const callback req.query.callback; // 获取前端传的回调函数名 if (callback) { // JSONP 格式callbackName(JSON数据) res.type(application/javascript); res.send(${callback}(${JSON.stringify(data)})); } else { // 普通 JSON 格式 res.json(data); } }); app.listen(9090);这种做法很讨巧同一个接口带callback参数就返回 JSONP不带就返回普通 JSON两种方式都能服务。很多第三方接口也是这么设计的。用 Python Flask 写的后端也类似from flask import Flask, request, jsonify, Response import json app Flask(__name__) app.route(/data) def data(): payload {data: [5, 10, 15, 20, 25]} callback request.args.get(callback) if callback: content f{callback}({json.dumps(payload)}) return Response(content, mimetypeapplication/javascript) return jsonify(payload) if __name__ __main__: app.run(port9090)后端只要保证返回的 JS 代码是可执行的前端那边的callback和这里读取的callback参数名一致就行。3.4 jQuery 之外手写一个极简 JSONP 加载器有时候项目中没引 jQuery或者不想为了一个请求引入整个库手写 JSONP 是更轻量的选择。核心代码十几行就能实现function jsonp(url, callbackName, onSuccess) { return new Promise((resolve, reject) { // 动态创建 script 标签 const script document.createElement(script); // 定义回调函数挂到 window 上因为后端返回的代码在全局作用域执行 window[callbackName] function(data) { resolve(data); // 回调完成后清理 document.body.removeChild(script); delete window[callbackName]; }; // 拼接 URL处理已有的 query 参数 const separator url.includes(?) ? : ?; script.src ${url}${separator}callback${callbackName}; script.onerror function() { reject(new Error(JSONP request failed)); document.body.removeChild(script); delete window[callbackName]; }; document.body.appendChild(script); }); } // 使用 jsonp(http://api.example.com/data, handleChartData) .then(res { const chart Highcharts.chart(container, { series: [{ data: res.data }] }); }) .catch(err console.error(err));这段代码有几点要说明。第一回调函数必须定义在window对象上因为后端返回的handleChartData(...)是在全局作用域里执行的找不到这个函数就直接报ReferenceError。第二请求完成后要主动把script标签从 DOM 里删掉否则页面里会积累一堆无用的标签。第三JSONP 超时不好统一处理script.onerror只处理加载失败网络超时就没那么灵了可以用setTimeout手动加一道防线。4. 实战对比同一个报表两种跨域方案全流程演示4.1 场景假设和准备工作假设现在要做一个近 7 日用户访问趋势图表页面部署在http://localhost:8080数据接口部署在http://localhost:9090。后端是 Express前端是原生 JS Highcharts。后端接口逻辑很简单返回 7 个随机数代表每日访问量。我先实现一个兼容两种模式的接口const express require(express); const cors require(cors); const app express(); // 允许跨域JSON 方案用 app.use(cors()); // 模拟数据生成 function getTrendData() { const days [周一, 周二, 周三, 周四, 周五, 周六, 周日]; return days.map(day ({ name: day, y: Math.floor(Math.random() * 500) 100 })); } app.get(/trend, (req, res) { const payload { categories: [周一, 周二, 周三, 周四, 周五, 周六, 周日], data: getTrendData() }; const callback req.query.callback; if (callback) { res.type(application/javascript); res.send(${callback}(${JSON.stringify(payload)})); } else { res.json(payload); } }); app.listen(9090);对了app.use(cors())这个写法表示对所有跨域请求都放开Access-Control-Allow-Origin: *生产环境建议限制来源。这里为了演示方便先放开。4.2 JSON 方案实操fetch Highcharts前端的完整逻辑是页面加载完成后用 fetch 请求接口拿到数据后赋值给 Highcharts 的 series。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleJSON 方案演示/title script srchttps://cdn.jsdelivr.net/npm/highcharts10.3.3/highcharts.js/script /head body div idcontainer stylewidth: 800px; height: 400px;/div script fetch(http://localhost:9090/trend) .then(response { if (!response.ok) { throw new Error(HTTP error: ${response.status}); } return response.json(); }) .then(payload { Highcharts.chart(container, { title: { text: 近 7 日用户访问趋势JSON }, xAxis: { categories: payload.categories }, yAxis: { title: { text: 访问量 } }, series: [{ type: line, name: 访问量, data: payload.data.map(item item.y) }] }); }) .catch(error { console.error(加载数据失败, error); document.getElementById(container).innerHTML p stylecolor:red;数据加载失败请查看控制台报错/p; }); /script /body /html这里要注意payload.data是一个对象数组每个对象有name和y两个属性Highcharts 的折线图其实可以直接吃这种格式直接data: payload.data也行。我上面写map(item item.y)只是为了演示数据转换实际项目中根据后端返回结构调整就行。运行这个页面你会在浏览器空口看到趋势图正常渲染。把 Network 面板打开能看到一个trend请求Response Headers 里有access-control-allow-origin: *这就是 CORS 生效的直接证据。4.3 JSONP 方案实操动态 script Highcharts换到 JSONP 方案前端代码要改两个地方请求方式从 fetch 换成 script 标签动态加载数据处理从 await/回调换成全局回调函数接收。直接看完整代码!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleJSONP 方案演示/title script srchttps://cdn.jsdelivr.net/npm/highcharts10.3.3/highcharts.js/script /head body div idcontainer stylewidth: 800px; height: 400px;/div script // 1. 定义全局回调函数后端返回数据时会自动执行这个函数 window.renderTrend function(payload) { if (payload payload.data) { Highcharts.chart(container, { title: { text: 近 7 日用户访问趋势JSONP }, xAxis: { categories: payload.categories }, yAxis: { title: { text: 访问量 } }, series: [{ type: line, name: 访问量, data: payload.data.map(item item.y) }] }); } }; // 2. 动态创建 script 标签发起请求 function loadJsonp(url, callbackName) { return new Promise((resolve, reject) { const script document.createElement(script); const separator url.includes(?) ? : ?; script.src ${url}${separator}callback${callbackName}; script.onload () { resolve(); }; script.onerror () { reject(new Error(script 加载失败)); }; document.body.appendChild(script); }); } // 3. 页面加载后调用 loadJsonp(http://localhost:9090/trend, renderTrend) .catch(error { console.error(JSONP 请求失败, error); document.getElementById(container).innerHTML p stylecolor:red;数据加载失败请查看控制台报错/p; }); /script /body /html整个过程的关键点在于loadJsonp把script标签塞进 DOM浏览器发请求后端返回renderTrend({...})这段可执行 JS浏览器执行它并调用window.renderTrend数据就到了 Highcharts 配置里。如果你的后端返回格式没问题但图表不渲染最可能的原因是回调函数名对不上——后端返回的是renderTrend(...)前端全局必须有一个同名的函数。4.4 两种方案的对比总结从实践角度拉一张对比表方便你决策时一眼看清维度JSON CORSJSONP请求方式GET/POST/PUT/DELETE 都支持仅支持 GET请求头控制支持可带 Authorization、Content-Type 等不支持无法自定义请求头Cookie 携带支持需配置 credentials不支持跨域下基本不考虑 Cookie后端配合需要配置 CORS 响应头需要支持 callback 参数并返回 JS 包装格式错误处理走 HTTP 状态码和 fetch/axios 的 error 回调比较规范只有 script.onerror超时和业务错误难区分性能标准 HTTP 响应任何中间层都好处理额外执行一层 JS体积和数据量大的时候有开销安全性相对更安全结合 CORS 白名单控制来源本质是执行外部脚本风险更高适用场景后端可控前后端都是自己维护的项目后端不可控、只提供 GET 接口或历史遗留的 JSONP 接口看了这张表你就明白JSONP 更像是一种不得已的选择。如果条件允许尽量走 JSON CORS。5. 踩坑记录JSONP 的五个坑与应对方案5.1 回调函数名不一致图表默默不显示JSONP 最典型的报错是控制台出现ReferenceError: xxx is not defined或者页面空白的。原因几乎都是回调函数名对不上。前端期望后端调用handleData但后端返回的是callback123(...)或者反过来。接口规范如果不统一前端和后端各写各的就会出问题。排查方法很简单打开 Network 面板直接看这个请求对应响应的内容——如果返回的是handleData({...})而你代码里定义的函数叫renderTrend名字不一致浏览器执行到handleData时找不到函数就报错了。注意给回调函数命名时尽量用全局唯一、不容易和别人代码冲突的名字比如__chartDataCallback_20240101这种带项目标识和时间戳的风格。踩过一次和别的脚本函数重名的坑之后你就会理解这个建议的必要性。5.2 缓存问题图表数据一直不更新JSONP 走的是script加载浏览器对这类资源有缓存策略。如果你发现改了后端数据、刷新页面图表却是旧的不用怀疑多半是浏览器给你缓存了响应。解决方向有两个。第一前端在请求 URL 上人为加一个时间戳参数让每次请求的 URL 都不一样跳过浏览器缓存。jQuery 的$.ajax在dataType: jsonp模式下默认会自动加_时间戳手写方案就需要自己加const separator url.includes(?) ? : ?; script.src ${url}${separator}callback${callbackName}_${Date.now()};第二后端在响应头里加上Cache-Control: no-store明确告诉浏览器这个接口不缓存。如果前端时间戳改了还不行就去检查后端这个头有没有设置。5.3 安全性JSONP 本质是执行外部脚本JSONP 的安全风险要认真对待。它在浏览器里做的事等价于引用一个外部 JS 文件。如果后端被劫持、返回恶意脚本前端没有任何机制拦截恶意代码可以直接在页面上下文执行。说得直白点这是比 XSS 更直接的注入通道。所以用 JSONP 时务必遵守两条原则。第一JSONP 接口必须是可信来源且接口建议走 HTTPS避免数据在传输过程中被篡改。第二回调函数里对接收的数据做校验结构不对就舍弃不要直接信任并渲染到页面上。如果是高安全要求的系统比如涉及金融、用户隐私数据JSONP 基本不该出现直接用 JSON CORS配合后端白名单控制来源。5.4 大数据的性能隐患JSONP 对数据量有隐藏限制。因为它的载体是script标签返回内容会被当作 JS 源码解析执行。如果你一次性给图表塞了几十万个点的数据拼接出来的 回调函数调用字符串 会非常大浏览器解析耗时会明显增加而且这类代码无法流式传输必须等整个脚本下载完才能执行。相比之下JSON CORS 走标准 HTTP很多服务器和浏览器层面都有 gzip 压缩、增量传输等优化。我做过一个监控大屏后端返回 5 万个点的折线数据JSONP 明显比 JSON 慢了一截后来果断切成 JSON CORS。所以如果你要加载的数据量比较大JSONP 绝对不是首选。5.5 什么时候该放弃 JSONP 改用别的方案除了 CORS跨域数据加载还有几个现代方案虽然不等同于 JSONP 的定位但值得放在一起对比。其中之一是postMessage适合 iframe 嵌入场景下父子页面通信。另一个是 WebSocket本身就是全双工跨域通信没有 CORS 拦截的问题适合实时推送图表数据。还有一个思路是让请求绕道——用后端做一层代理前端先请求同源的后端代理接口由代理去取目标服务器的数据再转发给前端。这个方案其实非常可靠很多企业内部的报表就是这么做前后端数据中转的。真心建议一旦遇到 JSONP 搞不定的复杂场景优先考虑后端代理或直接说服后端开 CORS。JSONP 是好用但它属于那个能用就行的年代不是所有新问题的最佳答案。我在实际项目里的选择逻辑通常是这样的后端是我自己能改的一律 JSON CORS干净、可控、安全后端是第三方的、只给了 GET 接口且明确不支持 CORS才上 JSONP如果是实时更新的图表直接考虑 WebSocket 或者轮询 JSON。数据量大的时候宁可让后端做代理也不轻易上 JSONP。这套思路在多个大屏和后台项目里验证过基本不会出岔子。最后再分享一个小技巧。不管用哪种方案前端代码里都建议给数据加载加一个统一的错误提示层不要让用户看着一个空白图表干瞪眼。同时把请求 URL、回调函数名、响应结构这三个关键信息写清楚放在注释里因为这类跨域问题一旦出 bug排查链路往往比想象中长良好的注释能帮你省下大量时间。
返回列表