Edit Page

使用 Kotlin/JS IR 编译器

最终更新: 2024/03/21

Kotlin/JS IR 编译器后端是 Kotlin/JS 的主要创新方向, 并为以后的技术发展探索道路.

Kotlin/JS IR 编译器后端不是从 Kotlin 源代码直接生成 JavaScript 代码, 而是使用一种新方案. Kotlin 源代码首先转换为 Kotlin 中间代码(intermediate representation, IR), 然后再编译为 JavaScript. 对于 Kotlin/JS, 这种方案可以实现更加积极的优化, 并能够改进以前的编译器中出现的许多重要问题, 比如, 生成的代码大小(通过死代码清除), 以及 JavaScript 和 TypeScript 生态环境的交互能力, 等等.

从 Kotlin 1.4.0 开始, 可以通过 Kotlin Multiplatform Gradle 插件使用 IR 编译器后端. 要在你的项目中启用它, 需要在你的 Gradle 构建脚本中, 向 js 函数传递一个编译器类型参数:

kotlin {
    js(IR) { // 或者: LEGACY, BOTH
        // ...
        binaries.executable() // 不兼容 BOTH, 详情请见下文
    }
}
  • IR 对 Kotlin/JS 使用新的 IR 编译器后端.
  • LEGACY 使用旧的编译器后端.
  • BOTH 编译项目时使用新的 IR 编译器以及默认的编译器后端. 主要用于 编写同时兼容于两种后端的库.

从 Kotlin 1.8.0 开始, 旧的编译器后端已被废弃. 从 Kotlin 1.9.0 开始, 使用 LEGACYBOTH 编译器类型会发生错误.

编译器类型也可以在 gradle.properties 文件中通过 kotlin.js.compiler=ir 来设置. 但是这个设置会被 build.gradle(.kts) 中的任何设置覆盖.

顶级属性(top-level property)的延迟初始化(Lazy initialization)

为了改善应用程序的启动速度, Kotlin/JS IR 编译器会对顶级属性(top-level property)进行延迟初始化(Lazy initialization). 通过这种方式, 应用程序启动时不会初始化它的代码中的全部顶级属性. 而只会初始化在启动阶段需要的那些顶级属性; 其他属性的初始化会被延迟, 直到使用它们的代码真正被执行时才会生成属性值.

val a = run { 
    val result = // 假设这里是一段计算密集的代码
    println(result)
    result 
} // 属性值直到初次使用时才会计算

如果由于某些原因你需要(在应用程序启动阶段)提早初始化一个属性, 可以对它标注 @EagerInitialization 注解.

对开发阶段二进制文件进行增量编译

JS IR 编译器提供了 对开发阶段二进制文件的增量编译模式 , 可以对开发过程提高速度. 在这种模式下, 编译器会在模型层级缓存 Gradle task compileDevelopmentExecutableKotlinJs 的结果. 在后续的编译中, 对未修改的源代码文件使用缓存的编译结果, 可以使得编译更快完成, 尤其是在对代码进行少量修改的情况.

增量编译是默认启用的. 如果要对开发阶段二进制文件禁用增量编译, 请向项目的 gradle.propertieslocal.properties 文件添加以下设置:

kotlin.incremental.js.ir=false // 默认为 true

在增量编译模式中, 完整编译通常会变得更慢, 因为需要创建和生成缓存.

输出 .js 文件: 对每模块输出一个文件, 或对整个项目输出一个文件

作为编译结果, JS IR 编译器对项目的每个模块输出单独的 .js 文件. 你也可以选择将整个项目编译为单个 .js 文件, 方法是向 gradle.properties 添加以下设置:

kotlin.js.ir.output.granularity=whole-program // 默认为 'per-module'

忽略编译错误

忽略编译错误 模式还处于 实验阶段. 它随时有可能变更或被删除. 使用这个功能需要明确要求使用者同意(详情请见下文), 而且你应该只用来进行功能评估, 不要用在你的正式产品中. 希望你能通过我们的 问题追踪系统 提供你的反馈意见.

Kotlin/JS IR 编译器提供了一个在默认的编译器后端中没有的新编译模式 – 忽略编译错误 模式. 在这个模式下, 即使代码包含错误, 你也可以试用你的应用程序. 比如, 你可能在进行一个非常复杂的代码重构时, 或者在编写系统的某一部分, 与发生编译错误的另一部分完全无关.

在这个新的编译器模式下, 编译器会忽略所有的错误代码. 因此, 你可以运行应用程序, 并试用与错误代码无关的那部分功能. 如果你试图运行那些存在编译错误的代码, 那么将会发生运行期异常.

要忽略你代码中的编译错误, 可以选择两种错误宽容策略:

  • SEMANTIC. 编译器会接受语法正确但语义上无意义的代码. 比如, 将数值赋值给字符串变量 (类型不匹配).
  • SYNTAX. 编译器会接受任何代码, 即使包含语法错误. 无论你编写什么样的代码, 编译器都会尝试生成可执行的代码.

作为一个试验性的功能, 忽略编译错误需要使用者同意(Opt-in). 要开始这个模式, 需要添加 -Xerror-tolerance-policy={SEMANTIC|SYNTAX} 编译器选项:

