Pengalaman Rambler Group: bagaimana kami mulai sepenuhnya mengontrol pembentukan dan perilaku komponen Bereaksi front-end


Ada banyak cara untuk membuat aplikasi web modern, tetapi masing-masing tim mau tidak mau menghadapi set pertanyaan yang sama: bagaimana mendistribusikan tanggung jawab depan dan belakang, bagaimana meminimalkan tampilan duplikat logika - misalnya, ketika memvalidasi data, perpustakaan mana yang akan digunakan untuk menggunakan, bagaimana memastikan dapat diandalkan dan transpor transparan antara depan dan belakang dan mendokumentasikan kodenya.

Menurut pendapat kami, kami berhasil menerapkan contoh yang baik dari solusi yang seimbang dalam kompleksitas dan keuntungan, yang kami berhasil gunakan dalam produksi berdasarkan Symfony dan React.

Format pertukaran data apa yang dapat kita pilih saat merencanakan pengembangan API backend dalam produk web yang dikembangkan secara aktif yang berisi formulir dinamis dengan bidang terkait dan logika bisnis yang kompleks?

  • SWAGGER adalah pilihan yang baik, ada dokumentasi dan alat debugging yang nyaman. Selain itu, ada perpustakaan untuk Symfony yang mengotomatisasi proses, tetapi sayangnya Skema JSON ternyata lebih disukai;
  • Skema JSON - opsi ini ditawarkan oleh pengembang front-end. Mereka sudah memiliki perpustakaan yang memungkinkan mereka untuk menampilkan formulir. Ini menentukan pilihan kami. Format ini memungkinkan Anda untuk menggambarkan pemeriksaan primitif yang dapat dilakukan di browser. Ada juga dokumentasi yang menjelaskan semua opsi yang memungkinkan untuk skema;
  • GraphQL cukup muda. Tidak begitu banyak perpustakaan sisi dan frontend server. Pada saat sistem dibuat, itu tidak dianggap, di masa depan - cara terbaik untuk membuat API, akan ada artikel terpisah tentang ini;
  • SOAP - memiliki pengetikan data yang ketat, kemampuan untuk membangun dokumentasi, tetapi tidak mudah berteman dengan front Bereaksi. SOAP juga memiliki overhead yang lebih besar untuk jumlah data transmisi yang dapat digunakan yang sama;

Semua format ini tidak sepenuhnya memenuhi kebutuhan kita, jadi saya harus menulis pemanen saya sendiri. Pendekatan serupa dapat memberikan solusi yang sangat efektif untuk aplikasi tertentu, tetapi ini membawa risiko:

  • probabilitas bug yang tinggi;
  • sering tidak 100% dokumentasi dan cakupan tes;
  • "modularitas" rendah karena kedekatan API perangkat lunak. Biasanya, solusi semacam itu ditulis di bawah monolit dan tidak menyiratkan pembagian antara proyek dalam bentuk komponen, karena ini membutuhkan konstruksi arsitektur khusus (baca biaya pengembangan);
  • tingkat masuk yang tinggi dari pengembang baru. Mungkin perlu waktu lama untuk memahami semua kesejukan sepeda;

Oleh karena itu, merupakan praktik yang baik untuk menggunakan perpustakaan umum dan stabil (seperti bantalan kiri dari npm) oleh aturan - kode terbaik adalah yang tidak pernah Anda tulis, tetapi menyelesaikan masalah bisnis. Pengembangan backend aplikasi web dalam teknologi periklanan dari Rambler Group dilakukan di Symfony. Kami tidak akan membahas semua komponen kerangka yang digunakan, di bawah ini kami akan berbicara tentang bagian utama, atas dasar di mana pekerjaan tersebut dilaksanakan - bentuk Symfony . Frontend menggunakan React dan pustaka yang sesuai yang memperluas JSON Schema untuk spesifik WEB - React JSON Schema Form .

Skema kerja umum:



