Bagaimana kami membangun komponen UI di Rails at Flywheel

Diterbitkan: 2019-11-16

Mempertahankan konsistensi visual dalam aplikasi web besar adalah masalah bersama di banyak organisasi. Di Flywheel, kami tidak berbeda. Aplikasi web utama kami dibangun dengan Ruby on Rails dan kami memiliki sekitar 15 pengembang Rails dan tiga pengembang front-end yang melakukan kode untuk itu pada hari tertentu. Kami juga hebat dalam desain (itu salah satu nilai inti kami sebagai perusahaan), dan memiliki tiga desainer yang bekerja sangat erat dengan pengembang di tim Scrum kami.

Tujuan utama kami adalah memastikan bahwa pengembang Flywheel mana pun dapat membuat halaman responsif tanpa hambatan apa pun. Penghalang pandang umumnya termasuk tidak mengetahui komponen mana yang akan digunakan untuk membuat mockup (yang mengarah pada pengembangan basis kode dengan komponen yang sangat mirip dan berlebihan) dan tidak tahu kapan harus mendiskusikan penggunaan ulang dengan desainer. 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 sekarang ke pendekatan baru (untuk kami) yang saya yakin akan mengatur kami untuk waktu yang lama yang akan datang. Jika Anda menghadapi masalah serupa di aplikasi Rails Anda dan Anda ingin mendekati komponen dari sisi server, saya harap artikel ini dapat memberi Anda beberapa ide.

Dalam artikel ini, saya akan menyelami:

  • Apa yang sedang kita selesaikan
  • Komponen pembatas
  • Merender komponen di sisi server
  • Di mana kami tidak dapat menggunakan komponen sisi server


Apa yang sedang kita selesaikan

Kami ingin sepenuhnya membatasi komponen UI kami dan menghilangkan kemungkinan UI yang sama dibuat dengan lebih dari satu cara. Meskipun pelanggan mungkin tidak dapat mengetahuinya (pada awalnya), tidak adanya kendala pada komponen menyebabkan pengalaman pengembang yang membingungkan, membuat hal-hal menjadi sangat sulit untuk dipelihara, dan menyulitkan untuk membuat perubahan desain global.

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

Ini bekerja dengan baik dan cocok untuk kami selama beberapa tahun, tetapi masalah mulai muncul saat 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 digunakan dan mana yang harus dihindari, dan urutan markup apa yang diperlukan untuk menghasilkan variasi yang diinginkan. Dan seringkali, desainer akan membuat sedikit tambahan atau penyesuaian pada komponen tertentu. Karena panduan gaya tidak cukup mendukung itu, peretasan alternatif untuk membuat tweak itu ditampilkan dengan benar (seperti mengkanibalisasi bagian dari komponen lain secara tidak tepat) menjadi sangat umum.

Contoh komponen yang tidak dibatasi

Untuk mengilustrasikan bagaimana ketidakkonsistenan muncul dari waktu ke waktu, saya akan menggunakan contoh sederhana (dan dibuat-buat) tetapi sangat umum dari salah satu komponen kami 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-left
    %h2 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

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

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2 Backups
...

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

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2.--ml-10 Backups
...

Nah itu secara teknis terlihat seperti mockup, kan?! Tentu, tetapi katakanlah sebulan kemudian, pengembang lain membutuhkan header kartu, tetapi tanpa ikon. Mereka menemukan contoh terakhir, salin/tempel, dan cukup hapus ikonnya.

Sekali lagi tampaknya benar, bukan? Di luar konteks, untuk seseorang yang tidak tertarik dengan desain, tentu saja! Tapi lihat di sebelah aslinya. Margin kiri pada gelar itu masih ada karena mereka tidak menyadari bahwa penolong kiri margin perlu dihilangkan!

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

Pengembang lain mungkin mencoba menggunakan kembali kode itu, tetapi dalam kasus ini, mereka sebenarnya membutuhkan perbatasan. Mari kita katakan 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 perbatasan, jadi mereka menerapkan kelas pembantu ke jam dan voila!

