如何在加载JSON字符串时保持原有键顺序?
问题描述
我从API加载JSON字符串,用以下代码格式化后加载到可编辑DIV中:
JSON.stringify(JSON.parse(rawJson.replace(/\s+/g, ' ').trim()), null, 1)
目前遇到的问题是:JSON字符串显示时无法保留原始的键顺序,加载后的顺序是混乱的。
处理JSON的函数如下:
// process incoming JSON template const processJSON = async (json: any, keys?: any[]) => { if (json) { let _matches = [] let _unMatch = []; // format json for display const jsonString = json && JSON.stringify(json, null, 1); // update the match keys with default values if (!keys) { // get all default keys const matches = jsonString.match(/{{(([^}][^}]?|[^}]}?)*)}}/gm); const defaultMatchClean = matches && matches.map(match => { //return match return match.replace(/{{|}}/gm, '') }) defaultMatchClean && defaultMatchClean.forEach(match => { _matches = [..._matches, { selectedString: match, key: match }] _unMatch = [..._unMatch, { key: match, html: match }] }) // rebuild display with default values if (matches) { matchKeys.current = _matches; unMatch.current = _unMatch } selectedRef.current = jsonString; setSelectedJSON(jsonString); console.log("🚀 ~ file: AlertBody.tsx ~ line 121 ~ processJSON ~ json", JSON.stringify(json), _matches) return } matchKeys.current = keys ? keys : _matches;; matchKeys.current.forEach(match => { _unMatch = match.key.includes('pw') ? [..._unMatch, { key: match.key, html: match.key }] : [..._unMatch, { key: match.selectedString, html: match.key }] }) unMatch.current = _unMatch selectedRef.current = jsonString; setSelectedJSON(jsonString); console.log("🚀 ~ file: AlertBody.tsx ~ line 121 ~ processJSON ~ json", jsonString) } }
示例JSON:
{ "blocks": [{"text": {"type": "plain_text","text": "{{pw.alert.severity}} - {{pw.alert.title}}"},"type": "header"} ], "attachments": [{"color": "{{pw.state.severityColor}} - {{pw.alert.title}}","blocks": [{"type": "section","text": {"type": "plain_text","text": "Triggered: {{pw.alert.timestamp.iso}}"}},{"type": "section","text": {"text": "{{pw.alert.description}}","type": "mrkdwn"}},{"type": "actions","elements": [{"type": "button","value": "View Alert","action_id": "button-action-1","text": {"text": "View Alert","type": "plain_text","emoji": true},"url": "{{pw.alert.alertUrl}}"}]},{"type": "header","text": {"type": "plain_text","text": "CORRELATIONS"}},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.link.openall}} | *Open All*> | <{{pw.link.downloadall}} | *Download All*>"}},{"text": {"type": "plain_text","emoji": true,"text": "{{pw.workflow.destinationGroup.1.name}}"},"type": "header"},{"type": "divider"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.0.url}} | * {{pw.workflow.destinationGroup.1.destination.0.name}}*>"}},{"text": {"text": "<{{pw.workflow.destinationGroup.1.destination.1.url}} | * {{pw.workflow.destinationGroup.1.destination.1.name}}*>","type": "mrkdwn"},"type": "section"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.1.destination.2.url}} | * {{pw.workflow.destinationGroup.1.destination.2.name}}*>","type": "mrkdwn"}},{"text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.3.url}} | * {{pw.workflow.destinationGroup.1.destination.3.name}}*>"},"type": "section"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.1.destination.4.url}} | * {{pw.workflow.destinationGroup.1.destination.4.name}}*>","type": "mrkdwn"}},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.5.url}} | * {{pw.workflow.destinationGroup.1.destination.5.name}}*>"},"type": "section"},{"text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.6.url}} | * {{pw.workflow.destinationGroup.1.destination.6.name}}*>"},"type": "section"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.7.url}} | * {{pw.workflow.destinationGroup.1.destination.7.name}}*>"},"type": "section"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.8.url}} | * {{pw.workflow.destinationGroup.1.destination.8.name}}*>"},"type": "section"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.9.url}} | * {{pw.workflow.destinationGroup.1.destination.9.name}}*>"},"type": "section"},{"type": "header","text": {"emoji": true,"text": "{{pw.workflow.destinationGroup.2.name}}","type": "plain_text"}},{"type": "divider"},{"text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.2.destination.0.url}} | * {{pw.workflow.destinationGroup.2.destination.0.name}}*>"},"type": "section"},{"text": {"type": "plain_text","text": "{{pw.workflow.destinationGroup.3.name}}","emoji": true},"type": "header"},{"type": "divider"},{"text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.3.destination.0.url}} | * {{pw.workflow.destinationGroup.3.destination.0.name}}*>"},"type": "section"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.3.destination.1.url}} | * {{pw.workflow.destinationGroup.3.destination.1.name}}*>","type": "mrkdwn"}},{"text": {"text": "<{{pw.workflow.destinationGroup.3.destination.2.url}} | * {{pw.workflow.destinationGroup.3.destination.2.name}}*>","type": "mrkdwn"},"type": "section"},{"type": "header","text": {"emoji": true,"type": "plain_text","text": "{{pw.workflow.destinationGroup.4.name}}"}},{"type": "divider"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.4.destination.0.url}} | * {{pw.workflow.destinationGroup.4.destination.0.name}}*>"}},{"text": {"text": "<{{pw.workflow.destinationGroup.4.destination.1.url}} | * {{pw.workflow.destinationGroup.4.destination.1.name}}*>","type": "mrkdwn"},"type": "section"},{"text": {"text": "<{{pw.workflow.destinationGroup.4.destination.2.url}} | * {{pw.workflow.destinationGroup.4.destination.2.name}}*>","type": "mrkdwn"},"type": "section"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.4.destination.3.url}} | * {{pw.workflow.destinationGroup.4.destination.3.name}}*>","type": "mrkdwn"}},{"type": "header","text": {"type": "plain_text","text": ":camera: Snapshots","emoji": true}},{"type": "divider"},{"type": "section","text": {"type": "mrkdwn","text": "<{{pw.workflow.destinationGroup.1.destination.0.snapshot.protectedUrl}} | *High-Resolution Image: {{pw.workflow.destinationGroup.1.destination.0.name}}*>"}},{"type": "image","title": {"type": "plain_text","emoji": true,"text": "{{pw.workflow.destinationGroup.1.destination.0.name}}"},"alt_text": "{{pw.workflow.destinationGroup.1.destination.0.name}}","image_url": "{{pw.workflow.destinationGroup.1.destination.0.snapshot.publicUrl}}"},{"type": "section","text": {"text": "<{{pw.workflow.destinationGroup.1.destination.3.snapshot.protectedUrl}} | *High-Resolution Image: {{pw.workflow.destinationGroup.1.destination.3.name}}*>","type": "mrkdwn"}},{"title": {"emoji": true,"text": "{{pw.workflow.destinationGroup.1.destination.3.name}}","type": "plain_text"},"alt_text": "{{pw.workflow.destinationGroup.1.destination.3.name}}","type": "image","image_url": "{{pw.workflow.destinationGroup.1.destination.3.snapshot.publicUrl}}"}]} ]}
解决方案
问题核心是:普通JS对象不保证键的顺序(ES6+虽有排序规则,但和原始JSON的顺序未必一致),当你用JSON.parse把字符串转成普通对象后,再用JSON.stringify输出,键的顺序就会被打乱。
以下是两种可行的解决方法:
方案1:直接格式化原始JSON字符串,避免解析为普通对象
既然要保留原始顺序,没必要先解析成对象再转回去。可以直接对原始rawJson进行格式化,比如使用prettier的JSON格式化能力,它会严格保留原始键顺序:
// 安装依赖:npm install prettier prettier/parser-json import prettier from "prettier"; import parserJson from "prettier/parser-json"; // 替换原来的JSON.stringify(JSON.parse(...), null, 1) const formattedJson = prettier.format(rawJson.replace(/\s+/g, ' ').trim(), { parser: "json", plugins: [parserJson], tabWidth: 1, });
方案2:用Map保留顺序,自定义stringify函数
如果必须解析成对象处理,可以用JSON.parse的reviver参数将对象转为Map(Map会严格保留插入顺序),再自定义一个能处理Map的stringify函数:
// 解析时生成有序Map const parseWithOrder = (jsonStr) => { return JSON.parse(jsonStr, (key, value) => { if (typeof value === 'object' && value !== null && !Array.isArray(value)) { return new Map(Object.entries(value)); } return value; }); }; // 自定义stringify处理Map const stringifyWithOrder = (value, indent = 1) => { if (value instanceof Map) { const entries = Array.from(value.entries()); const indentStr = ' '.repeat(indent); const nextIndent = indent + 1; return `{\n${entries.map(([k, v]) => `${indentStr}"${k}": ${stringifyWithOrder(v, nextIndent)}` ).join(',\n')}\n${' '.repeat(indent-1)}}`; } else if (Array.isArray(value)) { const indentStr = ' '.repeat(indent); const nextIndent = indent + 1; return `[\n${value.map(item => `${indentStr}${stringifyWithOrder(item, nextIndent)}` ).join(',\n')}\n${' '.repeat(indent-1)}]`; } else if (typeof value === 'string') { return `"${value.replace(/"/g, '\\"')}"`; } else { return JSON.stringify(value); } }; // 使用示例 const parsed = parseWithOrder(rawJson.replace(/\s+/g, ' ').trim()); const formattedJson = stringifyWithOrder(parsed, 1);
针对你的代码修改建议
如果采用方案1,需要调整processJSON的参数,传入原始JSON字符串而非解析后的对象:
const processJSON = async (rawJson: string, keys?: any[]) => { if (rawJson) { let _matches = []; let _unMatch = []; // 直接格式化原始JSON,保留顺序 import prettier from "prettier"; import parserJson from "prettier/parser-json"; const jsonString = prettier.format(rawJson.replace(/\s+/g, ' ').trim(), { parser: "json", plugins: [parserJson], tabWidth: 1, }); if (!keys) { // 后续逻辑保持不变 const matches = jsonString.match(/{{(([^}][^}]?|[^}]}?)*)}}/gm); // ... 其余代码原样保留 } // ... 其余代码原样保留 } };
这样处理后,JSON的键顺序就会和API返回的原始数据完全一致。
内容的提问来源于stack exchange,提问作者Rayon Hunte
相关产品推荐
相关产品推荐

