ظهرت فكرة نقل المقدمة إلى أي إطار js في وقت واحد مع القدرة على كتابة React في Kotlin. وقررت أن أحاول. المشكلة الرئيسية: قليل من المواد والأمثلة (سأحاول تصحيح هذا الوضع). لكن لدي كتابة كاملة ، إعادة هيكلة خالية من الخوف ، جميع ميزات Kotlin ، والأهم من ذلك ، الرمز العام للواجهة الخلفية على JVM والواجهة الأمامية على Javascript.
في هذه المقالة ، سنكتب صفحة على Javasript + React بالتوازي مع نظيرتها على Kotlin + React. لجعل المقارنة صادقة ، أضفت الكتابة إلى Javasript.

لم تكن إضافة الكتابة إلى جافا سكريبت بهذه السهولة. إذا كنت بحاجة إلى Kotlin ، gradp و npm و webpack ، فقد كنت بحاجة إلى npm و webpack و flow و babel مع رد فعل مسبق وتدفق و es2015 و stage-2. في نفس الوقت ، يكون التدفق هنا بطريقة ما على الجانب ، وتحتاج إلى تشغيله بشكل منفصل وأن نكون أصدقاء مع IDE بشكل منفصل. إذا وضعت قوسًا للتجميع وما شابه ، فعندئذ لكتابة التعليمات البرمجية المباشرة ، من ناحية ، يبقى Kotlin + React ، ومن جهة أخرى Javascript + React + babel + Flow + ES5 | ES6 | ES7.
على سبيل المثال ، سننشئ صفحة تحتوي على قائمة السيارات والقدرة على التصفية حسب العلامة التجارية واللون. نقوم بتصفية العلامة التجارية واللون الذي نسحبه من الخلف مرة واحدة أثناء التمهيد الأول. يتم حفظ عوامل التصفية المحددة في الاستعلام. نعرض السيارات في لوحة. لا يتعلق مشروعي بالسيارات ، ولكن الهيكل العام مشابه بشكل عام لما أعمل به بانتظام.
تبدو النتيجة كالتالي (لن أكون مصممًا):