Pendekatan ini memiliki banyak keunggulan:

  • dokumentasi dihasilkan di luar kotak, seperti kemampuan untuk membangun tes otomatis - lagi sesuai dengan skema;
  • semua data yang dikirimkan diketik;
  • Dimungkinkan untuk mengirimkan informasi tentang aturan validasi dasar;
    Integrasi cepat layer transport di React - karena perpustakaan Mozilla React JSON Schema;
  • kemampuan untuk menghasilkan komponen web front-end dari kotak melalui integrasi bootstrap;
  • pengelompokan logis, satu set validasi dan kemungkinan nilai elemen HTML, serta semua logika bisnis dikendalikan pada satu titik - di backend, tidak ada duplikasi kode;
  • sesederhana mungkin untuk port aplikasi ke platform lain - bagian tampilan dipisahkan dari kontrol (lihat paragraf sebelumnya), alih-alih Bereaksi dan browser, aplikasi Android atau iOS dapat membuat dan memproses permintaan pengguna;

Mari kita lihat komponen dan skema interaksi mereka secara lebih rinci.

Awalnya, Skema JSON memungkinkan Anda untuk menguraikan pemeriksaan primitif yang dapat dilakukan pada klien, seperti mengikat atau mengetik berbagai bagian skema:

const schema = { "title": "A registration form", "description": "A simple form example.", "type": "object", "required": [ "firstName", "lastName" ], "properties": { "firstName": { "type": "string", "title": "First name" }, "lastName": { "type": "string", "title": "Last name" }, "password": { "type": "string", "title": "Password", "minLength": 3 }, "telephone": { "type": "string", "title": "Telephone", "minLength": 10 } } } 

Untuk bekerja dengan skema front-end, ada perpustakaan React JSON Schema Form yang populer yang menyediakan add-on yang diperlukan untuk JSON Schema untuk pengembangan web:

uiSchema - Skema JSON sendiri menentukan jenis parameter untuk dilewati, tetapi ini tidak cukup untuk membangun aplikasi web. Misalnya, bidang tipe String dapat direpresentasikan sebagai <input ... /> atau sebagai <textarea ... />, ini adalah nuansa penting, dengan mempertimbangkan yang Anda perlukan untuk menggambar diagram untuk klien dengan benar. UiSchema juga berfungsi untuk menyampaikan nuansa ini, misalnya, untuk Skema JSON yang disajikan di atas, Anda dapat menentukan komponen web visual dari uiSchema berikut:

 const uiSchema = { "firstName": { "ui:autofocus": true, "ui:emptyValue": "" }, "age": { "ui:widget": "updown", "ui:title": "Age of person", "ui:description": "(earthian year)" }, "bio": { "ui:widget": "textarea" }, "password": { "ui:widget": "password", "ui:help": "Hint: Make it strong!" }, "date": { "ui:widget": "alt-datetime" }, "telephone": { "ui:options": { "inputType": "tel" } } } 

Contoh Live Playground dapat dilihat di sini .

Dengan penggunaan skema ini, rendering front-end akan diimplementasikan oleh komponen bootstrap standar dalam beberapa baris:

 render(( <Form schema={schema} uiSchema={uiSchema} /> ), document.getElementById("app")); 

Jika widget standar yang datang dengan bootstrap tidak cocok untuk Anda dan Anda perlu penyesuaian - untuk beberapa tipe data Anda dapat menentukan templat Anda sendiri di uiSchema, pada saat penulisan, string , angka , integer , boolean didukung.

FormData - berisi data formulir, misalnya:

 { "firstName": "Chuck", "lastName": "Norris", "age": 78, "bio": "Roundhouse kicking asses since 1940", "password": "noneed" } 

Setelah rendering, widget akan diisi dengan data ini - berguna untuk mengedit formulir, serta untuk beberapa mekanisme kustom yang kami tambahkan untuk bidang terkait dan formulir kompleks, lebih lanjut tentang itu di bawah ini.

Anda dapat membaca lebih lanjut tentang semua nuansa pengaturan dan menggunakan bagian yang dijelaskan di atas pada halaman plugin .

Di luar kotak, perpustakaan memungkinkan Anda untuk bekerja hanya dengan tiga bagian ini, tetapi untuk aplikasi web yang lengkap, Anda perlu menambahkan sejumlah fitur:

