ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSON.parse报错终结指南:彻底解决Unexpected end与token u

JSON.parse报错终结指南:彻底解决Unexpected end与token u 1. 先搞懂这两个报错到底在说什么如果你跟 JSON 打过交道肯定见过这两张“熟面孔”SyntaxError: Unexpected end of JSON input SyntaxError: Unexpected token u in JSON at position 0一个是“JSON 输入意外结束”一个是“在位置 0 出现意外的标记 u”。很多新手第一次看到token u时一脸懵这个u到底是什么为什么它出现在位置 0我当初第一次见到这俩报错时也是在控制台里反复打印数据折腾了小半天才反应过来。先说结论两个报错的根源其实非常接近都是JSON.parse()拿到了“不该拿到的内容”。Unexpected end of JSON input通常意味着你传给JSON.parse()的是一个空字符串或者一个只有空白字符的字符串。Unexpected token u in JSON at position 0则几乎可以断定你传给JSON.parse()的是undefined字符串化后就是undefined而JSON.parse()解析到第一个字母u就知道这不是合法 JSON直接原地报错。这两个错误高发在前后端数据交互、本地存储读取、文件配置解析、Node.js 脚本处理数据这些场景里。尤其是前端接接口时后端返回一个 200 但 body 为空前端顺手JSON.parse(await res.text())立刻就被“教育”了。这篇文章我不会只甩给你两三行 try-catch而是把这两个报错背后的原理、常见的触发场景、真正的排查思路和一套可以抄作业的封装方案一次性讲清楚。无论你是用 fetch、axios、Node 的 fs 模块还是在 Vue 或 React 项目里接接口大概率都能从这里找到对应的解法。2. 从底层原理看 JSON.parse 为什么会翻车2.1 JSON.parse 对输入类型的“强制转换”陷阱很多人以为JSON.parse()只会被动地解析字符串但 ECMAScript 规范里JSON.parse的第一步其实是对传入参数执行ToString(value)也就是说如果你传的是undefined它会被转成字符串undefined然后词法解析器扫描到第一个字符u发现这既不是{、[也不是合法数字或字符串字面量于是抛出SyntaxError: Unexpected token u in JSON at position 0。注意报错里很贴心地告诉你是位置 0因为问题就出在第一个字符。那如果传入的是空字符串呢本身是合法字符串但 JSON 文本的语法要求必须有内容空文本不是合法的 JSON 值。所以解析器扫描完之后发现什么都没读到就报Unexpected end of JSON input。如果你传入的字符串是多个空格比如 同理也会报这个错因为在跳过空白后依旧没有遇到任何有效 token 就结束了。还有一个容易被忽略的坑JSON.parse(null)其实不会报错结果返回null因为String(null)是null这个是合法 JSON 字面量。JSON.parse(false)、JSON.parse(123)也都能正常解析。真正危险的是undefined、function(){}、Symbol()这些转换后不是合法 JSON 的值。我见过有人写JSON.parse(JSON.stringify(data))时如果data本身是undefinedJSON.stringify(undefined)返回undefined而不是字符串于是变成JSON.parse(undefined)报错就来了。2.2 报错信息里的“位置 0”意味着什么at position 0是 V8 引擎Chrome 和 Node.js 的默认 JS 引擎给出的定位信息。它表示解析器在扫描到第 0 个字符时发现了无法识别的 token。这个信息很有用因为它提醒你问题发生在字符串的最开头而不是中间某个地方。所以当你看到Unexpected token u in JSON at position 0时基本不用去想是不是字符串中某个字符写错了——除非你看到的是position 1234之类的那才需要考虑是不是字符串中间被截断了。如果你看到的是Unexpected token o in JSON at position 1那大概率是解析了一个对象{}因为String({})得到[object Object]第 0 位是[第 1 位是o。很多人在控制台手滑写了JSON.parse(某个对象)就会看到这种报错。理解了这层关系以后看到任何Unexpected token X先想想X是由什么类型的值转换来的排查方向就清晰了。2.3 “解析空响应”为什么成了重灾区除了直接调用 JSON.parse 传错参数最常见的情况是后端接口返回的 body 是空的。比如用res.json()去解析一个没有返回体的 204 响应或者网关超时返回了一个空的 HTML 页面浏览器实际拿到的文本是res.json()内部其实就是先读取文本再执行JSON.parse(text)于是触发Unexpected end of JSON input。更隐蔽的是后端返回了null这种字符串解析出来结果是null不报错但逻辑往下走容易出问题。还有返回undefined字符串的接口前端JSON.parse(undefined)就会报Unexpected token u。有些后端框架在异常时会把对象转成{error: ...}并返回 200前端解析不报错但要访问data.list时才发现list不存在这类问题往往比语法错误更磨人。3. 分场景的解决方案与安全解析封装3.1 基础防御调用前先做类型与内容检查其实大多数情况下你不需要升级什么高级库只需在调用JSON.parse前检查一下参数是不是一个“看起来像 JSON 的字符串”。最简单的做法是function safeJsonParse(str, fallback null) { if (typeof str ! string) { return fallback; } // 去掉首尾空白后判断是否为空字符串 const trimmed str.trim(); if (!trimmed) { return fallback; } // 不是字符串字面量时必须保证以 { 或 [ 开头也可以是数字、true、false、null try { return JSON.parse(trimmed); } catch (e) { console.warn(JSON parse failed:, e.message); return fallback; } }这段逻辑覆盖了两种报错的主要源头undefined非字符串类型和空字符串。typeof str ! string直接拦截了undefined、null、对象、数字等类型。注意我这里把trimmed判空放在 try 外面因为空字符串的JSON.parse()本身就会抛错先拦下来能少走一层异常。但要注意如果字符串是123或者trueJSON.parse也能正常解析所以我没有强制要求必须以{或[开头但如果你预期的数据永远是对象或数组也可以加上这个判断来提前暴露问题。有人会问fallback参数设成null还是{}更合适我的习惯是解析函数返回类型应该与预期一致。如果业务代码期望返回一个对象那么默认值最好给一个空对象{}否则拿到null后你还要再判断一次。但如果你需要区分“解析失败”和“解析成功但值为 null”那 fallback 可以给一个独特的哨兵值比如Symbol(parse_failed)然后在调用处判断是否等于这个哨兵。3.2 处理 fetch 响应时不要盲信 res.json()用fetch请求接口时很多人喜欢这样写const res await fetch(/api/user); const data await res.json();这段代码在上游接口不稳定的时候非常容易炸。因为res.json()遇到 204 No Content、502 Bad Gateway 返回的 HTML 页面、或者是空 body 时都会因为内部尝试解析空文本而抛出Unexpected end of JSON input。正确的姿势是先把响应文本读出来看看它到底是不是 JSON再决定是否解析const res await fetch(/api/user); // 1. 先检查 HTTP 状态 if (!res.ok) { throw new Error(Request failed with status ${res.status}); } // 2. 读取文本内容 const text await res.text(); // 3. 如果接口约定返回 JSON但 text 为空或非 JSON记录下来而不是抛给用户 let data null; try { data text ? JSON.parse(text) : null; } catch (e) { console.error(API response is not valid JSON:, text); throw new Error(接口返回数据格式异常); } // 4. 继续业务逻辑 console.log(data);这里res.text()永远不会因为响应体内容问题而报错它只负责把 body 内容读成字符串。拿到text之后你可以加日志、可以trim()、可以做二次处理自由度比直接res.json()大得多。尤其在做接口联调的时候能看到“接口原样返回的文本”比屏幕上只有一个泛白的错误要直观得多。我通常会在调试阶段把text直接console.log出来很多时候问题根本不在 JSON.parse而是后端返回了Internal Server Error之类的纯文本。3.3 axios 场景下拦截器如何统一兜底axios 和 fetch 不太一样axios 在默认情况下会根据Content-Type自动把响应数据解析成对象如果响应头是application/json它底层也调用了JSON.parse所以空响应或非 JSON 响应同样会抛错。在你的项目里最好封装一个统一的响应拦截器把解析和数据校验都放在一个地方import axios from axios; const http axios.create({ baseURL: /api, timeout: 10000, }); http.interceptors.response.use( (response) { // 这里拿到的 response.data 已经是解析后的对象或数组 // 但为了保险仍然可以检查一下 const data response.data; if (typeof data string) { // 如果后端偶尔返回字符串说明可能走了非 JSON 分支 try { return JSON.parse(data); } catch (e) { return Promise.reject(new Error(后端返回了非 JSON 内容)); } } return data; }, (error) { // 如果请求本身失败了网络错误、超时、5xx // error.response 可能是 undefined不要直接访问 data if (error.response error.response.data) { console.error(Response data:, error.response.data); return Promise.reject(new Error(请求失败状态码${error.response.status})); } return Promise.reject(new Error(网络异常或请求超时)); } ); export default http;这样封装之后业务代码里再调用http.get(/user)拿到的data一定是解析好的对象或数组不会在业务代码层突然冒出一个JSON.parse的报错。很多团队问“为什么我的线上项目报了 SyntaxError 还定位不到代码”原因就是把JSON.parse撒得到处都是出了错根本不知道是哪个接口、哪一行干的。统一拦截器 拦截器内 try/catch 打日志能让你少熬夜。3.4 Node.js 中读取并解析文件时的防御在 Node 环境里也经常遇到这两个错误。最常见的场景是用fs.readFileSync读取 JSON 配置文件然后直接JSON.parse但这个文件可能因为编辑器保存了空内容或者编码不对导致读出来是乱码甚至文件里有多余的 BOM 头。const fs require(fs); const path require(path); function readJsonFile(filePath) { const raw fs.readFileSync(filePath, utf-8); // 去掉 BOM 头常见于 Windows 下用记事本保存的 UTF-8 文件 const noBom raw.replace(/^\uFEFF/, ); // 去掉首尾空白后再判断 const trimmed noBom.trim(); if (!trimmed) { throw new Error(配置文件为空: ${filePath}); } try { return JSON.parse(trimmed); } catch (e) { // 可以尝试定位是哪个文件、哪一段内容出了问题 const preview trimmed.slice(0, 200); throw new Error(解析 JSON 配置失败: ${filePath}内容预览: ${preview}); } }为什么先去掉 BOM 头因为有些 JSON 文件开头带着\uFEFF这个字符对 JSON 解析器来说是无效 token会直接导致Unexpected token \uFEFF或者位置 0 相关的报错。虽然你的问题标题是Unexpected token u但实际开发中 BOM 头引发的报错也极其常见所以我在写文件读取工具时都会顺手处理。还有一点如果用JSON.parse(raw)时文件内容带着注释比如很多前端配置文件允许//注解但标准 JSON 不允许这个时候你需要在解析前自己剥离注释或者换用JSON5这类库。3.5 在 Vue / React 中应对接口返回的即时处理前端框架中容易出现这个问题的地方往往不是主动调用JSON.parse而是从本地存储、URL 参数、事件回调里拿数据后直接解析。比如从localStorage读取数据const cached localStorage.getItem(user_info); const user JSON.parse(cached); // 如果缓存被清空或不存在这里立刻炸localStorage.getItem在键不存在时会返回null而JSON.parse(null)虽然不报错但会让很多人误以为都 OK。更危险的是有些用户手动改了 localStorage 的值存了一个undefined进去。所以从存储中读取时一定要做同样的安全解析。再比如 Vue 3 项目里很多人把JSON.parse直接写在setup里的响应式数据初始化逻辑中const userInfo ref(JSON.parse(localStorage.getItem(user_info) || {}));这个写法在大多数情况下没事但如果localStorage里的值恰好是null那么userInfo就变成null页面模板中userInfo.name就会报Cannot read properties of null。如果把这段逻辑抽成一个useSafeLocalStorage的组合式函数统一处理取默认值、异常捕获、类型校验以后维护起来会轻松很多import { ref, watch } from vue; export function useSafeLocalStorage(key, defaultValue) { const data ref(defaultValue); try { const stored localStorage.getItem(key); if (stored ! null stored.trim() ! ) { const parsed JSON.parse(stored); data.value parsed; } } catch (e) { console.warn(读取 localStorage 失败键${key}, e.message); data.value defaultValue; } watch(data, (val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) { console.warn(写入 localStorage 失败, e.message); } }, { deep: true }); return data; }在 React 里逻辑是一样的可以用一个自定义 Hook 把这段安全解析逻辑封装好。重点不是代码长短而是让“读写本地存储”这个操作永远不抛出 JSON 解析错误。4. 真实案例排查过程实录4.1 案例一后端返回 200但 body 为空前端一直报 Unexpected end有个朋友的项目遇到过这么一个问题登录接口偶尔会 200但用户信息接口返回空 body。前端用fetch拿到res.ok true然后直接res.json()结果时好时坏随机报Unexpected end of JSON input。排查的时候他一度怀疑是浏览器缓存问题换了无痕模式还是有。后来我让他把响应文本打印出来把所有res.json()改成res.text()先看内容。抓了几次之后发现当接口成功时返回的是{id:123}但当后端服务内部某个缓存过期时会返回一个 200 且 body 为空的响应。这就是典型的“HTTP 状态码无法代表业务成功”的坑。解决方案是在fetch后先读文本然后判断text.length 0时走异常分支并提示“服务暂时不可用”。同时我们还在后端网关层面加了一个“空 body 不返回 200”的规则但这个碰不到后端代码时前端防御就是最后一道防线。这个案例里最值得记录的不是代码而是排查方法——先看到原始数据再谈解析逻辑。4.2 案例二请求拦截器里的空数组被转换成了 undefined另一个故事来自一个 Node.js 脚本需要从外部接口拉取一批用户 ID然后批量处理。代码大概是这样const res await fetch(https://api.example.com/users); const data await res.json(); const ids data.users.map(u u.id);某个用户组的数据为空后端响应的结构是{users: []}但脚本却报了Cannot read properties of undefined (reading map)不是 JSON 解析错误。后来发现是流程中有一段JSON.parse(JSON.stringify(data))当传入的是undefined时JSON.stringify返回的是undefined而JSON.parse(undefined)就直接抛了Unexpected token u in JSON at position 0。因为它报错的地方正好在一个 try/catch 里异常又被吞掉了外层代码拿到的是一个undefined再往下.map就炸了。这类错误最坑的地方在于真正的报错被 try/catch 静默吞掉然后在很后面才以一个看似无关的错误暴露出来。所以排查时如果发现Unexpected token u被 try/catch 吞了最好在 catch 里至少打一条console.error并且把原始值也打印出来。吞异常相当于蒙住眼睛走路省一时麻烦欠一身债。4.3 案例三JSON 数据里混入了“undefined”文本还有一个场景不常见但很经典后端在拼接 JSON 字符串时用的是模板字符串而非 JSON 序列化工具。比如// 伪代码 return {name: ${username}, age: ${age}};当username是undefined时生成出来的字符串就是{name: undefined, age: 18}。前端JSON.parse解析时会在undefined的u位置报错——但请注意这个报错信息可能是Unexpected token u in JSON at position 10因为前面有{name:这些字符占位。这与标题里说的“position 0”不完全一样但本质上都是undefined混入了 JSON 文本所以一定要学会看错误信息里的具体位置而不是只记一个关键词。这类问题只能通过规范后端的序列化方式来解决比如统一使用JSON.stringify禁止手拼 JSON。前端遇到这种报错时可以把原始响应文本打印出来一眼就能看到哪个字段的值是undefined。排查多了你会发现很多 JSON 问题本质上是“某个人在某个地方偷懒手拼了 JSON”。4.4 浏览器控制台与断点调试的实战技巧如果错误发生在自己的代码里直接在报错行打断点然后看调用栈里的参数值一般几秒就能定位。但如果是经过多个 Promise、多个封装函数之后才报的错建议在断点处右键选择Add script breakpoint或者在报错信息左边点击行号打断点然后看Scope面板中str、text或response变量的值。另一个非常实用的小技巧是在报错之前主动触发一个全局日志比如在fetch封装内部加一个console.debug([response], url, status, text)。这行日志平时用调试级别打印不污染控制台一旦线上出问题可以通过采样开关打开调试日志。很多团队用 Sentry 之类的工具也别忘了把text作为额外上下文上报这样问题定位会快得多。5. 避坑经验与团队协作里的统一约定5.1 建立“不信任外部输入”的编码习惯在写任何涉及数据解析的代码时我心里默认一个原则外部输入接口、存储、文件、用户输入都是不可信的。JSON.parse本身是一个非常严格的解析器它不允许任何不合法的 JSON 文本进入。你没有办法改变这个行为只能在上游多加一层防线。这条防线应当包含三个动作检查类型必须是字符串杜绝undefined、对象、数组等直接传入。检查内容非空、去空白、必要时检查首字符是否为{或[。兜底解析try/catch 捕获异常并向上传递有意义的信息而不是让原始错误裸奔。你可以把这套封装成一个工具函数放到公共代码库团队成员直接引用而不是各写各的。如果项目里已经写了很多处裸调JSON.parse建议用正则搜索慢慢替换。不要想着一口气改完改一处测一处重点优先接口层和存储层。5.2 日志里永远打印“原始文本”而不是只打印解析结果很多排查困难的根源是日志不够。比如只打印error.message你只知道“JSON parse failed”但不知道解析的原始文本是什么。正确的做法是像下面这样try { return JSON.parse(text); } catch (e) { console.error(JSON.parse error:, e.message, | input:, text); return fallback; }打印原始文本时要注意长度如果文本太长截取前 500 个字符就够了否则日志会爆炸。另外注意不要打印包含用户敏感信息的完整响应体如果需要排查可以先脱敏。这个习惯能让你在拿到线上日志时分分钟定位到是哪个接口返回了空值不用再去猜。5.3 String 和 Number 字段的溢出陷阱虽然Unexpected token u看起来只是类型问题但在 JSON 数据从后端传到前端的过程中还有一个容易忽略但很常见的陷阱大整数。比如后端返回{id: 4824834180805116928}前端JSON.parse解析结果是4824834180805116900精度丢失。这种情况下你不会收到任何 SyntaxError因为它是合法的 JSON 数字但数据已经变了。我们讨论的 JSON 解析错误本质上是对“数据格式异常”的兜底而大整数属于“数据格式正确但语义失真”的问题两者都值得警惕。如果你的项目有这种超长数字后端应该把 ID 转成字符串或者在领域模型层对长整型字段做特殊处理。用JSON.parse的第二个参数reviver也可以对某个 key 做特殊转换但性能会有一定损耗不建议全局用。只有在明确知道哪些字段可能是大整数时才在 reviver 里做针对性处理。5.4 相关常见报错的区分不要把“invalid or unexpected token”也混进来有时候你会在控制台看到SyntaxError: Invalid or unexpected token这和Unexpected token u虽然类似但原因完全不同。Invalid or unexpected token通常是普通 JS 语法错误比如代码中出现了非法字符、中文标点、或者字符串引号没闭合。你在 Vue 模板中写表达式时手滑打了全角空格也容易报这个错误。这类问题不属于JSON.parse的锅排查时看代码编辑器里的波浪线即可。还有Uncaught SyntaxError: Unexpected token 多半是请求返回了 HTML 页面比如错误页、登录页却被当成 JS 解析了前端拿到 HTML 第一行是!DOCTYPE html所以就成了第一个非法 token。这个错误和Unexpected token u的排查思路一致先看原始响应内容再决定是不是 JSON 解析问题。尤其是部署前端项目时如果路由是 history 模式而不是 hash 模式后端没配置 fallback刷新页面就可能返回 404 HTML前端 JS 加载时就会报这样的错误。这里的解决方向是让后端把所有非接口路由都指向index.html而不是在前端胡乱 try/catch。5.5 善用格式化工具和库降低手动解析率如果你经常跟复杂的 JSON 调试打交道建议在编辑器里装上 JSON 格式化插件比如 VS Code 自带的功能就够用而在代码里遇到“JSON 格式不固定”的场景可以考虑使用JSON5或yaml这类更宽容的格式库。例如有些配置文件里允许注释、尾逗号、单引号标准JSON.parse完全不兼容而JSON5可以解析大部分此类内容使用JSON5.parse就不会报那两个经典错误。不过我要提醒一下不要动不动就把JSON5或第三方库塞进项目。如果接口的数据来源完全受控标准 JSON 就够用引入额外依赖反而可能带来解析行为不一致的问题。前端项目里我建议只在“本地配置文件”“脚手架模板”这种非严格标准 JSON 的场景使用JSON5。当你在服务端接口和客户端之间传输数据时永远使用标准JSON.parse并通过统一的封装函数保证安全。6. 自己动手封装一个团队通用的 safeJsonParse最后分享一个我在团队里常用的完整版安全解析工具它兼顾了类型检查、空值处理、错误日志和可选项校验你可以直接复制到项目里按需修改/** * 安全解析 JSON 字符串 * param {any} value - 待解析的数据可以是任意类型 * param {object} options * param {*} [options.defaultValuenull] - 解析失败时返回的默认值 * param {boolean} [options.logtrue] - 是否打印错误日志 * param {function} [options.validator] - 校验解析结果的函数返回 false 则使用 defaultValue * returns {*} */ function safeJsonParse(value, options {}) { const { defaultValue null, log true, validator null } options; // 1. 将输入规范化为字符串 let str; if (typeof value string) { str value; } else { // 不是字符串尝试用 JSON.stringify 转成 JSON 字符串 // 注意 JSON.stringify(undefined) 返回 undefined不是字符串 try { str JSON.stringify(value); } catch (e) { if (log) console.warn(safeJsonParse: stringify failed, e.message); return defaultValue; } } // 2. 如果 stringify 返回 undefined说明值本身就是 undefined 或函数等 if (typeof str ! string) { if (log) console.warn(safeJsonParse: value cannot be stringified, got, value); return defaultValue; } // 3. 判断空字符串 const trimmed str.trim(); if (!trimmed) { if (log) console.warn(safeJsonParse: input is empty); return defaultValue; } // 4. 真实解析 try { const result JSON.parse(trimmed); // 5. 如果调用方定义了校验函数则执行校验 if (typeof validator function !validator(result)) { if (log) console.warn(safeJsonParse: validation failed, result); return defaultValue; } return result; } catch (e) { if (log) console.warn(safeJsonParse: parse error, e.message, | input:, trimmed.slice(0, 200)); return defaultValue; } } // 使用示例 const user safeJsonParse(localStorage.getItem(user), { defaultValue: { name: guest, roles: [] }, validator: (val) val ! null typeof val object });这个工具函数有几个细节值得说一下。JSON.stringify(value)这一步是为了兼容某些场景下你错误地传入了对象或数组。比如你本来想传{a:1}结果手滑传了{a:1}这里会自动帮你转成 JSON 字符串再解析虽然它不会报错但我不建议你依赖这个行为因为格式不确定时静默的转换反而会掩盖问题。所以在团队里我会强调接口响应永远传字符串本地测试数据也尽量用字符串字面量。validator参数是很多人容易忽略的加分项。它能让你在解析之后立即校验关键字段而不是等业务逻辑跑起来再报错。比如一个用户信息接口你至少可以校验typeof result.id number不符合预期就返回默认值。这样代码的可维护性会高不少因为你在数据进入系统的边界处就卡住了异常而不是让脏数据在整个应用里流转。我在实际项目里一般在三层地方调用这个函数浏览器 localStorage 读写、fetch/axios 的响应拦截器、Node.js 的配置文件读取。每一层都只负责“拿到原始数据 - 安全解析 - 提供干净的数据结构”后面的业务代码完全不用关心底层数据是怎么来的。这样既不会出现Unexpected end of JSON input到处乱炸也能让你把精力集中在业务逻辑而不是解析异常上。7. 写在最后的一点经验聊了这么多你会发现SyntaxError: Unexpected end of JSON input和Unexpected token u都不是什么高深的问题说白了就是给了JSON.parse一个空字符串或undefined。但这类报错之所以让人头疼是因为它们经常藏着“上游数据异常”的线索而你手头只有一句干巴巴的错误信息不知道那句话是谁传来的、什么时候传来的。我自己的体会是解决这类问题不能只靠 try/catch 把异常吞掉而是要追到数据源头去看一看到底是谁生产了非法 JSON。哪怕这次只是本地一个变量写错了下一次可能就是一个第三方接口返回了空 body。把安全解析封装成工具函数、在拦截器里统一处理日志、排查时先打印原始文本只要养成这三个习惯你在 JSON 相关 bug 上花费的时间能少掉一半以上。最后再分享一个小技巧如果你在开发时经常要手测后端返回的 JSON 是否正确不要只盯着浏览器 Network 面板看。可以直接在控制台用fetch请求接口然后用res.text()拿到原始文本并右键格式化或者把文本贴进任意 JSON 格式化工具里检查。这样测试出来的结果更干净不会被浏览器的自动解析或代理插件干扰。等你把“数据源头是否合法”确认了再回到代码里看JSON.parse方向就不会偏。
返回列表