加入我们,于10月8日至10日在纽约市,了解关于GraphQL联接和API平台工程的最新技巧、趋势和新闻。参加2024年在纽约市的GraphQL峰会
文档
免费开始

JS互操作性


Kotlin/JS是一个非常强大的工具,允许您将Kotlin编译为JavaScript。apollo-runtime默认支持Kotlin/JS,无需修改代码。

不过,默认实现存在一些性能限制。Kotlin/JS显著增加了基本Kotlin数据结构(特别是ListSetMap)的额外开销,因此性能敏感的工作负载(如Kotlin JSON解析代码路径中的工作负载)可能会很慢。

为了解决这个问题,提供了两种替代方案,以更快的速度与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 jsExport
jsExport.set(true)
// jsExport only works with responseBased codegen
codegenModels.set("responseBased")
}
}

在您的通用源中定义一个简单的 executeApolloOperation

expect suspend fun <D : Operation.Data> JsonHttpClient.executeApolloOperation(
operation: Operation<D>,
): D?

对于非-JS 实现,使用您喜欢的 HTTP 客户端实现 executeApolloOperation(见 在无需 apollo-runtime 的情况下使用模型)和 parseJsonResponse

// non-js implementation
actual 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中,您可以使用 fetchunsafeCast() 来将返回的JavaScript对象转换为 @JsExportresponseBased 模型:

// js implementation
actual 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: 10
y: 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 = 10
y = 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 x
this.x_1 = x;
this.y_1 = y;
}
}

为了解决这个问题,您需要告诉编译器不要改写属性名,您可以通过使用 @JsExport 标注类来实现。当您在您的服务上设置 jsExport 选项时,您是在告诉Apollo为每个生成的类使用 @JsExport 标注,这样属性名就不会被改写,并且您可以安全地进行转换。

访问器和多态性

通常 responseBased 代码生成会为多态模型创建带访问器的伴随对象。例如:

public interface Animal {
public val __typename: String
public val species: String
public companion object {
public fun Animal.asLion() = this as? Lion
public fun Animal.asCat() = this as? Cat
}
}

遗憾的是,@JsExport 不支持伴随对象也不支持扩展函数(详见 限制)。更有甚者,@JsExport 在从JS获取的运行时类型信息,因此无法在运行时确定给定的实例是 Cat 还是 Lion。要进行此检查,使用 __typenameapolloUnsafeCast

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模型,并且性能不是那么关键,你可以使用 DynamicJsJsonReaderDynamicJsJsonReader与在JS端已经解析的JavaScript对象一起工作。

在JS中,从字节序列中的字节数据逐个读取响应会带来很多开销,因为Kotlin在其数组中使用 Long索引,而Longs没有JS实现。

通过绕过这种读取,DynamicJsJsonReader允许快速的响应读取,同时仍然保持完整的Kotlin类型信息。

在我们的测试中,与使用 @JsExport方法相比,我们在这个解析器上实现了约 ~25x 的性能提升。

要使用 DynamicJsJsonReader,你的JS实现将变为:

// js implementation
actual 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 + unsafeCastJsExporrt
  • JSON.parse + DynamicJsJSONReader
  • BufferedSourceJsonReader(默认配置)

我们在 2021 款 Macbook Pro M1 Max 上,使用 Chrome 112 浏览器和 Kotlin 1.8.21 运行测试。

以下是结果

parse with js export 40327.72686447989 ops/sec
parse with js export 68 runs
parse with dnymaic reader 9989.38589840788 ops/sec
parse with dnymaic reader 54 runs
parse with buffer reader 394.15146896515483 ops/sec
parse with buffer reader 63 runs

DynamicJsJsonReader 的速度是默认配置的约 25 倍,而 JsExport 的速度是 DynamicJsJsonReader 的约 4 倍。

生成这些结果所使用的代码如下

上一页
编译器插件
下一页
响应式代码生成
评分文章评分在GitHub上编辑编辑论坛Discord

©2024Apollo Graph Inc.,作为Apollo GraphQL运营。

隐私政策

公司