原文连接:点此进入,原作者:Federico Tomassetti
本文与以下代码仓库匹配,可以此获取新新的代码。
为什么说 WebAssembly 是意义非凡的?
对于此问题的完整答案,请阅读我们的 WebAssembly 简介: 你为什么需要关注?
简而言之,WebAssembly 可以将非常复杂的应用程序编译为高效的二进制格式,并且可以在 Web 浏览器中以优异的性能运行。
在此之前,我们只有 JavaScript,现在有了用于 Web 的程序集,并且可以将各种语言编译为WebAssembly(也称作 WASM)。 想想 C,C ++,Rust 和 …Kotlin,显然全部可被编译为WASM。
浏览器对 WASM 的支持
在写 71% 浏览器已支持 WASM 的时候,Edge、Firefox、Chrome、Safari 已全部支持 WASM。还在使用 IE 或是怪异的移动浏览器的用户被冷落了,但是所有在本世纪下载了桌面浏览器的用户均已得到了 WASM 的支持。
在某些浏览器中 WASM 已被支持,但在默认情况下禁用。 在最近的 Chrome 和 Firefox 上,WASM 是默认启用的。
是时候用上 WASM了,还是说你依然想玩 Cobol 和Fortran?
Kotlin 对 WASM 支持的现状
我们应该注意的第一件事是 Kotlin 通过其 Kotlin/Native 编译器来支持 WASM。 Kotlin/Native 编译器基于LLVM,并且LLVM支持 WebAssembly,因此我们可以从 Kotlin 源代码获取 WASM 文件。
很好,但这不是我们所需要的。在将 Kotlin 编译为 WASM 时,我们需要更多的东西来提高生产力,并且目前情况非常艰难,我们需要更强大的支持才能完成。但是到目前为止,编译为 WASM 的事情比将 Kotlin 编译为 JVM 或 JavaScript 要困难得多。 你喜欢生活在边缘并展望未来吗? 很酷,但是不要期望一流的服务。
如何通过命令行执行 Kotlin/Native 编译器
在以下两种场景下,你需要从命令行执行 Kotlin/Native 编译器:
当你打算使用 Gradle 和 Konan 插件来编译项目时
当你打算使用 jsinterop(你可以在 伴生仓库 里找到预编译好的文件) 来编译库文件时
无论如何,只要你希望能够直接调用编译器,就需要执行此操作。
首先,你需要下载 Kotlin/Native 的二进制文件。 你可以在 这里 找到它们。
下载二进制文件后,解压缩它们,然后将二进制文件添加到 PATH。 这是很古老是的但是很有用的做法,也许你可以编写自己的小脚本来做到这一点:
#!/usr/bin/env bash
KOTLIN_NATIVE_HOME=/Users/federico/tools/kotlin-native-macos-0.6.2
export PATH=$KOTLIN_NATIVE_HOME/bin:$PATH
现在我们可以看看使用 Gradle 时是如何运作的。
如何使用 Gradle 来编译你的 Kotlin WASM 项目
那很简单,你所需要做的只是在你的 build.gradle
文件贴上这些代码:
buildscript {
repositories {
jcenter()
maven { url "http://kotlin.bintray.com/kotlinx" }
maven { url "https://plugins.gradle.org/m2/" }
maven { url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies" }
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version"
classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:$kotlin_native_version"
}
}
apply plugin: 'konan'
konanArtifacts {
program('stats', targets: ['wasm32']) {
srcDir 'src/main/kotlin'
libraries {
useRepo 'lib'
klib 'dom'
}
}
}
现在,你只需使用构建目标就可以了。
并且,几乎在所有场景下,你依然需要使用 DOM 库。
DOM 库
当前你可能要与 WASM 一起使用两个库,你可以使用 Kotlin/Native 附带的 jsinterop 工具来构建它们。
尝试一下,这不是真的超级灵活吗?
似乎在 Kotlin 和 WASM 运行时,只支持 两个库,因此你可以构建这两个库,而无需使用 jsinterop 工具。 为了节省时间,我只建立了 DOM 和数学库并将其添加到仓库的 lib 目录中。
好的,现在我们可以真正开始了,我们只需要启动我们的IDE,对吗?但是我有件事必须告诉你...
IDE 的问题
当前仅有一个支持 Kotlin/Native 并同时支持将 Kotlin 应用编译为 WebAssembly 的 IDE,它就是由 Jetbrains 提供的 CLion,并且它并非免费的。
这算是一个问题吗?是的,不过并不像你想象的那么糟。
首先,许多人都是在公司中靠软件赚钱的专业人员,因此购买一些工具不应成为禁忌。不过我知道这对于所有正在学习且无力支付许可证费用的孩子来说都是个问题。 另外还有两件事需要考虑:
1 – 不使用 IDE 是可能的
你可以只使用 IntelliJ IDEA 来高亮 Kotlin 语法,但是 IDEA 无法理解 WASM 库,并且不知道如何编译为wasm,因此你必须从命令行来进行编译。 基本上在这个场景下 IntelliJ IDEA 只作为一个平常的编辑器,而不是完整的IDE。
2 – 免费的 IDE 似乎快要来了
因此,不要过分强调IDE。 如果您保持内心纯洁,并一直真诚地希望这一点,那么伟大的事情将会发生。 同时,让我们像祖父们一样编程。 或者就像还在使用 vim 的怪异家伙那样。
我们提供的实例
我们提供的实例基于 Jetbrains 在第一届 KotlinConf 上所采用的应用程序。
该应用程序读取一些投票数据,并不断更新图表以显示五个团队之间的投票分布。
它看起来像这样:
好的,来看一下要如何实现它。
文件 src/main/kotlin/main.kt
让我们从 main 函数开始:
import kotlinx.interop.wasm.dom.*
import kotlinx.wasm.jsinterop.*
fun loop(canvas: Canvas) {
fetch("/stats.json").
then { args: ArrayList ->
val response = Response(args[0])
response.json()
}.
then { args: ArrayList ->
val json = args[0]
val colors = JsArray(json.getProperty("colors"))
assert(colors.size == Model.tupleSize)
val tuple = arrayOf(0, 0, 0, 0, 0)
for (i in 0 until colors.size) {
val color = colors[i].getInt("color")
val counter = colors[i].getInt("counter")
tuple[color - 1] = counter
}
Model.push(tuple)
}.
then { View(canvas).render() }
}
fun main(args: Array<String>) {
val canvas = document.getElementById("myCanvas").asCanvas
setInterval(100) {
loop(canvas)
}
}
main 函数从 DOM 里找到 canvas 元素,然后开启一个无限循环,在这个循环里,每隔 100 毫秒就触发一次循环函数.
循环函数里做了什么呢?
从 stats.json 获取数据
拿到数据把它们塞进 Model
通知视图更新,以显示新的数据
文件 src/main/kotlin/model.kt
现在我们来看看 Model,需要注意,这是一个对象,而不是一个类。 这意味着我们只有 Model 的一个实例,而系统的其余部分(最重要的是 View)可以在不需要引用的情况下访问它。
object Model {
val tupleSize = 5
val styles = Style.styles
val backLogSize = 100
private val backLog = IntArray(backLogSize * tupleSize, {0})
private fun offset(time: Int, color: Int) = time * tupleSize + color
var current = 0
var maximal = 0
fun colors(time: Int, color: Int): Int = backLog[offset(time, color)]
fun tuple(time: Int) = backLog.slice(time * tupleSize .. (time + 1) * tupleSize - 1)
fun push(new: Array<Int>) {
assert(new.size == tupleSize)
new.forEachIndexed { index, it ->
backLog[offset(current, index)] = it
}
current = (current+1) % backLogSize
new.forEach {
if (it > maximal) maximal = it
}
}
}
这个 model 做了什么呢?
该模型仅接收数据数组(每个“团队”一个值)并将其添加到待办事项列表中。 待办事项列表基本上是 500 个值的数组,换句话说,就是最后 100 个条目,每个条目有 5 个值。
最初,它被设置为仅包含零,但随着时间的流逝,它开始被通过推入接收的实际值填充。
文件 src/main/kotlin/view.kt
太好了,现在我们有了数据,现在是展示该数据的时候了。
在视图文件中,我们有一个对象和两个类:
Style 包含一些关于颜色的常数
Layout 包含有关元素位置,填充,大小等的常量
View 是有趣的事情发生的地方
基本上在View中,我们绘制数据并更新标签。 最外部的标签仅表示团队的 ID:1到5之间的数字。更多的内部标签表示每个团队的最新值。
import kotlinx.interop.wasm.dom.*
import kotlinx.wasm.jsinterop.*
object Style {
val backgroundColor = "#16103f"
val teamNumberColor = "#38335b"
val fontColor = "#000000"
val styles = arrayOf("#ff7616", "#f72e2e", "#7a6aea", "#4bb8f6", "#ffffff")
}
open class Layout(val rect: DOMRect) {
val lowerAxisLegend = 0.1
val fieldPartHeight = 1.0 - lowerAxisLegend
val teamNumber = 0.10
val result = 0.20
val fieldPartWidth = 1.0 - teamNumber - result
val teamBackground = 0.05
val legendPad = 50
val teamPad = 50
val resultPad = 40
val teamRect = 50
val rectLeft = rect.getInt("left")
val rectTop = rect.getInt("top")
val rectRight = rect.getInt("right")
val rectBottom = rect.getInt("bottom")
val rectWidth = rectRight - rectLeft
val rectHeight = rectBottom - rectTop
val fieldWidth: Int = (rectWidth.toFloat() * fieldPartWidth).toInt()
val fieldHeight: Int = (rectHeight.toFloat() * fieldPartHeight).toInt()
val teamWidth = (rectWidth.toFloat() * teamNumber).toInt()
val teamOffsetX = fieldWidth
val teamHeight = fieldHeight
val resultWidth = (rectWidth.toFloat() * result).toInt()
val resultOffsetX = fieldWidth + teamWidth
val resultHeight = fieldHeight
val legendWidth = fieldWidth
val legendHeight = (rectWidth.toFloat() * lowerAxisLegend)
val legendOffsetY = fieldHeight
}
class View(canvas: Canvas): Layout(canvas.getBoundingClientRect()) {
val context = canvas.getContext("2d");
fun poly(x1: Int, y11: Int, y12: Int, x2: Int, y21: Int, y22: Int, style: String) = with(context) {
beginPath()
lineWidth = 2; // In pixels
setter("strokeStyle", style)
setter("fillStyle", style)
moveTo(x1, fieldHeight - y11)
lineTo(x1, fieldHeight - y12)
lineTo(x2, fieldHeight - y22)
lineTo(x2, fieldHeight - y21)
lineTo(x1, fieldHeight - y11)
fill()
closePath()
stroke()
}
fun showValue(index: Int, value: Int, color: String) = with(context) {
val textCellHeight = teamHeight / Model.tupleSize
val textBaseline = index * textCellHeight + textCellHeight / 2
// The team number rectangle
fillStyle = Style.teamNumberColor
fillRect(teamOffsetX + teamPad, teamHeight - textBaseline - teamRect/2, teamRect, teamRect)
// The team number rectangle
fillStyle = color
fillRect(resultOffsetX, teamHeight - textBaseline - teamRect/2, teamRect/2, teamRect)
}
fun showText(index: Int, value: Int, color: String) = with(context) {
val textCellHeight = teamHeight / Model.tupleSize
val textBaseline = index * textCellHeight + textCellHeight / 2
// The team number in the rectangle
setter("font", "16px monospace")
setter("textAlign", "center")
setter("textBaseline", "middle")
fillStyle = Style.fontColor
fillText("${index + 1}", teamOffsetX + teamPad + teamRect/2, teamHeight - textBaseline, teamWidth)
// The score
setter("textAlign", "right")
fillStyle = Style.fontColor
fillText("$value", resultOffsetX + resultWidth - resultPad, resultHeight - textBaseline, resultWidth)
}
fun showLegend() = with(context){
setter("font", "16px monospace")
setter("textAlign", "left")
setter("textBaseline", "top")
fillStyle = Style.fontColor
fillText("-10 sec", legendPad, legendOffsetY + legendPad, legendWidth)
setter("textAlign", "right")
fillText("now", legendWidth - legendPad, legendOffsetY + legendPad, legendWidth)
}
fun scaleX(x: Int): Int {
return x * fieldWidth / (Model.backLogSize - 2)
}
fun scaleY(y: Float): Int {
return (y * fieldHeight).toInt()
}
fun clean() {
context.fillStyle = Style.backgroundColor
context.fillRect(0, 0, rectWidth, rectHeight)
}
fun render() {
clean()
// we take one less, so that there is no jump from the last to zeroth.
for (t in 0 until Model.backLogSize - 2) {
val index = (Model.current + t) % (Model.backLogSize - 1)
val oldTotal = Model.tuple(index).sum()
val newTotal = Model.tuple(index + 1).sum()
if (oldTotal == 0 || newTotal == 0) continue // so that we don't divide by zero
var oldHeight = 0;
var newHeight = 0;
for (i in 0 until Model.tupleSize) {
val style = Model.styles[i]
val oldValue = Model.colors(index, i)
val newValue = Model.colors(index+1, i)
val x1 = scaleX(t)
val x2 = scaleX(t+1)
val y11 = scaleY(oldHeight.toFloat() / oldTotal.toFloat())
val y21 = scaleY(newHeight.toFloat() / newTotal.toFloat())
val y12 = scaleY((oldHeight + oldValue).toFloat() / oldTotal.toFloat())
val y22 = scaleY((newHeight + newValue).toFloat() / newTotal.toFloat())
poly(x1, y11, y12, x2, y21, y22, style);
oldHeight += oldValue
newHeight += newValue
}
}
for (i in 0 until Model.tupleSize) {
val value = Model.colors((Model.current + Model.backLogSize - 1) % Model.backLogSize, i)
showValue(i, value, Model.styles[i])
}
for (i in 0 until Model.tupleSize) {
val value = Model.colors((Model.current + Model.backLogSize - 1) % Model.backLogSize, i)
showText(i, value, Model.styles[i])
}
showLegend()
}
}
现在我们有代码了,但是如何使用它?
我们将在下一段中看到这一点。
整合
我们将把 Web 应用程序打包,现在我们需要:
一个得到数据并显示的方法
一个 HTML 页面
包含了编译后代码的 wasm 文件
一个 js 文件用于调用 wasm 代码
为了简便起见,我们将直接从简单的 JSON 文件中获取数据。 当然,在实际的应用程序中,您可能希望数据源更具动态性……
这个就是我们秀出天际的 stats.json:
{
"colors" : [
{
"color": 1,
"counter": 4
},
{
"color": 2,
"counter": 14
},
{
"color": 3,
"counter": 9
},
{
"color": 4,
"counter": 7
},
{
"color": 5,
"counter": 6
}
]
}
HTML页面实际上将非常简单,因为它将仅包含画布和加载脚本的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>WASM with Kotlin</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0px;
}
</style>
</head>
<body>
<canvas id="myCanvas">
</canvas>
<script wasm="./stats.wasm" src="./stats.wasm.js"></script>
</body>
</html>
最后是 stats.wasm 和 stats.wasm.js。 我们通过运行 ./gradlew build
来获得它们。 但这还不够,我们还需要将这些文件放在我们 http 服务的目录中。 因此我们要做的只是将 wasm 和 wasm.js 文件从 build/konan/bin/wasm32/
复制到 web
。
现在我们有了所有需要的东西,我们只需要将所有内容发送到浏览器即可。 使用一个非常简单的 http 服务器即可。
使用 simplehttp2server 来提供文件服务
在开发期间,我喜欢使用一个名为 simplehttp2server 的简单解决方案。 你肯定是一个聪明的读者,能够弄清楚如何在平台上安装它或找到有效的替代方法。
举例来说,在 mac 上你可以简单的执行:
brew tap GoogleChrome/simplehttp2server https://github.com/GoogleChrome/simplehttp2server
brew install simplehttp2server
安装完毕后,你要做的就是进入 web 目录并运行 simplehttp2server
。 此时的目录应包含 html 文件,wasm 文件,wasm.js 文件和 json 文件。
现在你可以访问 http://localhost:5000 并看到这个应用程序;
好的,什么都没发生,但是如果你打开 stats.json 文件并进行更改,就会看到图像更改。
从 WASM 调用 JavaScript 函数
与 JavaScript 的互操作性是需要改进的。 基本上,编译器生成的包装器向 WASM 文件公开了一些符号。 问题在于,目前显然没有适当的方法向该列表添加更多符号。
假设我们想要添加一个函数以在收到数据后显示警报,我们需要像这样修改 Model 文件:
object Model {
val tupleSize = 5
val styles = Style.styles
val backLogSize = 100
private val backLog = IntArray(backLogSize * tupleSize, {0})
private fun offset(time: Int, color: Int) = time * tupleSize + color
var current = 0
var maximal = 0
fun colors(time: Int, color: Int): Int = backLog[offset(time, color)]
fun tuple(time: Int) = backLog.slice(time * tupleSize .. (time + 1) * tupleSize - 1)
fun push(new: Array<Int>) {
igotdata()
assert(new.size == tupleSize)
new.forEachIndexed { index, it ->
backLog[offset(current, index)] = it
}
current = (current+1) % backLogSize
new.forEach {
if (it > maximal) maximal = it
}
}
}
@SymbolName("imported_igotdata")
external public fun igotdata()
好的,但是如何将 JS 函数公开给 Kotlin 代码?
不,目前没有适当的方法,但是你可以使用以下技巧:
在加载程序创建了一些结构后,故意使加载失败
在这些结构中插入一些额外的符号
运行 WASM 文件
对于第一点,我们可以简单地从脚本中删除 wasm 属性:
<script src="./stats.wasm.js"></script>
现在,如果您尝试加载页面,则会出现错误:
现在,让我们插入符号 imported_igotdata
并运行 webassembly:
<script>
konan.libraries.push({"imported_igotdata":function(msg){ alert("I got the data, updating");}})
var filename = "./stats.wasm";
fetch(filename).then( function(response) {
return response.arrayBuffer();
}).then(function(arraybuffer) {
instantiateAndRun(arraybuffer, [filename]);
});
</script>
建议你在尝试此代码之前将循环从 100ms 减慢到 1000ms。
从 JavaScript 调用 WASM 函数
为此我们可以使用 WebAssembly 对象来编译整个脚本,然后运行它们或运行单个函数。 但这并不是 Kotlin 特有的。
摘要
第一个将 Kotlin 应用程序编译为WASM 为实例
到目前为止还是非常原始的,它显然存在一些问题:
只提供有限的标准库
唯一的 IDE 不是免费的
但是在 Kotlin 的世界中,一切都发展得很快。
我们已经支持跨平台项目。 这意味着我们可以从现在开始构建可在 JVM 和 JavaScript 的世界中使用并编译为 wasm 的库。
一个免费的 IDE 即将面世。 Kotlin/Native 编译器正在快速发展。
事情将很快变得非常有趣,我们建议你做好准备,并开始意识到这个新世界的模样。