HarmonyKit | JSON 格式化工具:从三行核心算法到完整交互设计

HarmonyKit | JSON 格式化工具:从三行核心算法到完整交互设计

引言:为什么 JSON 格式化排在第一个

打开 HarmonyKit 的首页,五个工具分类中排在最前面的是"格式化",其中第一个工具是"JSON 格式化"。这不是随机排列——JSON 格式化是开发者移动端最高频的需求。线上排查问题时复制一段 JSON 日志,需要格式化查看结构;书写 API 文档时需要把压缩的 JSON 展开;对接第三方接口时需要验证返回的 JSON 是否合法。

HarmonyKit 的 JSON 格式化工具看似简单——三行核心算法代码——但它背后涉及的类型约束、错误处理哲学、缩进切换设计和格式化/压缩双模式,构成了一个完整的产品设计案例。这篇文章将详细拆解 JSON 格式化工具的全部实现细节。

项目仓库:https://atomgit.com/VON-/harmony-kit

核心算法:三行代码,两层考量

JsonUtils 的核心逻辑文件仅有 27 行,包含三个静态方法:

interfaceValidateResult{valid:boolean;error:string;}exportclassJsonUtils{staticformat(input:string,indent:number=2):string{constobj:Object=JSON.parse(input);returnJSON.stringify(objasobject,null,indent);}staticcompress(input:string):string{constobj:Object=JSON.parse(input);returnJSON.stringify(objasobject);}staticvalidate(input:string):ValidateResult{try{JSON.parse(input);constresult:ValidateResult={valid:true,error:''};returnresult;}catch(e){constresult:ValidateResult={valid:false,error:(easError).message};returnresult;}}}

三行核心代码拆开后,每行都有两个层面的考量:

第一层:功能正确性。JSON.parse(input)将字符串解析为 JavaScript 对象。JSON.stringify(obj, null, indent)将对象重新序列化为带缩进的字符串——null是 replacer(不进行属性过滤),indent是缩进空格数。

第二层:ArkTS 类型约束。const obj: Object = JSON.parse(input)中的Object不能省略——ArkTS 禁止隐式类型推导。obj as object不能省略——Object类型(包含 string/number)不能直接传参给JSON.stringify(),必须先断言为objectconst result: ValidateResult = {...}不能省略——ArkTS 禁止未标注类型的对象字面量。

这反映了 HarmonyKit 中所有工具类的一个共同特征:核心逻辑可能只有两三个 API 调用,但为了让这些调用在 ArkTS 的类型系统中合法运行,需要额外的类型标注和断言。这些标注不是"累赘"——它们让每一次类型转换都在编译期被验证,消除了运行时隐式类型转换的风险。

JSON.stringify 的三个参数详解

JSON.stringify()在 ArkTS 中的完整签名是:

JSON.stringify(value: object, replacer?: (key: string, value: Object) => Object | null, space?: string | number): string

第一个参数 value:要序列化的对象。类型必须是object(不能是stringnumber等基本类型),这就是为什么需要obj as object断言。

第二个参数 replacer:可选。一个过滤函数或属性名数组,用于控制哪些属性被包含在输出中。在 HarmonyKit 中传入null,表示不进行属性过滤——所有可枚举属性都会被序列化。

第三个参数 space:可选。控制输出的缩进。传入number时表示缩进空格数(最大值 10),传入string时表示使用该字符串作为缩进前缀(如'\t'表示 Tab 缩进)。在 HarmonyKit 中传入indent(2 或 4),这是最常见的两种缩进风格。

压缩模式实际上就是省略第三个参数——JSON.stringify(obj)等价于JSON.stringify(obj, null, 0)(仅当第三个参数是数字且为 0 时)。但传入undefinednull作为第三个参数表示"不格式化",输出完全不带缩进和换行:

// 格式化JSON.stringify({a:1},null,2)// '{\n "a": 1\n}'// 压缩JSON.stringify({a:1})// '{"a":1}'

错误处理:定位信息比错误本身更重要

JSON 格式化的错误处理是 HarmonyKit 中最仔细设计的交互之一。JSON.parse()抛出的SyntaxError包含错误发生的位置信息:

SyntaxError: Unexpected token } in JSON at position 42

这个错误信息中的position 42是最有价值的部分——它告诉用户 JSON 的第 42 个字符处有问题。HarmonyKit 不做任何信息过滤,直接将这个错误原封不动地展示给用户:

// 在 JsonFormatter 页面中if(this.errorMsg){Text(this.errorMsg).fontSize(12).fontColor('#ff3b30').width('100%').padding({left:16,right:16,top:4,bottom:4});}

错误信息以红色小字展示在格式化结果区域的上方。#ff3b30是 iOS 的系统红(System Red),选择这个颜色而不是鲜艳的红,是因为它和 HarmonyKit 的整体灰白配色更兼容。

