Rails'de UI Bileşenlerini Nasıl Oluşturuyoruz?

Yayınlanan: 2024-06-28

Büyük bir web uygulamasında görsel tutarlılığı korumak birçok kuruluşta ortak bir sorundur. Flywheel ürünümüzün arkasındaki ana web uygulaması Ruby on Rails ile oluşturulmuştur ve herhangi bir günde buna kod taahhüt eden birden fazla Rails geliştiricimiz ve üç ön uç geliştiricimiz vardır. Tasarım konusunda da çok iddialıyız (bu, şirket olarak temel değerlerimizden biridir) ve Scrum ekiplerimizde geliştiricilerle çok yakın çalışan üç tasarımcımız var.

iki kişi bir web sitesi tasarımı üzerinde işbirliği yapıyor

Temel hedeflerimizden biri, herhangi bir geliştiricinin herhangi bir engel olmadan duyarlı bir sayfa oluşturabilmesini sağlamaktır. Birlikte gösterimler genellikle bir model oluşturmak için hangi mevcut bileşenlerin kullanılacağını bilmemeyi (bu da kod tabanının çok benzer, gereksiz bileşenlerle şişirilmesine yol açar) ve tasarımcılarla yeniden kullanılabilirliğin ne zaman tartışılacağını bilmemeyi içeriyordu. Bu, tutarsız müşteri deneyimlerine, geliştiricilerin hayal kırıklığına uğramasına ve geliştiriciler ile tasarımcılar arasında farklı bir tasarım diline neden olur.

Kullanıcı arayüzü kalıplarını ve bileşenlerini oluşturma/sürdürme yöntemleri ve stil kılavuzlarının birkaç yinelemesini inceledik ve her yineleme, o sırada karşılaştığımız sorunların çözülmesine yardımcı oldu. Yeni yaklaşımımızın bizi uzun süre ayakta tutacağından eminiz. Rails uygulamanızda benzer sorunlarla karşılaşıyorsanız ve bileşenlere sunucu tarafından yaklaşmak istiyorsanız bu makalenin size fikir verebileceğini umuyorum.

Sakallı bir adam, kod satırlarını görüntüleyen bir bilgisayar monitörünün önünde otururken kameraya gülümsüyor

Bu yazıda şunları ele alacağım:

  • Ne için çözüyoruz
  • Kısıtlayıcı bileşenler
  • Bileşenlerin sunucu tarafında görüntülenmesi
  • Sunucu tarafı bileşenlerini kullanamadığımız yerler

Ne İçin Çözüyoruz

Kullanıcı arayüzü bileşenlerimizi tamamen kısıtlamak ve aynı kullanıcı arayüzünün birden fazla şekilde oluşturulma olasılığını ortadan kaldırmak istedik. Müşteri (ilk başta) bunu anlayamasa da, bileşenler üzerinde kısıtlamaların olmaması kafa karıştırıcı bir geliştirici deneyimine yol açar, işleri sürdürmeyi çok zorlaştırır ve genel tasarım değişiklikleri yapmayı zorlaştırır.

Bileşenlere yaklaşmamızın geleneksel yolu, belirli bir bileşeni oluşturmak için gereken işaretlemelerin tamamını listeleyen stil kılavuzumuzdu. Örneğin, çıta bileşenimizin stil kılavuzu sayfası şöyle görünüyordu:

Çıta bileşeni için stil kılavuzu sayfası

Bu birkaç yıl boyunca işe yaradı, ancak bileşeni kullanmanın varyantlarını, durumlarını veya alternatif yollarını eklediğimizde sorunlar yavaş yavaş ortaya çıkmaya başladı. Karmaşık bir kullanıcı arayüzü parçası nedeniyle, hangi sınıfların kullanılacağını, hangilerinden kaçınılacağını ve istenen varyasyonun çıktısını almak için işaretlemenin hangi sırayla olması gerektiğini bilmek için stil kılavuzuna başvurmak hantal hale geldi.

Çoğu zaman tasarımcılar belirli bir bileşene küçük eklemeler veya ince ayarlar yaparlar. Stil kılavuzu bunu tam olarak desteklemediğinden, bu ince ayarın doğru şekilde görüntülenmesini sağlayacak alternatif hileler (başka bir bileşenin bir kısmını uygunsuz bir şekilde yamyamlamak gibi) rahatsız edici derecede yaygın hale geldi.

