Gagasan untuk mentransfer bagian depan ke kerangka kerja apa pun muncul bersamaan dengan kemampuan untuk menulis Bereaksi di Kotlin. Dan saya memutuskan untuk mencoba. Masalah utama: beberapa bahan dan contoh (saya akan mencoba memperbaiki situasi ini). Tetapi saya memiliki pengetikan penuh, refactoring tanpa rasa takut, semua fitur Kotlin, dan yang paling penting, kode umum untuk backend pada JVM dan bagian depan pada Javascript.
Pada artikel ini, kita akan menulis halaman tentang Javasript + Bereaksi secara paralel dengan rekannya di Kotlin + Bereaksi. Untuk membuat perbandingan itu jujur, saya menambahkan mengetik ke Javasript.

Menambahkan mengetik ke Javascript tidak mudah. Jika untuk Kotlin saya membutuhkan gradle, npm dan webpack, untuk Javascript saya membutuhkan npm, webpack, flow, dan babel dengan preset bereaksi, mengalir, es2015 dan stage-2. Pada saat yang sama, aliran di sini entah bagaimana ada di samping, dan Anda perlu menjalankannya secara terpisah dan secara terpisah berteman dengan IDE. Jika Anda mengurung rakitan dan sejenisnya, maka untuk penulisan kode langsung, di satu sisi, Kotlin + React tetap, dan di sisi lain Javascript + Bereaksi + babel + Aliran + ES5 | ES6 | ES7.
Sebagai contoh kami, kami akan membuat halaman dengan daftar mobil dan kemampuan untuk memfilter berdasarkan merek dan warna. Kami memfilter merek dan warna yang kami seret dari belakang sekali selama boot pertama. Filter yang dipilih disimpan dalam kueri. Kami memajang mobil di piring. Proyek saya bukan tentang mobil, tetapi struktur keseluruhan umumnya mirip dengan apa yang saya kerjakan secara teratur.
Hasilnya terlihat seperti ini (saya tidak akan menjadi desainer):

