expo-json-utils 变更日志全解析:Expo 跨平台 JSON 读取工具的原生实现与演进史
2026/9/10 6:45:03 网站建设 项目流程

expo-json-utils 变更日志全解析:Expo 跨平台 JSON 读取工具的原生实现与演进史

【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo

本文以 expo-json-utils 的变更日志 为主线,完整梳理这个 Expo 原生工具包从 0.1.0 到 57.0.1 的全部版本变更,并结合仓库中的 Android/Kotlin 与 iOS/Objective-C 源码、测试用例和构建配置,剖析JSONObject.require<T>NSDictionary (EXJSONUtils)分类等核心 API 的实现原理、平台最低版本要求的演进,以及最新未发布修复所解决的 Kotlin 反射类型匹配问题。

模块定位:一个没有 JS API 的纯原生工具包

expo-json-utils的定位在 README 中只有一句话:"Utilities for reading JSONObjects (Android) and NSDictionaries (iOS) for React Native and Expo apps"——为 Android 端读取JSONObject、iOS 端读取NSDictionary提供类型安全的取值工具。

从源码结构看,它是一个纯原生模块:

  • index.js 的全部内容就是module.exports = null;,即不向 JavaScript 运行时暴露任何 API;
  • expo-module.config.json 声明其运行平台为["apple", "android"];
  • 真正的功能实现全部位于原生侧:Android 是 JSONObjectUtils.kt 中的两个 Kotlin 扩展函数,iOS 是 NSDictionary+EXJSONUtils 分类方法族。

iOS 实现中的宏命名为EXGetNonNullManifestValue/EXGetNullableManifestValue(见 NSDictionary+EXJSONUtils.m),从命名可以推断这套工具最初就是为 App 读取 Manifest(JSON 格式)时的类型安全取值而设计的。因此它属于 Expo 体系中的"内部支撑包":开发者通常不直接import它,而是随 Expo SDK 整体升级,并间接受其平台最低版本约束的影响。

版本时间线总览:从 0.1.0 到 57.0.1

以下是变更日志中全部版本条目的完整继承(变更类型按原文档分类:🛠 Breaking changes / 🎉 New features / 🐛 Bug fixes / 📚 3rd party library updates / 💡 Others / ⚠️ Notices):