Kısıtlanmamış Bileşen Örneği

Tutarsızlıkların zaman içinde nasıl yüzeye çıktığını göstermek için Flywheel uygulamasındaki bileşenlerimizden birinin basit (ve yapmacık) ama çok yaygın bir örneğini kullanacağım: kart başlıkları.

Bir tasarım maketinden yeni başlayarak, kart başlığı böyle görünüyordu. Bir başlık, bir düğme ve bir alt çerçeveyle oldukça basitti.

 .kart__başlığı
  .card__başlık-sol
    %h2 Yedeklemeler
  .card__başlık-sağ
    = link_to "#" yap
      = icon("plus_small")

Kodlandıktan sonra, bir tasarımcının başlığın soluna bir simge eklemek istediğini hayal edin. Kutunun dışında simge ile başlık arasında herhangi bir kenar boşluğu olmayacak.

 ...
  .card__başlık-sol
    = icon("arrow_backup", color: "gray25")
    %h2 Yedeklemeler
...

İdeal olarak bunu kart başlıkları için CSS'de çözerdik, ancak bu örnekte başka bir geliştiricinin şöyle düşündüğünü varsayalım: "Ah, biliyorum! Bazı marj yardımcılarımız var. Başlığa bir yardımcı sınıf tokat atacağım.

 ...
  .card__başlık-sol
    = icon("arrow_backup", color: "gray25")
    %h2.--ml-10 Yedeklemeler
...

Teknik olarak maketteki gibi görünüyor, değil mi? Elbette, ancak diyelim ki bir ay sonra başka bir geliştiricinin simge olmadan bir kart başlığına ihtiyacı var. Son örneği bulurlar, kopyalayıp yapıştırırlar ve simgeyi kaldırırlar.

Yine doğru görünüyor, değil mi? Bağlamın dışında, tasarım konusunda keskin bir bakış açısına sahip olmayan biri için elbette! Ama orijinalin yanında bakın. Başlıktaki sol kenar boşluğu hala orada çünkü sol kenar boşluğu yardımcısının kaldırılması gerektiğinin farkına varmadılar!

Bu örneği bir adım daha ileri götürerek, başka bir modelin alt kenarlığı olmayan bir kart başlığı gerektirdiğini varsayalım. Stil rehberimizde “kenarlıksız” diye bir durumu bulup uygulayabilirsiniz. Mükemmel!

Başka bir geliştirici daha sonra bu kodu yeniden kullanmayı deneyebilir, ancak bu durumda aslında bir kenarlığa ihtiyaçları vardır. Varsayımsal olarak, stil kılavuzunda belgelenen doğru kullanımı göz ardı ettiklerini ve kenarlıksız sınıfı kaldırmanın onlara kenarlık kazandıracağının farkında olmadıklarını varsayalım. Bunun yerine yatay bir kural ekliyorlar. Başlık ile kenarlık arasında fazladan bir dolgu malzemesi kalıyor, bu yüzden saate yardımcı bir sınıf uyguluyorlar ve işte!

Orijinal kart başlığındaki tüm bu değişikliklerle birlikte artık kodda bir karışıklık var.

 .card__header.--kenarlıksız
  .card__başlık-sol
    %h2.--ml-10 Yedeklemeler
  .card__başlık-sağ
    = link_to "#" yap
      = icon("plus_small")
  %hr.--mt-0.--mb-0

Yukarıdaki örneğin, kısıtlanmamış bileşenlerin zaman içinde nasıl dağınık hale gelebileceğini göstermek için olduğunu unutmayın. Ekibimizden herhangi biri kart başlığının bir varyasyonunu göndermeye çalışırsa, bunun bir tasarım incelemesi veya kod incelemesi tarafından tespit edilmesi gerekir . Ancak bunun gibi şeyler bazen gözden kaçar, bu yüzden de kurşun geçirmez şeylere ihtiyacımız var!


Kısıtlayıcı Bileşenler

