# 教程 - 使用 React 和 Kotlin/JS 创建 Web 应用程序 This tutorial will teach you how to build a browser application with Kotlin/JS and the [React](https://reactjs.org/) framework. You will: * Complete common tasks associated with building a typical React application. * Explore how [Kotlin's DSLs](type-safe-builders.html) can be used to help express concepts concisely and uniformly without sacrificing readability, allowing you to write a full-fledged application completely in Kotlin. * Learn how to use ready-made npm components, use external libraries, and publish the final application. The output will be a KotlinConf Explorer web app dedicated to the [KotlinConf](https://kotlinconf.com/) event, with links to conference talks. Users will be able to watch all the talks on one page and mark them as seen or unseen. The tutorial assumes you have prior knowledge of Kotlin and basic knowledge of HTML and CSS. Understanding the basic concepts behind React may help you understand some sample code, but it is not strictly required. > **Note:** > You can get the final application [here](https://github.com/kotlin-hands-on/web-app-react-kotlin-js-gradle/tree/finished). ## Before you start 1. Download and install the latest version of [IntelliJ IDEA](https://www.jetbrains.com/idea/download/). 2. Clone the [project template](https://github.com/kotlin-hands-on/web-app-react-kotlin-js-gradle) and open it in IntelliJ IDEA. The template includes a basic Kotlin Multiplatform Gradle project with all required configurations and dependencies * Dependencies and tasks in the `build.gradle.kts` file: ```KOTLIN dependencies { // React, React DOM + Wrappers implementation(enforcedPlatform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:1.0.0-pre.430")) implementation("org.jetbrains.kotlin-wrappers:kotlin-react") implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom") // Kotlin React Emotion (CSS) implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion") // Video Player implementation(npm("react-player", "2.12.0")) // Share Buttons implementation(npm("react-share", "4.4.1")) // Coroutines & serialization implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") } ``` * An HTML template page in `src/jsMain/resources/index.html` for inserting JavaScript code that you'll be using in this tutorial: ```HTML Hello, Kotlin/JS!
``` Kotlin/JS projects are automatically bundled with all of your code and its dependencies into a single JavaScript file with the same name as the project, `confexplorer.js`, when you build them. As a typical [JavaScript convention](https://faqs.skillcrush.com/article/176-where-should-js-script-tags-be-linked-in-html-documents), the content of the body (including the `root` div) is loaded first to ensure that the browser loads all page elements before the scripts. * A code snippet in `src/jsMain/kotlin/Main.kt`: ```KOTLIN import kotlinx.browser.document fun main() { document.bgColor = "red" } ``` ### Run the development server By default, the Kotlin Multiplatform Gradle plugin comes with support for an embedded `webpack-dev-server`, allowing you to run the application from the IDE without manually setting up any servers. To test that the program successfully runs in the browser, start the development server by invoking the `run` or `browserDevelopmentRun` task (available in the `other` or `kotlin browser` directory) from the Gradle tool window inside IntelliJ IDEA: ![Gradle tasks list](images/browser-development-run.png) To run the program from the Terminal, use `./gradlew run` instead. When the project is compiled and bundled, a blank red page will appear in a browser window: ![Blank red page](images/red-page.png) ### Enable hot reload / continuous mode Configure [continuous compilation](dev-server-continuous-compilation.html) mode so you don't have to manually compile and execute your project every time you make changes. Make sure to stop all running development server instances before proceeding. 1. Edit the run configuration that IntelliJ IDEA automatically generates after running the Gradle `run` task for the first time: ![Edit a run configuration](images/edit-configurations-continuous.png) 2. In the Run/Debug Configurations dialog, add the `--continuous` option to the arguments for the run configuration: ![Enable continuous mode](images/continuous-mode.png) After applying the changes, you can use the Run button inside IntelliJ IDEA to start the development server back up. To run the continuous Gradle builds from the Terminal, use `./gradlew run --continuous` instead. 3. To test this feature, change the color of the page to blue in the `Main.kt` file while the Gradle task is running: ```KOTLIN document.bgColor = "blue" ``` The project then recompiles, and after a reload the browser page will be the new color. You can keep the development server running in continuous mode during the development process. It will automatically rebuild and reload the page when you make changes. > **Note:** > You can find this state of the project on the `master` branch [here](https://github.com/kotlin-hands-on/web-app-react-kotlin-js-gradle/tree/master). ## Create a web app draft ### Add the first static page with React To make your app display a simple message, replace the code in the `Main.kt` file with the following: ```KOTLIN import kotlinx.browser.document import react.* import emotion.react.css import csstype.Position import csstype.px import react.dom.html.ReactHTML.h1 import react.dom.html.ReactHTML.h3 import react.dom.html.ReactHTML.div import react.dom.html.ReactHTML.p import react.dom.html.ReactHTML.img import react.dom.client.createRoot import kotlinx.serialization.Serializable fun main() { val container = document.getElementById("root") ?: error("Couldn't find root container!") createRoot(container).render(Fragment.create { h1 { +"Hello, React+Kotlin/JS!" } }) } ``` * The `render()` function instructs [kotlin-react-dom](https://github.com/JetBrains/kotlin-wrappers/tree/master/kotlin-react-dom) to render the first HTML element inside a [fragment](https://reactjs.org/docs/fragments.html) to the `root` element. This element is a container defined in `src/jsMain/resources/index.html`, which was included in the template. * The content is an `