版本发布日期变更类型变更内容关联 PR / 贡献者
Unpublished🐛 Bug fixes[Android] 在JSONObject.require<T>中改用javaObjectType,使原始类型分支(Long/Int/Double/Boolean)能够正确匹配,而不再静默落入兜底else分支并抛出ClassCastException#46181 / @jakequade-pc
57.0.12026-07-15无用户可见变更
57.0.02026-06-25无用户可见变更
56.0.02026-05-05🛠 Breaking changesiOS/tvOS 最低版本提升至 16.4,macOS 提升至 13.4#43296 / @tsapeta
55.0.22026-04-02无用户可见变更
55.0.12026-04-02无用户可见变更
55.0.02026-01-21无用户可见变更
0.15.02025-04-04💡 Others[Android] 开始使用 Expo modules Gradle 插件#34176 / @lukmccall
0.14.02024-10-22🛠 Breaking changesiOS 与 tvOS 部署目标提升至 15.1#30840 / @tsapeta
0.13.12024-04-29无用户可见变更
0.13.02024-04-18💡 Others移除已废弃的向后兼容 Gradle 配置#28083 / @kudo
0.12.32024-01-18无用户可见变更
0.12.22024-01-10无用户可见变更
0.12.12023-12-19无用户可见变更
0.12.02023-11-14🛠 Breaking changesiOS 部署目标提升至 13.4;AndroidcompileSdkVersiontargetSdkVersion提升至 34#25063 / @gabrieldonadel、#24708 / @alanjhughes
0.12.02023-11-14💡 Othersunimodule.json更名为expo-module.config.json#25100 / @reichhartd
0.11.02023-10-17🛠 Breaking changes放弃对 Android SDK 21、22 的支持#24201 / @behenate
0.10.02023-09-15🎉 New features新增 Apple tvOS 平台支持#24329 / @douglowder
0.9.02023-09-04🎉 New features新增对 React Native 0.73 的支持#24018 / @kudo
0.8.02023-07-28无用户可见变更
0.7.12023-07-10🛠 Breaking changes[iOS] 为分类(Category)方法添加前缀,降低命名冲突概率#23441 / @wschurman
0.7.02023-06-21📚 3rd party updatesjunit更新至 4.13.2#22395 / @josephyanks
0.7.02023-06-21🐛 Bug fixes修复 Gradle 8 下的 Android 构建警告#22537、#22609 / @kudo
0.6.02023-05-08💡 Others将 EXManifests 的 iOS 实现转换为 Swift#21298 / @wschurman
0.5.12023-02-09无用户可见变更
0.5.02023-02-03💡 OthersAndroidcompileSdkVersiontargetSdkVersion提升至 33#20721 / @lukmccall
0.4.02022-10-25🛠 Breaking changesiOS 部署目标提升至 13.0,废弃对 iOS 12 的支持#18873 / @tsapeta
0.3.02022-04-18⚠️ NoticesAndroidcompileSdkVersion提升至 31、targetSdkVersion提升至 31、Java版本提升至 11#16941 / @bbarthec
0.2.12022-02-01🐛 Bug fixes修复 Android Gradle 7 下Plugin with id 'maven' not found构建错误#16080 / @kudo
0.2.02021-09-28🛠 Breaking changes放弃对 iOS 11.0 的支持#14383 / @cruzach
0.2.02021-09-28🐛 Bug fixes修复 Podfile 中use_frameworks!引发的构建错误#14523 / @kudo
0.1.02021-09-09初始版本

从这条时间线可以读出三个清晰的阶段特征:

  1. 0.1.0(2021-09)~ 0.15.0(2025-04)阶段:版本号与 Expo 主仓库解耦,按自身节奏迭代,主要工作是跟进 Android SDK 31→33→34、iOS 部署目标 13.0→13.4→15.1 等平台要求的逐年提升,以及引入 tvOS、React Native 0.73 等能力;
  2. 55.0.0(2026-01)起阶段:版本号切换为与 Expo SDK 大版本对齐(55 → 56 → 57),0.15.0 与 55.0.0 之间存在一次版本体系迁移,期间无用户可见变更;
  3. 当前 57.0.1 之后:处于 Unpublished 状态,仅包含一条 Android 端的类型匹配修复(下文详解)。

Unpublished 修复剖析:JSONObject.require 的原始类型匹配问题

变更日志中最具技术含量的一条,是当前尚未发布的 Android Bug fix。修复后的实现位于 JSONObjectUtils.kt:

@Throws(JSONException::class) inline fun <reified T : Any> JSONObject.require(key: String): T { return when (T::class.javaObjectType) { String::class.javaObjectType -> this.getString(key) as T Double::class.javaObjectType -> this.getDouble(key) as T Int::class.javaObjectType -> this.getInt(key) as T Long::class.javaObjectType -> this.getLong(key) as T Boolean::class.javaObjectType -> this.getBoolean(key) as T JSONArray::class.javaObjectType -> this.getJSONArray(key) as T JSONObject::class.javaObjectType -> this.getJSONObject(key) as T else -> this.get(key) as T } }

问题的技术背景可以这样理解:

  • 在 Kotlin 中,T::class.javaT::class.javaObjectType对原始类型(如LongIntDoubleBoolean)返回的Class对象不同——前者可能返回 Kotlin/JVM 的原始类型表示,后者稳定返回对应的 Java 装箱类型(java.lang.Long等)。当调用方以require<Long>之类的原始类型实参发起调用时,若when的主题表达式与分支常量两侧的取值口径不一致,原始类型分支就不会命中,代码会静默落入兜底的else分支;
  • 落入else后执行this.get(key) as T。而 Android 的org.json.JSONObject对"能放进 int 的 JSON 数字"内部存的是Integer,Integer as Long的强转在 JVM 上会直接抛出ClassCastException——这正是变更日志描述的行为;
  • 修复方案是让when的主题统一使用T::class.javaObjectType,与分支常量保持一致的装箱类型口径,使Long/Int/Double/Boolean分支正确命中,从而走getLong/getInt/getDouble/getBoolean这些会做类型转换的取值方法。