Kesalahan - perlu juga untuk mentransfer kesalahan dari berbagai pemeriksaan backend untuk dirender kepada pengguna, dan kesalahan dapat berupa validasi sederhana - misalnya, keunikan login saat mendaftar pengguna, atau yang lebih kompleks berdasarkan pada logika bisnis - yaitu. kita harus dapat menyesuaikan nomor (kesalahan) dan teks pemberitahuan yang ditampilkan. Untuk melakukan ini, selain yang dijelaskan di atas, bagian Kesalahan ditambahkan ke set data yang dikirimkan - untuk setiap bidang, daftar kesalahan untuk rendering didefinisikan di sini

Tindakan , Metode - untuk mengirim data yang disiapkan oleh pengguna ke backend, dua atribut ditambahkan yang berisi URL backend dari pengontrol yang melakukan pemrosesan dan metode pengiriman HTTP

Akibatnya, untuk komunikasi antara depan dan belakang, kami mendapat json dengan bagian berikut:

 { "action": "https://...", "method": "POST", "errors":{}, "schema":{}, "formData":{}, "uiSchema":{} } 

Tetapi bagaimana cara menghasilkan data ini di backend? Pada saat pembuatan sistem, tidak ada perpustakaan siap pakai yang memungkinkan Anda untuk mengkonversi Formulir Symfony ke Skema JSON. Sekarang mereka sudah muncul, tetapi memiliki kelemahan mereka - misalnya, LiformBundle menginterpretasikan Skema JSON dengan cukup bebas dan mengubah standar atas kebijakannya sendiri, jadi, sayangnya, saya harus menulis implementasi saya sendiri.

Sebagai dasar untuk generasi, bentuk Symfony standar digunakan . Cukup menggunakan pembuat dan menambahkan bidang yang diperlukan:
Contoh formulir
 $builder ->add('title', TextType::class, [ 'label' => 'label.title', 'attr' => [ 'title' => 'title.title', ], ]) ->add('description', TextareaType::class, [ 'label' => 'label.description', 'attr' => [ 'title' => 'title.description', ], ]) ->add('year', ChoiceType::class, [ 'choices' => range(1981, 1990), 'choice_label' => function ($val) { return $val; }, 'label' => 'label.year', 'attr' => [ 'title' => 'title.year', ], ]) ->add('genre', ChoiceType::class, [ 'choices' => [ 'fantasy', 'thriller', 'comedy', ], 'choice_label' => function ($val) { return 'genre.choice.'.$val; }, 'label' => 'label.genre', 'attr' => [ 'title' => 'title.genre', ], ]) ->add('available', CheckboxType::class, [ 'label' => 'label.available', 'attr' => [ 'title' => 'title.available', ], ]); 


Pada output, formulir ini diubah menjadi sirkuit bentuk:
Contoh JsonSchema
 { "action": "//localhost/create.json", "method": "POST", "schema": { "properties": { "title": { "maxLength": 255, "minLength": 1, "type": "string", "title": "label.title" }, "description": { "type": "string", "title": "label.description" }, "year": { "enum": [ "1981", "1982", "1983", "1984", "1985", "1986", "1987", "1988", "1989", "1990" ], "enumNames": [ "1981", "1982", "1983", "1984", "1985", "1986", "1987", "1988", "1989", "1990" ], "type": "string", "title": "label.year" }, "genre": { "enum": [ "fantasy", "thriller", "comedy" ], "enumNames": [ "genre.choice.fantasy", "genre.choice.thriller", "genre.choice.comedy" ], "type": "string", "title": "label.genre" }, "available": { "type": "object", "title": "label.available" } }, "required": [ "title", "description", "year", "genre", "available" ], "type": "object" }, "formData": { "title": "", "description": "", "year": "", "genre": "" }, "uiSchema": { "title": { "ui:help": "title.title", "ui:widget": "text" }, "description": { "ui:help": "title.description", "ui:widget": "textarea" }, "year": { "ui:widget": "select", "ui:help": "title.year" }, "genre": { "ui:widget": "select", "ui:help": "title.genre" }, "available": { "ui:help": "title.available", "ui:widget": "checkbox" }, "ui:widget": "mainForm" } } 


Semua kode yang mengonversi formulir ke JSON ditutup dan hanya digunakan di Rambler Group, jika komunitas memiliki minat pada topik ini, kami akan membuatnya dalam format bundel dalam repositori github kami.