لن أصف تكوين آلة الشيطان بأكملها هنا ، هذا موضوع لمقال منفصل (في الوقت الحالي ، يمكنك تدخين المصادر من هذا واحد).
تحميل البيانات من الخلف
تحتاج أولاً إلى تحميل العلامات التجارية والألوان المتاحة من الخلف.
جافا سكريبت
| كوتلن
|
---|
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 ) }
|
تبدو متشابهة للغاية. ولكن هناك اختلافات:
- يمكن كتابة القيم الافتراضية في نفس المكان حيث تم تعريف النوع. هذا يجعل من السهل الحفاظ على سلامة التعليمات البرمجية.
- يسمح لك Lateinit بعدم تعيين قيمة افتراضية على الإطلاق لما سيتم تحميله لاحقًا. أثناء التجميع ، يعتبر هذا المتغير NotNull ، ولكن في كل مرة يتم فحصه ، يتم ملؤه ويتم إنشاء خطأ يمكن قراءته بواسطة الإنسان. سيكون هذا صحيحًا بشكل خاص مع كائن أكثر تعقيدًا من صفيف. أعلم أنه يمكن تحقيق الشيء نفسه بالتدفق ، ولكنه مرهق للغاية لم أحاول.
- رد فعل kotlin خارج الصندوق يعطي وظيفة setState ، لكنه لا يتحد مع coroutines لأنه غير مضمن. كان علي نسخ ووضع مضمنة.
- في الواقع ، coroutines . هذا بديل غير متزامن / انتظار وأكثر من ذلك بكثير. على سبيل المثال ، يتم العائد من خلالهم. من المثير للاهتمام ، يتم إضافة كلمة تعليق فقط إلى بناء الجملة ، كل شيء آخر هو مجرد رمز. لذلك ، المزيد من حرية الاستخدام. وقليلا من التحكم على مستوى التجميع. لذلك ، لا يمكنك تجاوز ComponDidMount باستخدام مُعدِّل
suspend
، وهو أمر منطقي: إن ComponDidMount طريقة متزامنة. ولكن يمكنك إدراج launch { }
كتلة غير متزامنة في أي مكان في الرمز. من الممكن قبول وظيفة غير متزامنة بشكل صريح في معلمة أو حقل من فئة (أدناه فقط مثال من مشروعي). - في جافا سكريبت ، يكون التحكم الأقل لاغياً. لذلك في الحالة الناتجة ، يمكنك تغيير إبطال العلامة التجارية واللون والحقول المحملة وسيتم جمع كل شيء. في إصدار Kotlin ، ستكون هناك أخطاء ترجمة مبررة.
الرحلات الموازية مع كوروتين suspend fun parallel(vararg tasks: suspend () -> Unit) { tasks.map { async { it.invoke() }
الآن قم بتحميل الآلات من الخلف باستخدام المرشحات من الاستعلام
شبيبة:
async loadCars() { let url = `/api/cars?brand=${this.state.brand || ""}&color=${this.state.color || ""}`; this.setState({ cars: await (await fetch(url)).json(), loaded: true }); }
كوتلن:
private suspend fun loadCars() { val url = "/api/cars?brand=${state.brand.orEmpty()}&color=${state.color.orEmpty()}" updateState { cars = fetchJson(url, Car::class.serializer().list)
أريد الانتباه إلى قائمة
Car::class.serializer().list
. والحقيقة هي أن jetBrains قد كتب مكتبة للتسلسل / إلغاء التسلسل تعمل بنفس الطريقة على JVM و JS. أولاً ، مشاكل ورمز أقل في حالة وجود الواجهة الخلفية على JVM. ثانيًا ، يتم التحقق من صلاحية json الوارد أثناء إلغاء التسلسل ، وليس في وقت ما أثناء المكالمة ، لذلك عند تغيير إصدار الواجهة الخلفية ، وعند الدمج من حيث المبدأ ، ستكون المشاكل أسرع.
نرسم غطاء مع مرشحات
نكتب مكونًا عديم الحالة لعرض قائمتين منسدلتين. في حالة Kotlin ، ستكون مجرد وظيفة ، في حالة js ، فهي مكون منفصل سيتم إنشاؤه بواسطة محمل التفاعل أثناء التجميع.
جافا سكريبت
| كوتلن
|
---|
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
|
أول شيء يلفت انتباهك - HomeHeaderProps في الجزء JS ، نحن مضطرون لإعلان المعلمات الواردة بشكل منفصل. بشكل غير مريح.
لقد تغير بناء جملة Dropdown قليلاً. أستخدم
primereact هنا ، بالطبع ، كان علي أن أكتب غلاف kotlin. من ناحية ، هذا عمل غير ضروري (الحمد لله أن هناك
ts2kt ) ، ولكن من ناحية أخرى ، إنها فرصة لجعل واجهة برمجة التطبيقات أكثر ملاءمة في الأماكن.
حسنًا ، القليل من السكر النحوي في تشكيل العناصر للسحب.
})))}
في إصدار js تبدو مثيرة للاهتمام ، ولكن لا يهم. لكن تسوية تسلسل الكلمات أجمل بكثير: "نقوم بتحويل الألوان إلى عناصر ونضيف" الكل "افتراضيًا" ، بدلاً من "إضافة" الكل "إلى الألوان المحولة إلى عناصر". يبدو وكأنه مكافأة صغيرة ، ولكن عندما يكون لديك العديد من الانقلابات المتتالية ...
نحفظ المرشحات في الاستعلام
الآن عند اختيار المرشحات حسب العلامة التجارية واللون ، تحتاج إلى تغيير الحالة ، وتحميل السيارات من الخلف وتغيير عنوان url.
جافا سكريبت
| كوتلن
|
---|
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() } }
|
وهنا مرة أخرى ، المشكلة مع القيم الافتراضية للمعلمات. لسبب ما ، لم يسمح لي التدفق بالحصول على تصنيف ، ومدمّر ، وقيمة افتراضية مأخوذة من الحالة في نفس الوقت. ربما مجرد خطأ. ولكن ، إذا حدث ذلك ، فسيكون من الضروري الإعلان عن النوع خارج الفصل ، أي عموما شاشة أعلى أو أقل.
ارسم طاولة
آخر شيء يجب علينا القيام به هو كتابة مكون عديم الحالة لتقديم الجدول مع الآلات.
جافا سكريبت
| كوتلن
|
---|
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}" } } }
|
هنا يمكنك أن ترى كيف أقومت بتسوية واجهات api وكيفية التصميم في رد فعل kotlin. هذا هو json عادي ، كما هو الحال في إصدار js. في مشروعي ، قمت بإنشاء غلاف يبدو متشابهًا ، ولكن بكتابة قوية ، قدر الإمكان مع أنماط html.
الخلاصة
إن المشاركة في التكنولوجيا الجديدة أمر محفوف بالمخاطر. هناك عدد قليل من الأدلة ، ولا يوجد شيء على تجاوز سعة المكدس ، وبعض الأشياء الأساسية مفقودة. ولكن في حالة Kotlin ، تم دفع تكاليفي.
أثناء إعداد هذا المقال ، تعلمت مجموعة من الأشياء الجديدة حول جافا سكريبت الحديث: التدفق ، بابل ، غير متزامن / انتظار ، قوالب jsx. أتساءل كيف تصبح هذه المعرفة عفا عليها الزمن؟ وكل هذا ليس ضروريًا إذا كنت تستخدم Kotlin. في نفس الوقت ، تحتاج إلى معرفة القليل عن رد الفعل ، لأن معظم المشاكل يتم حلها بسهولة بمساعدة اللغة.
وما رأيك في استبدال كل حديقة الحيوانات هذه بلغة واحدة بمجموعة كبيرة من الكعك بالإضافة إلى ذلك؟
للمهتمين ،
رموز المصدر .
ملاحظة: تخطط لكتابة مقالات حول التكوينات والتكامل مع JVM وحول dsl لتشكيل كل من dom-dom و html العادي.
مقالات مكتوبة بالفعل حول Kotlin:
الطعم من Kotlin ، الجزء 1الطعم من Kotlin ، الجزء 2الطعم من Kotlin ، الجزء 3. Coroutines - مشاركة وقت المعالج