配套的测试用例在 JSONObjectUtilsTest.kt 中,注意它们都刻意通过toString()再重新解析 JSON,模拟真实的解析路径,验证"往返"后的类型正确性:

@Test @Throws(Exception::class) fun testRequireLongRoundTripsForIntSizedValues() { val parsed = JSONObject(JSONObject(mapOf("duration" to 6000L)).toString()) val out: Long = parsed.require("duration") out shouldBeEqualTo 6000L } @Test @Throws(Exception::class) fun testRequireIntRoundTrips() { val parsed = JSONObject(JSONObject(mapOf("count" to 42)).toString()) parsed.require<Int>("count") shouldBeEqualTo 42 }

testRequireLongRoundTripsForIntSizedValues就是该修复的典型场景:6000L 这样的"int 尺寸"数值经过 JSON 文本往返后在JSONObject中变为Integer,只有正确命中Long分支调用getLong(JVM 上getLong会对底层Integer做加宽转换)才能拿到6000L,否则get返回Integer后强转Long即崩溃。此外同文件中的 testGetOrNull 还验证了另一个 API 的契约:

inline fun <reified T : Any> JSONObject.getNullable(key: String): T? { return if (!this.has(key)) { null } else { this.require(key) } }

getNullable<T>在 key 不存在时返回null,在 key 存在但类型不符或要求值时行为与require<T>一致(抛出JSONException,测试中断言错误消息为No value for non-existent-key)。

iOS 端实现:带断言的 NSDictionary 分类方法族

iOS 侧的能力由NSDictionary分类提供,方法声明见 NSDictionary+EXJSONUtils.h:

- (NSString *)expo_stringForKey:(KeyType)key; - (nullable NSString *)expo_nullableStringForKey:(KeyType)key; - (NSNumber *)expo_numberForKey:(KeyType)key; - (nullable NSNumber *)expo_nullableNumberForKey:(KeyType)key; - (NSArray *)expo_arrayForKey:(KeyType)key; - (nullable NSArray *)expo_nullableArrayForKey:(KeyType)key; - (NSDictionary *)expo_dictionaryForKey:(KeyType)key; - (nullable NSDictionary *)expo_nullableDictionaryForKey:(KeyType)key;

每个基础类型都成对提供"非空版"与"nullable 版",与 Android 端require<T>/getNullable<T>的语义一一对应。实现见 NSDictionary+EXJSONUtils.m,核心是两个 Objective-C 宏:

#define EXGetNonNullManifestValue(Type, key) \ ({ \ id value = [self objectForKey:key]; \ NSAssert(value != nil, @"Value for (key = %@) should not be null", key); \ NSAssert([value isKindOfClass:[Type class]], @"Value for (key = %@) should be a %@", key, NSStringFromClass([Type class])); \ value; \ })

非空版通过两条NSAssert校验:key 缺失时断言失败,类型不符时同样断言失败——这相当于 Android 端抛出JSONException的对应物(在 iOS 上表现为 Debug 断言崩溃,而非受检异常)。nullable 版则放宽为"要么为 nil,要么类型正确"。