Yukarıda sıralanan sorunların bileşenlerle zaten net bir şekilde çözüldüğünü düşünüyor olabilirsiniz. Bu doğru bir varsayım! React ve Vue gibi ön uç çerçeveler tam da bu amaç için son derece popülerdir; kullanıcı arayüzünü kapsüllemek için harika araçlardır. Ancak bunların her zaman hoşlanmadığımız bir sorunu var: Kullanıcı arayüzünüzün JavaScript tarafından oluşturulmasını gerektiriyorlar.

Flywheel uygulamamız, esas olarak sunucu tarafından oluşturulan HTML'den dolayı oldukça arka uç ağırlıklıdır; ancak bizim için şans eseri, bileşenler birçok biçimde olabilir. Günün sonunda, bir kullanıcı arayüzü bileşeni, işaretlemeyi tarayıcıya gönderen stillerin ve tasarım kurallarının kapsüllenmesidir. Bu farkındalıkla, aynı yaklaşımı bileşenlere de uygulayabiliriz, ancak bunu bir JavaScript çerçevesinin yükü olmadan gerçekleştirebiliriz.

Kısıtlı bileşenleri nasıl oluşturduğumuzu aşağıda ele alacağız, ancak burada bunları kullanarak bulduğumuz avantajlardan birkaçı yer alıyor:

  • Bir bileşeni bir araya getirmenin hiçbir zaman yanlış bir yolu yoktur.
  • Bileşen sizin için tüm tasarım düşüncesini gerçekleştirir. (Sadece seçenekleri belirtin!)
  • Bir bileşen oluşturmanın sözdizimi çok tutarlıdır ve akıl yürütmesi kolaydır.
  • Bir bileşende tasarım değişikliği gerekiyorsa, bunu bileşende bir kez değiştirebiliriz ve her yerde güncellendiğinden emin olabiliriz.

Sunucu Tarafında Bileşenleri Oluşturma

Peki bileşenleri kısıtlayarak neyden bahsediyoruz? Hadi kazalım!

Daha önce de belirttiğimiz gibi, uygulamada çalışan herhangi bir geliştiricinin bir sayfanın tasarım maketine bakabilmesini ve o sayfayı hiçbir engel olmadan hemen oluşturabilmesini istiyoruz. Bu, kullanıcı arayüzünü oluşturma yönteminin A) çok iyi belgelenmiş olması ve B) çok açıklayıcı ve tahmine dayalı olmaması gerektiği anlamına gelir.

Kurtarmanın Kısmileri (ya da biz öyle düşündük)

Geçmişte denediğimiz ilk şey Rails kısmi parçalarını kullanmaktı. Parçalar, Rails'in şablonlarda yeniden kullanılabilirlik için size sunduğu tek araçtır. Doğal olarak herkesin ulaştığı ilk şey onlar. Ancak bunlara güvenmenin önemli dezavantajları vardır çünkü mantığı yeniden kullanılabilir bir şablonla birleştirmeniz gerekiyorsa iki seçeneğiniz vardır: mantığı kısmiyi kullanan her denetleyiciye kopyalamak veya mantığı kısminin kendisine gömmek.

Kısmi parçalar kopyalama/yapıştırma çoğaltma hatalarını engeller ve bir şeyi yeniden kullanmanız gereken ilk birkaç seferde sorunsuz çalışırlar. Ancak deneyimlerimize göre, kısmi parçalar çok geçmeden daha fazla işlevsellik ve mantık desteğiyle darmadağın oluyor. Ancak mantık şablonlarda yaşamamalı!

Hücrelere Giriş

Neyse ki, hem kodu yeniden kullanmamıza hem de mantığı gözden uzak tutmamıza olanak tanıyan, kısmi parçalara daha iyi bir alternatif var. Buna Trailblazer tarafından geliştirilen bir Ruby taşı olan Cells denir. Hücreler, React ve Vue gibi ön uç çerçevelerdeki popülerliğin artmasından çok önce ortalıktaydı ve hem mantığı hem de şablonlamayı yöneten kapsüllenmiş görünüm modelleri yazmanıza izin veriyorlar. Rails'in aslında kutudan çıkaramadığı bir görünüm modeli soyutlaması sağlıyorlar. Aslında bir süredir Flywheel uygulamasında Hücreleri kullanıyoruz, ancak küresel, süper yeniden kullanılabilir ölçekte değil.

