Создание полнофункционального веб-приложения на Котлине

Мы говорим об API в стиле микросервисов и о скриптах на стороне браузера на Kotlin (Kotlin2JS).

Поскольку Kotlin теперь является официально поддерживаемым языком для Android, вполне естественно, что люди начнут принимать Kotlin быстрее, чем Анджелина Джоли сможет усыновить детей из Африки. Большинство из нас знает о том, что мы можем просто добавить Kotlin в любой проект Java / J2EE без каких-либо проблем. Однако, как было ясно показано на сеансе ввода-вывода Google на Kotlin, Kotlin также можно интерпретировать вплоть до JavaScript и Objective-C. Это означает, что вы можете иметь API, веб-приложение, приложение для Android, приложение для iOS и использовать для этого только Kotlin. Какие могут быть более захватывающие возможности для тех, кто хочет изучить Kotlin!

Это испытание 🔥!

Не поклонник испытания огнем? Не бойтесь, молодой падаван, Доктор поможет вам прожить долгую жизнь и преуспеть в этом путешествии, а также позаботиться о том, чтобы, что бы ни случилось, вы не пересекли ручьи 👻 (Я еще не обидел всех гиков?)

Здесь есть о чем поговорить, поэтому, как я уже упоминал во вступлении, мы будем рассматривать только создание API и создание скриптов на стороне браузера с использованием Kotlin. О создании приложений для Android с использованием Kotlin написано / записано много, поэтому я не чувствую необходимости побеждать мертвого осла при создании приложений для Android на Kotlin.

Настройка вашего проекта

Вначале я хотел бы отметить, что я собираюсь сделать это предельно простым. Таким образом, структура моего проекта может немного отличаться от примеров Kotlin, которые вы можете найти в Интернете. Во-первых, мы установили несколько ожиданий:

  • Мы будем использовать Gradle для сборки нашего проекта.
  • Вы не можете использовать плагин kotlin и плагин kotlin2js в одном модуле, поэтому мы создадим проект с полным стеком и добавим к нему модули backend и frontend. Преимущество этого подхода состоит в том, что вы можете поместить весь свой общий код в проект «полного стека», хотя это не тот способ, которым большинство примеров в Интернете структурируют свой код.
  • Мы настраиваем API в стиле микросервисов. Так что ожидайте, что он будет самодостаточным, минималистичным. В этом случае у Kotlin есть проект под названием ktor, который обеспечивает интеграцию с Netty и Jetty в качестве встроенного сервера. Мы будем использовать ktor с Netty, имейте в виду, что ktor использует сопрограммы и может быть не готов к производству.
  • Не ожидайте, что этот код будет оптимизирован и готов к работе. Это просто пример того, что возможно с Kotlin.
  • Я не собираюсь использовать сервер webpack-dev или что-то в этом роде для отслеживания изменений в файлах, поэтому вам придется создавать вручную после каждого изменения. Это позволит вам привыкнуть к происходящему, и после этого вы сможете автоматизировать процесс по своему усмотрению.
  • Я буду использовать IntelliJ IDEA CE.

Начните с запуска IntelliJ IDEA ›Создать новый проект

Настройте свой проект полного стека Kotlin, как показано ниже. Мы собираемся использовать Gradle, чтобы выбрать его слева, а справа добавим поддержку Java и Kotlin (Java). Цель проекта полного стека - разместить общий код, если он у нас есть, поэтому поддержка JavaScript не требуется.

Ваш GroupId - это в основном имя пакета, которое вы хотите дать проекту. Ваш ArtifactId похож на название проекта.

Мне нравится проверять «Использовать автоматический импорт» и «Автоматически создавать каталоги для пустых корней содержимого». Это стилистический выбор, вы можете оставить параметры по умолчанию и продолжить, если хотите.

Выберите фактическое имя проекта и путь.

Когда вы нажмете «Готово», вы увидите проект, как показано ниже. Теперь мы собираемся добавить серверную часть и интерфейсный модуль.

Щелкните правой кнопкой мыши имя проекта и добавьте новый модуль, как показано.

Сначала мы настроим бэкэнд-модуль. Настройка очень похожа на процесс, который мы прошли для проекта полного стека. Вы можете выбрать Java и Kotlin в фреймворках и библиотеках, которые хотите поддерживать, или просто Kotlin, если хотите.

