@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
  • @ExperimentalJsExport vs @JsExport


  • Limitations
  • Collections
  • Long

  • Interface
  • Solution - Using Implementation class
  • Solution - Using Expect-Actual

  • Enum
  • Sealed classes
  • Code mangling
  • Suspended functions


  • 용법

    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 in root/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.



    내보낼 수 있는 개체의 모든 공용 속성도 내보낼 수 있어야 한다는 점을 염두에 두는 것이 중요합니다.

    다음 예에서 CustomObjMyDataClass를 내보내기 위해 내보낼 수도 있어야 합니다.

    @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 사용을 제거해야 하거나 mobilejs 쪽에서 다른 구현이 있어야 합니다. kotlin.js.Json 쪽에서 jsMain 개체를 사용한 다음 필요할 때마다 Kotlin 맵에 매핑할 수 있습니다.

    JS 특정 구현의 경우 Record 라이브러리에서 kotlin-extensions을 사용할 수도 있습니다.

    목록


    List 사용법을 Array로 바꿔 모든 플랫폼에 대해 동일한 코드를 유지할 수 있습니다. 단순 교체일 수도 있고 아닐 수도 있습니다.

    예를 들어, Array는 API 응답을 구문 분석하기 위해 객체에서만 사용되는 경우 작동합니다. Array 클래스에 Data가 있으면 자신의 equalshashcode 구현을 제공해야 합니다.

    Note that moving from List to Array might have an impact on generated code for iOS. List becomes NSArray on iOS side but Array 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 a mangled 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');
    };
    
    We would need to use the @JsName 주석을 사용하여 생성된 이름을 제공합니다. JS 측에서 _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을 원할 것입니다.

    좋은 웹페이지 즐겨찾기