kotlin {
    js(IR) {
        compilations.all {
            compileTaskProvider.configure {
                compilerOptions.freeCompilerArgs.add("-Xerror-tolerance-policy=SYNTAX")
            }
        }
    }
}

在产品(Production)模式中对成员名称的极简化(Minification)

Kotlin/JS IR 编译器会使用它的内部信息 关于 你的 Kotlin 类和函数之间的关系, 来实现更加有效的极简化(Minification), 缩短函数, 属性, 和类的名称. 这样可以缩减打包完成的应用程序的大小.

当你使用 产品(Production) 模式构建你的 Kotlin/JS 应用程序时, 会自动应用这样的极简化处理, 并默认启用. 要关闭对成员名称的极简化处理, 请使用 -Xir-minimized-member-names 编译器选项:

kotlin {
    js(IR) {
        compilations.all {
            compileTaskProvider.configure {
                compilerOptions.freeCompilerArgs.add("-Xir-minimized-member-names=false")
            }
        }
    }
}

预览: 生成 TypeScript 声明文件 (d.ts)

生成 TypeScript 声明文件 (d.ts)功能还处于 实验阶段. 它随时有可能变更或被删除. 使用这个功能需要明确要求使用者同意(详情请见下文), 而且你应该只用来进行功能评估, 不要用在你的正式产品中. 希望你能通过我们的 问题追踪系统 提供你的反馈意见.

Kotlin/JS IR 编译器能够从你的 Kotlin 代码生成 TypeScript 定义. 在开发混合 App(hybrid app)时, JavaScript 工具和 IDE 可以使用这些定义, 来提供代码自动完成, 支持静态分析, 使得在 JavaScript 和 TypeScript 项目中包含 Kotlin 代码变得更加便利.

如果你的项目输出可执行文件 (binaries.executable()), Kotlin/JS IR 编译器会收集所有标注了 @JsExport 注解的顶级声明, 并自动在一个 .d.ts 文件中生成 TypeScript 定义.

如果你想要生成 TypeScript 定义, 你需要在 Gradle 构建文件中明确进行配置. 请在你的 build.gradle.kts 文件的 js 小节 中添加 generateTypeScriptDefinitions(). 例如:

kotlin {
    js {
        binaries.executable()
        browser {
        }
        generateTypeScriptDefinitions()
    }
}

这些声明位于 build/js/packages/<package_name>/kotlin 目录中, 与相应的未经 webpack 处理的 JavaScript 代码在一起.

IR 编译器目前的限制

新的 IR 编译器后端的一个重要变化是与默认后端之间 没有二进制兼容性. 使用新的 IR 编译器后端创建的库, 会使用 klib 格式, 在默认后端中将无法使用. 同时, 使用旧编译器创建的库, 就是一个包含 js 文件的 jar, 也不能在 IR 后端中使用.

如果你希望在你的项目中使用 IR 编译器后端, 那么需要 将所有的 Kotlin 依赖项升级到支持这个新后端的版本. JetBrains 针对 Kotlin/JS 平台, 对 Kotlin 1.4+ 版本发布的库, 已经包含了使用新的 IR 编译器后端所需要的全部 artifact.

如果你是库的开发者, 希望为现在的编译器后端和新的 IR 编译器后端同时提供兼容性, 请阅读 针对 IR 编译器编写库 小节.

与默认后端相比, IR 编译器后端还存在一些差异. 试用新的后端时, 需要知道存在这些可能的问题.

  • 有些 库依赖于默认后端的独有的特性, 比如 kotlin-wrappers, 可能会出现一些问题. 你可以通过 YouTrack 跟踪这个问题的调查结果和进展.
  • 默认情况下, IR 后端 完全不会让 Kotlin 声明在 JavaScript 中可见. 要让 JavaScript 可以访问 Kotlin 声明, 这些声明 必须 添加 @JsExport 注解.

将既有的项目迁移到 IR 编译器

由于两种 Kotlin/JS 编译器的接口签名的不同, 要让你的 Kotlin/JS 代码能由 IR 编译器来编译, 可能需要你修改部分代码. 关于如何将既有的 Kotlin/JS 工程迁移到 IR 编译器, 请参见 Kotlin/JS IR 编译器 迁移向导.

针对 IR 编译器开发向后兼容的库

如果你是库的维护者, 希望同时兼容默认后端和新的 IR 编译器后端, 有一种编译器选择设置可以让你对两种后端都创建 artifact, 因此可以支持下一代 Kotlin 编译器, 同时又对你的既有用户保持兼容性. 这就是所谓的 both 模式, 可以在 gradle.properties 文件中通过 kotlin.js.compiler=both 来启用, 或者, 也可以在 build.gradle(.kts) 文件的 js 代码块之内, 设置为项目独有的选项:

kotlin {
    js(BOTH) {
        // ...
    }
}

使用 both 模式时, 从你的源代码构建库时, IR 编译器后端和默认编译器后端都会被使用(如同它的名称所指的那样). 也就是说, Kotlin IR 的 klib 文件, 以及默认编译器的 jar 文件都会生成. 当发布到相同的 Maven 路径时, Gradle 会根据使用场景自动选择正确的 artifact – 对旧的编译器是 js, 对新的编译器是 klib. 因此对于使用两种编译器后端中的任何一个的项目, 你都可以编译并发布你的库.