Mari kita lihat beberapa aspek lagi yang tanpanya sulit untuk membangun aplikasi web modern:

Validasi bidang


Ini diatur menggunakan symfony validator , yang menjelaskan aturan untuk memvalidasi objek, contoh validator:

 <property name="title"> <constraint name="Length"> <option name="min">1</option> <option name="max">255</option> <option name="minMessage">title.min</option> <option name="maxMessage">title.max</option> </constraint> <constraint name="NotBlank"> <option name="message">title.not_blank</option> </constraint> </property> 


Dalam contoh ini, batasan tipe NotBlank memodifikasi skema dengan menambahkan bidang ke array bidang yang diperlukan dari skema, dan batasan tipe Panjang menambahkan atribut schema-> properties-> title-> maxLength dan schema-> properties-> title-> minLength, yang validasinya harus sudah memperhitungkan di ujung depan.

Mengelompokkan item


Dalam kehidupan nyata, bentuk-bentuk sederhana lebih cenderung merupakan pengecualian terhadap aturan. Sebagai contoh, sebuah proyek mungkin memiliki formulir dengan sejumlah besar bidang dan memberikan semuanya dalam daftar yang solid bukanlah pilihan terbaik - kita harus menjaga pengguna aplikasi kita:

Keputusan yang jelas adalah untuk membagi formulir ke dalam grup logis elemen kontrol sehingga lebih mudah bagi pengguna untuk menavigasi dan membuat lebih sedikit kesalahan:

Seperti yang Anda ketahui, kemampuan Formulir Symfony di luar kotak cukup besar - misalnya, formulir dapat diwarisi dari formulir lain, ini nyaman, tetapi dalam kasus kami ada kelemahan. Dalam implementasi saat ini, urutan dalam Skema JSON menentukan urutan di mana elemen bentuk digambar di browser, warisan mungkin melanggar urutan ini. Salah satu opsi adalah mengelompokkan elemen, misalnya:

Contoh Formulir Bersarang
 $info = $builder ->create('info',FormType::class,['inherit_data'=>true]) ->add('title', TextType::class, [ 'label' => 'label.title', 'attr' => [ 'title' => 'title.title', ], ]) ->add('description', TextareaType::class, [ 'label' => 'label.description', 'attr' => [ 'title' => 'title.description', ], ]); $builder ->add($info) ->add('year', ChoiceType::class, [ 'choices' => range(1981, 1990), 'choice_label' => function ($val) { return $val; }, 'label' => 'label.year', 'attr' => [ 'title' => 'title.year', ], ]) ->add('genre', ChoiceType::class, [ 'choices' => [ 'fantasy', 'thriller', 'comedy', ], 'choice_label' => function ($val) { return 'genre.choice.'.$val; }, 'label' => 'label.genre', 'attr' => [ 'title' => 'title.genre', ], ]) ->add('available', CheckboxType::class, [ 'label' => 'label.available', 'attr' => [ 'title' => 'title.available', ], ]); 


Formulir ini akan dikonversi menjadi sirkuit bentuk:

Contoh JsonSchema bersarang
 "schema": { "properties": { "info": { "properties": { "title": { "type": "string", "title": "label.title" }, "description": { "type": "string", "title": "label.description" } }, "required": [ "title", "description" ], "type": "object" }, "year": { "enum": [ "1981", "1982", "1983", "1984", "1985", "1986", "1987", "1988", "1989", "1990" ], "enumNames": [ "1981", "1982", "1983", "1984", "1985", "1986", "1987", "1988", "1989", "1990" ], "type": "string", "title": "label.year" }, "genre": { "enum": [ "fantasy", "thriller", "comedy" ], "enumNames": [ "genre.choice.fantasy", "genre.choice.thriller", "genre.choice.comedy" ], "type": "string", "title": "label.genre" }, "available": { "type": "object", "title": "label.available" } }, "required": [ "info", "year", "genre", "available" ], "type": "object" } 


dan uiSchema yang sesuai
 "uiSchema": { "info": { "title": { "ui:help": "title.title", "ui:widget": "text" }, "description": { "ui:help": "title.description", "ui:widget": "textarea" }, "ui:widget": "form" }, "year": { "ui:widget": "select", "ui:help": "title.year" }, "genre": { "ui:widget": "select", "ui:help": "title.genre" }, "available": { "ui:help": "title.available", "ui:widget": "checkbox" }, "ui:widget": "group" } 