validate方法的职责分离值得注意。它只做验证并返回结果,不修改任何状态(不设置output,不修改input)。formatcompress方法也不重复做验证——它们信任调用方已经通过validate验证了输入的合法性。这种职责分离让每个方法的目的单一明确:validate告诉你"能不能格式化",format执行格式化,compress执行压缩。三个方法相互独立,没有任何隐藏的依赖关系。

缩进切换:2 空格 vs 4 空格

JSON 格式化工具的缩进切换看似简单(两个按钮,二元选择),但背后考虑了一个实际的产品问题:不同技术栈的缩进习惯不同。

前端开发者(React、Vue、Angular)通常使用 2 空格缩进——因为 JSX/模板语法的嵌套层级深,2 空格在水平方向上更紧凑。后端开发者(Java、Go、Python)通常使用 4 空格缩进——符合各自语言社区的代码规范。一些开发者可能使用 Tab 缩进。两个按钮的选择覆盖了约 90% 的开发者偏好。

实现方式是:

@StateindentSize:number=2;// 2空格按钮Button('2空格').fontSize(12).height(32).backgroundColor(this.indentSize===2?'#007aff':'#f0f0f0').fontColor(this.indentSize===2?'#ffffff':'#333333').borderRadius(6).onClick(()=>{this.indentSize=2;});// 4空格按钮Button('4空格').fontSize(12).height(32).backgroundColor(this.indentSize===4?'#007aff':'#f0f0f0').fontColor(this.indentSize===4?'#ffffff':'#333333').borderRadius(6).margin({left:8}).onClick(()=>{this.indentSize=4;});