Вы можете щелкнуть правой кнопкой мыши и открыть изображения в новой вкладке. Я поместил все три строки в строку, чтобы сэкономить место.

Теперь вы должны увидеть интерфейсный модуль в структуре вашего проекта. Снова щелкните правой кнопкой мыши имя проекта и добавьте другой модуль для внешнего интерфейса. На этот раз мы хотим добавить поддержку Kotlin (JavaScript), а не «Java».

Остальная часть настройки очень похожа на приведенную выше.

В конце этого процесса у вас должен быть проект со структурой, как показано.

Настройка нашего API стиля микросервисов

Мы настроим файл Kotlin, который будет содержать наш микросервис. Если вы посмотрите файл readme для ktor, вы увидите пример того, как это сделать. На снимке экрана ниже показано сообщение, которое вы получите при первом изменении файла build.gradle. Обычно я просто принимаю это предложение.

Мы собираемся добавить зависимость от ktor и Gson для нашего проекта. Я выделил изменения, которые я внес в файл build.gradle модулей серверной части.

Примечание.

Код моего микросервиса

У нас будет простой микросервис, который будет принимать в качестве параметра пути число и возвращать JsonArray этого размера.

Щелкните правой кнопкой мыши каталог ваших внутренних модулей src / main / kotlin и выберите запуск нового файла kotlin. Какое имя вы ему дадите, на данном этапе не имеет значения. Я назвал свой файл Main.kt.

Код для Main.kt выглядит следующим образом:

//Main.kt
import com.google.gson.Gson
import org.jetbrains.ktor.application.call
import org.jetbrains.ktor.host.embeddedServer
import org.jetbrains.ktor.http.ContentType
import org.jetbrains.ktor.netty.Netty
import org.jetbrains.ktor.response.header
import org.jetbrains.ktor.response.respondText
import org.jetbrains.ktor.routing.get
import org.jetbrains.ktor.routing.routing

fun main(args: Array<String>) {
    embeddedServer(Netty, 8080) {
        routing {
            get("/api/ping/{count?}") {
                var count: Int = Integer.valueOf(call.parameters["count"]?: "1")
                if (count < 1) {
                    count = 1
                }
                var obj = Array<Entry>(count, {i -> Entry("$i: Hello, World!")})
                val gson = Gson()
                var str = gson.toJson(obj)
                call.response.header("Access-Control-Allow-Origin", "*")
                call.respondText(str, ContentType.Application.Json)

            }
        }
    }.start(wait = true)
}

data class Entry(val message: String)

Когда вы запускаете этот сервер, встроенный сервер Netty будет работать на порту 8080 с использованием сопрограмм Kotlin. Он будет прослушивать GET на / api / ping и необязательный параметр пути счетчика. Затем он создаст массив объектов Entry, количество объектов зависит от переданного счетчика. Введите класс данных, который является сокращением для Java Bean. Запись определяется внизу, это просто класс с одной строкой «сообщения» в нем). Мы используем Gson для преобразования массива в JSON и его возврата.

Примечание. На этом этапе требуется заголовок Access-Control-Allow-Origin, потому что на нашем локальном компьютере интерфейсная часть будет работать на другом http-сервере и будет иметь другой порт, поэтому браузер будет жаловаться на междоменные атаки. Чтобы обойти эту проблему, мы добавляем заголовок.

Скопировав этот код в Main.kt, вы можете щелкнуть правой кнопкой мыши Main.kt и выбрать Run ‘Main.kt’.

Если вы теперь посетите http: // localhost: 8080 / api / ping, вы должны увидеть результат. Вы можете добавить необязательный параметр пути, если вам нравится http: // localhost: 8080 / api / ping / 3.

Вот и все, у вас есть простая настройка микросервиса!

Настроить веб-приложение

Мы уже создали интерфейсный модуль.

Теперь нам нужно настроить наш интерфейсный файл build.gradle для вывода файлов JavaScript в правильном месте и настроить нашу веб-страницу.

Настройте веб-страницу

Сначала мы настраиваем веб-страницу, потому что это даст нам представление о том, где IntelliJ создает проект. HTML-страница будет создана в каталоге front-end ›src› main ›resources. Назовите его index.html. Вы можете настроить его как хотите. вы можете увидеть мой код для index.html ниже.

