@JsExport Kotlin을 JS에 노출하기 위한 가이드
Note that this post focuses on JS output for Kotlin. There is also a Typescript output (.d.ts file) with some unique issues that this post doesn't cover in detail.
이전 에서는 기존 KMM 라이브러리에
Kotlin/JS
지원을 추가했습니다. 이제 JS 측에서 작동하는 코드를 추가합니다.목차
Usage
Limitations
Interface
용법
It is critical to understand @JsExport 외부 JS 라이브러리로 Kotlin/JS를 통해 Kotlin 코드를 노출하는 경우 주석 및 주변의 모든 문제새로운IR compiler을 사용하면 Kotlin 선언이 기본적으로 JavaScript에 노출되지 않습니다. JavaScript에서 Kotlin 선언을 볼 수 있도록 하려면 @JsExport로 주석을 달아야 합니다.
Note that @JsExport is experimental as of the posted date of this post (with Kotlin 1.6.10)
아주 기본적인 예부터 시작하겠습니다.
// commonMain - Greeting.kt
class Greeting {
fun greeting(): String {
return "Hello World!"
}
}
이 시점에서 생성된
.js
라이브러리 파일에는 Greeting 클래스에 대한 참조가 없습니다. 그 이유는 @JsExport
주석이 없기 때문입니다.You can generate JS library code via
./gradlew jsBrowserDistribution
. You would find the.js, .d.ts and map
file inroot/build/js/packages/<yourlibname>/kotlin
folder.
이제 주석을 추가하여 JS 코드를 생성합니다.
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport
@ExperimentalJsExport
@JsExport
class Greeting {
fun greeting(): String {
return "Hello World!"
}
}
.js
및 .d.ts
파일에 이제 인사말 참조가 포함됩니다.생성된 .js 파일
function Greeting() {
}
Greeting.prototype.greeting = function () {
return 'Hello World!';
};
Greeting.$metadata$ = {
simpleName: 'Greeting',
kind: 'class',
interfaces: []
};
생성된 .d.ts 파일
export namespace jabbar.jigariyo.kmplibrary {
class Greeting {
constructor();
greeting(): string;
}
}
이제 JavaScript에서
Greeting
를 호출할 수 있습니다.console.log(new jabbar.jigariyo.kmplibrary.Greeting().greeting())
// Hello World!
Note that you would have to use fully qualified Kotlin names in JavaScript because Kotlin exposes its package structure to JavaScript.
내보낼 수 있는 개체의 모든 공용 속성도 내보낼 수 있어야 한다는 점을 염두에 두는 것이 중요합니다.
다음 예에서
CustomObj
는 MyDataClass
를 내보내기 위해 내보낼 수도 있어야 합니다.@JsExport
data class MyDataClass(
val strVal: String,
val customObj: CustomObj // This would need to be exportable
)
@ExperimentalJsExport 대 @JsExport
@JsExport
is the annotation you need to tell the compiler to generate JavaScript code, and @ExperimentalJsExport
is an opt-in marker annotation to use @JsExport
as it is experimental to use.
You can get rid of the requirement of adding @ExperimentalJsExport
in code by declaring it as OptIn
in languageSettings
for all source sets in your kotlin
block.
kotlin {
sourceSets {
all {
languageSettings.apply {
optIn("kotlin.js.ExperimentalJsExport")
}
}
}
}
제한 사항
As of Kotlin 1.6.10
, there are heavy limitations on what Kotlin types one can export to JavaScript.
You will most likely face one of these limitations if you add JS support in an existing KMP library.
Whenever something is not-exportable
, you would get either an error or a warning:
- Code does not compile with such errors
- Code compiles with such warnings, but you might have run-time issues
컬렉션
Kotlin's collections APIs are not exportable, so you would have to come up with different strategies to deal with them. Some examples would be:
지도
JS로 내보내는
Map
코드에서 common
사용을 제거해야 하거나 mobile
및 js
쪽에서 다른 구현이 있어야 합니다. kotlin.js.Json
쪽에서 jsMain
개체를 사용한 다음 필요할 때마다 Kotlin
맵에 매핑할 수 있습니다.JS 특정 구현의 경우 Record 라이브러리에서 kotlin-extensions을 사용할 수도 있습니다.
목록
List
사용법을 Array
로 바꿔 모든 플랫폼에 대해 동일한 코드를 유지할 수 있습니다. 단순 교체일 수도 있고 아닐 수도 있습니다.예를 들어,
Array
는 API 응답을 구문 분석하기 위해 객체에서만 사용되는 경우 작동합니다. Array
클래스에 Data
가 있으면 자신의 equals
및 hashcode
구현을 제공해야 합니다.Note that moving from
List
toArray
might have an impact on generated code foriOS
.List
becomesNSArray
on iOS side butArray
becomes a Kotlin object wrapping the array
jsMain
에 대한 별도의 구현을 원할 경우 kotlin-extensions
라이브러리는 Iterator, Set, and ReadOnlyArray과 같은 유용한 JS 특정 클래스를 제공합니다.긴
Long
is not mapped to anything as there is no equivalent in the JavaScript
world. You would see the non-exportable
warning if you export Long
via Kotlin
.
If you ignore the warning, then Long
still kinda works. It just takes any value from JS. Kotlin will receive the input as Long
if JavaScript code sends a BigInt
.
It will not work for Typescript
unless you set skipLibCheck = true
in the config as type kotlin.Long
is not available.
// Kotlin
@JsExport
class Greeting {
@Suppress("NON_EXPORTABLE_TYPE")
fun printLong(value: Long) {
print(value)
}
}
// Generated .js
Greeting.prototype.printLong = function (value) {
print(value);
};
// Generated .d.ts
printLong(value: kotlin.Long): void;
// Usage from JS
const value = "0b11111111111111111111111111111111111111111111111111111"
Greeting().printLong(BigInt(value)) // This works
You can use @Suppress("NON_EXPORTABLE_TYPE") to suppress the exportable warning
상호 작용
Kotlin interfaces are not exportable. It gets annoying when a library has an interface-driven design, where it exposes the interface in public API rather than a specific implementation.
Interfaces will be exportable starting upcoming Kotlin 1.6.20! We would have to play around with that to see it working.
JavaScript
에서 인터페이스가 작동하도록 하는 해결 방법이 있습니다.다음은 인터페이스를 우회하는 몇 가지 예입니다.
구현 클래스 사용
@JsExport
interface HelloInterface {
fun hello()
}
The above code would show the non-exportable error. You can use the interface
indirectly via its implementation class to work around that problem.
@JsExport
object Hello : HelloInterface {
override fun hello() {
console.log("HELLO from HelloInterface")
}
}
Generated JS code for the above
hello
method will have amangled
name. Read more about it in code-mangling section
interface HelloInterface {
@JsName("hello")
fun hello()
}
@JsExport
object Hello : HelloInterface {
override fun hello() {
console.log("HELLO from HelloInterface")
}
}
마찬가지로 다음은 사용할 수 있는 몇 가지 변형입니다
HelloInterface
,// Variation (2)
@JsExport
object HelloGet {
fun getInterface(): HelloInterface {
return Hello
}
}
// Variation (3)
@JsExport
class HelloWrapper(@JsName("value") val value: HelloInterface)
// Variation (4)
@JsExport
data class HelloWrapperData(@JsName("value") val value: HelloInterface)
위의 모든 변형은 인터페이스 사용에 대한
JS
경고가 있더라도 non-exportable
쪽에서 사용할 수 있습니다./**
* JS side calling code
* (1)
* Hello.hello()
*
* (2)
* HelloGet.getInterface().hello()
*
* (3)
* const wrapperObj = HelloWrapper(Hello)
* wrapperObj.value.hello()
*
* (4)
* const wrapperDataObj = HelloWrapperData(Hello)
* wrapperDataObj.value.hello()
*/
예상-실제 패턴 사용
Another idea for using interfaces is to use the expect-actual
pattern to define a Kotlin interface in common
and mobile
platforms and define an external interface
for the JS side. This approach might not scale well but can be very useful for simple cases.
// commonMain
expect interface Api {
fun getProfile(callback: (Profile) -> Unit)
}
// jsMain
// Here external makes it a normal JS object in generated code
actual external interface Api {
actual fun getProfile(callback: (Profile) -> Unit)
}
// mobileMain
actual interface Api {
actual fun getProfile(callback: (Profile) -> Unit)
}
These examples showcase workarounds that might or might not work for a particular project.
열거
As of Kotlin 1.6.10, enums are not exportable. It can create issues for projects that have a lot of existing enums.
Good news is that its support coming in Kotlin 1.6.20
There is also a trick to export and use enums on JS. It requires defining a JS-specific object with attributes that point to actual enums.
For example, this code won't compile,
@JsExport
enum Gender {
MALE,
FEMALE
}
Instead, you can do this indirectly by re-defining them through object fields. It works with a non-exportable warning. Note the warning suppression with annotation.
@Suppress("NON_EXPORTABLE_TYPE")
@ExperimentalJsExport
@JsExport
object GenderType {
val male = Gender.MALE
val female = Gender.FEMALE
}
봉인된 수업
Sealed classes are exportable, but they’re buggy as of Kotlin 1.6.10
You can export a data or regular class as subclasses inside a Sealed class body, but not an object.
@JsExport
sealed class State {
object Loading: State() // This won't be visible
data class Done(val value: String): State() // This would be visible
}
You can work around this problem by moving the subclasses outside the body of the sealed class, but then you cannot write it like State.Loading
. It is more of a readability issue in that case.
Also, sealed classes have known issues with typescript binding as well.
코드 맹글링
The Kotlin compiler mangles the names of the functions and attributes. It can be frustrating to deal with mangled names.
For example,
@JsExport
object Hello : HelloInterface {
override fun hello() {
console.log("HELLO from HelloInterface")
}
}
Generated JS code for hello
method looks like,
Hello.prototype.hello_sv8swh_k$ = function () {
console.log('HELLO from HelloInterface');
};
_something_0, _value_3
와 같은 속성 이름에 숫자가 표시되면 @JsName
측에서 Kotlin
주석을 통해 제어된 이름을 제공해야 한다는 신호입니다.위의 예에서
@JsName("hello")
를 추가한 후 내부적으로 hello
를 참조하는 새로운 hello_sv8swh_k$
메서드가 있는 경우 생성된 코드는 다음과 같습니다.Hello.prototype.hello_sv8swh_k$ = function () {
console.log('HELLO from HelloInterface');
};
Hello.prototype.hello = function () {
return this.hello_sv8swh_k$();
};
Note that
@JsName
is prohibited for overridden members, so you would need to set it to a base class property or method.
중단된 기능
You cannot expose suspended functions to JS. You would need to convert them into JavaScript Promise
object.
The easiest way to do that would be to wrap suspend calls inside,
GlobalScope.promise {
// suspend call
}
This function comes from Promise.kt
in the coroutine library
. It returns a generic type.
As mentioned earlier, some of these issues would get resolved with Kotlin 1.6.20, so keep that in mind.
In the next post, we will look at different ways to distribute Kotlin/JS library since we've some JS exportable code.
Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at @ on Twitter, or Kotlin Slack . 이 모든 것이 흥미롭다면 work with 또는 work at Touchlab을 원할 것입니다.Reference
이 문제에 관하여(@JsExport Kotlin을 JS에 노출하기 위한 가이드), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/touchlab/jsexport-guide-for-exposing-kotlin-to-js-20l9텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)