Saya tidak akan menjelaskan konfigurasi seluruh mesin syaitan ini di sini, ini adalah topik untuk artikel terpisah (untuk saat ini, Anda dapat merokok sumber dari yang ini).
Memuat data dari belakang
Pertama, Anda perlu memuat merek dan warna yang tersedia dari belakang.
javascript
| kotlin
|
---|
class Home extends React.Component <ContextRouter, State>{ state = { loaded: false,
| class Home( props: RouteResultProps<*> ) : RComponent <RouteResultProps<*>, State> (props) { init { state = State( color = queryAsMap( props.location.search )["color"], brand = queryAsMap( props.location.search )["brand"] ) } override fun componentDidMount() { launch { updateState { //(3) brands = fetchJson( //(4) "/api/brands", StringSerializer.list ) colors = fetchJson( //(4) "/api/colors", StringSerializer.list ) } } } } class State( var color: String?, //(5) var brand: String? //(5) ) : RState { var loaded: Boolean = false //(1) lateinit var brands: List<String> //(2) lateinit var colors: List<String> //(2) } private val serializer: JSON = JSON() suspend fun <T> fetchJson( //(4) url: String, kSerializer: KSerializer<T> ): T { val json = window.fetch(url) .await().text().await() return serializer.parse( kSerializer, json ) }
|
Terlihat sangat mirip. Tetapi ada perbedaan:
- Nilai default dapat ditulis di tempat yang sama dengan tipe yang dideklarasikan. Ini membuatnya lebih mudah untuk menjaga integritas kode.
- lateinit memungkinkan Anda untuk tidak menetapkan nilai default sama sekali untuk apa yang akan dimuat nanti. Selama kompilasi, variabel seperti itu dianggap sebagai NotNull, tetapi setiap kali diperiksa, itu diisi dan kesalahan yang dapat dibaca manusia dihasilkan. Ini akan menjadi benar terutama dengan objek yang lebih kompleks daripada array. Saya tahu hal yang sama dapat dicapai dengan aliran, tetapi sangat rumit sehingga saya tidak mencoba.
- Kota-bereaksi keluar dari kotak memberikan fungsi setState, tetapi tidak bergabung dengan coroutine karena tidak inline. Saya harus menyalin dan menaruh inline.
- Sebenarnya, coroutine . Ini adalah pengganti async / menunggu dan banyak lagi. Misalnya, hasil dibuat melalui mereka. Menariknya, hanya kata menangguhkan ditambahkan ke sintaks, yang lainnya hanya kode. Karena itu, lebih banyak kebebasan penggunaan. Dan kontrol sedikit lebih ketat di tingkat kompilasi. Jadi, Anda tidak bisa menimpa componentDidMount dengan pengubah
suspend
, yang logis: componentDidMount adalah metode sinkron. Tetapi Anda dapat menyisipkan launch { }
blok asinkron di mana saja dalam kode. Dimungkinkan untuk secara eksplisit menerima fungsi asinkron dalam parameter atau bidang kelas (tepat di bawah adalah contoh dari proyek saya). - Dalam Javascript, sedikit kontrol tidak dapat dibatalkan. Jadi pada kondisi yang dihasilkan, Anda dapat mengubah nullability merek, warna, dan bidang isian dan semuanya akan dikumpulkan. Dalam versi Kotlin akan ada kesalahan kompilasi yang dibenarkan.
Trekking paralel dengan corutin suspend fun parallel(vararg tasks: suspend () -> Unit) { tasks.map { async { it.invoke() }
Sekarang muat mesin dari belakang menggunakan filter dari kueri
JS:
async loadCars() { let url = `/api/cars?brand=${this.state.brand || ""}&color=${this.state.color || ""}`; this.setState({ cars: await (await fetch(url)).json(), loaded: true }); }
Kotlin:
private suspend fun loadCars() { val url = "/api/cars?brand=${state.brand.orEmpty()}&color=${state.color.orEmpty()}" updateState { cars = fetchJson(url, Car::class.serializer().list)
Saya ingin memperhatikan
Car::class.serializer().list
. Faktanya adalah bahwa jetBrains telah menulis perpustakaan untuk serialisasi / deserialisasi yang berfungsi sama pada JVM dan JS. Pertama, lebih sedikit masalah dan kode jika backend ada di JVM. Kedua, validitas json yang masuk diperiksa selama deserialisasi, dan tidak kadang-kadang selama panggilan, jadi ketika mengubah versi back-end, dan ketika mengintegrasikan pada prinsipnya, masalah akan lebih cepat.
Kami menggambar topi dengan filter
Kami menulis komponen stateless untuk menampilkan dua daftar drop-down. Dalam kasus Kotlin, itu hanya akan berfungsi, dalam kasus js, itu adalah komponen terpisah yang akan dihasilkan oleh loader reaksi selama perakitan.
javascript
| kotlin
|
---|
type HomeHeaderProps = { brands: Array<string>, brand?: string, onBrandChange: (string) => void, colors: Array<string>, color?: string, onColorChange: (string) => void } const HomeHeader = ({ brands, brand, onBrandChange, colors, color, onColorChange }: HomeHeaderProps) => ( <div> Brand: <Dropdown value={brand} onChange={e => onBrandChange(e.value) } options={withDefault("all", brands.map(value => ({ label: value, value: value })))} /> Color: <Dropdown value={color} onChange={e => onColorChange(e.value) } options={withDefault("all", colors.map(value => ({ label: value, value: value })))} /> </div> ); function withDefault( label, options ) { options.unshift({ label: label, value: null }); return options; }
| private fun RBuilder.homeHeader( brands: List<String>, brand: String?, onBrandChange: (String?) -> Unit, colors: List<String>, color: String?, onColorChange: (String?) -> Unit ) { +"Brand:" dropdown( value = brand, onChange = onBrandChange, options = brands.map { SelectItem( label = it, value = it ) } withDefault "all" ) {} +"Color:" dropdown( value = color, onChange = onColorChange, options = colors.map { SelectItem( label = it, value = it ) } withDefault "all" ) {} } infix fun <T : Any> List<SelectItem<T>>.withDefault( label: String ) = listOf( SelectItem( label = label, value = null ) ) + this
|
Hal pertama yang menarik perhatian Anda - HomeHeaderProps di bagian JS, kami dipaksa untuk mendeklarasikan parameter yang masuk secara terpisah. Nyaman
Sintaks Dropdown telah berubah sedikit. Saya menggunakan
primereact di sini , tentu saja, saya harus menulis bungkus kotlin. Di satu sisi, ini adalah pekerjaan yang berlebihan (terima kasih Tuhan ada
ts2kt ), tetapi di sisi lain, ini adalah kesempatan untuk membuat api lebih nyaman di tempat.
Nah, sedikit sintaksis gula dalam pembentukan item untuk dropdown.
})))}
dalam versi js terlihat menarik, tetapi itu tidak masalah. Tetapi meluruskan urutan kata jauh lebih baik: "kami mengubah warna menjadi item dan menambahkan` semua` secara default ", alih-alih" menambahkan `semua` ke warna yang dikonversi ke item". Sepertinya bonus kecil, tetapi ketika Anda memiliki beberapa kudeta berturut-turut ...
Kami menyimpan filter dalam kueri
Sekarang ketika memilih filter berdasarkan merek dan warna, Anda perlu mengubah status, memuat mobil dari belakang dan mengubah url.
javascript
| kotlin
|
---|
render() { if (!this.state.loaded) return null; return ( <HomeHeader brands={this.state.brands} brand={this.state.brand} onBrandChange={brand => this.navigateToChanged({brand})} colors={this.state.colors} color={this.state.color} onColorChange={color => this.navigateToChanged({color})} /> ); } navigateToChanged({ brand = this.state.brand, color = this.state.color }: Object) { //(*) this.props.history.push( `?brand=${brand || ""}` + `&color=${color || ""}`); this.setState({ brand, color }); this.loadCars() }
| override fun RBuilder.render() { if (!state.loaded) return homeHeader( brands = state.brands, brand = state.brand, onBrandChange = { navigateToChanged(brand = it) }, colors = state.colors, color = state.color, onColorChange = { navigateToChanged(color = it) } ) } private fun navigateToChanged( brand: String? = state.brand, color: String? = state.color ) { props.history.push( "?brand=${brand.orEmpty()}" + "&color=${color.orEmpty()}") updateState { this.brand = brand this.color = color } launch { loadCars() } }
|
Dan di sini lagi, masalah dengan nilai default dari parameter. Untuk beberapa alasan, aliran tidak memungkinkan saya untuk memiliki tipifikasi, destruktor, dan nilai default yang diambil dari keadaan pada saat yang sama. Mungkin hanya bug. Tetapi, jika itu terjadi, akan perlu untuk menyatakan tipe di luar kelas, yaitu umumnya layar lebih tinggi atau lebih rendah.
Gambar sebuah meja
Hal terakhir yang perlu kita lakukan adalah menulis komponen stateless untuk membuat tabel dengan mesin.
javascript
| kotlin
|
---|
const HomeContent = (props: { cars: Array<Car> }) => ( <DataTable value={props.cars}> <Column header="Brand" body={rowData => rowData["brand"] }/> <Column header="Color" body={rowData => <span style={{ color: rowData['color'] }}> {rowData['color']} </span> }/> <Column header="Year" body={rowData => rowData["year"]} /> </DataTable> );
| private fun RBuilder.homeContent( cars: List<Car> ) { datatable(cars) { column(header = "Brand") { +it.brand } column(header = "Color") { span { attrs.style = js { color = it.color } +it.color } } column(header = "Year") { +"${it.year}" } } }
|
Di sini Anda dapat melihat bagaimana saya meluruskan primata api dan cara bergaya di kotlin-react. Ini adalah json biasa, seperti pada versi js. Dalam proyek saya, saya membuat pembungkus yang terlihat sama, tetapi dengan pengetikan yang kuat, sebanyak mungkin dengan gaya html.
Kesimpulan
Terlibat dalam teknologi baru berisiko. Ada beberapa panduan, tidak ada pada stack overflow, beberapa hal dasar hilang. Tetapi dalam kasus Kotlin, biaya saya terbayar.
Ketika saya sedang mempersiapkan artikel ini, saya belajar banyak hal baru tentang Javascript modern: flow, babel, async / wait, jsx templates. Saya bertanya-tanya seberapa cepat pengetahuan ini menjadi usang? Dan semua ini tidak perlu jika Anda menggunakan Kotlin. Pada saat yang sama, Anda perlu tahu sedikit tentang Bereaksi, karena sebagian besar masalah mudah diselesaikan dengan bantuan bahasa.
Dan apa pendapat Anda tentang mengganti semua kebun binatang ini dengan satu bahasa dengan sejumlah besar roti?
Bagi mereka yang tertarik,
kode sumber .
PS: Berencana untuk menulis artikel tentang konfigurasi, integrasi dengan JVM dan tentang dsl membentuk kedua reaksi-dom dan html biasa.
Artikel yang sudah ditulis tentang Kotlin:
The aftertaste dari Kotlin, bagian 1The aftertaste dari Kotlin, bagian 2Aftertaste dari Kotlin, bagian 3. Coroutines - berbagi waktu prosesor