☰
Highcharts跨域数据加载:JSON与JSONP方案全解析
2026/11/1 17:18:16 网站建设 项目流程

做前端的都清楚,图表类需求只要一上量,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 方案对应的正统跨域机制叫 CORS(Cross-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 标签不守规矩

JSONP(JSON with Padding)能够跨域,靠的是浏览器对script标签的特殊待遇。你用script标签加载一个外部 JS 文件时,浏览器不会执行同源策略检查。比如你在页面里写<script src="https://cdn.example.com/lib.js"></script>,这个跨域请求是能正常加载执行的。

JSONP 的思路就是把这个特性利用起来:后端返回的内容不是一个 JSON 对象,而是一段可以执行的 JS 代码,这段代码把数据当作参数塞进一个函数调用里。前端提前把这个函数定义好,数据自然就"回调"回来了。

具体交互流程分三步:

  1. 前端构造<script>标签,src 指向后端接口,并带上一个回调函数名参数,比如?callback=handleChartData。
  2. 后端收到请求后,不返回{"data": [1,2,3]},而是返回handleChartData({"data": [1,2,3]})。
  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?callback=handleData&_=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, mimetype='application/javascript') return jsonify(payload) if __name__ == '__main__': app.run(port=9090)

后端只要保证返回的 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 lang="zh-CN"> <head> <meta charset="UTF-8"> <title>JSON 方案演示</title> <script src="https://cdn.jsdelivr.net/npm/highcharts@10.3.3/highcharts.js"></script> </head> <body> <div id="container" style="width: 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 style="color: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 lang="zh-CN"> <head> <meta charset="UTF-8"> <title>JSONP 方案演示</title> <script src="https://cdn.jsdelivr.net/npm/highcharts@10.3.3/highcharts.js"></script> </head> <body> <div id="container" style="width: 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 style="color: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,排查链路往往比想象中长,良好的注释能帮你省下大量时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询