变更日志 0.7.1 版本中的条目 "[ios] Prefix category methods to reduce likelihood of conflicts"(#23441)与当前源码直接对应:如今所有方法名统一带有expo_前缀(expo_stringForKey:等),这正是为了避免分类方法与宿主工程中其他分类的同名方法发生符号冲突——这是一次带破坏性(Breaking)性质的命名迁移,依赖旧无前缀方法名的下游代码需要随之更新。

测试覆盖见 NSDictionary+EXJSONUtilsTest.m,其断言模式与 Android 测试完全对称:

- (void)test_stringForKey { XCTAssertEqual([self.testData expo_stringForKey:@"string"], @"hello"); XCTAssertThrows([self.testData expo_stringForKey:@"number"]); XCTAssertThrows([self.testData expo_stringForKey:@"nonexistent"]); } - (void)test_nullableStringForKey { XCTAssertEqual([self.testData expo_nullableStringForKey:@"string"], @"hello"); XCTAssertNil([self.testData expo_nullableStringForKey:@"nonexistent"]); XCTAssertThrows([self.testData expo_nullableStringForKey:@"number"]); }

即:正确取值、缺失 key(nullable 版返回 nil / 非空版抛错)、类型不符(两版均抛错)三类行为都有验证。

平台最低版本演进与构建配置佐证

变更日志中多条 Breaking changes 都是"抬高平台地板"的操作,这些声明在仓库的构建配置中都能找到对应证据:

  • 56.0.0:iOS/tvOS 最低 16.4,macOS 最低 13.4(#43296)。CocoaPods 侧,EXJSONUtils.podspec 声明s.platforms = { :ios => '16.4', :tvos => '16.4' };Swift Package Manager 侧,spm.config.json 声明"platforms": ["iOS(\"16.4\")"];
  • 0.14.0 声明的 15.1 部署目标已被 56.0.0 的 16.4 取代,版本演进方向单一:只升不降;
  • 0.12.0 的 Android SDK 34 提升(#24708)之后,0.13.0 又移除了向后兼容的旧 Gradle 配置(#28083),0.15.0 则"开始使用 Expo modules Gradle 插件"(#34176)——从变更日志可以推断,Android 构建侧的演进是"提 SDK 版本 → 清理旧配置 → 换用统一 Gradle 插件"三步走的收敛过程;
  • 0.10.0 的 tvOS 支持(#24329)在配置上体现为 podspec 中:tvos平台的显式声明。

podspec 还包含一些值得注意的构建细节:EXJSONUtils.podspec 声明static_framework = true(静态框架,与 Expo iOS 侧一贯的链接方式一致),开启GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORSGCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS(指针类型不兼容、隐式函数声明均视为编译错误),并通过test_spec 'Tests'将 ios/Tests 目录下的测试文件独立成一个测试 target。spm.config.json 同样通过"exclude": ["Tests/**"]将测试代码排除在发布产物之外,两个配置对测试目录的处理保持一致。

对使用者的实际意义

综合变更日志与源码,expo-json-utils对 Expo 项目使用者的实际影响集中在三点:

  1. 跟随 SDK 升级,无需单独操作:它是 Expo SDK 的组成模块,package.json中当前版本为 57.0.1(与变更日志最新已发布版本一致),用户随 Expo SDK 大版本(55/56/57)一并获得,无独立的 npm 安装入口;
  2. 关注其平台地板的变化:56.0.0 起 iOS/tvOS 最低 16.4、macOS 最低 13.4,0.11.0 起 Android 最低 SDK 为 23(21/22 被移除),0.14.0 起 iOS 部署目标 15.1——如果你的业务对低版本系统有硬性要求,升级 Expo SDK 时应先核对这些"地板";
  3. 它是内部支撑包,不承诺 JS API:由于 index.js 不导出任何内容,不建议在业务代码中依赖它的任何 JavaScript 行为;真正面向原生开发者的价值在于,其他 Expo 原生模块读取 JSON 时获得了跨平台一致的类型安全取值语义(非空/可空成对 API,类型错误时 Android 抛JSONException、iOS 触发断言)。

理解这份变更日志,也就理解了 Expo 生态中"平台要求逐年抬升 + 模块配置统一化 + 内部支撑包随 SDK 对齐版本号"这三条贯穿整个仓库的版本演进规律。

【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询