<!DOCTYPE html> <!-- index.html -->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Console Output</title>
</head>
<body>
<div style="display:block">
    <label for="count_id">Enter number of elements to fetch:</label>
    <input id="count_id" name="count" value="1"/>
</div>
<button id="button_id" type="button" style="display:block">Submit</button>
<textarea id="textarea_id" style="width: 100%; height: 200px">
</textarea>

<script type="text/javascript" src="js/lib/kotlin.js"></script>
<script type="text/javascript" src="js/kotlinfrontend_main.js"></script>
</body>
</html>

Причина, по которой важно сначала настроить веб-страницу, заключается в том, что мы хотим увидеть, что произойдет, когда вы скажете IntelliJ IDEA создать интерфейсный модуль. Если вы нажмете на интерфейсный модуль, перейдите в Сборка ›Сборка модуля…, вы увидите, что IntelliJ выводит директорию ресурсов в каталог проекта на том же уровне, что и модули. Это красный каталог под названием out /. Мы хотим вывести наш JavaScript в том же каталоге, желательно по пути, который мы указали в тегах ‹script› выше.

Настройте свой интерфейсный файл build.gradle

Теперь, когда вы знаете, где находятся ваши ресурсы при создании проекта, этот следующий шаг будет иметь больше смысла, мы хотим настроить Kotlin2JS для вывода нашего JavaScript в том же каталоге, и мы делаем это, настраивая наши интерфейсные модули build.gradle файл (посмотрите на последние 4 строки).

Если вам нужно скопировать и вставить это, вы можете получить код из моего репозитория на GitHub.

Теперь нам нужно написать немного Kotlin для генерации JavaScript.

Написание Kotlin для генерации JavaScript

Признаюсь, эта часть показалась мне немного странной. По какой-то причине я могу сделать логический переход к использованию Kotlin в качестве альтернативы Java с использованием API-интерфейсов Java, и поэтому я отчасти ожидал, что смогу использовать API-интерфейсы Java для написания JavaScript. Оказалось, что это совсем не так. Вместо этого написание Kotlin для браузера было очень похоже на написание JavaScript, в котором было что-то вроде гибридного приложения Java.

Щелкните правой кнопкой мыши каталог внешнего интерфейса ›src› main ›kotlin и создайте новый файл Kotlin с именем main.kt. Обратите внимание, что main.kt написан в нижнем регистре, поэтому вы знаете, что это файл JavaScript, а не файл класса Java. Вы можете дать ему другое имя, я понимаю, что это может сбивать с толку многих людей. Если вы дадите ему другое имя, вам придется изменить теги скрипта в вашем HTML, подробнее об этом позже.

Теперь мы собираемся написать код, чтобы взять число с веб-страницы и использовать его для вызова нашего API и записи вывода на веб-страницу. Это потребует от нас привязки элементов DOM, прослушивания событий и даже выполнения асинхронного вызова AJAX для нашего собственного API.

Мне нравится думать, что приведенный ниже код не требует пояснений. Но я попытаюсь кое-что объяснить. Основная проблема, с которой я столкнулся при написании этого, заключалась в том, что я чувствовал, что API не очень хорошо документирован, и, конечно, не хватало примеров. Отсюда и эта статья.

import org.w3c.dom.Element
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.events.Event
import org.w3c.dom.get
import org.w3c.xhr.XMLHttpRequest
import kotlin.browser.document
import kotlin.browser.window
import kotlin.js.Json

fun main(args: Array<String>) {
    window.onload = {
        fetch("1")
        //Example of how to add stylesheets dynamically
        //add stylesheet if we have any
        val head = document.getElementsByTagName("head")
        head[0]?.appendChild(createStylesheetLink("style.css"))
        //bind elements
        val input = document.getElementById("count_id") as HTMLInputElement
        val button = document.getElementById("button_id")
        //bind click listener on button
        button?.addEventListener("click", fun(event: Event) {
            fetch(input.value)
        })
    }
}