选中的按钮显示蓝色填充(#007aff)和白色文字,未选中的按钮显示灰色背景(#f0f0f0)和深灰文字(#333333)。视觉上选中的按钮是"激活态",未选中的按钮是"待选态"。这种二元开关不用 Toggle 组件的原因是:Toggle 需要一个"标签"来描述它控制什么,而两个按钮本身就是标签——"2空格"和"4空格"是自描述的,不需要额外的标签文字。

UI 层面的一个细节:两个按钮之间用margin({ left: 8 })分隔,而不是用 Row 的space属性统一管理。原因是这两个按钮是"互斥选项组"——它们之间的间距应该与其他按钮(格式化、压缩)不同,后者需要更大的视觉分离度。

格式化 vs 压缩:两个按钮的哲学

从技术上讲,"格式化"和"压缩"是同一个操作的两个参数化变体:

// 格式化staticformat(input:string,indent:number=2):string{constobj:Object=JSON.parse(input);returnJSON.stringify(objasobject,null,indent);}// 压缩staticcompress(input:string):string{constobj:Object=JSON.parse(input);returnJSON.stringify(objasobject);}

format调用JSON.stringify(obj, null, indent)compress调用JSON.stringify(obj, null, undefined)(第三个参数为 undefined 表示不格式化)。两者的区别仅在于第三个参数。

但 UI 上将它们设计为两个独立的按钮而不是一个"缩进=0 就是压缩"的滑块/输入框,有一个产品层面的考量:降低用户的理解成本。如果只有一个"缩进空格数"输入框,用户需要理解"把缩进设为 0 就是压缩"。这个映射对开发者来说简单,但对"试用应用"的新用户来说需要思考。两个独立按钮将"格式化"和"压缩"作为两个独立操作呈现——两个操作都是"带明确标签的按钮",不需要用户做任何思维映射。

按钮的颜色也反映了它们的"角色":

  • “格式化"按钮用蓝色(#007aff)——这是"正向操作"的颜色,暗示"这就是你要找的功能”
  • "压缩"按钮用绿色(#34c759)——这是"辅助操作"的颜色,区分于主操作

输入输出的交互流程

JsonFormatter 页面的完整交互流程如下:

  1. 用户在顶部 TextArea 中输入或粘贴 JSON 字符串
  2. onChange回调更新this.input,同时清空this.errorMsg(输入的变更意味着之前的错误可能已被修复)
  3. 用户点击"格式化"按钮 →formatJson()方法被调用
  4. formatJson()首先检查输入是否为空 → 如果空,设置错误信息并返回
  5. 然后调用JsonUtils.validate()验证 JSON 合法性 → 如果不合法,设置错误信息并返回
  6. 验证通过后,调用JsonUtils.format()执行格式化 → 设置this.output
  7. 输出结果显示在下方的 TextArea 中
  8. 用户在输出结果旁边的 CopyButton 上点击 → 结果被复制到系统剪贴板
formatJson(){if(!this.input.trim()){this.errorMsg='请输入 JSON 字符串';return;}letresult=JsonUtils.validate(this.input);if(!result.valid){this.errorMsg=result.error;this.output='';return;}this.errorMsg='';this.output=JsonUtils.format(this.input,this.indentSize);}

这个流程的每一步都有"提前返回"(early return)的 guard clause——空输入、验证失败的 guard 都在执行格式化之前拦截并返回。GGuard clause 是 HarmonyKit 中所有工具方法的标准模式——先检查所有"不该发生的情况",快速退出,最后才是"正常情况下的执行逻辑"。这种模式让代码的阅读路径从"成功逻辑中嵌入错误处理"变为"先处理所有错误,最后执行成功逻辑"——代码的嵌套层级更浅,更容易理解。

compressJson()方法的流程完全相同,只是最后调用的是JsonUtils.compress()而不是JsonUtils.format()

工具页的一致化设计

JsonFormatter 页面遵循了 HarmonyKit 所有工具页的统一布局模板:

Column(全屏容器) ├── Row(标题栏:返回按钮 + 页面标题) └── Scroll(可滚动内容区) └── Column ├── Row(输入标签 + CopyButton) ├── TextArea(输入框) ├── Row(操作按钮组) ├── [错误信息 Text](条件渲染) ├── Row(输出标签 + CopyButton) └── TextArea(输出框)

这个模板在 10 个工具页中保持一致。唯一的变量是:操作按钮区域的内容(ToolBar 模式 vs 单个按钮 vs 两个按钮 vs 带选项的按钮组),输入框的类型(单行 TextInput vs 多行 TextArea),是否有额外的辅助控件(缩进选择、算法选择、flag 切换等)。

标题栏的返回按钮使用了系统图标:

Button({type:ButtonType.Circle}){Image($r('sys.media.ohos_ic_public_arrow_left')).width(24).height(24).fillColor('#333333')}.width(32).height(32).backgroundColor(Color.Transparent).onClick(()=>this.getUIContext().getRouter().back());

$r('sys.media.ohos_ic_public_arrow_left')是鸿蒙系统内置的返回箭头图标。sys.media前缀表明这是系统资源而非应用资源。使用系统图标的好处是:用户看到的是一个"熟悉的"返回箭头——所有鸿蒙应用都用同一个图标,一致性体验。

为什么用 TextArea 而不是 TextInput

JSON 格式化工具的输入和输出都使用了TextArea(多行文本输入框)而不是TextInput(单行输入框)。原因是 JSON 字符串本质上是一个多行文本块——即使压缩后的 JSON 只有一行,格式化后的 JSON 一定包含多行。

TextArea的配置参数:

TextArea({text:this.input,placeholder:'粘贴 JSON 字符串...'}).height(160).fontSize(13).fontFamily('monospace').backgroundColor('#ffffff').borderRadius(8).padding(12).margin({left:16,right:16}).onChange((value:string)=>{this.input=value;this.errorMsg='';});

几个值得注意的样式选择:

  • fontFamily('monospace'):等宽字体保证了 JSON 的结构缩进在水平方向对齐。如果使用默认的比例字体,层级缩进会出现视觉上的"阶梯"——因为不同字符的宽度不同
  • fontSize(13):比正文字体(通常 15-16px)略小,在有限的屏幕空间内显示更多内容。13px 是移动端等宽字体的一个经验值——12px 太紧凑,14px 浪费空间
  • backgroundColor('#ffffff'):白色背景区分于页面的浅灰背景(#f5f5f5),明确标识"这是交互区域"
  • borderRadius(8):适中的圆角让输入框看起来友好但不幼稚
  • padding(12):内容区域与边框之间留出足够间距,文字不会"贴边"

性能考量

JSON 格式化工具的性能瓶颈只有一个:JSON.stringify()处理超大 JSON 时的内存占用。一个 10MB 的 JSON 字符串,JSON.parse()会创建一个约 15-20MB 的对象(JavaScript 对象的内存开销通常大于原始字符串),JSON.stringify()再创建一个约 10MB 的输出字符串。总计约 25-30MB 的临时内存分配。

对于移动端来说,30MB 的内存在一部 8GB RAM 的手机上不成问题。但如果用户连续处理多个超大 JSON(比如从 50MB 的日志文件中反复复制粘贴片段),内存占用会累积。HarmonyKit 没有做内存优化(没有使用流式解析、没有主动触发 GC),因为"开发者工具箱"的真实使用场景中,JSON 片段通常不超过几百 KB——一段 API 响应、一个配置文件、一小段日志。

总结

JSON 格式化工具是 HarmonyKit 中最"简单"的工具——三行核心逻辑。但它的实现是完整的:验证先行的安全策略、错误信息的透明展示、缩进切换的用户选择、格式化/压缩的双操作设计、系统图标的一致性、等宽字体的排版保障。这些细节单个看都很微小,但堆叠在一起构成了从"功能可用的 JSON 格式化"到"体验完备的 JSON 格式化工具"之间的差距。

对于 ArkUI 开发者来说,JSON 格式化工具也展示了如何在严格的类型系统下正确使用JSON.parse()JSON.stringify()——通过Object类型标注、as object断言、ValidateResultinterface 提取,让标准库 API 在 ArkTS 的类型约束下合规运行。

项目仓库:https://atomgit.com/VON-/harmony-kit