Bagaimana kami Membangun Komponen UI di Rails

Diterbitkan: 2024-06-28

Mempertahankan konsistensi visual dalam aplikasi web besar adalah masalah bersama di banyak organisasi. Aplikasi web utama di balik produk Flywheel kami dibuat dengan Ruby on Rails, dan kami memiliki beberapa pengembang Rails dan tiga pengembang front-end yang melakukan kode pada aplikasi tersebut pada hari tertentu. Kami juga sangat menyukai desain (ini adalah salah satu nilai inti kami sebagai sebuah perusahaan), dan memiliki tiga desainer yang bekerja sangat erat dengan pengembang di tim Scrum kami.

dua orang berkolaborasi dalam desain situs web

Tujuan utama kami adalah memastikan bahwa pengembang mana pun dapat membuat halaman responsif tanpa hambatan apa pun. Kendala umumnya mencakup tidak mengetahui komponen mana yang akan digunakan untuk membuat mockup (yang menyebabkan pengembangan basis kode dengan komponen yang sangat mirip dan berlebihan) dan tidak mengetahui kapan harus mendiskusikan kegunaan kembali dengan desainer. Hal ini berkontribusi pada pengalaman pelanggan yang tidak konsisten, frustrasi pengembang, dan bahasa desain yang berbeda antara pengembang dan desainer.

Kami telah melalui beberapa iterasi panduan gaya dan metode membangun/mempertahankan pola dan komponen UI, dan setiap iterasi membantu memecahkan masalah yang kami hadapi saat itu. Kami yakin pendekatan baru kami akan mempersiapkan kami untuk jangka panjang. Jika Anda menghadapi masalah serupa di aplikasi Rails dan ingin mendekati komponen dari sisi server, saya harap artikel ini dapat memberi Anda beberapa ide.

seorang pria berjanggut tersenyum ke arah kamera sambil duduk di depan monitor komputer yang menampilkan baris-baris kode

Dalam artikel ini, saya akan mendalami:

  • Untuk apa kami menyelesaikannya
  • Membatasi komponen
  • Merender komponen di sisi server
  • Dimana kita tidak bisa menggunakan komponen sisi server

Apa yang Kami Selesaikan

Kami ingin membatasi sepenuhnya komponen UI kami dan menghilangkan kemungkinan pembuatan UI yang sama dengan lebih dari satu cara. Meskipun pelanggan mungkin tidak dapat mengetahuinya (pada awalnya), tidak adanya batasan pada komponen akan menyebabkan pengalaman pengembang yang membingungkan, membuat pemeliharaan menjadi sangat sulit, dan membuat perubahan desain global menjadi sulit.

Cara tradisional kami mendekati komponen adalah melalui panduan gaya kami, yang mencantumkan seluruh markup yang diperlukan untuk membuat komponen tertentu. Misalnya, inilah tampilan halaman panduan gaya untuk komponen slat kita:

halaman panduan gaya untuk komponen slat

Ini berfungsi dengan baik selama beberapa tahun, namun masalah mulai muncul ketika kami menambahkan varian, status, atau cara alternatif untuk menggunakan komponen. Dengan bagian UI yang kompleks, menjadi rumit untuk mereferensikan panduan gaya untuk mengetahui kelas mana yang harus digunakan dan mana yang harus dihindari, dan urutan markup yang diperlukan untuk menghasilkan variasi yang diinginkan.

Seringkali, desainer membuat sedikit penambahan atau penyesuaian pada komponen tertentu. Karena panduan gaya tidak cukup mendukung hal itu, peretasan alternatif agar perubahan tersebut ditampilkan dengan benar (seperti melakukan kanibalisasi bagian dari komponen lain secara tidak tepat) menjadi hal yang sangat umum.

Contoh Komponen Tidak Dibatasi

Untuk mengilustrasikan bagaimana ketidakkonsistenan muncul dari waktu ke waktu, saya akan menggunakan contoh sederhana (dan dibuat-buat) namun sangat umum dari salah satu komponen kita di aplikasi Flywheel: header kartu.

Dimulai dari mockup desain, seperti inilah tampilan header kartu. Itu cukup sederhana dengan judul, tombol, dan batas bawah.

 .card__header
  .card__header-kiri
    %h2 Cadangan
  .card__header-kanan
    = link_to "#" lakukan
      = ikon("plus_kecil")