fun fetch(count: String): Unit {
    val url = "http://localhost:8080/api/ping/$count"
    val req = XMLHttpRequest()
    req.onloadend = fun(event: Event){
        val text = req.responseText
        println(text)
        val objArray  = JSON.parse<Array<Json>>(text)
        val textarea = document.getElementById("textarea_id") as HTMLTextAreaElement
        textarea.value = ""
        objArray.forEach {
            val message = it["message"]
            textarea.value += "$message\n"
        }
    }
    req.open("GET", url, true)
    req.send()
}

fun createStylesheetLink(filePath: String): Element {
    val style = document.createElement("link")
    style.setAttribute("rel", "stylesheet")
    style.setAttribute("href", filePath)
    return style
}

external fun alert(message: Any?): Unit

Взгляните на эту строку:

val objArray  = JSON.parse<Array<Json>>(text)

В чем разница между JSON и Json? JSON - это конкретный класс, у которого есть методы для управления объектами и строками JSON. Json - это внешний интерфейс, что означает, что мы сообщаем Kotlin, что это объект JavaScript Json. Это может быть плохим объяснением.

Что произойдет, когда вы сейчас создадите свой интерфейсный проект? В папке ‹project dir› / out / production / kotlinfrontend / вы должны увидеть ожидаемую нами структуру проекта.

Вы можете видеть, что имя файла JavaScript - kotlinfrontend_main.js. Это потому, что мой интерфейсный модуль назывался kotlinfrontend, а мой файл kotlin назывался main.kt. Если вы дадите своему javascript-файлу kotlin другое имя, вам следует обновить тег ‹script› в вашем HTML-файле.

Запуск проекта в целом

У нас есть настроенные API, и мы знаем, как их запускать. Запустите свой API, щелкнув правой кнопкой мыши backend /… / Main.kt и выбрав «Run‘ MainKt ’». Затем щелкните правой кнопкой мыши файл index.html в каталоге out и откройте его в браузере. Если все прошло по плану, вы должны увидеть загрузку страницы и «0: Hello, world!» вывод в TextArea.

Теперь вы можете попробовать изменить значение в области ввода, чтобы проверить, работает ли ваш проект.

Вы заметите, что отсутствует style.css. Очевидно, это потому, что мы никогда не создаем его в структуре нашего проекта. Я просто хотел включить пример того, как вы могли бы динамически создать элемент или добавить таблицу стилей, если хотите.

Также существует Aza-Kotlin-CSS, благодаря которому вы можете обрабатывать весь свой CSS в Kotlin и даже не беспокоиться о раздражающем фактическом CSS.

Если вы должны написать HTML-код… ну, есть ограничения, но вы все равно можете создавать большую часть своей DOM в Kotlin, используя библиотеку Kotlinx.html, это упрощает управление DOM. Взгляните на этот пример:

System.out.appendHTML().html {
	body {
		div {
			a("http://kotlinlang.org") {
				target = ATarget.blank
				+"Main site"
			}
		}
	}
}

Или вот этот:

window.setInterval({
    val myDiv = document.create.div("panel") {
        p { 
            +"Here is "
            a("http://kotlinlang.org") { +"official Kotlin site" } 
        }
    }

    document.getElementById("container")!!.appendChild(myDiv)

    document.getElementById("container")!!.append {
        div {
            +"added it"
        }
    }
}, 1000L)

Ознакомьтесь с разделом Котлины, работающие с документацией JavaScript, чтобы узнать больше и узнать, как использовать с ним jQuery. Я уверен, что многие захотят это сделать.

Обновление: примечания по использованию Gradle для сборки

Итак, этот код загадочным образом перестал работать на следующий день, и выходной каталог для ресурсов изменился с «out» на «build». Не уверен, что там произошло. Однако есть другая проблема, с которой мы сталкиваемся при создании с помощью Gradle.

В моем kotlinfrontend / build.gradle вы могли заметить строку:

compileKotlin2Js.kotlinOptions.outputFile = "${projectDir}/../out/production/kotlinfrontend/js/app.js"

Возможно, вам показалось странным, что app.js не был выпущен как часть нашего процесса сборки. Вместо этого у нас было что-то вроде kotlinfrontend_main.js. Это потому, что мы создавали 1 файл JS. Если вы используете gradle, он поместит весь ваш JavaScript в один JS-файл, имя которого вы укажете выше. Кроме того, вы заметите, что /js/lib/kotlin.js будет отсутствовать, поскольку этот файл помещен в jar-файл как часть процесса сборки. Исправить это просто:

  • Скопируйте статические ресурсы в каталог по вашему выбору в вашей сборке gradle.
  • Разархивируйте банку и извлеките нужные нам js-файлы.

