Веб приложение с Kotlin.js: Начало Почему Kotlin.js? Почему стоит использовать Kotlin для веб разработки? Вот несколько причин: Знакомство: Если вы пришли из компилируемого языка типа Java, Swift или C#, вы найдете Kotlin очень простым для изучения, и вы должны уже быть знакомы с ним, если вы разработчик для андроид. Дружелюбные инструменты: IDE может помочь вам во многом с JavaScript, так как Kotlin идет с первоклассной поддержкой в IDE, значит вы можете находить, общие ошибки при наборе кода. Распространение кода между платформами: С помощью Kotlin Multiplatform Project, вы можете написать бизнес логику вашего приложения один раз и распространять его на множество платформ включая бэкенд, браузер фротенд, Android и iOS клиенты. Совместимость: Возможность вызывать JavaScript код из Kotlin кода очень важна. Она позволяет вам использовать уже готовый JavaScript код, который был уже написан на Kotlin. Эта совместимость имеет и обратное направление. Можно вызывать Kotlin код из JavaScript. Проблемы JavaScript: Kotlin отличный выбор если отказались от веб разработки в связи с распространением JavaScript проблем, таких как необходимость работы с динамической типизацией, странным логическим сравнением или прототипированием. Требования: для этой инструкции, вам необходимо базовое знание веб-программирования и знакомство с Kotlin и IntelliJ Idea. Если вы полностью новичок в Kotlin, возможно вы захотите почитать книгу "Kotlin Apprentice" или видео курс "Программирования на Kotlin" Начнем. Вы собираетесь создать приложение книжный магазин. Приложение будет получать данные о книгах с вебсервиса, отображать обложки книг с названием, описанием, ценой и кнопкой открыть страничку с деталями. Вот как будет выглядеть конечный результат: Чтобы следовать данному руководству, вам необходимо использовать IntelliJ IDEA 2018.8.7 или позднее (бесплатная Community Edition будет достаточно), и Kotlin 1.2.61 или более поздний. Начинаем с загрузки материала для этого руководства используя кнопку "Download Materials" в верхней или нижней части окна программы. Затем запускаем IntelliJ IDEA и создаем новый проект. В окне нового проекта, выберите Kotlin в левой части панели и Kotlin/JS в правой части. Затем нажмите "Next" Новый проект, шаг 1 Для имени проекта используйте bookstore . Выберите место расположения проекта, или используйте по умолчанию. Затем нажмите "Finish" Новый проект, шаг 2 Распакуйте скаченные материалы, и из начальной папки, скопируйте index.html и styles.css в вашу папку с проектом. Структура проекта должна выглядеть следующим образом: Создание главной функции Создайте новый Kotlin файл с именем Main.kt в папке src и добавьте в нее main() функцию как показано ниже: fun main(args: Array) { println("Hello world!") } Заметка : Функция приложения main() может быть в любом Kotlin файле с любым именем, поэтому использование Main.kt не строго обязательно, но вы должны иметь только одну функцию main() в вашем проекте. Теперь откроем "Build" меню и нажмем "Build Project" чтобы скомпилировать ваш Kotlin код в JavaScript. IDE сгенерирует следующую папку в корне вашего проекта. Нам нужно упомянуть 2 выходных файла в данном проекте в index.html : kotlin.js: это стандартная Kotlin библиотека, реализованная в JavaScript bookstore.js: Это JavaScript, в который был скомпилирован Kotlin. Заметка : Оба файла уже ссылаются для вас внутри скриптами на начальный index.html файл, но вам возможно нужно проверить их пути на случай если вы ввели другое название проекта. Теперь откроем index.html в вашем браузере, и затем откроем консоль разработчика. Вы должны увидеть "Hello World!",как показано на скриншоте: Заметка : Каждый раз меняя код kotlin, необходимо билдить ваш проект и затем обновлять HTML страницу чтобы увидеть изменения. Вызываем JavaScript код из Kotlin В index.html файле, внутри тега со скриптом, вы найдете JavaScript функцию названную getApiUrl() , который возвращает URL необходимый вам для получения данных магазина в формате JSON Есть множество путей для доступа JavaScript функции или переменно из кода Kotlin. Один из них это использование функции js() , которая позволяет передавать простой код JavaScript как строку. Добавим следуюущую строку кода в Main.kt файл, вне main() функции. val API_URL = js("getApiUrl()") as String Здесь вы передаете строку getApiUrl() в js() функцию. Теперь getApiUrl() функция всегда возвращает строку, вы можете привести её безопасно к String Kotlin, и хранить как обычное значение. Теперь, обновим main() функцию для выведения значения API_URL свойства вместо "Hello world!" fun main(args: Array) { println(API_URL) } Билдим проект и обновляем index.html в браузере. Вы дожны увидеть значение API_URL переменной, выведенной в консоль, как на скриншоте ниже Теперь у вас есть URL сохраненный в API_URL значении, который вы используете позднее. Очистим main() функцию, чтобы подготовить её к тому, что будет происходить дальше. Чтобы получить данные от сервера их отобразить их в UI, необходимо создать новый Kotlin класс для отображения одной книги. Создадим файл Book.kt в src папке и поместит тут класс данных: data class Book(val title: String, val price: String, val description: String, val url: String, val coverUrl: String) Каждая книга имеет заголовок, цену, описание, URL для страницы с деталями на сайте и ссылку на картинку обложки. Создаем приложение Вы будете использовать простую архитектуру MVP в приложении. Класс отображению будет содержать всю бизнес логику, пока класс страницы работает как вью. Перед созаднием этих классов, создадим контракт между ними. Заметка : Если вы не имеет опыта работы с MVP, то посмотрите учебное пособие Getting Started with MVP on Android . Создадим новый Kotlin интерфейс, названный BookStoreContract (как обычно, в его собственном файле в папке src ) который определяет подключение между вью и представителем. Добавим следующий код: interface BookStoreContract { interface View { fun showBooks(books: List) // 1 fun showLoader() // 2 fun hideLoader() // 3 } interface Presenter { fun attach(view: View) // 4 fun loadBooks() // 5 } } Эта вью сможет: Отображать список книг, предоставленных ей Показывать индикатор загрузки пока приложение получает данные от сервера Прятать индикатор загрузки. Что касается от представителя, он может: Отображать результаты на любой вью которая предоставлена Начинать загрузку данных о книгах из источника данных, в этом случае, это удаленный сервер. Выполнив это, вы теперь можете создать класс BookStorePage , и добавить в нее такой код: class BookStorePage(private val presenter: BookStoreContract.Presenter) : BookStoreContract.View { override fun showBooks(books: List) { } override fun showLoader() { } override fun hideLoader() { } } Этот класс имеет конструктор с BookStoreContract.Presenter параметром. Он реализует BookStoreContract.View интерфейс с тремя необходимыми методами (пока пустыми) Создадим BookStorePresenter класс и добавим следующий код: // 1 class BookStorePresenter : BookStoreContract.Presenter { // 2 private lateinit var view: BookStoreContract.View // 3 override fun attach(view: BookStoreContract.View) { this.view = view } // 4 override fun loadBooks() { } } В этом классе, вы: Реализовали BookStoreContract.Presenter интерфейс. Добавили свойство для хранения сылки на вью. Реализовали метод attach() из BookStoreContract.Presenter интерфейса, и проинициализировать вью свойство из полученного параметра. Реализовали loadBooks() метод требуемый BookStoreContract.Presenter интерфейсом(пока пустым). Получение данных с сервера Вам нужен пут для получения данных с сервера. Чтоыб это сделать, добавьте следующий метод в BookStorePresenter класс. // 1 private fun getAsync(url: String, callback: (String) -> Unit) { // 2 val xmlHttp = XMLHttpRequest() // 3 xmlHttp.open("GET", url) // 4 xmlHttp.onload = { // 5 if (xmlHttp.readyState == 4.toShort() && xmlHttp.status == 200.toShort()) { // 6 callback.invoke(xmlHttp.responseText) } } // 7 xmlHttp.send() } Нажмите option+return на Mac или Alt+Enter на PC, чтобы добавить в импорт класс XMLHttpRequest. Пройдем по тому, что мы делаем, шаг за шагом. Создание метода который делает сетевые запросы. Он берет URL для получения, так же как и функция со String параметром, которая передать результат сетевого вызова. Создаем объект XMLHttpRequest . ` Указываем этот запрос посылает HTTP GET запрос на заданный URL. Указываем callback который будет выполнен, когда запрос завершится. Проверяем если запрос имеет состояние выполнен, и если он имеет статус код 200. Вызываем callback функцию, принимаемую как параметр, и передаем её содержание сетевого ответа как строки. Вызываем send() чтобы произвести HTTP запрос настроенный ранее. Теперь, вы можете использовать вспомогательный метод для реализации loadBooks() : override fun loadBooks() { //1 view.showLoader() //2 getAsync(API_URL) { response -> //3 val books = JSON.parse>(response) //4 view.hideLoader() //5 view.showBooks(books.toList()) } } В этой части кода: Просим вью показать загрузочный индикатор прежде, чем начать загружать данные. Делаем асинхронный запрос чтобы получить данные о книгах Парсим JSON ответ полученный как массив объектов данных клласа Book Просим вью спрятать индикатор загрузки, так как вы закончили загрузку и парсинг Просим вью отобразить список книг Вы можете пройтись по массиву книг и распечатать заголовок каждый книги в консоль, чтобы быть уверенным, что все работает правильно. Чтобы это сделать добавим следующие линии кода после того, как книги будут распарсены: books.forEach { book -> println(book.title) } Чтобы протестировать код представителя, обновим main() функцию для чтения: fun main(args: Array) { val bookStorePresenter = BookStorePresenter() val bookStorePage = BookStorePage(bookStorePresenter) bookStorePresenter.attach(bookStorePage) bookStorePresenter.loadBooks() } Тут мы создаем новый объект BookStorePresenter , и затем объект BookStorePage , переданные странице от объекта представителя через его конструктор. Теперь вы добавляете страницу в представителя и вызывается loadBooks() непосредственно на представителе. Сбилдим и запустим проект и обновим index.html . Вы должны увидеть лог как на картинке ниже: После выполнения этого тестирования, уберите цикл forEach с выражением print внутри loadBooks() Заметка : Если вы пытаетесь печатать саму книгу( println(book) ), это нормально видеть только объект Object повторяющийся снова и снова в выходе. Это потому что вызов JSON.parce создает чистый JavaScript объект вместо вызова конструктора класса Book Kotlin. Это значит что вы сможете читать его свойства, но любой метод ожидающий класс будет пропущен - включая автогерированную реализацию toSting() . Если вам необходим более надежный метод анализа решения который сделает это правильно, вы можете изучить подробнее kotlinx.serialization библиотеку. Создание UI Файл index.html содержит два div тега с ID , названный loader и content . Для начала загрузочный индикатор, который вы можете показывать пока ваше приложение загружает данные, и прячет, когда эта загрузка выполнена. Затем контейнер, в который будет добавлены все карточки книг. Для доступа в DOM элементы в вашем Kotlin коде, добавьте следующие два новых свойства в класс BookStorePage как показано ниже. private val loader = document.getElementById("loader") as HTMLDivElement private val content = document.getElementById("content") as HTMLDivElement Вы всегда можете получить элемент в DOM по его ID , используя объект документа и getElementeById() метод, так же как это делается в JavaScript. Метод getElementById() возвращает общий Element , который вы можете привести к другому типу элемента если нужно.(похоже на то на как метод findViewById() работает в Android). Меняем видимость загрузчика Обновим методы showLoader() и hideLoader() в BookStorePage следующий образом. override fun showLoader() { loader.style.visibility = "visible" } override fun hideLoader() { loader.style.visibility = "hidden" } Снова, вы используете обычную DOM модель для изменения визуальныйх свойств элементов visible и hidden , как и трубется. Элемент загрузчик, видимый по умолчанию, поэтому вы должны видеть его, когда открываете страницу index.html. Проверьте ваш код и спрячьте загрузчик изменим следующим образом функцию main() : fun main(args: Array) { val bookStorePresenter = BookStorePresenter() val bookStorePage = BookStorePage(bookStorePresenter) bookStorePage.hideLoader() } Вы обновили main функцию вызывать hideLoader() напрямую чтобы прятать индикатор который был до этого видим. Сбилдим проект и обновим index.html в вашем браузере. Загрузчик должен теперь исчезнуть. Создание элементов книги. Теперь, вы создадите карточку отображения каждой книги, как показано ниже Создайте новый класс и назовите его CardBuilder . В этом классе вы создадите HTMLElement для представления книги, свяжите детали книги с ним, и примените CSS. Начните с класса как показано ниже: class CardBuilder { fun build(book: Book): HTMLElement { // 1 val containerElement = document.createElement("div") as HTMLDivElement val imageElement = document.createElement("img") as HTMLImageElement val titleElement = document.createElement("div") as HTMLDivElement val priceElement = document.createElement("div") as HTMLDivElement val descriptionElement = document.createElement("div") as HTMLDivElement val viewDetailsButtonElement = document.createElement("button") as HTMLButtonElement // 2 bind(book = book, imageElement = imageElement, titleElement = titleElement, priceElement = priceElement, descriptionElement = descriptionElement, viewDetailsButtonElement = viewDetailsButtonElement) // 3 applyStyle(containerElement, imageElement = imageElement, titleElement = titleElement, priceElement = priceElement, descriptionElement = descriptionElement, viewDetailsButtonElement = viewDetailsButtonElement) // 4 containerElement .appendChild( imageElement, titleElement, descriptionElement, priceElement, viewDetailsButtonElement ) // 5 return containerElement } // 6 private fun Element.appendChild(vararg elements: Element) { elements.forEach { this.appendChild(it) } } } Нужно много чего еще сделать, давайте посмотрим на шаги по очереди: Создать новые элементы используя createElement() API барузера, передавая имя HTML тэга для созадния. Для примера, используейте div для создания HTMLDivElement() и img чтобы создать HTMLImageElement . Свяжите класс данных book c созданным HTML элементом. Вы скоро реализуете этот метод bind() Примените некоторые CSS классы к HTML элементам. Так же реализуете applyStyle() метод ниже. Добавите все отдельные HTML элементы в один контейнер. Вернете контейнер, который является корневым элементов карточек. Напишите расширение функции которая позволяет вам добавлять переменное количество дочерних к элементы, вместо вызова обычного appendChild() метода множество раз. Создание данных Заполните элементы данными, добавьте следующий код. Заполнените элементы данными, добавьте следующие метод в класс CardBuilder : private fun bind(book: Book, imageElement: HTMLImageElement, titleElement: HTMLDivElement, priceElement: HTMLDivElement, descriptionElement: HTMLDivElement, viewDetailsButtonElement: HTMLButtonElement) { // 1 imageElement.src = book.coverUrl // 2 titleElement.innerHTML = book.title priceElement.innerHTML = book.price descriptionElement.innerHTML = book.description viewDetailsButtonElement.innerHTML = "view details" // 3 viewDetailsButtonElement.addEventListener("click", { window.open(book.url) }) } В этом методе, вы: Укажите ссылку на обложку книги как источник элемента картинки в карточке. Укажите текстовое содержание для различных текстовых элементов. Добавьте слушателя события click к элементу кнопка, которая будет вести к URL книги если нажата кнопка. Применение CSS До сих пор отсутствует метод applyStyle() , который вы должны так же добавить в класс CardBuilder . private fun applyStyle(containerElement: HTMLDivElement, imageElement: HTMLImageElement, titleElement: HTMLDivElement, priceElement: HTMLDivElement, descriptionElement: HTMLDivElement, viewDetailsButtonElement: HTMLButtonElement) { containerElement.addClass("card", "card-shadow") imageElement.addClass("cover-image") titleElement.addClass("text-title", "float-left") descriptionElement.addClass("text-description", "float-left") priceElement.addClass("text-price", "float-left") viewDetailsButtonElement.addClass("view-details", "ripple", "float-right") } Этот метода добавляет верные CSS классы, который вам нужны для стилизации карточек книг с помощью "material design". Вы можете найти эти классы уже настроенными в style.css. Для примера, тень карточки CSS класса дает "material" тень карточке контейнера, и float-left css класс выравнивает элементы по левой стороне. Создание карточки. Вернемся назад в BookStorePage класс и начнем использовать код создания этой карточки. Первый, добавим свойство в класс, который будет хранить объект CardBuilder private val cardBuilder = CardBuilder() Теперь идем в showBooks() метод и добавялем следующий код: books.forEach { book -> val card = cardBuilder.build(book) content.appendChild(card) } Этот код проходит через список книг, и для каждой книги, созадет HTML элемент, отображающий её. Теперь, это добавляет элемент к содержанию div который мы наблюдали из DOM ранее. Отображение Страницы книжного магазина Вы почти закончили. Добавим следующий метод в класс BookStorePage . fun show() { presenter.attach(this) presenter.loadBooks() } Этот код указывает текущий BookStorePage экземпляр как представитель вью, который может получать callback от него. И затем просит представителя начать загрузку книг. Идите в main() функцию и обновите, чтобы она вызывала show() метод в bookStorePage . Полный метод main() должен теперь выглядеть таким образом: fun main(args: Array) { val bookStorePresenter = BookStorePresenter() val bookStorePage = BookStorePage(bookStorePresenter) bookStorePage.show() } Сбилдим проект, и обновим index.html . Вы должны увидеть загрузчик мельком, прежде чем приложение закончит загрузку книги. Затем отобразятся книжные каточки. Карточки должны иметь теперь, когда вы будете наводить на нее мышку, и кнопка "View Details" должна вас вести на нужную страницу книги. Ура! Вы воздали ваше первое web приложение на Kotlin :] Счастливое лицо!