` header and uses a typesafe DSL to render HTML. * `h1` is a function that takes a lambda parameter. When you add the `+` sign in front of a string literal, the `unaryPlus()` function is actually invoked using [operator overloading](operator-overloading.html). It appends the string to the enclosed HTML element. When the project recompiles, the browser displays this HTML page: ![An HTML page example](images/hello-react-js.png) ### Convert HTML to Kotlin's typesafe HTML DSL The Kotlin [wrappers](https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-react/README.md) for React come with a [domain-specific language (DSL)](type-safe-builders.html) that makes it possible to write HTML in pure Kotlin code. In this way, it's similar to [JSX](https://reactjs.org/docs/introducing-jsx.html) from JavaScript. However, with this markup being Kotlin, you get all the benefits of a statically typed language, such as autocomplete or type checking. Compare the classic HTML code for your future web app and its typesafe variant in Kotlin: HTML: ```HTML

KotlinConf Explorer

Videos to watch

John Doe: Building and breaking things

Jane Smith: The development process

Matt Miller: The Web 7.0

Videos watched

Tom Jerry: Mouseless development

John Doe: Building and breaking things

``` Kotlin: ```KOTLIN h1 { +"KotlinConf Explorer" } div { h3 { +"Videos to watch" } p { + "John Doe: Building and breaking things" } p { +"Jane Smith: The development process" } p { +"Matt Miller: The Web 7.0" } h3 { +"Videos watched" } p { +"Tom Jerry: Mouseless development" } } div { h3 { +"John Doe: Building and breaking things" } img { src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder" } } ``` Copy the Kotlin code and update the `Fragment.create()` function call inside the `main()` function, replacing the previous `h1` tag. Wait for the browser to reload. The page should now look like this: ![The web app draft](images/website-draft.png) ### Add videos using Kotlin constructs in markup There are some advantages to writing HTML in Kotlin using this DSL. You can manipulate your app using regular Kotlin constructs, like loops, conditions, collections, and string interpolation. You can now replace the hardcoded list of videos with a list of Kotlin objects: 1. In `Main.kt`, create a `Video` [data class](data-classes.html) to keep all video attributes in one place: ```KOTLIN data class Video( val id: Int, val title: String, val speaker: String, val videoUrl: String ) ``` 2. Fill up the two lists, for unwatched videos and watched videos, respectively. Add these declarations at file-level in `Main.kt`: ```KOTLIN val unwatchedVideos = listOf( Video(1, "Opening Keynote", "Andrey Breslav", "https://youtu.be/PsaFVLr8t4E"), Video(2, "Dissecting the stdlib", "Huyen Tue Dao", "https://youtu.be/Fzt_9I733Yg"), Video(3, "Kotlin and Spring Boot", "Nicolas Frankel", "https://youtu.be/pSiZVAeReeg") ) val watchedVideos = listOf( Video(4, "Creating Internal DSLs in Kotlin", "Venkat Subramaniam", "https://youtu.be/JzTeAM8N1-o") ) ``` 3. To use these videos on the page, write a Kotlin `for` loop to iterate over the collection of unwatched `Video` objects. Replace the three `p` tags under "Videos to watch" with the following snippet: ```KOTLIN for (video in unwatchedVideos) { p { +"${video.speaker}: ${video.title}" } } ``` 4. Apply the same process to modify the code for the single tag following "Videos watched" as well: ```KOTLIN for (video in watchedVideos) { p { +"${video.speaker}: ${video.title}" } } ``` Wait for the browser to reload. The layout should stay the same as before. You can add some more videos to the list to make sure that the loop is working. ### Add styles with typesafe CSS The [kotlin-emotion](https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-emotion/) wrapper for the [Emotion](https://emotion.sh/docs/introduction) library makes it possible to specify CSS attributes – even dynamic ones – right alongside HTML with JavaScript. Conceptually, that makes it similar to [CSS-in-JS](https://reactjs.org/docs/faq-styling.html#what-is-css-in-js) – but for Kotlin. The benefit of using a DSL is that you can use Kotlin code constructs to express formatting rules. The template project for this tutorial already includes the dependency needed to use `kotlin-emotion`: ```KOTLIN dependencies { // ... // Kotlin React Emotion (CSS) (chapter 3) implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion") // ... } ``` With `kotlin-emotion`, you can specify a `css` block inside HTML elements `div` and `h3`, where you can define the styles. To move the video player to the top right-hand corner of the page, use CSS and adjust the code for the video player (the last `div` in the snippet): ```KOTLIN div { css { position = Position.absolute top = 10.px right = 10.px } h3 { +"John Doe: Building and breaking things" } img { src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder" } } ``` Feel free to experiment with some other styles. For example, you could change the `fontFamily` or add some `color` to your UI. ## Design app components The basic building blocks in React are called [components](https://reactjs.org/docs/components-and-props.html). Components themselves can also be composed of other, smaller components. By combining components, you build your application. If you structure components to be generic and reusable, you'll be able to use them in multiple parts of the app without duplicating code or logic. The content of the `render()` function generally describes a basic component. The current layout of your application looks like this: ![Current layout](images/current-layout.png) If you decompose your application into individual components, you'll end up with a more structured layout in which each component handles its responsibilities: ![Structured layout with components](images/structured-layout.png) Components encapsulate a particular functionality. Using components shortens source code and makes it easier to read and understand. ### Add the main component To start creating the application's structure, first explicitly specify `App`, the main component for rendering to the `root`element: 1. Create a new `App.kt` file in the `src/jsMain/kotlin` folder. 2. Inside this file, add the following snippet and move the typesafe HTML from `Main.kt` into it: ```KOTLIN import kotlinx.coroutines.async import react.* import react.dom.* import kotlinx.browser.window import kotlinx.coroutines.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import emotion.react.css import csstype.Position import csstype.px import react.dom.html.ReactHTML.h1 import react.dom.html.ReactHTML.h3 import react.dom.html.ReactHTML.div import react.dom.html.ReactHTML.p import react.dom.html.ReactHTML.img val App = FC { // typesafe HTML goes here, starting with the first h1 tag! } ``` The `FC` function creates a [function component](https://reactjs.org/docs/components-and-props.html#function-and-class-components). 3. In the `Main.kt` file, update the `main()` function as follows: ```KOTLIN fun main() { val container = document.getElementById("root") ?: error("Couldn't find root container!") createRoot(container).render(App.create()) } ``` Now the program creates an instance of the `App` component and renders it to the specified container. For more information about React concepts, see the [documentation and guides](https://reactjs.org/docs/hello-world.html#how-to-read-this-guide). ### Extract a list component Since the `watchedVideos` and `unwatchedVideos` lists each contain a list of videos, it makes sense to create a single reusable component, and only adjust the content displayed in the lists. The `VideoList` component follows the same pattern as the `App` component. It uses the `FC` builder function, and contains the code from the `unwatchedVideos` list. 1. Create a new `VideoList.kt` file in the `src/jsMain/kotlin` folder and add the following code: ```KOTLIN import kotlinx.browser.window import react.* import react.dom.* import react.dom.html.ReactHTML.p val VideoList = FC { for (video in unwatchedVideos) { p { +"${video.speaker}: ${video.title}" } } } ``` 2. In `App.kt`, use the `VideoList` component by invoking it without parameters: ```KOTLIN // . . . div { h3 { +"Videos to watch" } VideoList() h3 { +"Videos watched" } VideoList() } // . . . ``` For now, the `App` component has no control over the content that is shown by the `VideoList` component. It's hard-coded, so you see the same list twice. ### Add props to pass data between components Since you're going to reuse the `VideoList` component, you'll need to be able to fill it with different content. You can add the ability to pass the list of items as an attribute to the component. In React, these attributes are called props. When the props of a component are changed in React, the framework automatically re-renders the component. For `VideoList`, you'll need a prop containing the list of videos to be shown. Define an interface that holds all the props which can be passed to a `VideoList` component: 1. Add the following definition to the `VideoList.kt` file: ```KOTLIN external interface VideoListProps : Props { var videos: List