@ kosexinをJSに露出させるjsexportガイド


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.


前に、我々は追加Kotlin/JS 既存のKMMライブラリへのサポート.さて、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 , デフォルトでは、Javmlcriptの宣言はJavaScriptに公開されません.kotlin宣言をJavaScriptに見えるようにするには@ 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 ライブラリファイルには、グリーティングクラスへの参照はありません.理由は、それが行方不明であるということです@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!"
        }
    }
    
    The .js and .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;
        }
    }
    
    今すぐ呼び出すことができますGreeting JavaScriptから
    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.


    あなたのExportableオブジェクトのすべての公的な属性がまた、移植可能である必要があることを心に留めておくことは重要です.
    次の例ではCustomObj また、輸出に輸出可能である必要があるでしょうMyDataClass ,
    @JsExport
    data class MyDataClass(
        val strVal: String,
        val customObj: CustomObj // This would need to be exportable
    )
    

    @実験的なjsexport対@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:


    マップ
    削除する必要がありますMap 使用方法common また、JSにエクスポートするコード、またはmobile and js 側.あなたはkotlin.js.Json オブジェクトをjsMain その後、それを地図にKotlin 必要なときにマップ.
    JS固有の実装については、Record からkotlin-extensions 図書館.

    リスト
    を置き換えることができますList による使用Array すべてのプラットフォームに対して同じコードを保持する.これは単純な置換ではないかもしれません.
    例えば、Array API応答を解析するためにオブジェクトで使用されるならば、働きます.は、Array インData クラスは自分自身を提供する必要がありますequals and hashcode 実装.

    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 , then kotlin-extensions ライブラリはいくつかの役に立つJS固有のクラスを提供しますIterator, Set, and ReadOnlyArray

    ロング

    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 Aでさえ側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()
     */
    

    Using Expect-Actual Pattern

    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 注釈は、生成された名前を提供します.属性名の数字を見ると_something_0, _value_3 JS側では、それを介して制御名を提供する必要がある記号です@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 or work at TouchLab