Setelah diberi kode, bayangkan seorang desainer ingin menambahkan ikon di sebelah kiri judul. Di luar kotak, tidak akan ada margin apa pun antara ikon dan judul.

 ...
  .card__header-kiri
    = ikon("panah_cadangan", warna: "abu-abu25")
    %h2 Cadangan
...

Idealnya kita menyelesaikannya di CSS untuk header kartu, namun untuk contoh ini, katakanlah pengembang lain berpikir “Oh, saya tahu! Kami memiliki beberapa pembantu margin. Saya hanya akan memberikan judul pada kelas pembantu.”

 ...
  .card__header-kiri
    = ikon("panah_cadangan", warna: "abu-abu25")
    %h2.--ml-10 Cadangan
...

Secara teknis itu terlihat seperti mockupnya, kan?! Tentu, tapi katakanlah sebulan kemudian, pengembang lain memerlukan header kartu, tetapi tanpa ikon. Mereka menemukan contoh terakhir, menyalin/menempelkannya, dan menghapus ikonnya.

Sekali lagi sepertinya benar, bukan? Di luar konteks, bagi seseorang yang tidak tertarik pada desain, tentu saja! Tapi lihatlah di sebelah aslinya. Margin kiri pada judul tersebut masih ada karena mereka tidak menyadari bahwa margin kiri helper perlu dihilangkan!

Mengambil contoh ini satu langkah lebih jauh, katakanlah mockup lain meminta header kartu tanpa batas bawah. Seseorang mungkin menemukan keadaan yang kita miliki dalam panduan gaya yang disebut “tanpa batas” dan menerapkannya. Sempurna!

Pengembang lain mungkin akan mencoba menggunakan kembali kode tersebut, namun dalam kasus ini, mereka sebenarnya memerlukan pembatas. Katakanlah secara hipotetis bahwa mereka mengabaikan penggunaan yang tepat yang didokumentasikan dalam panduan gaya, dan tidak menyadari bahwa menghapus kelas tanpa batas akan memberi mereka batas. Sebaliknya, mereka menambahkan aturan horizontal. Akhirnya ada beberapa padding tambahan antara judul dan batas, jadi mereka menerapkan kelas pembantu pada jam dan voila!

Dengan semua modifikasi pada header kartu asli ini, sekarang kita mempunyai kode yang berantakan.

 .card__header.--tanpa batas
  .card__header-kiri
    %h2.--ml-10 Cadangan
  .card__header-kanan
    = link_to "#" lakukan
      = ikon("plus_kecil")
  %jam.--mt-0.--mb-0

Ingatlah bahwa contoh di atas hanya untuk mengilustrasikan bagaimana komponen yang tidak dibatasi dapat menjadi berantakan seiring berjalannya waktu. Jika ada orang di tim kami yang mencoba mengirimkan variasi header kartu, hal itu harus ditangkap oleh tinjauan desain atau tinjauan kode. Namun hal-hal seperti ini terkadang luput dari perhatian, oleh karena itu kita perlu melakukan hal-hal antipeluru!


Komponen Pembatas

Anda mungkin berpikir bahwa masalah yang tercantum di atas telah diselesaikan dengan jelas menggunakan komponen. Itu adalah asumsi yang benar! Kerangka kerja front-end seperti React dan Vue sangat populer untuk tujuan ini; itu adalah alat luar biasa untuk merangkum UI. Namun, ada satu masalah yang tidak selalu kami sukai—mereka mengharuskan UI Anda dirender oleh JavaScript.

Aplikasi Flywheel kami sangat back-end dengan HTML yang dirender di server—tetapi untungnya bagi kami, komponen dapat hadir dalam berbagai bentuk. Pada akhirnya, komponen UI adalah enkapsulasi gaya dan aturan desain yang menghasilkan markup ke browser. Dengan realisasi ini, kita dapat mengambil pendekatan yang sama terhadap komponen, namun tanpa overhead kerangka JavaScript.

Kami akan membahas cara membuat komponen yang dibatasi di bawah ini, namun berikut adalah beberapa manfaat yang kami temukan dengan menggunakannya:

  • Tidak pernah ada cara yang salah untuk menyatukan suatu komponen.
  • Komponen ini melakukan semua pemikiran desain untuk Anda. (Anda cukup memberikan opsi!)
  • Sintaks untuk membuat komponen sangat konsisten dan mudah dipahami.
  • Jika perubahan desain diperlukan pada suatu komponen, kita dapat mengubahnya satu kali dalam komponen tersebut dan yakin bahwa perubahan tersebut diperbarui di semua tempat.

Merender Komponen di Sisi Server