Я решил упростить процесс, скопировав все файлы интерфейсных проектов в веб-каталог. Ниже приведены изменения в моем build.gradle файле. Единственный способ, которым это повлияет на код в этом проекте, - это то, что вам нужно внести небольшое изменение в свой index.html, чтобы он указывал на app.js вместо kotlinfrontend_main.js, и ваш веб-каталог будет <front-end module>/web/ вместо <project root>/out/.

compileKotlin2Js.kotlinOptions.sourceMap = true
compileKotlin2Js.kotlinOptions.outputFile = "${projectDir}/web/js/app.js"
compileKotlin2Js.kotlinOptions.suppressWarnings = true
compileKotlin2Js.kotlinOptions.verbose = true

build.doLast {
//Copy kotlin library files to the web directory 
// Copy kotlin.js and kotlin-meta.js from jar into web directory
    configurations.compile.each { File file ->
        copy {
            includeEmptyDirs = false

            from zipTree(file.absolutePath)
            into "${projectDir}/web/js/lib"
            include { fileTreeElement ->
                def path = fileTreeElement.path
                path.endsWith(".js") && (path.startsWith("META-INF/resources/") || !path.startsWith("META-INF/"))
            }
        }
    }
//Copy static resources to the web directory
    copy {
        includeEmptyDirs = false
        from "${buildDir}/resources/main"
        into "${projectDir}/web"
    }
}
//Delete the web director as part of the clean command
clean.doFirst {
    delete "${projectDir}/web"

}

Другое преимущество этого процесса сейчас заключается в том, что если вы не хотите выполнять сборки из командной строки каждый раз, когда вы вносите изменения в свои файлы, вы можете использовать непрерывные сборки gradle для просмотра файлов и запускать сборку, когда файлы меняются. Из командной строки просто запустите:

./gradlew -t build

Теперь каждый раз, когда вы вносите изменения в файл Kotlin JS или статический файл HTML, CSS, вы должны просто иметь возможность обновлять свой браузер.

Последние мысли

То, что я написал выше, является результатом огромных усилий. Я действительно не понимаю, почему люди думают, что использовать один язык вместо другого - это «весело», но это упражнение для меня было определенно не «развлечением», оно было чрезвычайно разочаровывающим, когда мне постоянно приходилось бороться с недостатком примеров того, как даже сделать простой вызов AJAX без использования jQuery. Большинство примеров использования Kotlin для JavaScript были простыми примерами, которые не делали вызовов AJAX и не анализировали JSON и не выясняли все это, особенно когда использование JSON или Json было чрезвычайно разочаровывающим. Кроме того, постоянно возникала путаница между тем, что было родным для Kotlin, например Array, тем, что было родным для JavaScript, и тем, что было родным для Java. В Kotlin (JavaScript) пример ниже показывает, что я имею в виду: первый работает, второй - нет.

//Works
JSON.parse<Array<Json>>(text)
//Broken
JSON.parse<ArrayList<Json>>(text)

Вероятно, это из-за моего незнания Котлина. Я знаю, что Array в основном похож на примитивный массив Java, но вы можете увидеть, как вы можете в конечном итоге использовать там List или ArrayList, и просто расстроитесь, почему он не работает.

Другой пример:

//Works as expected, has all element properties
document.getElementById("count_id") as HTMLInputElement
//Does not work at all
document.getElementById("count_id")

Просто приведение здесь имело все значение, проблема для меня заключалась в том, что мне нужно было открыть API, я не мог найти в Google только то, что я должен там делать, и просто выяснил, что мне нужно иметь возможность использовать элемент, соответствующий его точному типу, чтобы иметь возможность получить от него какую-либо полезную функциональность, расстраивало. Без него было бы очень мало функциональности. Да, возможно, здесь помог бы kotlinx.html.

Наконец-то

Вы можете найти все исходники этого проекта на моем GitHub.

Чтобы создавать отличные приложения для Android, прочтите больше моих статей.

Ура! ты добрался до конца! Мы должны потусоваться! не стесняйтесь подписываться на меня в Medium, LinkedIn, Google+ или Twitter.