Dengan semua modifikasi pada header kartu asli, kita sekarang memiliki kekacauan di tangan kita dalam kode.

.card__header.--borderless
  .card__header-left
    %h2.--ml-10 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

  %hr.--mt-0.--mb-0

Ingatlah bahwa contoh di atas hanya untuk mengilustrasikan poin bagaimana komponen yang tidak dibatasi dapat menjadi berantakan seiring waktu. Jika ada orang di tim kami yang mencoba mengirimkan variasi tajuk kartu, itu harus ditangkap oleh tinjauan desain atau tinjauan kode. Tetapi hal-hal seperti ini terkadang lolos dari celah, oleh karena itu kebutuhan kita akan barang-barang antipeluru!


Komponen pembatas

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

Aplikasi Flywheel sangat berat di bagian belakang dengan sebagian besar HTML yang dirender oleh server – tetapi untungnya bagi kami, komponen dapat datang dalam berbagai bentuk. Pada akhirnya, komponen UI adalah enkapsulasi gaya dan aturan desain yang menampilkan markup ke browser. Dengan realisasi ini, kita dapat mengambil pendekatan yang sama untuk komponen, tetapi tanpa overhead kerangka JavaScript.

Kita akan membahas bagaimana kita membangun komponen yang dibatasi di bawah ini, tetapi berikut adalah beberapa manfaat yang kami temukan dengan menggunakannya:

  • Tidak pernah ada cara yang salah untuk menyatukan komponen.
  • Komponen melakukan semua pemikiran desain untuk Anda. (Anda baru saja memberikan opsi!)
  • Sintaks untuk membuat komponen sangat konsisten dan mudah dinalar.
  • Jika perubahan desain diperlukan pada suatu komponen, kami dapat mengubahnya sekali dalam komponen dan yakin bahwa itu diperbarui di mana-mana.

Merender komponen di sisi server

Jadi apa yang kita bicarakan dengan membatasi komponen? Mari kita menggali!

Seperti yang disebutkan sebelumnya, kami ingin setiap pengembang yang bekerja di aplikasi Roda Gila dapat melihat mockup desain halaman dan dapat segera membuat halaman itu tanpa hambatan. Itu berarti metode pembuatan UI harus A) didokumentasikan dengan sangat baik dan B) sangat deklaratif dan bebas dari dugaan.

Sebagian untuk menyelamatkan (atau begitulah menurut kami)

Penusukan pertama yang kami coba di masa lalu adalah menggunakan parsial Rails. Partials adalah satu-satunya alat yang diberikan Rails untuk dapat digunakan kembali dalam template. Secara alami, mereka adalah hal pertama yang dicapai semua orang. Tetapi ada kelemahan signifikan untuk mengandalkannya 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.

Partials DO mencegah kesalahan duplikasi salin/tempel dan mereka berfungsi dengan baik untuk beberapa kali pertama Anda perlu menggunakan kembali sesuatu. Tetapi dari pengalaman kami, sebagian segera menjadi berantakan dengan dukungan untuk lebih banyak fungsi dan logika. Tetapi logika tidak boleh hidup di templat!

Pengenalan Sel

Untungnya, ada alternatif yang lebih baik untuk parsial yang memungkinkan kita untuk menggunakan kembali kode dan menjaga logika tidak terlihat. Ini disebut Cells, permata Ruby yang dikembangkan oleh Trailblazer. Sel telah ada jauh sebelum popularitas meningkat di kerangka kerja front-end seperti React dan Vue dan mereka memungkinkan Anda untuk menulis model tampilan terenkapsulasi yang menangani logika dan templating. Mereka menyediakan abstraksi model tampilan, yang sebenarnya tidak dimiliki Rails di luar kotak. Kami sebenarnya telah menggunakan Sel di aplikasi Roda Gila untuk sementara waktu sekarang, hanya saja tidak dalam skala global yang sangat dapat digunakan kembali.

Pada tingkat yang paling sederhana, Sel memungkinkan kita untuk mengabstraksikan potongan markup seperti ini (kita menggunakan Haml untuk bahasa templating kita):

