JS互操作性
Kotlin/JS是一个非常强大的工具,允许您将Kotlin编译为JavaScript。apollo-runtime
默认支持Kotlin/JS,无需修改代码。
不过,默认实现存在一些性能限制。Kotlin/JS显著增加了基本Kotlin数据结构(特别是List
、Set
和Map
)的额外开销,因此性能敏感的工作负载(如Kotlin JSON解析代码路径中的工作负载)可能会很慢。
为了解决这个问题,Apollo Kotlin提供了两种替代方案,以更快的速度与JS交互:
jsExport
可实现最高约 100 倍的速度提升,但不支持 Kotlin 的 类型系统 以及operationBased
的代码生成。DynamicJsJsonReader
可实现最高约 25 倍的速度提升,但需要在 JS 中绕过 ApolloClient 的某些部分。
jsExport
jsExport
使用 @JsExport 注解,以便从 Kotlin 直接调用动态 JS 对象。
JsExport
目前在 Apollo Kotlin 中属于 试验性功能。如果您对此有任何反馈,请通过 GitHub 问题 或在 Kotlin Slack 社区中联系我们。
由于它绕过了 Kotlin 的 type system ,使用 jsExport
存在一定限制。有关更多详细信息,请参阅 限制。
用法
要使用它,请在您的 Gradle 脚本中将 jsExport
设置为 true
:
// build.gradle[.kts]apollo {service("service") {packageName.set("jsexport")// opt in jsExportjsExport.set(true)// jsExport only works with responseBased codegencodegenModels.set("responseBased")}}
在您的通用源中定义一个简单的 executeApolloOperation
:
expect suspend fun <D : Operation.Data> JsonHttpClient.executeApolloOperation(operation: Operation<D>,): D?
对于非-JS 实现,使用您喜欢的 HTTP 客户端实现 executeApolloOperation
(见 在无需 apollo-runtime 的情况下使用模型)和 parseJsonResponse
:
// non-js implementationactual suspend fun <D : Operation.Data> JsonHttpClient.executeApolloOperation(operation: Operation<D>,): D? {val body = buildJsonString {operation.composeJsonRequest(this)}val bytes = yourHttpClient.execute(somePath, body)val response = operation.parseJsonResponse(BufferedSourceJsonReader(Buffer().write(bytes)))return response.data}
在JavaScript中,您可以使用 fetch
和 unsafeCast()
来将返回的JavaScript对象转换为 @JsExport
的 responseBased
模型:
// js implementationactual suspend fun <D : Operation.Data> JsonHttpClient.executeApolloOperation(operation: Operation<D>,): D? {val body = buildJsonString {operation.composeJsonRequest(this)}val response = fetch(somePath, body).await()val dynamicJson = response.json().await().asDynamic()/*** Because responseBased codegen maps to the response data and the models have* @JsExport annotations, you can use unsafeCast directly*/return dynamicJson["data"].unsafeCast()}
更多的示例请查看 此代码片段,它使用了Ktor应用于非JavaScript客户端。
工作原理
JavaScript是一种动态语言,这意味着如果您不需要方法/原型功能,您可以取任意JS对象将其转换为与其形状匹配的生成代码。例如,考虑下面的JavaScript:
// Imagine Kotlin generated a class like this:class Point {constructor(x, y) {this.x = x;this.y = y;}}// And we had data like this:val point = {x: 10y: 10}// This would be perfectly valid code, even though `point` is not actually a `Point`:console.log(point.x)
在Kotlin中,这看起来像是
data class Point(val x: Int, val y: Int)val point = jso<dynamic> {x = 10y = 10}val typedPoint = point.unsafeCast<Point>()console.log(typedPoint.x)
但是!由于Kotlin编译器默认都会对属性名进行改写,即这意味着在Kotlin编译后,Point
数据类生成的代码会是如下这样:
class Point {constructor(x, y) {// Note how it's x_1 here and not just xthis.x_1 = x;this.y_1 = y;}}
为了解决这个问题,您需要告诉编译器不要改写属性名,您可以通过使用 @JsExport
标注类来实现。当您在您的服务上设置 jsExport
选项时,您是在告诉Apollo为每个生成的类使用 @JsExport
标注,这样属性名就不会被改写,并且您可以安全地进行转换。
访问器和多态性
通常 responseBased
代码生成会为多态模型创建带访问器的伴随对象。例如:
public interface Animal {public val __typename: Stringpublic val species: Stringpublic companion object {public fun Animal.asLion() = this as? Lionpublic fun Animal.asCat() = this as? Cat}}
遗憾的是,@JsExport
不支持伴随对象也不支持扩展函数(详见 限制)。更有甚者,@JsExport
在从JS获取的运行时类型信息,因此无法在运行时确定给定的实例是 Cat
还是 Lion
。要进行此检查,使用 __typename
和 apolloUnsafeCast
:
when (animal.__typename) {"Lion" -> animal.apolloUnsafeCast<Lion>()"Cat" -> animal.apolloUnsafeCast<Cat>()}
apolloUnsafeCast
:
- 在非JS目标上使用
as
转换 - 在JS中使用
unsafeCast()
(文档)不会进行任何类型检查。如果你的响应没有预期的形状,你的程序将会失败。
限制
@JsExport
是Kotlin和Apollo Kotlin中的一个实验性特性,并且可能会在未来版本中更改。@JsExport
仅在基于响应的代码生成时才有意义,因为它要求Kotlin模型与JSON具有相同的形状。- 在JS上,无法检查一个
@JsExport
实例是否实现了给定的类。如果你需要多态,你必须检查__typename
来确定要使用哪个接口。 - 当使用此技术时,生成的代码的扩展函数会中断,因为我们正在将原始JS对象进行转换,而没有实际实例化类。
generateAsInternal = true
与@JsExport
不兼容,因为编译器最终将内部修饰符的优先级赋予,从而破坏了属性名。- 自定义适配器只能在它们的目标类型由JS支持时使用(请参阅 支持类型的完整列表)。
- 枚举不支持
@JsExport
,并以String
的形式生成。仍然会生成Kotlin枚举,因此你可以使用safeValueOf()
从一个String
DynamicJsJsonReader
如果你更喜欢使用 operationBased
模型,并且性能不是那么关键,你可以使用 DynamicJsJsonReader
。 DynamicJsJsonReader
与在JS端已经解析的JavaScript对象一起工作。
在JS中,从字节序列中的字节数据逐个读取响应会带来很多开销,因为Kotlin在其数组中使用 Long
索引,而Longs没有JS实现。
通过绕过这种读取,DynamicJsJsonReader
允许快速的响应读取,同时仍然保持完整的Kotlin类型信息。
在我们的测试中,与使用 @JsExport
方法相比,我们在这个解析器上实现了约 ~25x 的性能提升。
要使用 DynamicJsJsonReader
,你的JS实现将变为:
// js implementationactual suspend fun <D : Operation.Data> JsonHttpClient.executeApolloOperation(operation: Operation<D>,headers: Array<Array<String>> = emptyArray(),method: HttpMethod = HttpMethod.Post): ApolloResponse<D> {val body = buildJsonString {operation.composeJsonRequest(this)}val response = fetch(somePath, body).await()val dynamicJson = response.json().await().asDynamic()return operation.parseJsonResponse(DynamicJsJsonReader(dynamicJson))}
基准测试
我们已经使用GitHub API的一个大规模多态查询结果进行了一些基准测试。
目标是比较
JSON.parse
+unsafeCast
(JsExporrt
)JSON.parse
+DynamicJsJSONReader
BufferedSourceJsonReader
(默认配置)
我们在 2021 款 Macbook Pro M1 Max 上,使用 Chrome 112 浏览器和 Kotlin 1.8.21 运行测试。
以下是结果
parse with js export 40327.72686447989 ops/secparse with js export 68 runsparse with dnymaic reader 9989.38589840788 ops/secparse with dnymaic reader 54 runsparse with buffer reader 394.15146896515483 ops/secparse with buffer reader 63 runs
DynamicJsJsonReader
的速度是默认配置的约 25 倍,而 JsExport
的速度是 DynamicJsJsonReader
的约 4 倍。
生成这些结果所使用的代码如下