Jadi apa yang kita bicarakan dengan membatasi komponen? Mari kita gali lebih dalam!

Seperti disebutkan sebelumnya, kami ingin setiap pengembang yang bekerja dalam aplikasi dapat melihat mockup desain suatu halaman dan dapat segera membangun halaman tersebut tanpa hambatan. Artinya, metode pembuatan UI harus A) didokumentasikan dengan sangat baik dan B) sangat deklaratif dan bebas dari dugaan.

Sebagian dari Penyelamatan (atau begitulah yang kami pikir)

Upaya pertama yang pernah kami coba di masa lalu adalah menggunakan sebagian Rails. Parsial adalah satu-satunya alat yang diberikan Rails kepada Anda agar dapat digunakan kembali dalam templat. Tentu saja, itu adalah hal pertama yang dicari semua orang. Namun ada kelemahan yang signifikan jika mengandalkan logika tersebut karena jika Anda perlu menggabungkan logika dengan templat yang dapat digunakan kembali, Anda memiliki dua pilihan: menduplikasi logika di setiap pengontrol yang menggunakan parsial atau menyematkan logika ke dalam parsial itu sendiri.

Parsial JANGAN mencegah kesalahan duplikasi salin/tempel dan berfungsi dengan baik untuk beberapa kali pertama Anda perlu menggunakan kembali sesuatu. Namun dari pengalaman kami, sebagiannya segera menjadi berantakan dengan dukungan untuk lebih banyak fungsi dan logika. Namun logika tidak seharusnya hidup dalam pola!

Pengantar Sel

Untungnya, ada alternatif yang lebih baik daripada parsial yang memungkinkan kita menggunakan kembali kode dan menjaga logika agar tidak terlihat. Namanya Cells, permata Ruby yang dikembangkan oleh Trailblazer. Sel telah ada jauh sebelum popularitas kerangka kerja front-end seperti React dan Vue meningkat dan memungkinkan Anda menulis model tampilan terenkapsulasi yang menangani logika dan templating. Mereka menyediakan abstraksi model tampilan, yang tidak dimiliki oleh Rails. Kami sebenarnya sudah cukup lama menggunakan Sel di aplikasi Flywheel, hanya saja tidak dalam skala global yang sangat dapat digunakan kembali.

Pada tingkat paling sederhana, Sel memungkinkan kita mengabstraksi sebagian markup seperti ini (kita menggunakan Haml untuk bahasa templating kita):

 %div
  %h1 Halo dunia!

Menjadi model tampilan yang dapat digunakan kembali (sangat mirip dengan sebagian pada saat ini), dan ubah menjadi ini:

 = sel("halo_dunia")

Hal ini pada akhirnya membantu kita membatasi komponen di mana kelas pembantu atau komponen turunan yang salah tidak dapat ditambahkan tanpa memodifikasi sel itu sendiri.

Membangun Sel

Kami meletakkan semua Sel UI kami di direktori app/cells/ui. Setiap sel harus berisi hanya satu file Ruby, yang diberi akhiran _cell.rb. Secara teknis Anda dapat menulis templat langsung di Ruby dengan helper content_tag, tetapi sebagian besar Sel kami juga berisi templat Haml terkait yang berada di folder yang diberi nama oleh komponen tersebut.

Sel super dasar tanpa logika di dalamnya terlihat seperti ini:

 // sel/ui/slat_cell.rb
modul UI
  kelas SlatCell < ViewModel
    pasti pertunjukan
    akhir
  akhir
akhir

Metode show adalah apa yang dirender saat Anda membuat instance sel dan secara otomatis akan mencari file show.haml yang sesuai di folder dengan nama yang sama dengan sel tersebut. Dalam hal ini, itu adalah app/cells/ui/slat (kami mencakup semua Sel UI kami ke modul UI).

Di templat, Anda bisa mengakses opsi yang diteruskan ke sel. Misalnya, jika sel dipakai dalam tampilan seperti = sel(“ui/slat”, judul: “Judul”, subjudul: “Subjudul”, label: “Label”), kita dapat mengakses opsi tersebut melalui objek opsi.

 // sel/ui/slat/show.haml
.slat
  .slat__inner
    .slat__konten
      %h4= pilihan[:judul]
      %p= pilihan[:teks film]
      = ikon(pilihan[:ikon], warna: "biru")

Seringkali kita akan memindahkan elemen sederhana dan nilainya ke dalam metode di dalam sel untuk mencegah elemen kosong dirender jika opsi tidak ada.

 // sel/ui/slat_cell.rb