Metode pengelompokan ini tidak cocok untuk kami karena formulir untuk data mulai bergantung pada presentasi dan tidak dapat digunakan, misalnya, dalam API atau formulir lainnya. Diputuskan untuk menggunakan parameter tambahan di uiSchema tanpa melanggar standar Skema JSON saat ini. Akibatnya, opsi tambahan tentang jenis berikut ditambahkan ke formulir simfoni:

 'fieldset' => [ 'groups' => [ [ 'type' => 'base', 'name' => 'info', 'fields' => ['title', 'description'], 'order' => ['title', 'description'] ] ], 'type' => 'base' ] 

Ini akan dikonversi ke skema berikut:

 "ui:group": { "type": "base", "groups": [ { "type": "group", "name": "info", "title": "legend.info", "fields": [ "title", "description" ], "order": [ "title", "description" ] } ], "order": [ "info" ] }, 


Versi lengkap skema dan uiSchema
 "schema": { "properties": { "title": { "maxLength": 255, "minLength": 1, "type": "string", "title": "label.title" }, "description": { "type": "string", "title": "label.description" }, "year": { "enum": [ "1989", "1990" ], "enumNames": [ "1989", "1990" ], "type": "string", "title": "label.year" }, "genre": { "enum": [ "fantasy", "thriller", "comedy" ], "enumNames": [ "genre.choice.fantasy", "genre.choice.thriller", "genre.choice.comedy" ], "type": "string", "title": "label.genre" }, "available": { "type": "boolean", "title": "label.available" } }, "required": [ "title", "description", "year", "genre", "available" ], "type": "object" } 

 "uiSchema": { "title": { "ui:help": "title.title", "ui:widget": "text" }, "description": { "ui:help": "title.description", "ui:widget": "textarea" }, "year": { "ui:widget": "select", "ui:help": "title.year" }, "genre": { "ui:widget": "select", "ui:help": "title.genre" }, "available": { "ui:help": "title.available", "ui:widget": "checkbox" }, "ui:group": { "type": "base", "groups": [ { "type": "group", "name": "info", "title": "legend.info", "fields": [ "title", "description" ], "order": [ "title", "description" ] } ], "order": [ "info" ] }, "ui:widget": "fieldset" } 


Karena di sisi front-end React library yang kami gunakan tidak mendukung ini di luar kotak, saya harus menambahkan fungsi ini sendiri. Dengan penambahan elemen baru "ui: group", kami mendapat kesempatan untuk sepenuhnya mengontrol proses pengelompokan elemen dan formulir menggunakan API saat ini.

Bentuk dinamis


Bagaimana jika satu bidang bergantung pada bidang lainnya, misalnya, daftar turun bawah subkategori tergantung pada kategori yang dipilih?



FORMULIR Symfony memberi kita kesempatan untuk membuat formulir dinamis dengan bantuan Acara, tetapi, sayangnya, pada saat implementasi, fitur ini tidak didukung oleh Skema JSON, meskipun fitur ini muncul dalam versi terbaru. Awalnya, idenya adalah untuk memberikan seluruh daftar ke objek Enum dan EnumNames, berdasarkan yang digunakan untuk memfilter nilai:

 { "properties": { "genre": { "enum": [ "fantasy", "thriller", "comedy" ], "enumNames": [ "genre.choice.fantasy", "genre.choice.thriller", "genre.choice.comedy" ], "type": "string", "title": "label.genre" }, "sgenre": { "enum": [ "eccentric", "romantic", "grotesque" ], "enumNames": [ { "title": "sgenre.choice.eccentric", "genre": "comedy" }, { "title": "sgenre.choice.romantic", "genre": "comedy" }, { "title": "sgenre.choice.grotesque", "genre": "comedy" } ], "type": "string", "title": "label.genre" } }, "type": "object" } 