En basit düzeyde, Hücreler bunun gibi bir işaretleme yığınını soyutlamamıza olanak tanır (şablonlama dilimiz için Haml'ı kullanırız):

 %div
  %h1 Merhaba dünya!

Yeniden kullanılabilir bir görünüm modeline (bu noktada kısmi modellere çok benzer) ve bunu şuna dönüştürün:

 = hücre("merhaba_dünya")

Bu sonuçta bileşeni, hücrenin kendisini değiştirmeden yardımcı sınıfların veya yanlış alt bileşenlerin eklenemeyeceği şekilde sınırlamamıza yardımcı olur.

Hücreleri Oluşturmak

Tüm UI Hücrelerimizi app/cells/ui dizinine koyuyoruz. Her hücrenin son eki _cell.rb olan tek bir Ruby dosyası içermesi gerekir. Teknik olarak şablonları content_tag yardımcısıyla doğrudan Ruby'de yazabilirsiniz, ancak Hücrelerimizin çoğu aynı zamanda bileşen tarafından adlandırılan bir klasörde yaşayan karşılık gelen bir Haml şablonu da içerir.

Hiçbir mantığı olmayan süper basit bir hücre şuna benzer:

 // hücreler/ui/slat_cell.rb
modül kullanıcı arayüzü
  sınıf SlatCell < ViewModel
    kesinlikle gösteri
    son
  son
son

Show yöntemi, hücreyi başlattığınızda oluşturulan yöntemdir ve klasörde hücreyle aynı adı taşıyan karşılık gelen show.haml dosyasını otomatik olarak arayacaktır. Bu durumda, app/cells/ui/slat olur (tüm UI Hücrelerimizi UI modülüne dahil ederiz).

Şablonda hücreye iletilen seçeneklere erişebilirsiniz. Örneğin, hücre = cell("ui/slat", title: "Title", altyazı: "Subtitle", label: "Label") gibi bir görünümde başlatılmışsa, bu seçeneklere seçenekler nesnesi aracılığıyla erişebiliriz.

 // hücreler/ui/slat/show.haml
.çıta
  .slat__inner
    .slat__içerik
      %h4= seçenekler[:başlık]
      %p= seçenekler[:altyazı]
      = simge(seçenekler[:ikon], renk: "mavi")

Çoğu zaman, bir seçenek yoksa boş öğelerin oluşturulmasını önlemek için basit öğeleri ve değerlerini hücredeki bir yönteme taşırız.

 // hücreler/ui/slat_cell.rb
tanım başlığı
  seçenekler olmadığı sürece geri dön[:başlık]
  content_tag :h4, seçenekler[:başlık]
son
def altyazı
  seçenekler olmadığı sürece geri dön[:subtitle]
  content_tag :p, seçenekler[:altyazı]
son
 // hücreler/ui/slat/show.haml
.çıta
  .slat__inner
    .slat__içerik
      = başlık
      = altyazı

Hücreleri bir UI Yardımcı Programıyla Sarma

Bunun büyük ölçekte işe yarayabileceği konseptini kanıtladıktan sonra, bir hücreyi çağırmak için gereken gereksiz işaretlemenin üstesinden gelmek istedim. Sadece pek doğru akmıyor ve hatırlaması zor. Biz de bunun için küçük bir yardımcı yaptık! Artık = ui “bileşenin_adı”nı çağırabilir ve satır içi seçenekleri aktarabiliriz.

 = ui "çıta", başlık: "Başlık", alt başlık: "Altyazı", etiket: "Etiket"

Seçenekleri Satır İçi Yerine Blok Olarak Aktarmak

Kullanıcı arayüzü yardımcı programını biraz daha ileri götürdüğümüzde, bir dizi seçeneğin tek bir satırda yer aldığı bir hücrenin takip edilmesinin çok zor ve çirkin olacağı kısa sürede ortaya çıktı. Satır içi tanımlanmış birçok seçeneğe sahip bir hücre örneği:

 = ui “çıta”, başlık: “Başlık”, alt başlık: “Alt başlık”, etiket: “Etiket”, bağlantı: “#”, üçüncül_başlık: “Üçüncül”, devre dışı: doğru, kontrol listesi: [“Öğe 1”, “Öğe 2”, “Öğe 3”]

Bu çok hantaldır, bu da bizi, Hücre ayarlayıcı yöntemlerine müdahale eden ve bunları karma değerlerine çeviren ve daha sonra seçeneklerle birleştirilen OptionProxy adında bir sınıf yaratmaya yönlendirir. Bu kulağa karmaşık geliyorsa endişelenmeyin; benim için de karmaşık. Kıdemli yazılım mühendislerimizden biri olan Adam'ın yazdığı OptionProxy sınıfının özetini burada bulabilirsiniz.

Hücremizde OptionProxy sınıfını kullanmanın bir örneğini burada bulabilirsiniz:

 modül kullanıcı arayüzü
  sınıf SlatCell < ViewModel
    kesinlikle gösteri
      OptionProxy.new(self).yield!(seçenekler, &blok)
      Süper()
    son
  son
son

Artık bunu yerine getirdiğimizde, hantal satır içi seçeneklerimizi daha hoş bir bloğa dönüştürebiliriz!

 = ui "çıta" do |çıta|
  - slat.title = "Başlık"
  - slat.subtitle = "Altyazı"
  - slat.label = "Etiket"
  - slat.link = "#"
  - slat.tertiary_title = "Üçüncül"
  - slat.disabled = doğru
  - slat.checklist = ["Öğe 1", "Öğe 2", "Öğe 3"]

Mantığa Giriş

Bu noktaya kadar örnekler, görünümün görüntülediği şeyle ilgili herhangi bir mantık içermemekteydi. Cells'in sunduğu en iyi şeylerden biri bu, hadi bunun hakkında konuşalım!

Çıta bileşenimize bağlı kalarak, bir bağlantı seçeneğinin mevcut olup olmadığına bağlı olarak bazen her şeyi bir bağlantı olarak, bazen de bir div olarak işleme ihtiyacımız olur. Bunun bir div veya bağlantı olarak işlenebilen tek bileşenimiz olduğuna inanıyorum, ancak Hücrelerin gücünün oldukça güzel bir örneği.

Aşağıdaki yöntem [:link] seçeneklerinin varlığına bağlı olarak ya bir link_to ya da content_tag yardımcısını çağırır.

 def kapsayıcı(&blok)
  etiket =
    eğer seçenekler[:bağlantı]
      [:bağlantı_için, seçenekler[:bağlantı]]
    başka
      [:content_tag, :div]
    son
  send(*tag, class: “slat__inner”, &block)
son

Bu, şablondaki .slat__inner öğesini bir konteyner bloğuyla değiştirmemize olanak tanır:

 .çıta
  = konteyner yapmak
  ...

Hücrelerde çok kullandığımız bir başka mantık örneği de sınıfların koşullu çıktısıdır. Diyelim ki hücreye engelli seçeneği ekledik. Hücrenin çağrılmasında, artık bir devre dışı bırakılmış: doğru seçeneği iletebilmeniz ve her şeyin devre dışı bırakılmış bir duruma (tıklanamayan bağlantılarla grileştirilmiş) dönüşmesini izleyebilmeniz dışında başka hiçbir şey değişmez.

 = ui "çıta" do |çıta|
  ...
  - slat.disabled = doğru

Devre dışı seçeneği doğru olduğunda, istenen devre dışı görünümü elde etmek için şablondaki öğelere yönelik sınıfları ayarlayabiliriz.

 .slat{ sınıf: mümkün_sınıflar("--disabled": seçenekler[:disabled]) }
  .slat__inner
    .slat__içerik
      %h4{ sınıf: mümkün_sınıflar("--alt": seçenekler[:disabled]) }= seçenekler[:başlık]
      %p{ sınıf: mümkün_sınıflar("--alt": seçenekler[:devre dışı]) }=
      seçenekler[:altyazı]
      = simge(seçenekler[:ikon], renk: "gri")

Geleneksel olarak, engelli durumda her şeyin doğru şekilde çalışması için hangi bireysel öğelerin ek sınıflara ihtiyaç duyduğunu hatırlamamız (veya stil kılavuzuna başvurmamız) gerekirdi. Hücreler bir seçeneği beyan etmemize ve ardından ağır yükü bizim için halletmemize olanak tanır.

Not: mümkün_sınıflar, Haml'da sınıfların koşullu olarak güzel bir şekilde uygulanmasına izin vermek için oluşturduğumuz bir yöntemdir.


Sunucu Tarafı Bileşenlerini Kullanamadığımız Yerler

Hücre yaklaşımı, özel uygulamamız ve çalışma şeklimiz açısından son derece yararlı olsa da sorunlarımızın %100'ünü çözdüğünü söylemeyi ihmal etmem. Hala JavaScript yazıyoruz (çoğunlukla) ve uygulamamız boyunca Vue'da pek çok deneyim geliştiriyoruz. Zamanın %75'inde Vue şablonumuz hala Haml'da yaşıyor ve Vue örneklerimizi içeren öğeye bağlıyoruz, bu da hücre yaklaşımından hâlâ faydalanmamızı sağlıyor.

Ancak bir bileşeni tamamen tek dosyalı bir Vue örneği olarak sınırlamanın daha anlamlı olduğu yerlerde Cells'i kullanamayız. Örneğin seçme listelerimizin tümü Vue.js'dir. Ama bence sorun değil! Hem Hücreler hem de Vue bileşenlerinde bileşenlerin kopya sürümlerine sahip olma ihtiyacıyla gerçekten karşılaşmadık, bu nedenle bazı bileşenlerin %100 Vue ile, bazılarının ise Hücreler ile oluşturulmuş olmasında sorun yoktur.

Bir bileşen Vue ile oluşturulmuşsa, bu, onu DOM'da oluşturmak için JavaScript'in gerekli olduğu ve bunu yapmak için Vue çerçevesinden yararlandığımız anlamına gelir. Ancak diğer bileşenlerimizin çoğu için JavaScript gerektirmezler ve eğer gerektiriyorsa, DOM'un önceden oluşturulmuş olmasını gerektirirler ve biz sadece bağlanıp olay dinleyicileri ekleriz.

Hücre yaklaşımında ilerlemeye devam ettikçe, bileşenleri oluşturmanın ve kullanmanın tek ve tek bir yoluna sahip olmak için kesinlikle hücre bileşenlerinin ve Vue bileşenlerinin kombinasyonunu deneyeceğiz. Bunun neye benzediğini henüz bilmiyorum, o yüzden oraya vardığımızda köprüyü geçeceğiz!


Sonuçlarımız

Şu ana kadar en çok kullandığımız görsel bileşenlerden otuza yakınını Hücrelere dönüştürdük. Bize büyük bir üretkenlik patlaması sağladı ve geliştiricilere, oluşturdukları deneyimlerin doğru olduğuna ve birlikte saldırıya uğramadığına dair bir doğrulama duygusu veriyor.

Tasarım ekibimiz, uygulamamızdaki bileşenlerin ve deneyimlerin Adobe XD'de tasarladıklarıyla 1:1 olduğundan her zamankinden daha fazla emin. Bileşenlerdeki değişiklikler veya eklemeler artık yalnızca bir tasarımcı ve ön uç geliştiriciyle etkileşim yoluyla gerçekleştiriliyor; bu, ekibin geri kalanının odaklanmasını ve bir tasarım modeline uyacak şekilde bir bileşenin nasıl ince ayar yapılacağını bilme endişesini ortadan kaldırmasını sağlıyor.

Kullanıcı arayüzü bileşenlerini kısıtlamaya yönelik yaklaşımımızı sürekli olarak yeniliyoruz, ancak umarım bu makalede gösterilen teknikler, bizim için neyin iyi çalıştığına dair size bir fikir verir!


Gelin Bizimle Çalışın!

Ürünlerimiz üzerinde çalışan her departmanın müşterilerimiz ve kârlılığımız üzerinde anlamlı bir etkisi vardır. Müşteri desteği, yazılım geliştirme, pazarlama veya bu ikisi arasındaki herhangi bir konu olsun, insanların gerçekten aşık olabileceği bir barındırma şirketi kurma misyonumuz doğrultusunda hep birlikte çalışıyoruz.

Ekibimize katılmaya hazır mısınız? İşe alıyoruz! Buraya başvur.