judul pasti
  kembalikan kecuali pilihan[:judul]
  content_tag :h4, opsi[:judul]
akhir
def subtitle
  kembali kecuali opsi[:subtitle]
  content_tag :p, pilihan[:subtitle]
akhir
 // sel/ui/slat/show.haml
.slat
  .slat__inner
    .slat__konten
      = judul
      = subjudul

Membungkus Sel dengan Utilitas UI

Setelah membuktikan konsep bahwa ini bisa berhasil dalam skala besar, saya ingin mengatasi markup asing yang diperlukan untuk memanggil sel. Itu tidak mengalir dengan benar dan sulit untuk diingat. Jadi kami membuat sedikit pembantu untuk itu! Sekarang kita cukup memanggil = ui “name_of_component” dan meneruskan opsi sebaris.

 = ui "slat", judul: "Judul", subjudul: "Subjudul", label: "Label"

Meneruskan Opsi sebagai Blok, Bukan Sebaris

Mengambil utilitas UI sedikit lebih jauh, dengan cepat menjadi jelas bahwa sel dengan banyak pilihan semua dalam satu baris akan sangat sulit untuk diikuti dan benar-benar jelek. Berikut ini contoh sel dengan banyak opsi yang ditentukan sebaris:

 = ui “slat", judul: “Judul”, subjudul: “Subjudul”, label: “Label”, tautan: “#”, judul_tersier: “Tersier”, dinonaktifkan: benar, daftar periksa: [“Item 1”, “Item 2”, “Barang 3”]

Ini sangat rumit, yang mengarahkan kita untuk membuat kelas bernama OptionProxy yang mencegat metode penyetel Sel dan menerjemahkannya menjadi nilai hash, yang kemudian digabungkan menjadi opsi. Jika kedengarannya rumit, jangan khawatir – bagi saya ini juga rumit. Berikut adalah inti dari kelas OptionProxy yang ditulis oleh Adam, salah satu insinyur perangkat lunak senior kami.

Berikut ini contoh penggunaan kelas OptionProxy di dalam sel kita:

 modul UI
  kelas SlatCell < ViewModel
    pasti pertunjukan
      OptionProxy.new(self).yield!(opsi, &blok)
      super()
    akhir
  akhir
akhir

Sekarang dengan itu, kita dapat mengubah opsi inline yang rumit menjadi blok yang lebih menyenangkan!

 = ui "slat" lakukan |slat|
  - slat.title = "Judul"
  - slat.subtitle = "Subjudul"
  - slat.label = "Label"
  - slat.link = "#"
  - slat.tertiary_title = "Tersier"
  - slat.disabled = benar
  - slat.checklist = ["Item 1", "Item 2", "Item 3"]

Memperkenalkan Logika

Hingga saat ini, contoh belum menyertakan logika apa pun seputar tampilan yang ditampilkan. Itu salah satu hal terbaik yang ditawarkan Cells, jadi mari kita bicarakan!

Tetap menggunakan komponen slat, terkadang kami perlu merender semuanya sebagai link dan terkadang merendernya sebagai div, berdasarkan ada atau tidaknya opsi link. Saya percaya ini adalah satu-satunya komponen yang kita miliki yang dapat dirender sebagai div atau tautan, tapi ini adalah contoh yang cukup bagus tentang kekuatan Sel.

Metode di bawah ini memanggil helper link_to atau content_tag bergantung pada keberadaan opsi [:link] .

 def wadah(&blok)
  tanda =
    jika opsi[:tautan]
      [:link_to, opsi[:link]]
    kalau tidak
      [:tag_konten, :div]
    akhir
  kirim(*tag, kelas: “slat__inner”, &blok)
akhir

Itu memungkinkan kita mengganti elemen .slat__inner di template dengan blok container:

 .slat
  = wadah lakukan
  ...

Contoh logika lain dalam Sel yang sering kita gunakan adalah kelas keluaran bersyarat. Katakanlah kita menambahkan opsi yang dinonaktifkan ke sel. Tidak ada hal lain dalam pemanggilan sel yang berubah, selain Anda sekarang dapat memberikan opsi yang dinonaktifkan: benar dan melihat semuanya berubah menjadi status nonaktif (berwarna abu-abu dengan tautan yang tidak dapat diklik).

 = ui "slat" lakukan |slat|
  ...
  - slat.disabled = benar