%div
  %h1 Hello, world!

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

= cell("hello_world")

Ini pada akhirnya membantu kami membatasi komponen ke tempat kelas pembantu atau komponen anak yang salah tidak dapat ditambahkan tanpa memodifikasi sel itu sendiri.

Membangun Sel

Kami menempatkan semua Sel UI kami di direktori app/cells/ui. Setiap sel harus berisi hanya satu file Ruby, diakhiri dengan _cell.rb. Secara teknis Anda dapat menulis template langsung di Ruby dengan helper content_tag, tetapi sebagian besar Sel kami juga berisi template Haml yang sesuai yang berada di folder yang dinamai oleh komponen.

Sel super dasar tanpa logika di dalamnya terlihat seperti ini:

// cells/ui/slat_cell.rb

module UI
  class SlatCell < ViewModel
    def show
    end
  end
end

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

Di templat, Anda dapat mengakses opsi yang diteruskan ke sel. Misalnya, jika sel dipakai dalam tampilan seperti = cell(“ui/slat”, title: “Title”, subtitle: “Subtitle”, label: “Label”), kita dapat mengakses opsi tersebut melalui objek opsi.

// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      %h4= options[:title]
      %p= options[:subtitle]
      = icon(options[:icon], color: "blue")

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

// cells/ui/slat_cell.rb
def title
  return unless options[:title]
  content_tag :h4, options[:title]
end

def subtitle
  return unless options[:subtitle]
  content_tag :p, options[:subtitle]
end
// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      = title
      = subtitle

Membungkus Sel dengan utilitas UI

Setelah membuktikan konsep bahwa ini dapat bekerja 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 pembantu kecil untuk itu! Sekarang kita bisa memanggil = ui “name_of_component” dan meneruskan opsi inline.

= ui "slat", title: "Title", subtitle: "Subtitle", label: "Label"

Melewati opsi sebagai blok alih-alih sebaris

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

= ui “slat", title: “Title”, subtitle: “Subtitle”, label: “Label”, link: “#”, tertiary_title: “Tertiary”, disabled: true, checklist: [“Item 1”, “Item 2”, “Item 3”]

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

Berikut adalah contoh penggunaan kelas OptionProxy di dalam sel kita:

module UI
  class SlatCell < ViewModel
    def show
      OptionProxy.new(self).yield!(options, &block)
      super()
    end
  end
end

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

= ui "slat" do |slat|
  - slat.title = "Title"
  - slat.subtitle = "Subtitle"
  - slat.label = "Label"
  - slat.link = "#"
  - slat.tertiary_title = "Tertiary"
  - slat.disabled = true
  - slat.checklist = ["Item 1", "Item 2", "Item 3"]

Memperkenalkan logika

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

Berpegang pada komponen slat kami, kami memiliki kebutuhan untuk terkadang membuat semuanya sebagai tautan dan terkadang menjadikannya sebagai div, berdasarkan apakah ada opsi tautan atau tidak. Saya percaya ini adalah satu-satunya komponen yang kami miliki yang dapat dirender sebagai div atau tautan, tetapi ini adalah contoh yang cukup rapi tentang kekuatan Sel.

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

Catatan: Ini terinspirasi dan dibuat oleh Adam Lassek, yang sangat berpengaruh dalam membantu kami membangun seluruh metode pengembangan UI ini dengan Sel.

def container(&block)
  tag =
    if options[:link]
      [:link_to, options[:link]]
    else
      [:content_tag, :div]
    end

  send(*tag, class: “slat__inner”, &block)
end

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

.slat
  = container do
  ...

Contoh lain dari logika di Sel yang sering kita gunakan adalah kelas keluaran bersyarat. Katakanlah kita menambahkan opsi yang dinonaktifkan ke sel. Tidak ada yang lain dalam permintaan perubahan sel, selain Anda sekarang dapat melewati opsi yang dinonaktifkan: benar dan perhatikan saat semuanya berubah menjadi keadaan dinonaktifkan (berwarna abu-abu dengan tautan yang tidak dapat diklik).