Tetapi dengan pendekatan ini, untuk setiap elemen seperti itu perlu untuk menulis pemrosesan sendiri di front-end, belum lagi fakta bahwa semuanya menjadi sangat rumit ketika ada beberapa objek ini atau satu elemen tergantung pada beberapa daftar. Selain itu, jumlah data yang dikirim ke frontend tumbuh secara signifikan untuk pemrosesan dan rendering semua dependensi yang benar. Misalnya, bayangkan gambar bentuk yang terdiri dari tiga bidang yang saling berhubungan - negara, kota, jalan. Jumlah data awal yang perlu dikirim ke backend ke ujung depan dapat mengganggu klien tipis, dan, seperti yang Anda ingat, kami harus menjaga pengguna kami. Oleh karena itu, diputuskan untuk mengimplementasikan dinamika dengan menambahkan atribut khusus:

  • SchemaID - atribut skema, berisi alamat pengontrol untuk memproses FormData yang dimasukkan saat ini dan memperbarui skema formulir saat ini, jika diperlukan oleh logika bisnis;
  • Reload - atribut yang memberitahu frontend bahwa perubahan dalam bidang ini memulai pembaruan ke sirkuit dengan mengirimkan data formulir ke backend;

Kehadiran SkemaID mungkin tampak seperti duplikasi - lagipula, ada atribut tindakan , tetapi di sini kita berbicara tentang pembagian tanggung jawab - pengendali SkemaID bertanggung jawab atas pembaruan antara skema dan UISchema , dan pengontrol tindakan melakukan tindakan bisnis yang diperlukan - misalnya, membuat atau memperbarui suatu objek dan tidak mengizinkan bagian dari formulir dikirim sebagai menghasilkan pemeriksaan validasi. Dengan penambahan ini, skema mulai terlihat seperti ini:

 { "schemaId": "//localhost/schema.json", "properties": { "genre": { "enum": [ "fantasy", "thriller", "comedy" ], "enumNames": [ "genre.choice.fantasy", "genre.choice.thriller", "genre.choice.comedy" ], "type": "string", "title": "label.genre" }, "sgenre": { "enum": [], "enumNames": [], "type": "string", "title": "label.sgenre" } }, "uiSchema": { "genre": { "ui:options": { "reload": true }, "ui:widget": "select", "ui:help": "title.genre" }, "sgenre": { "ui:widget": "select", "ui:help": "title.sgenre" }, "ui:widget": "mainForm" }, "type": "object" } 

Dalam hal mengubah bidang "genre", frontend mengirim seluruh formulir dengan data saat ini ke backend, menerima sebagai tanggapan satu set bagian yang diperlukan untuk membuat formulir:

 { action: โ€œhttps://...โ€, method: "POST", schema:{} formData:{} uiSchema:{} } 

dan render alih-alih bentuk saat ini. Apa yang sebenarnya akan berubah setelah pengiriman ditentukan oleh bagian belakang, komposisi atau jumlah bidang dapat berubah, dll. - perubahan apa pun yang diperlukan logika bisnis aplikasi.

Kesimpulan


Karena perluasan kecil dari pendekatan standar, kami mendapat sejumlah fitur tambahan yang memungkinkan kami untuk sepenuhnya mengontrol pembentukan dan perilaku komponen Bereaksi front-end, membangun sirkuit dinamis berdasarkan logika bisnis, memiliki satu titik untuk pembentukan aturan validasi, dan kemampuan untuk secara cepat dan fleksibel membuat bagian VIEW baru - misalnya, ponsel atau desktop aplikasi. Datang ke dalam eksperimen yang berani, Anda perlu mengingat standar atas dasar di mana Anda bekerja dan mempertahankan kompatibilitas dengan itu. Alih-alih Bereaksi, perpustakaan lain dapat digunakan di frontend, hal utama adalah menulis adaptor transport ke JSON Schema dan menghubungkan beberapa form rendering library. Bootstrap bekerja dengan baik dengan React karena kami memiliki pengalaman bekerja dengan tumpukan teknologi ini, tetapi pendekatan yang kami bicarakan tidak membatasi Anda dalam memilih teknologi. Di tempat Symfony, bisa juga ada kerangka kerja lain yang memungkinkan Anda untuk mengkonversi formulir ke format Skema JSON.

Pembaruan: Anda dapat melihat laporan kami tentang Pertemuan # 14 Symfony Moscow tentang hal ini mulai 1:15:00.

Source: https://habr.com/ru/post/id417023/


All Articles