Jika opsi yang dinonaktifkan benar, kita dapat mengatur kelas pada elemen dalam template yang diperlukan untuk mendapatkan tampilan nonaktif yang diinginkan.

 .slat{ kelas: kemungkinan_kelas("--disabled": opsi[:disabled]) }
  .slat__inner
    .slat__konten
      %h4{ kelas: kemungkinan_kelas("--alt": opsi[:disabled]) }= opsi[:judul]
      %p{ kelas: kemungkinan_kelas("--alt": opsi[:dinonaktifkan]) }=
      pilihan[:teks film]
      = ikon(pilihan[:ikon], warna: "abu-abu")

Secara tradisional, kita harus mengingat (atau merujuk pada panduan gaya) elemen mana yang memerlukan kelas tambahan agar semuanya berfungsi dengan benar dalam keadaan nonaktif. Sel memungkinkan kita mendeklarasikan satu opsi dan kemudian melakukan tugas berat untuk kita.

Catatan: kemungkinan_kelas adalah metode yang kami buat untuk memungkinkan penerapan kelas secara kondisional di Haml dengan cara yang baik.


Dimana kita tidak dapat menggunakan Server Side Component

Meskipun pendekatan sel sangat membantu untuk aplikasi khusus kita dan cara kita bekerja, saya lalai mengatakan bahwa pendekatan ini 100% menyelesaikan masalah kita. Kami masih menulis JavaScript (banyak sekali) dan membangun beberapa pengalaman di Vue di seluruh aplikasi kami. 75% dari waktu, template Vue kita masih ada di Haml dan kita mengikat instance Vue kita ke elemen yang memuatnya, yang memungkinkan kita untuk tetap memanfaatkan pendekatan sel.

Namun, jika lebih masuk akal untuk membatasi komponen sepenuhnya sebagai instance Vue file tunggal, kita tidak dapat menggunakan Sel. Daftar pilihan kami, misalnya, semuanya adalah Vue. Tapi menurutku tidak apa-apa! Kami belum benar-benar mengalami kebutuhan untuk memiliki versi duplikat komponen di komponen Cells dan Vue, jadi tidak masalah jika beberapa komponen 100% dibuat dengan Vue dan ada pula yang menggunakan Cells.

Jika suatu komponen dibangun dengan Vue, itu berarti JavaScript diperlukan untuk membangunnya di DOM dan kami memanfaatkan kerangka Vue untuk melakukannya. Namun, untuk sebagian besar komponen kami yang lain, komponen tersebut tidak memerlukan JavaScript dan jika diperlukan, komponen tersebut memerlukan DOM yang sudah dibuat dan kami cukup menyambungkan dan menambahkan event listening.

Seiring kemajuan kita dalam pendekatan sel, kita pasti akan bereksperimen dengan kombinasi komponen sel dan komponen Vue sehingga kita memiliki satu-satunya cara untuk membuat dan menggunakan komponen. Saya belum tahu seperti apa bentuknya, jadi kita akan menyeberangi jembatan itu ketika sampai di sana!


Kesimpulan kami

Sejauh ini kami telah mengonversi sekitar tiga puluh komponen visual yang paling sering kami gunakan menjadi Sel. Hal ini memberi kami peningkatan produktivitas yang besar dan memberi pengembang rasa validasi bahwa pengalaman yang mereka bangun adalah benar dan tidak diretas secara bersamaan.

Tim desain kami semakin yakin bahwa komponen dan pengalaman dalam aplikasi kami 1:1 dengan apa yang mereka desain di Adobe XD. Perubahan atau penambahan komponen kini ditangani hanya melalui interaksi dengan desainer dan pengembang front-end, yang membuat seluruh tim tetap fokus dan bebas khawatir karena mengetahui cara mengubah komponen agar sesuai dengan maket desain.

Kami terus mengulangi pendekatan kami untuk membatasi komponen UI, namun saya harap teknik yang diilustrasikan dalam artikel ini memberi Anda gambaran sekilas tentang apa yang berhasil bagi kami!


Ayo Bekerja Bersama Kami!

Setiap departemen yang mengerjakan produk kami memiliki dampak yang berarti terhadap pelanggan dan keuntungan kami. Baik itu dukungan pelanggan, pengembangan perangkat lunak, pemasaran, atau apa pun di antaranya, kami semua bekerja sama mewujudkan misi kami untuk membangun perusahaan hosting yang benar-benar membuat orang jatuh cinta.

Siap bergabung dengan tim kami? Kami sedang merekrut! Lamar di sini.