
上周帮朋友调一个报表中心的前端页面折腾了一下午最后发现所有问题都集中在同一个环节Highcharts 向跨域接口拉数据时被浏览器拦截了。控制台那行报错——Access to XMLHttpRequest at http://api.internal.example.com/data.php from origin http://localhost:8080 has been blocked by CORS policy——写过前后端分离项目的人应该都见过但很多人第一反应是接口坏了。其实接口在 Postman 里调得通curl 也能拿到完整 JSON问题根本不在服务端而在浏览器的同源策略。这篇就把 Highcharts 跨域数据加载这件事讲透JSON CORS 和 JSONP 两条路各自怎么走、后端和前端分别要做什么、选型怎么定以及实战里反复出现的坑。这里面的经验对两类人尤其有用一类是被大屏报表折腾得够呛的前端另一类是写数据接口但没怎么配过跨域的后端顺便还能帮你避开不少用 Highcharts 加载数据时的隐性坑。1. 为什么Highcharts用户总是第一个碰到跨域问题1.1 同源策略的真正含义同源策略是浏览器内置的一套安全规则它的核心逻辑就一句话一个页面里通过脚本发起的网络请求只能访问同源的地址。所谓同源指的是协议、域名、端口三个要素完全一致缺一个都算跨域。举个例子你本地开发环境是 http://localhost:8080后端接口是 http://api.example.com/data.php这俩协议都是 http但域名一个是 localhost 一个是 api.example.com端口也不同放在浏览器眼里就是两个完全不同的世界请求自然而然地被判定为跨域。很多人会把跨域和接口连不上混为一谈这是最大的误区。跨域并不意味着服务器拒绝了你实际上请求往往已经发出去了后端也正常处理了响应也回来了只是浏览器发现响应头里没有允许跨域的凭证于是把响应内容留在了自己手里不肯交给 JavaScript 代码。这也是为什么 Postman、curl 这些工具从来不会报跨域错误——因为它们根本不执行浏览器的同源策略它们只是纯粹的 HTTP 客户端。1.2 浏览器到底拦截了什么要理解拦截的粒度得分两种情况看。第一种是简单请求通常是 GET 请求或者少数特定的 POST 请求浏览器直接发出请求拿到响应后检查响应头里有没有 Access-Control-Allow-Origin。有且匹配数据交给页面没有或者不匹配控制台报错数据作废。第二种是非简单请求比如带 Content-Type: application/json 的 POST、带自定义 header 的请求、PUT/DELETE 请求浏览器会先发一个 OPTIONS 预检请求去试探服务端你允许我这么干吗预检通过后才发起真实请求。在 Highcharts 项目里最常见的场景是页面初始化时用 $.getJSON 向后端拉图表数据这就是简单请求无非是响应里少了一个响应头浏览器直接拦截但如果你用了 axios 并以 JSON 格式 POST 查询条件那就会多一轮 OPTIONS 预检后端如果没处理 OPTIONS前端看到的报错往往就不是跨域被拒绝而是preflight 失败很多人会在这里卡很久。1.3 Highcharts本身不负责请求数据加载要靠自己接Highcharts 本质上只是一套纯客户端的绘图库它接收你准备好的数据然后画成图。它并不像某些后台框架那样自带网络请求层也不管你数据是怎么来的。使用 Highcharts 加载数据通常有三条路一是把数据直接写死在图表配置里适合演示和静态数据二是用 Highcharts Data 模块去加载外部 CSV、JSON 数据源这个适合表格型数据三是最主流的做法自己在业务代码里用 $.getJSON、$.ajax、fetch 去请求接口拿到数据后再用 series 或 setData 塞给图表。第三条路最关键因为只要用到异步请求就立刻撞上跨域规则。换句话说不是 Highcharts 特别容易跨域而是做报表图表本来就是数据可视化项目中前后端分离程度最高的场景之一前端页面放一台服务器数据接口放另一台中间隔着域名和端口跨域几乎必然发生。所以与其说Highcharts 跨域不如说做图表的工程里跨域问题更常见。理解这一点后面的排查思路会清晰很多。2. 方案一JSON CORS正规军的配置姿势2.1 CORS的完整工作流程与两种请求类型CORSCross-Origin Resource Sharing是目前官方标准里的跨域方案基本思路是后端在 HTTP 响应头里明确声明我这个接口允许哪些来源访问浏览器检查声明后决定是否放行数据。你不需要在前端做任何特殊处理普通 ajax 请求该怎么写就怎么写一切由浏览器和后端握手完成。简单请求的响应只需要一个关键头Access-Control-Allow-Origin。它的值可以是具体的源比如 http://localhost:8080也可以是 * 表示允许所有来源。而需要预检的请求除了 Allow-Origin还需要 Access-Control-Allow-Methods 声明允许的 HTTP 方法以及 Access-Control-Allow-Headers 声明允许的自定义请求头。有个细节很多人容易忽略如果前端带了 Cookie 或者 Authorization 头Access-Control-Allow-Origin 就不能用 *而且后端还要额外返回 Access-Control-Allow-Credentials: true否则浏览器依然拒绝。简单总结一下两种请求的差异对比项简单请求预检请求Preflight触发条件GET、HEAD或特定 Content-Type 下的 POSTPUT/DELETE或带 application/json、自定义 header前置过程无直接发真实请求先发 OPTIONS 请求通过后再发真实请求后端需要返回的头Access-Control-Allow-OriginAllow-Origin、Allow-Methods、Allow-Headers常见坑Origin 不匹配OPTIONS 返回 4xx 或 5xx2.2 后端如何配置PHP、Java、Node 三端实例无论什么语言CORS 配置本质都是加响应头。我用 PHP 写一个最完整的示例这个文件同时考虑了预检请求和字符集问题?php // data.php header(Content-Type: application/json; charsetutf-8); header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization); // 处理预检请求 if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit; } $chartData array( categories array(一月, 二月, 三月, 四月, 五月, 六月), series array( array(name 销售额, data array(12000, 13500, 14200, 15800, 16900, 18200)), array(name 转化率, data array(3.2, 3.5, 3.4, 3.9, 4.1, 4.4)) ) ); echo json_encode($chartData, JSON_UNESCAPED_UNICODE);Java 服务端用 Servlet 的路子也很直接在过滤器里统一加头response.setHeader(Access-Control-Allow-Origin, *); response.setHeader(Access-Control-Allow-Methods, GET, POST, OPTIONS); response.setHeader(Access-Control-Allow-Headers, Content-Type);Node 的 Express 项目很多人被 CORS 坑是因为忘了处理 OPTIONS下面这段中间件写了就能顶住大部分场景app.use((req, res, next) { res.setHeader(Access-Control-Allow-Origin, *); res.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type, Authorization); if (req.method OPTIONS) { return res.sendStatus(204); } next(); });需要特别提醒的是如果你用了 Spring Boot、Express 的 cors 中间件或 Nginx 的 add_header不要在下层业务代码里再手写一遍同样的响应头。一旦同一份响应里出现多个 Access-Control-Allow-Origin浏览器会直接拒绝报错信息反而比不加还难懂。2.3 前端配合用 $.getJSON 去拉数据并交给 HighchartsCORS 配好之后前端代码就是最朴素的 ajax 写法没有任何特殊参数$.getJSON(http://api.example.com/data.php, function (res) { const chart Highcharts.chart(container, { chart: { type: column }, title: { text: 2024 上半年经营数据 }, xAxis: { categories: res.categories }, series: res.series }); });这里有个关键点接口返回的数据结构必须和 Highcharts 要的数据结构对齐。Highcharts 的 series 标准结构是数组每个元素至少要包含 name 和 datadata 可以是数值数组也可以是对象数组。如果后端返回了 { code: 0, data: {...} } 这种统一封装格式前端拿到之后记得先解一层壳把真正要画的 series 取出来而不至于把整个 res 对象直接塞进 series那样图表只会一片空白。$.getJSON 其实只是 $.ajax 的语法糖它内部会把响应体按 JSON 解析。假如这里用的是 axios 或 fetch处理逻辑完全一样只要浏览器拿到合法的 CORS 响应头任何请求库都能用。而如果你在开发环境里用 $.getJSON 一直报错可以先用浏览器直接访问接口地址看看响应头里到底有没有 Allow-Origin很多问题在 Network 面板里一眼就能定位。3. 方案二JSONP借script标签绕开同源限制3.1 JSONP的本质把数据变成一段可执行的JavaScript代码JSONP 是一套历史悠久的跨域方案比 CORS 标准出现得早很多。它的原理其实特别朴素HTML 里的 script 标签加载脚本时浏览器是不检查跨域的。你可以在一个页面里引用任意 CDN 的 JS 文件浏览器照单全收并执行。JSONP 就是钻了这个空子——不再用 ajax 去拿数据而是动态创建一个 script 标签把接口地址塞进 src 里让浏览器以加载脚本的方式发起请求。但接口返回的如果是纯 JSON顶多算一个数据文件不会产生执行效果所以后端必须配合做一件事把 JSON 包在一个函数调用里返回例如 callbackName({name:value})。这段文本到了浏览器会被当作 JavaScript 执行调用你在全局定义好的 callbackName 函数数据就以参数的形式送进了你的业务代码。整个过程可以理解成你不是让快递员XHR跨城取包裹而是雇了个天生不怕跨城的人script 标签到取货点当面拆箱验货验完再把货递给你。3.2 手写一个JSONP加载器并说清关键细节虽然 jQuery 封装了 JSONP但很多人用的框架里不一定有 jQuery。手写一个轻量加载器并不难而且写一遍之后对原理的印象会深很多function loadJsonp(url, timeout) { return new Promise(function (resolve, reject) { const cbName cb_ Date.now() _ Math.floor(Math.random() * 1000); const script document.createElement(script); let timer null; function cleanup() { if (timer) clearTimeout(timer); try { delete window[cbName]; } catch (e) { window[cbName] undefined; } if (script.parentNode) script.parentNode.removeChild(script); } window[cbName] function (data) { cleanup(); resolve(data); }; script.onerror function () { cleanup(); reject(new Error(JSONP 请求失败: url)); }; timer setTimeout(function () { cleanup(); reject(new Error(JSONP 请求超时)); }, timeout || 8000); script.src url (url.indexOf(?) 0 ? : ?) callback cbName; document.head.appendChild(script); }); }这里面有好几个细节值得说。第一每次请求的回调函数名必须是随机生成的唯一名称如果固定用同一个名字页面里同时发起多个 JSONP 请求时后一个回调会覆盖前一个导致数据错乱。第二回调函数必须先注册到 window 上再插入 script 标签否则脚本加载太快时后端返回的代码执行了却发现函数还没定义。第三无论成功、失败还是超时都要清理掉全局回调和 script 标签不然长时间运行页面里会堆积大量废弃节点。3.3 jQuery的jsonp写法与Highcharts Data模块的jsonp选项如果用 jQuery代码会简洁很多jQuery 会自动生成回调名并管理全局函数$.ajax({ url: http://api.example.com/data.php, dataType: jsonp, jsonp: callback, success: function (res) { const chart Highcharts.chart(container, { xAxis: { categories: res.categories }, series: res.series }); }, error: function (xhr, textStatus) { console.error(JSONP 加载失败, textStatus); } });除了自己用 ajax 拉数据Highcharts 还额外提供了一个 Data 模块它支持直接配置 jsonp 选项来加载跨域数据。前提是页面里引入了 highcharts/modules/data.jsHighcharts.chart(container, { data: { json: { url: http://api.example.com/data.php, jsonp: callback }, firstRowAsNames: false } });Data 模块会依据 jsonp 参数决定用 JSONP 方式拉取数据省去了一部分手写 ajax 的代码但代价是数据解析规则要跟着 Data 模块的约定走比如 CSV 的列怎么对应 X 轴、Y 轴第一行是不是表头。实际项目里如果只是加载一组 series自己用 $.ajax 控制力更强调试起来也更直观。Data 模块更适合数据已经在 CSV/TSV 表格里、希望少写一点胶水代码的场景。3.4 JSONP的边界只支持GET、错误难以捕获、安全红线JSONP 虽然能用但它的局限性非常明显。首先script 标签的 src 只能是 GET 请求没有办法发 POST也带不了自定义 header这意味着你没法用它给接口传复杂的查询条件或加密凭证。其次错误处理能力很弱script 加载失败时虽然会触发 onerror但你拿不到 HTTP 状态码拿到也不知道是对应的哪个状态如果后端接口 404 了浏览器常常是静默失败只能靠超时兜底。还有一点容易踩JSONP 等于让后端往你的页面里注入一段可执行脚本如果加载的是第三方数据源原则上你是在执行对方的任意代码风险等级和引入一个未审计的第三方 JS 相当。反过来说后端也不该盲目信任 callback 参数必须先做函数名校验否则攻击者能把任意 JS 拼进响应里造成存储型 XSS 之类的安全风险。4. JSON 和 JSONP 六个维度对比到底该选谁4.1 一张表把差异看清老有人在群里问跨域到底用 JSON 还是 JSONP其实这俩不是一个层面的东西。JSON 是数据格式JSONP 是一种基于 JSON 包装出来的跨域传输技巧。要选型真正对比的是 JSON CORS 与 JSONP 两套方案对比维度JSON CORSJSONP请求方式XHR/fetch支持 GET/POST/PUT/DELETE只能 GET请求头支持 Authorization、Content-Type 等自定义头完全不支持自定义头错误处理能拿到完整 HTTP 状态码和响应体只有 onerror 和超时状态码基本拿不到后端配合只需配置响应头业务逻辑不用动要把 JSON 包成函数调用接口逻辑得改安全性受 CORS 策略约束相对可控本质是执行外部脚本需要特别谨慎兼容性IE10 以下基本不支持IE6 及以上全支持典型场景自有接口、REST API、需要 POST 查询老系统、公共数据接口、纯静态页面4.2 根据场景选型别为了能出图将就从实际工程角度我给一个明确的优先级排序同域 JSON 最优先根本不用处理跨域其次是用 CORS因为它标准、完整、可扩展以后接口要加 POST、要传 Token 都能继续用最后才考虑 JSONP。JSONP 适合的场景其实比较窄主要是你拿别人的接口对方只支持 JSONP 回调或者后端是那种很难改的遗留系统你没法说服对方加响应头但对方愿意改一行 echo 把 JSON 包起来。还有一类情况必须想清楚如果你的页面是纯静态文件放在对象存储或者某个不支持反向代理的服务器上后端接口又是另一套域名同时你还必须传 Token 才能拿数据那 CORS 几乎是唯一正解JSONP 根本带不了 Authorization 头。反过来如果只是给一个产品宣传页加载天气、行情这类公开数据对方碰巧支持 JSONP那用 JSONP 确实能少折腾事。选型的原则永远是看接口的控制权和数据敏感度而不是看哪个写法更短。很多项目先图省事用了 JSONP等后面要 POST 查询条件时才发现整条链路都得重构那才是最亏的。5. 完整实战PHP后端一份数据同时支持CORS与JSONPHighcharts双Y轴同步出图5.1 环境准备与接口约定这个实战案例我故意做成一份数据既能走 CORS 也能走 JSONP的形态这样你可以在同一个页面上同时观察两种跨域方式的差异也可以根据自己的接口状态选择其中任意一种。前端页面放在 http://localhost:8080 下数据接口放在 http://api.example.com/data.php两者跨域。数据结构约定为 categories 分类数组加两个系列的折线数据一个是销售额、一个是转化率量级差很大正好用双 Y 轴来展示。5.2 PHP接口完整代码与逐行解释?php // data.php $data array( categories array(一月, 二月, 三月, 四月, 五月, 六月), series array( array(name 销售额, yAxis 0, data array(12000, 13500, 14200, 15800, 16900, 18200)), array(name 转化率, yAxis 1, data array(3.2, 3.5, 3.4, 3.9, 4.1, 4.4)) ) ); header(Content-Type: application/json; charsetutf-8); header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, OPTIONS); header(Access-Control-Allow-Headers: Content-Type); if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit; } $json json_encode($data, JSON_UNESCAPED_UNICODE); if (isset($_GET[callback])) { $callback $_GET[callback]; if (preg_match(/^[A-Za-z_$][A-Za-z0-9_$]*$/, $callback) strlen($callback) 64) { header(Content-Type: application/javascript; charsetutf-8); echo $callback . ( . $json . );; exit; } http_response_code(400); echo invalid callback; exit; } echo $json;这段代码有几个设计意图要讲清楚。CORS 相关的头放在最前面意味着无论走哪个分支都会带上这样即使某个场景下 JSONP 分支没触发浏览器也能通过 CORS 授权返回纯 JSON。OPTIONS 预检请求在实际业务里经常被遗漏很多后端一看到 OPTIONS 就觉得奇怪直接当成非法请求处理了这里显式返回 204 很关键。JSONP 分支用正则校验 callback 是否为合法的 JavaScript 函数名长度限制在 64 以内这是防 XSS 的基础动作绝对不能省。最后注意 header(Content-Type) 在第二个分支里用 application/javascript 覆盖了前面的 application/json这是为了让浏览器以脚本方式执行响应。5.3 前端页面同时用CORS和JSONP加载两组数据前端页面我特意同时走两条路一个请求用普通 JSON 方式加载另一个请求用 JSONP 方式加载这样你能直观看到两种请求在 Network 面板里的形态差别。为了演示 Highcharts 双 Y 轴两个数据源各返回一个系列页面里把它们合并展示!DOCTYPE html html langzh-CN head meta charsetutf-8 titleHighcharts 跨域数据加载实战/title script srchttps://code.jquery.com/jquery-3.6.0.min.js/script script srchttps://code.highcharts.com/highcharts.js/script /head body div idcontainer styleheight:420px;/div script $.when( $.ajax({ url: http://api.example.com/data.php, dataType: json }), $.ajax({ url: http://api.example.com/data.php, dataType: jsonp, jsonp: callback }) ).then(function (resultCors, resultJsonp) { const corsData resultCors[0]; const jsonpData resultJsonp[0]; const chart Highcharts.chart(container, { chart: { type: spline }, title: { text: 2024 上半年经营数据 }, xAxis: { categories: corsData.categories }, yAxis: [ { title: { text: 销售额元 } }, { title: { text: 转化率% }, opposite: true } ], series: corsData.series.concat(jsonpData.series) }); }).fail(function () { console.error(数据加载失败请检查跨域配置); }); /script /body /html这里用 $.when 并发执行了两个 ajax 请求两者都成功后才渲染图表。$.when 的 then 回调里每个请求返回的是 arguments 数组所以要用 resultCors[0] 拿到真正的数据。这种写法不用给额外变量打补丁代码结构也挺干净比先加载一个再在回调里嵌套加载另一个要清晰得多。5.4 验证与调试从curl到Network看全链路代码写好之后不要急着打开浏览器。先在命令行里验证接口本身是通的这一步能筛掉一半以上的假跨域curl -i http://api.example.com/data.php curl -i http://api.example.com/data.php?callbackmyCb第一条命令看响应头里有没有 Access-Control-Allow-Origin响应体是不是合法 JSON。第二条命令确认 JSONP 分支能用 myCb 包住 JSON看到形如 myCb({...}); 的输出就说明接口没问题。接下来打开页面在 Network 面板里过滤 XHR 和 ScriptJSON 请求会显示为 XHRJSONP 请求会显示为 Script两者都能看到完整的请求 URL、状态码和响应体。如果此时报错第一件事就看响应头的 Access-Control-Allow-Origin 是什么很多配置不生效的问题在这一步就会现形。5.5 双Y轴配置的注意点双 Y 轴在 Highcharts 里配置并不复杂但有两个细节容易被漏掉。第一个是 series 里必须明确指定 yAxis 字段0 表示左轴1 表示右轴如果漏了所有系列都会默认画在左轴上销售额几万的量级和转化率几点的量级叠在一起小数值的折线会被压成一条平线图表难看得没法看。第二个是两个 Y 轴要设置 opposite 属性右轴通常设成 true 才会显示在图表的右侧否则两个轴挤在左侧坐标标签会重叠。你还会发现categories 这种分类轴在 X 轴上表示月份双 Y 轴分别用不同单位展示两组量级差异很大的数据这种图很适合给管理层看经营大屏。6. 开发环境正常、部署就跨域代理与生产环境的真实关系6.1 devServer代理只在开发期生效很多 Vue、React 项目在开发环境里配过代理比如 vue.config.js 里写了module.exports { devServer: { proxy: { /api: { target: http://api.example.com, changeOrigin: true, pathRewrite: { ^/api: } } } } };开发时你的页面跑在 http://localhost:8080请求地址写的是 /api/data.php浏览器认为这就是同源请求永远不会出现跨域问题。webpack-dev-server 在中间做了手脚它把 /api 开头的请求转发到了 http://api.example.com并去掉 /api 前缀。但很多人没想明白的是devServer 是开发服务器它只存在于本地开发时期。等你执行 npm run build 之后生成的是纯静态文件放到 Nginx 或对象存储上根本没有 devServer 这个中转角色如果页面里的请求地址仍然指向 http://api.example.com 这种绝对路径跨域问题立刻就会冒出来。这就是开发环境一切正常上线就跨域的根本原因。6.2 生产环境三个解决思路后端CORS、Nginx反向代理、JSONP兜底解决思路其实就三条。第一条是后端配置 CORS 响应头这是最直接的做法如果你能控制后端代码优先选这个。第二条是 Nginx 反向代理让前端请求看起来是站内路径Nginx 再转发到后端典型配置是server { listen 80; server_name chart.example.com; location /api/ { proxy_pass http://api.internal.example.com/; proxy_set_header Host $host; } }这么配下来前端请求 /api/data.phpNginx 会把请求转发给 http://api.internal.example.com/data.php浏览器看到的始终是同源请求也就不存在跨域拦截。第三条是 JSONP适用于后端确实改不了、也没有 Nginx 入口的极端场景但只能 GET、带不了 Token局限性前面说过。从工程角度看能在前面加一层代理最省事能改后端次之JSONP 属于最后的手段。6.3 完整排查链路如果你正在部署后跨域里挣扎我建议按这个链路排查。先在 Network 面板看实际请求的 URL如果 URL 是跨域的绝对地址说明请求确实跨域了如果 URL 是站内 /api 开头但报错说明代理配置没生效。接着在打包产物目录里全局搜一遍 /api确认静态文件里写死的请求地址到底是什么。第三步是到运维侧看 Nginx 访问日志确认代理转发的目标地址是否可达后端有没有真的收到请求。最后再用 curl -i 直接请求后端接口看响应头里有没有 CORS 相关响应头没有就补有就检查是不是被中间层覆盖。按照这个顺序排查基本都能找到问题在哪一层。7. 高频踩坑记录这些报错我帮人排查过无数次7.1 Access-Control-Allow-Origin 重复导致浏览器拒绝这是我见过次数最多的低级坑。后端框架本身已经挂了一个 CORS 中间件代码里又手写了一遍 Access-Control-Allow-OriginNginx 里又 add_header 了一次于是响应头里出现了多个 Allow-Origin。浏览器遇到这种情况直接拒绝报错信息会提示 The Access-Control-Allow-Origin header contains multiple values。解决办法是统一在一层配置要么用框架中间件管完全部 CORS 头要么自己在业务代码里写要么在 Nginx 里做不要层层叠加。排查时可以打开响应头数一数 Access-Control-Allow-Origin 出现了几次一目了然。7.2 带 Content-Type: application/json 的 POST 触发预检后端没处理 OPTIONS接口文档写的是 POST前端为了规范给 axios 设置了 Content-Type: application/json结果请求一发出去就报 preflight 相关错误。原因在于这个请求属于非简单请求浏览器先发 OPTIONS 预检后端却把 OPTIONS 当成非法请求返回 404 或 500预检都没通过真实请求自然发不出去。解决方法是后端要显式处理 OPTIONS返回 2xx 状态码同时带上 Access-Control-Allow-Headers 声明允许的请求头。如果你不希望每次配置都这么麻烦可以考虑后端统一加 CORS 过滤器把所有 OPTIONS 都拦截并放行。7.3 接口返回的是HTML但状态码是200JSON解析失败$.getJSON 发出请求后控制台报 parseerror或者浏览器提示 Unexpected token in JSON at position 0。这种往往是接口路径写错了后端框架返回了自定义的 404 页面但 HTTP 状态码却是 200导致 jQuery 走成功回调解析 HTML 文本时失败。更隐蔽的情况是接口前面的网关返回了登录页 HTML比如认证失效后统一跳转到登录页面前端拿到的自然不是 JSON。排查技巧很简单在 Network 面板里点开失败请求的 Response 标签看一眼响应体是什么。如果是 HTML再去跟后端确认接口路由和认证逻辑问题就不再是跨域而是接口路径或会话过期了。7.4 JSONP回调没触发或执行了两次手写 JSONP 时最常见的问题是回调函数还没注册script 已经加载完开始执行了导致后端返回的 callback(...) 执行时报错 is not defined。另一个高频问题是回调函数固定写死页面里同时发起两个 JSONP 请求后面的定义把前面的覆盖掉数据自然错乱。解决建议是像我在前面示例里那样每次请求动态生成唯一回调名注册完成后再插入 script并且成功或失败后立即清理。如果发现回调执行了两次基本可以断定是 script 标签没被移除或者同一个 URL 被浏览器缓存后又被加载了一次。7.5 数据塞进series但图表空白接口通了、数据也拿到手了、也塞进 series 了图就是不出来。这类问题先别急直接在控制台里打印返回的数据看每一项的实际类型。常见原因是接口返回的数值是字符串 12000虽然 Highcharts 通常能隐式转换但如果混入了空字符串或 null折线就会出现断点甚至整条不画。还有一类是时间戳数据13 位默认按毫秒处理但 xAxis 没有设置 type: datetime数据点会被当作普通分类坐标自然落不到可视范围内。处理方式是在塞给 Highcharts 之前先做一轮清洗把字符串转成 Number顺便过滤掉 null 和 undefined。数据清洗这一步看似多余实际项目里不可或缺。7.6 HTTPS页面拉取HTTP接口被拦截页面部署在 HTTPS 下接口却还是 HTTP浏览器会自动升级请求或者在 Network 里拦截控制台可能出现 Mixed Content 的警告。这类问题经常在本地开发时发现不了因为本地一般也用的 HTTP。线上出现这种情况最省心的方案是让运维把接口也切到 HTTPS或者至少做一层代理把外部 HTTP 接口统一代理成 HTTPS 站内路径。如果实在两者都不行可以考虑在后端定时把第三方 HTTP 数据拉下来缓存成自己的 API前端只访问自家 HTTPS 接口。最后分享一个我自己用顺手的排查套路。遇到任何跨域问题第一件事永远是在 Network 面板里确认请求到底发出没有、响应头是什么样的而不是急着改前端代码。请求没发出检查前端代码和代理配置请求发出但响应没有 CORS 头找后端请求发出响应头也带 CORS 头但还是报错那就去看 Allow-Origin 是否匹配、Allow-Headers 是否覆盖了前端实际发送的请求头。这套流程走一遍百分之九十的跨域问题都能在三五分钟内定位。剩下那百分之十大多藏在缓存、代理链路过长、或者证书协议不统一这些看起来和跨域无关的角落里但只要你手里握着请求链路和响应头的快照即使一时看不出来也能把信息整理得清清楚楚拿给同事或者运维看他们一眼就能明白问题出在哪。