用JSONCPP全案例:編譯、中文解析與防亂碼實(shí)戰(zhàn))
簡介這份資源面向仍在使用 Visual C 6.0 的開發(fā)者尤其是需要在老項(xiàng)目中處理 JSON 數(shù)據(jù)、又常被中文亂碼困擾的 C 程序員。它以 jsoncpp-src-0.5.0 源碼為基礎(chǔ)演示了在 VC6.0 中集成 JSONCPP 并正確解析、生成含中文字符 JSON 的完整案例屬于可直接復(fù)用的實(shí)戰(zhàn)型參考。壓縮包共 72 個(gè)文件約 3.77MB包含 24 個(gè) h 頭文件、9 個(gè) cpp 源文件、6 個(gè) inl 內(nèi)聯(lián)實(shí)現(xiàn)文件以及 dsp、dsw 工程文件、doc 使用說明、txt 必看提示和已編譯的 exe 可執(zhí)行文件覆蓋從源碼到可運(yùn)行工程的完整鏈路。已有 590 人學(xué)習(xí)下載。讀者可據(jù)此掌握源碼集成方式、UTF-8 編碼配置與中文防亂碼處理思路并借助說明文檔和測試案例快速驗(yàn)證解析與序列化結(jié)果減少在老版本編譯環(huán)境中踩坑的時(shí)間。1. 老編譯器遇上新數(shù)據(jù)格式VC6.0 調(diào) JSONCPP 到底卡在哪如果你手里維護(hù)著一套 VC6.0 的工控上位機(jī)、老 MFC 工具或者產(chǎn)線測試軟件最近突然被要求“把配置改成 JSON 下發(fā)”“把日志輸出成 JSON 給新平臺”那你大概率會經(jīng)歷一次血淚經(jīng)驗(yàn)VC6.0 的 C 標(biāo)準(zhǔn)庫殘缺得讓人懷疑人生而 JSONCPP 這種現(xiàn)代 C 庫默認(rèn)寫法根本喂不進(jìn)去。標(biāo)題里說的“VC6.0 調(diào)用 JSONCPP 全案例”核心要解決的就是三件事——讓 JSONCPP 在 VC6.0 下編譯通過、能正確解析帶中文的 JSON 字符串、并且不出現(xiàn)亂碼。這不是一個(gè)“隨便找個(gè)庫 include 一下”的活VC6.0 對模板、命名空間、std::string的實(shí)現(xiàn)都有坑JSONCPP 的版本選擇、字符集設(shè)置、編碼轉(zhuǎn)換每一步都能讓你翻車。適合誰看還在用 VC6.0 維護(hù)存量項(xiàng)目、又必須對接 JSON 接口的嵌入式/工控/桌面端開發(fā)者。下面我按“先跑通最小解析、再處理中文、最后避坑”的順序把可復(fù)現(xiàn)的路徑講清楚。2. 選對 JSONCPP 版本并讓 VC6.0 編譯通過2.1 為什么不能直接拿最新版 JSONCPP 塞進(jìn) VC6.0JSONCPP 從 0.x 到 1.x 的演進(jìn)過程中大量使用了 VC6.0 不支持的 C 特性std::auto_ptr的替代、nullptr、變參模板、std::unique_ptr、以及更嚴(yán)格的std::string接口。VC6.0 的 STL 是 1998 年之前的實(shí)現(xiàn)std::string沒有c_str()的 const 重載問題、沒有std::istringstream的完整支持模板偏特化也殘缺。常見做法是選 JSONCPP 0.6.0-rc2 或 0.5.0 這類老版本它們對 C98 的依賴更保守源碼里沒有nullptr和變參模板。我一般會直接找 0.6.0-rc2 的源碼包因?yàn)樗膉son_value.cpp、json_reader.cpp、json_writer.cpp三個(gè)文件在 VC6.0 下改動量最小。不要用 1.x哪怕你手動改代碼Json::Value內(nèi)部的std::map迭代器在 VC6.0 下也會因?yàn)閟td::map的erase返回值問題編譯失敗。2.2 把 JSONCPP 源碼加入 VC6.0 工程的步驟先建一個(gè) Win32 Console Application 空工程把 JSONCPP 的include/json目錄和src/lib_json下的.cpp文件加進(jìn)來。VC6.0 的工程文件是.dsp直接 Add Files to Project 即可。關(guān)鍵一步在Project Settings - C/C - Preprocessor里加上JSONCPP_NO_STDINT_H和_MBCS。JSONCPP_NO_STDINT_H是因?yàn)?VC6.0 沒有stdint.hJSONCPP 會嘗試用__int64替代_MBCS是讓 JSONCPP 內(nèi)部按多字節(jié)字符集處理避免它默認(rèn)走 Unicode 寬字符路徑。然后需要手動改幾處源碼json_value.cpp里所有std::auto_ptr改成裸指針加手動 deletejson_reader.cpp里std::istringstream換成std::stringstream并補(bǔ)#include sstream。下面是一個(gè)最小可編譯的工程配置片段放在stdafx.h之后// VC6.0 下 JSONCPP 最小配置頭文件 #define JSONCPP_NO_STDINT_H // 禁用 stdint.h 依賴 #define _MBCS // 多字節(jié)字符集中文解析基礎(chǔ) #include string #include map #include vector #include sstream // VC6.0 需手動補(bǔ)否則 json_reader 報(bào)錯 #include json/json.h // 路徑按實(shí)際 include 目錄調(diào)整 // 注意VC6.0 的 std::string 沒有 c_str() 的 const 重載問題 // 但 JSONCPP 內(nèi)部調(diào)用 c_str() 時(shí)可能返回非 const需在 json_value.cpp 里 // 把 const char* 強(qiáng)轉(zhuǎn)去掉 const否則編譯報(bào) C2440。邏輯說明JSONCPP_NO_STDINT_H讓 JSONCPP 用__int64定義Json::Int64VC6.0 能識別_MBCS保證Json::Value::asString()返回的是多字節(jié)字符串而不是寬字符。參數(shù)說明如果你的工程已經(jīng)用了_UNICODE必須改成_MBCS否則 JSONCPP 內(nèi)部std::string和std::wstring混用會直接編譯失敗。改完源碼后編譯順序建議先單獨(dú)編譯json_value.cpp再編譯json_reader.cpp最后編譯json_writer.cpp這樣報(bào)錯定位更準(zhǔn)。2.3 最小解析案例從字符串到 Json::Value編譯通過后先跑一個(gè)不涉及中文的解析確認(rèn)庫本身能用。新建一個(gè)main.cpp寫入以下代碼#include stdafx.h #include json/json.h #include iostream int main(int argc, char* argv[]) { // 構(gòu)造一個(gè)簡單 JSON 字符串注意轉(zhuǎn)義 const char* szJson {\name\:\test\,\value\:123,\flag\:true}; Json::Value root; Json::Reader reader; // parse 返回 bool失敗時(shí)可通過 reader.getFormatedErrorMessages() 看原因 if (!reader.parse(szJson, root)) { std::cout parse error: reader.getFormatedErrorMessages() std::endl; return -1; } // 讀取字段asString 默認(rèn)返回 std::string std::string name root[name].asString(); int value root[value].asInt(); bool flag root[flag].asBool(); std::cout name name , value value , flag flag std::endl; return 0; }邏輯說明Json::Reader::parse接受const char*和Json::Value解析成功返回 true。root[name]返回Json::Value引用asString()內(nèi)部調(diào)用std::string構(gòu)造。參數(shù)說明szJson里的雙引號必須轉(zhuǎn)義VC6.0 對字符串字面量長度有限制超過 16KB 的 JSON 建議用文件讀取。如果parse失敗getFormatedErrorMessages()會返回帶行號的錯誤信息但 VC6.0 下這個(gè)函數(shù)返回的std::string可能因?yàn)閟td::ostringstream問題顯示不全可以改用reader.getFormatedErrorMessages().c_str()打印。這一步跑通后說明 JSONCPP 在 VC6.0 下已經(jīng)能用了接下來處理中文。3. 中文解析與防亂碼從字節(jié)流到正確顯示3.1 中文亂碼的根因VC6.0 的字符集與 JSONCPP 的編碼假設(shè)VC6.0 默認(rèn)使用多字節(jié)字符集MBCS中文 Windows 下代碼頁是 GBK936。JSONCPP 內(nèi)部把字符串當(dāng)作字節(jié)流處理Json::Value::asString()返回的std::string里存的是原始字節(jié)。如果你從網(wǎng)絡(luò)或文件讀到的 JSON 是 UTF-8 編碼直接asString()得到的是 UTF-8 字節(jié)而 VC6.0 的std::cout或 MFC 控件按 GBK 解釋就會顯示亂碼。反過來如果你把 GBK 字符串直接塞進(jìn)Json::Value再toStyledString()輸出JSONCPP 不會做任何轉(zhuǎn)碼輸出到 UTF-8 環(huán)境也會亂。所以“防亂碼”的本質(zhì)是在 JSONCPP 邊界做一次編碼轉(zhuǎn)換要么統(tǒng)一轉(zhuǎn)成 GBK 再顯示要么統(tǒng)一轉(zhuǎn)成 UTF-8 再傳輸。我一般會在解析后立刻把std::string從 UTF-8 轉(zhuǎn)成 GBK寫入時(shí)再轉(zhuǎn)回 UTF-8。3.2 手寫 UTF-8 與 GBK 互轉(zhuǎn)函數(shù)VC6.0 可用VC6.0 沒有std::codecvt但可以用 Windows APIMultiByteToWideChar和WideCharToMultiByte做中轉(zhuǎn)。下面兩個(gè)函數(shù)放在stdafx.h之后直接調(diào)用#include windows.h #include string // UTF-8 轉(zhuǎn) GBK std::string Utf8ToGbk(const std::string strUtf8) { if (strUtf8.empty()) return ; // 先 UTF-8 轉(zhuǎn)寬字符 int nWideLen MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), -1, NULL, 0); if (nWideLen 0) return ; wchar_t* pWide new wchar_t[nWideLen 1]; MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), -1, pWide, nWideLen); pWide[nWideLen] L\0; // 寬字符轉(zhuǎn) GBK int nGbkLen WideCharToMultiByte(CP_ACP, 0, pWide, -1, NULL, 0, NULL, NULL); if (nGbkLen 0) { delete[] pWide; return ; } char* pGbk new char[nGbkLen 1]; WideCharToMultiByte(CP_ACP, 0, pWide, -1, pGbk, nGbkLen, NULL, NULL); pGbk[nGbkLen] \0; std::string strGbk(pGbk); delete[] pWide; delete[] pGbk; return strGbk; } // GBK 轉(zhuǎn) UTF-8 std::string GbkToUtf8(const std::string strGbk) { if (strGbk.empty()) return ; int nWideLen MultiByteToWideChar(CP_ACP, 0, strGbk.c_str(), -1, NULL, 0); if (nWideLen 0) return ; wchar_t* pWide new wchar_t[nWideLen 1]; MultiByteToWideChar(CP_ACP, 0, strGbk.c_str(), -1, pWide, nWideLen); pWide[nWideLen] L\0; int nUtf8Len WideCharToMultiByte(CP_UTF8, 0, pWide, -1, NULL, 0, NULL, NULL); if (nUtf8Len 0) { delete[] pWide; return ; } char* pUtf8 new char[nUtf8Len 1]; WideCharToMultiByte(CP_UTF8, 0, pWide, -1, pUtf8, nUtf8Len, NULL, NULL); pUtf8[nUtf8Len] \0; std::string strUtf8(pUtf8); delete[] pWide; delete[] pUtf8; return strUtf8; }邏輯說明MultiByteToWideChar第一個(gè)參數(shù)CP_UTF8表示源編碼是 UTF-8CP_ACP表示當(dāng)前系統(tǒng) ANSI 代碼頁中文 Windows 下是 GBK。-1表示字符串以 null 結(jié)尾函數(shù)會自動計(jì)算長度。參數(shù)說明nWideLen和nGbkLen是轉(zhuǎn)換后的字符數(shù)分配時(shí)加 1 給結(jié)尾符。注意 VC6.0 的new wchar_t[n]在 n 很大時(shí)可能失敗建議對超過 1MB 的字符串分塊處理。這兩個(gè)函數(shù)沒有依賴任何 C11 特性VC6.0 直接編譯通過。3.3 解析中文 JSON 并正確輸出的完整案例假設(shè)你從文件讀到一個(gè) UTF-8 編碼的 JSON內(nèi)容包含中文姓名和地址。用以下代碼解析并轉(zhuǎn)成 GBK 輸出到控制臺#include stdafx.h #include json/json.h #include fstream #include iostream #include string // 聲明前面寫的轉(zhuǎn)換函數(shù) std::string Utf8ToGbk(const std::string strUtf8); std::string GbkToUtf8(const std::string strGbk); int main(int argc, char* argv[]) { // 讀取 UTF-8 JSON 文件 std::ifstream ifs(data.json, std::ios::binary); if (!ifs.is_open()) { std::cout open file failed std::endl; return -1; } std::string strJson((std::istreambuf_iteratorchar(ifs)), std::istreambuf_iteratorchar()); ifs.close(); Json::Value root; Json::Reader reader; if (!reader.parse(strJson, root)) { std::cout parse error: reader.getFormatedErrorMessages() std::endl; return -1; } // 讀取中文字段此時(shí)是 UTF-8 字節(jié) std::string strNameUtf8 root[name].asString(); std::string strAddrUtf8 root[address].asString(); // 轉(zhuǎn)成 GBK 再輸出避免控制臺亂碼 std::string strNameGbk Utf8ToGbk(strNameUtf8); std::string strAddrGbk Utf8ToGbk(strAddrUtf8); std::cout name: strNameGbk std::endl; std::cout address: strAddrGbk std::endl; // 如果要寫回 JSON先把 GBK 轉(zhuǎn)回 UTF-8 Json::Value outRoot; outRoot[name] GbkToUtf8(strNameGbk); outRoot[address] GbkToUtf8(strAddrGbk); Json::StyledWriter writer; std::string strOut writer.write(outRoot); std::ofstream ofs(out.json, std::ios::binary); ofs.write(strOut.c_str(), strOut.size()); ofs.close(); return 0; }邏輯說明std::ifstream以二進(jìn)制模式讀取避免 VC6.0 文本模式把\r\n轉(zhuǎn)換導(dǎo)致 JSON 解析失敗。std::istreambuf_iterator在 VC6.0 下可用但需要#include iterator。參數(shù)說明data.json必須是 UTF-8 無 BOM 格式如果帶 BOMJSONCPP 會在第一個(gè)字符處報(bào)錯需要手動跳過前三個(gè)字節(jié)。StyledWriter輸出的字符串是 UTF-8寫文件時(shí)用二進(jìn)制模式否則 VC6.0 會插入\r。這一步跑通后中文解析和輸出就不會亂碼了。4. 避坑與排查VC6.0 JSONCPP 的 5 個(gè)典型翻車現(xiàn)場4.1 編譯報(bào)錯 C2440無法從 const char* 轉(zhuǎn)換為 char*現(xiàn)象編譯json_value.cpp時(shí)大量報(bào) C2440指向c_str()返回值。原因VC6.0 的std::string::c_str()返回const char*但 JSONCPP 老版本內(nèi)部有些函數(shù)參數(shù)是char*沒有 const 修飾。解決在json_value.cpp里找到報(bào)錯行把c_str()強(qiáng)轉(zhuǎn)成(char*)或者把函數(shù)參數(shù)改成const char*。注意不要全局替換只改報(bào)錯的那幾處。4.2 解析中文返回空字符串或問號現(xiàn)象root[name].asString()返回空或???。原因JSON 文件本身是 GBK 編碼但 JSONCPP 按 UTF-8 解析遇到 GBK 雙字節(jié)會當(dāng)成非法 UTF-8 序列解析失敗或截?cái)唷=鉀Q統(tǒng)一把 JSON 源文件轉(zhuǎn)成 UTF-8 無 BOM或者用GbkToUtf8先把整個(gè) JSON 字符串轉(zhuǎn)成 UTF-8 再parse。我一般會在讀取文件后立刻判斷編碼如果是 GBK 就先轉(zhuǎn)。4.3 輸出到 MFC 編輯框仍然亂碼現(xiàn)象控制臺正常但SetWindowText到 MFC 編輯框顯示亂碼。原因MFC 編輯框在_MBCS下期望 GBK但你傳入的是 UTF-8 的std::string。解決在SetWindowText之前調(diào)用Utf8ToGbk并且確保SetWindowText的參數(shù)是CString時(shí)用CString(strGbk.c_str())構(gòu)造。如果編輯框是 Unicode 的則不要轉(zhuǎn) GBK直接CString(strUtf8.c_str())但需要工程支持_UNICODE而 JSONCPP 在_UNICODE下編譯又是另一個(gè)坑建議統(tǒng)一用_MBCS。4.4 大 JSON 解析時(shí)程序崩潰或內(nèi)存泄漏現(xiàn)象解析超過 1MB 的 JSON 時(shí) VC6.0 程序無響應(yīng)或退出。原因VC6.0 的std::string在頻繁拼接時(shí)性能極差JSONCPP 內(nèi)部大量使用std::string拼接導(dǎo)致內(nèi)存碎片和棧溢出。解決把 JSON 拆成多個(gè)小對象分次解析或者改用Json::Reader::parse的流式接口如果版本支持。另外檢查json_value.cpp里std::auto_ptr改成裸指針后是否在異常路徑漏了 delete建議用std::auto_ptr的替代類封裝。4.5 寫入 JSON 時(shí)中文變成 \uXXXX 轉(zhuǎn)義現(xiàn)象StyledWriter輸出的中文變成\u4e2d\u6587。原因JSONCPP 默認(rèn)對非 ASCII 字符做 Unicode 轉(zhuǎn)義這是符合 JSON 標(biāo)準(zhǔn)的但有些下游系統(tǒng)不認(rèn)。解決在json_writer.cpp里找到writeString函數(shù)把轉(zhuǎn)義邏輯改成直接輸出原始字節(jié)。具體是注釋掉if (c 0x20 || c 0x7f)分支里的sprintf轉(zhuǎn)義直接*s c。注意這樣輸出的 JSON 仍然是 UTF-8 字節(jié)下游按 UTF-8 解析即可。5. 進(jìn)階把 JSONCPP 封裝成 VC6.0 可復(fù)用的配置讀寫類5.1 封裝目標(biāo)與接口設(shè)計(jì)在 VC6.0 項(xiàng)目里你不可能每次解析都寫一遍Reader和編碼轉(zhuǎn)換。我一般會封裝一個(gè)CJsonConfig類提供LoadFromFile、GetString、SetString、SaveToFile四個(gè)方法內(nèi)部自動處理 UTF-8/GBK 轉(zhuǎn)換和 JSONCPP 的Json::Value生命周期。這樣業(yè)務(wù)代碼只需要config.GetString(name)不用關(guān)心底層編碼。下面是一個(gè)精簡版實(shí)現(xiàn)// JsonConfig.h #ifndef __JSON_CONFIG_H__ #define __JSON_CONFIG_H__ #include string #include json/json.h class CJsonConfig { public: CJsonConfig(); ~CJsonConfig(); bool LoadFromFile(const std::string strFilePath); bool SaveToFile(const std::string strFilePath); std::string GetString(const std::string strKey, const std::string strDefault ); void SetString(const std::string strKey, const std::string strValue); int GetInt(const std::string strKey, int nDefault 0); void SetInt(const std::string strKey, int nValue); private: Json::Value m_root; bool m_bDirty; }; #endif // __JSON_CONFIG_H__邏輯說明m_root是Json::Value對象默認(rèn)構(gòu)造為 nullLoadFromFile成功后變成 object。m_bDirty標(biāo)記是否有修改SaveToFile時(shí)決定是否寫盤。參數(shù)說明strKey支持a.b.c形式的嵌套路徑需要在GetString里按.分割逐層查找。VC6.0 的std::string::find和substr可用但注意npos的判斷。5.2 實(shí)現(xiàn) LoadFromFile 與編碼自動識別// JsonConfig.cpp #include stdafx.h #include JsonConfig.h #include fstream #include iterator // 聲明編碼轉(zhuǎn)換函數(shù) std::string Utf8ToGbk(const std::string strUtf8); std::string GbkToUtf8(const std::string strGbk); CJsonConfig::CJsonConfig() : m_bDirty(false) { } CJsonConfig::~CJsonConfig() { } bool CJsonConfig::LoadFromFile(const std::string strFilePath) { std::ifstream ifs(strFilePath.c_str(), std::ios::binary); if (!ifs.is_open()) return false; std::string strContent((std::istreambuf_iteratorchar(ifs)), std::istreambuf_iteratorchar()); ifs.close(); // 簡單判斷如果包含非 ASCII 字節(jié)且不是合法 UTF-8則按 GBK 轉(zhuǎn) // 這里簡化處理先嘗試直接解析失敗再轉(zhuǎn) Json::Reader reader; if (!reader.parse(strContent, m_root)) { // 嘗試 GBK 轉(zhuǎn) UTF-8 后再解析 std::string strUtf8 GbkToUtf8(strContent); if (!reader.parse(strUtf8, m_root)) { return false; } } m_bDirty false; return true; }邏輯說明先按原始字節(jié)解析如果失敗再嘗試 GBK 轉(zhuǎn) UTF-8。這樣兼容兩種編碼的 JSON 文件。參數(shù)說明std::istreambuf_iterator在 VC6.0 下需要#include iterator且讀取大文件時(shí)可能較慢建議加一個(gè) 1MB 的大小限制。reader.parse會清空m_root再填充所以不需要手動 reset。5.3 GetString 的嵌套路徑查找與默認(rèn)值處理std::string CJsonConfig::GetString(const std::string strKey, const std::string strDefault) { // 按 . 分割路徑 std::vectorstd::string vecKeys; std::string strTemp strKey; std::string::size_type pos 0; while ((pos strTemp.find(.)) ! std::string::npos) { vecKeys.push_back(strTemp.substr(0, pos)); strTemp strTemp.substr(pos 1); } vecKeys.push_back(strTemp); // 逐層查找 Json::Value* pNode m_root; for (size_t i 0; i vecKeys.size(); i) { if (!pNode-isObject() || !pNode-isMember(vecKeys[i])) { return strDefault; } pNode ((*pNode)[vecKeys[i]]); } if (!pNode-isString()) return strDefault; // 返回 GBK 編碼方便 MFC 顯示 return Utf8ToGbk(pNode-asString()); }邏輯說明vecKeys存儲分割后的路徑段pNode從m_root開始逐層下鉆。isMember檢查鍵是否存在isObject確保當(dāng)前節(jié)點(diǎn)是對象。參數(shù)說明strDefault在路徑不存在或類型不匹配時(shí)返回。返回前調(diào)用Utf8ToGbk因?yàn)?JSON 內(nèi)部是 UTF-8而 VC6.0 業(yè)務(wù)層通常用 GBK。如果業(yè)務(wù)層需要 UTF-8可以再加一個(gè)GetStringUtf8方法。5.4 驗(yàn)證封裝類一個(gè)完整的讀寫測試寫一個(gè)測試函數(shù)加載 JSON、讀取中文、修改后再保存驗(yàn)證整個(gè)鏈路void TestJsonConfig() { CJsonConfig config; if (!config.LoadFromFile(config.json)) { std::cout load failed std::endl; return; } // 讀取中文GetString 內(nèi)部已轉(zhuǎn) GBK std::string strName config.GetString(user.name, unknown); std::cout name: strName std::endl; // 修改并保存 config.SetString(user.name, 測試用戶); config.SetInt(user.age, 30); if (config.SaveToFile(config_out.json)) { std::cout save ok std::endl; } }邏輯說明SetString內(nèi)部需要把 GBK 轉(zhuǎn)回 UTF-8 再存入Json::ValueSaveToFile用StyledWriter輸出。參數(shù)說明config.json和config_out.json都用二進(jìn)制模式讀寫。測試時(shí)先準(zhǔn)備一個(gè)包含{user:{name:張三,age:25}}的 UTF-8 文件運(yùn)行后檢查config_out.json是否中文正常。如果輸出正常說明封裝類可用。5.5 一個(gè)我常犯的錯誤忘記處理 BOM最后說一個(gè)我踩過的坑UTF-8 文件帶 BOM 時(shí)JSONCPP 解析會在第一個(gè)字符報(bào)錯錯誤信息是* Line 1, Column 1 Syntax error: value, object or array expected。我一開始以為是 JSON 格式問題查了半天才發(fā)現(xiàn)是 BOM。解決辦法是在LoadFromFile里讀取內(nèi)容后判斷前三個(gè)字節(jié)是否為0xEF 0xBB 0xBF如果是就strContent strContent.substr(3)。這個(gè)判斷要放在解析之前且對 GBK 文件不生效?,F(xiàn)在我的習(xí)慣是所有 JSON 文件保存時(shí)都選“UTF-8 無 BOM”并且在代碼里加一行 BOM 檢測作為后悔藥。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取