= ui "slat" do |slat|
  ...
  - slat.disabled = true

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

.slat{ class: possible_classes("--disabled": options[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ class: possible_classes("--alt": options[:disabled]) }= options[:title]
      %p{ class: possible_classes("--alt": options[:disabled]) }=
      options[:subtitle]
      = icon(options[:icon], color: "gray")

Secara tradisional, kita harus mengingat (atau merujuk panduan gaya) elemen individu mana yang membutuhkan kelas tambahan untuk membuat semuanya bekerja dengan benar dalam keadaan dinonaktifkan. Sel memungkinkan kami untuk mendeklarasikan satu opsi dan kemudian melakukan pekerjaan berat untuk kami.

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


Di mana kami tidak dapat menggunakan komponen sisi server

Sementara pendekatan sel sangat membantu untuk aplikasi khusus kami dan cara kami bekerja, saya akan lalai untuk mengatakan bahwa itu memecahkan 100% dari masalah kami. Kami masih menulis JavaScript (banyak) dan membangun beberapa pengalaman di Vue di seluruh aplikasi kami. 75% dari waktu, template Vue kami masih hidup di Haml dan kami mengikat instance Vue kami ke elemen yang berisi, yang memungkinkan kami untuk tetap memanfaatkan pendekatan sel.

Namun, di tempat yang lebih masuk akal untuk sepenuhnya membatasi komponen sebagai instance Vue file tunggal, kita tidak dapat menggunakan Cells. Daftar pilihan kami, misalnya, semuanya adalah Vue. Tapi saya pikir tidak apa-apa! Kami belum benar-benar mengalami kebutuhan untuk memiliki versi duplikat komponen di komponen Sel dan Vue, jadi tidak apa-apa jika beberapa komponen dibuat 100% dengan Vue dan beberapa dengan Sel. Jika sebuah komponen dibangun dengan Vue, itu berarti JavaScript diperlukan untuk membangunnya di DOM dan kami memanfaatkan kerangka kerja Vue untuk melakukannya. Untuk sebagian besar komponen kami yang lain, mereka tidak memerlukan JavaScript dan jika mereka melakukannya, mereka membutuhkan DOM yang sudah dibangun dan kami hanya menghubungkan dan menambahkan pendengar acara.

Saat kami terus maju dengan pendekatan sel, kami pasti akan bereksperimen dengan kombinasi komponen sel dan komponen Vue sehingga kami memiliki satu dan hanya satu cara untuk membuat dan menggunakan komponen. Saya belum tahu seperti apa bentuknya, jadi kita akan menyeberangi jembatan itu saat sampai di sana!


Kesimpulan kami

Sejauh ini kami telah mengonversi sekitar tiga puluh komponen visual kami yang paling sering digunakan ke Sel. Ini memberi kami ledakan produktivitas yang besar dan memberi pengembang rasa validasi bahwa pengalaman yang mereka bangun benar dan tidak diretas bersama.

Tim desain kami lebih yakin dari sebelumnya bahwa komponen dan pengalaman dalam aplikasi kami adalah 1:1 dengan apa yang mereka rancang di Adobe XD. Perubahan atau penambahan komponen sekarang ditangani hanya melalui interaksi dengan desainer dan pengembang front-end, yang membuat anggota tim lainnya tetap fokus dan tidak perlu khawatir karena mengetahui cara mengubah komponen agar sesuai dengan mockup desain.

Kami terus mengulangi pendekatan kami untuk membatasi komponen UI, tetapi saya harap teknik yang diilustrasikan dalam artikel ini memberi Anda gambaran sekilas tentang apa yang bekerja dengan baik untuk kami!


Ayo bekerja di Flywheel!

Di Flywheel, setiap departemen memiliki dampak yang berarti bagi pelanggan dan laba kami. Baik itu dukungan pelanggan, pengembangan perangkat lunak, pemasaran, atau apa pun di antaranya, kami semua bekerja sama untuk mewujudkan misi kami membangun perusahaan hosting yang benar-benar dapat membuat orang jatuh cinta.

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