Native Mobil Uygulamalar için Sunucu Güdümlü Arayüz
Sunucu güdümlü arayüz, sunucu tarafı kompozisyonun mobil karşılığıdır. Zor kısmı JSON render etmek değil, eski sürümlerde ayakta kalan sürümlenmiş bileşen sözleşmesidir.
Bir native uygulama yayınlarsınız, sonra her hafta bir vitrin ekranını, bir promosyon afişini veya bir ödeme adımını değiştirmeniz gerekir. Her değişiklik app-store incelemesini ve kullanıcıların güncellemesini bekler; zaten yüklü olan sürümler aylarca kullanıcıların cebinde kalır, yani bir sunucuyu yeniden dağıttığınız gibi istemciyi geri alamazsınız. Sunucu güdümlü arayüz (SDUI) buna, yeni native kod yerine bir arayüz tarifi göndererek yanıt verir: buradaki öneri, onu içerik biçimli yüzeyler için bir neşter gibi kullanmak ve sözleşmeyi eski istemciler zarifçe geri çekilecek biçimde tasarlamaktır.
Web Karşılığı ve Mobildeki Ek Kısıt
Web’de sunucu tarafı kompozisyon birleştirilmiş HTML gönderir ve değişiklik, tarayıcı sayfayı çektiği anda canlı olur. Native mobilde bunu yapamazsınız. App-store inceleme kapısı sizinle her ikili dosya arasında durur, yani native kodu talep üzerine gönderemezsiniz; en yeni istemciyi varsayan bir sunucu değişikliği, henüz güncellememiş herkesi bozar.
SDUI, o web kalıbının mobil karşılığıdır. Render edilmiş işaretleme veya yeni native kod yerine sunucu bir bileşen ağacı (genellikle JSON olan bir yerleşim tarifi) gönderir ve halihazırda yüklü native istemci bunu bir bileşen kayıt defteri (registry) üzerinden render eder. Airbnb’nin Ghost Platform’u, arayüz ile veriyi web, iOS ve Android için tek bir paylaşılan GraphQL şeması üzerinden birlikte taşır. REI ise filtreleme ve sıralama ekranlarını tamamen backend’in döndürdüğü esnek bir JSON şemasından üretir.
JSON’dan native’e render kolay kısımdır. Zor sorun sürümlenmiş bileşen sözleşmesidir; çünkü native istemci yalnızca daha önce yayınladığı bileşenleri render edebilir ve o yayınlanmış sürümler aylarca sahada kalır.
Bir SDUI Yanıtının Biçimi
Sunucu bir bileşen ağacı üretir. Her düğüm kayıtlı bir bileşen type’ı adlandırır, props veya veri taşır ve isteğe bağlı olarak children içerir. Örnek bir taslak (açıklayıcıdır, hiçbir sağlayıcının şemasının birebir kopyası değildir):
{
"screen": "home_promo",
"version": 3,
"components": [
{ "type": "Banner", "props": { "title": "Yaz indirimi", "imageUrl": "https://cdn.example.com/s.jpg" } },
{ "type": "ProductCarousel", "props": { "items": [] } },
{
"type": "RichTextCard",
"props": { "markdown": "**Yeni** yerleşim" },
"fallback": { "type": "TextCard", "props": { "text": "Yeni yerleşim" } }
}
]
}
Bu zarftaki iki şey tüm tasarımı taşır. Üst düzey version, istemcinin yükün neyi varsaydığı hakkında akıl yürütmesini sağlar. Düğüm başına fallback, daha yeni bir bileşen eksik olabilecekken sunucunun eski bir istemciye kesinlikle yayınladığı bir bileşeni vermesini sağlar.
İstemci Bileşen Kayıt Defteri
Native istemci bir kayıt defteri tutar: bir type dizesi, bir native görünüm üreticisine eşlenir. Ağacı dolaşır, her type’ı arar ve native görünümler kurar. Kritik nokta, kayıt defterinin bilmediği bir type için ne olduğudur. Bu, geriye dönük uyumluluğun bel kemiğidir, dolayısıyla sonradan akla gelen bir şey olamaz.
İşte zorunlu bilinmeyen-bileşen geri çekilmesiyle (fallback) birlikte SwiftUI’deki kayıt defteri. Aynı biçim bir Jetpack Compose @Composable haritasına veya bir React Native bileşen haritasına da uygulanır.
struct ComponentSpec: Decodable {
let type: String
let props: [String: JSONValue]
let fallback: Box<ComponentSpec>? // özyinelemeli, isteğe bağlı
}
@MainActor
final class ComponentRegistry {
typealias Builder = (ComponentSpec) -> AnyView
private var builders: [String: Builder] = [:]
func register(_ type: String, _ builder: @escaping Builder) {
builders[type] = builder
}
/// Bir spec'i bir görünüme çözer. Bilinmeyen tipler önce beyan
/// edilmiş fallback'i dener, sonra çökmek yerine hiçbir şey render etmez.
func view(for spec: ComponentSpec) -> AnyView {
if let builder = builders[spec.type] {
return builder(spec)
}
if let fallback = spec.fallback?.value {
return view(for: fallback) // güvenli alternatife geri dön
}
// Ne builder ne fallback var: bu düğümü atla, ekranı ayakta tut.
return AnyView(EmptyView())
}
}
Kayıt defterinin kodladığı kural basittir: bilinmeyen bir type asla çökmez ve tüm ekranı asla boş bırakmaz. Beyan ettiği fallback’i render eder veya hiçbir şey render etmez ve ağacın geri kalanını geçirir.
Sürümlenmiş Bileşen Sözleşmesi
Tezin yaşadığı yer burasıdır. İstemci yalnızca yayınladığını render edebilir ve mağaza dağıtımlı sürümler aylarca yüklü kalır; bu yüzden sözleşme, sürüm uyumsuzluğunu istisna değil normal durum olarak varsaymalıdır.
Belgelenmiş iki geri çekilme stratejisi vardır ve çoğu ekip ikisini de kullanır. İstemci tarafı fallback ile kayıt defterinin varsayılan bir işleyicisi vardır: bilinmeyen bir type genel bir yer tutucu döndürür veya hiçbir şey render etmez. REI, arayüz işlemlerini sabit kodlar; böylece istemci, backend’in henüz göndermeye hazır olmadığı veriyle ileriye dönük uyumlu kalır. Sunucu tarafı fallback ile yanıt bir alternatif gömer (bir RichTextCard, daha basit bir TextCard taşır). Sunucu, her bileşenin hangi uygulama sürümünden itibaren mevcut olduğunun haritasını tutar ve yükü buna göre uyarlar; bu yüzden istemciler her istekte framework, uygulama ve OS sürümünü bildiren bir başlık gönderir.
Sürüm uyumluluğu yüzeyi bir çizgi değil, bir matristir. Bir yük sürümünü yüklü bir istemci sürümüne eşlemek üç sonuç verir ve sözleşme, her hücreyi başarısızlık durumundan uzak tutmak için vardır.
Okuyucuya verilecek kural tek cümledir: props yalnızca eklemelidir; bir alanı asla yeniden amaçlandırmayın veya kaldırmayın, bileşeni değiştirmek yerine sürümleyin ve her yeni bileşen ya bir fallback beyan etmeli ya da yalnızca eklemeli olmalı. Apple’ın 2.5.2 yönergesi bunu bir veri-değil-mantık disiplinine dönüştürür. İstemcinin okuduğu bir arayüz tarifi göndermek sorun değildir. Uygulamanın davranış olarak değerlendirdiği bir yapılandırma göndermek ise, yönergenin izin vermediği indirilmiş yürütülebilir koda doğru kayar. Bu çizginin pratikte öznel olduğu kabul edilir; tam da bu yüzden açıkça bildirimsel tarafta kalmak sizi korur.
Spec Biçimi Seçenekleri
Ekipler, kabloya ne kadar güvendikleri konusunda keskin biçimde ayrışır. Üç bildirimsel format ve artık tam olarak bildirimsel olmayan bir sınır durumu vardır.
Sürümlenmiş JSON zarfı esnek, insan tarafından okunabilir ve zayıf tiplidir. REI esnek bir JSON şeması kullanır; DoorDash’in Facets framework’ü bir Facet’i bire bir bir görünüme eşler. Paylaşılan bir GraphQL şeması veri modelini standartlaştırır: Airbnb’nin Ghost Platform’u üç platformu da tek bir şemadan besler ve Apollo bunu, API’den alan verisi yerine ürün ve arayüz bilgisi döndürmek olarak belgeler. Protobuf veya tipli bir IDL güçlü tipleme ve iki uç için kod üretimi sağlar; MobileNativeFoundation katkıcıları, düğme ve yerleşim gibi ilkelleri protobuf’ta native ve web render edicilerle tanımlamayı anlatır.
Sınır durumu yalnızca veri değil, mantık göndermektir. Cash App’in Redwood’u, Treehouse ve Zipline ile birlikte, bildirimsel bir ağaç yerine istemcide yürütülen Kotlin/JS gönderir (WebAssembly ise belirttikleri gelecekteki yöndür). Bu, bildirimsel SDUI’ye göre indirilmiş yürütülebilir kod için 2.5.2 yönergesi çizgisine çok daha yakın oturur. Güçlü bir yaklaşımdır ama farklı bir risk duruşudur; bu yüzden onu varsayılan değil, tasarım alanının kenarı olarak ele alın.
Tarihsel bağlam için, Spotify’ın HubFramework’ü iOS’ta ölçekte bileşen güdümlü arayüz sistemlerinin en erkenlerindendi. Artık arşivlenmiş ve kullanımdan kaldırılmış durumda; bu yüzden onu benimsenecek bir şey olarak değil, bir öncül olarak anın.
Önerilen Varsayılan
SDUI’yi dinamik, içerik biçimli yüzeyler için bir neşter gibi kullanın: vitrin, promosyon, formlar, özellik bayraklı ve A/B testli yerleşimler. Etkileşim yoğun, jest ve animasyon zengini, gecikmeye duyarlı ve çevrimdışı öncelikli ekranları native olarak kurulu tutun. Sözleşme için, tam tipli bir protobuf IDL’i değil, açık bir type kayıt defteri ve zorunlu bir bilinmeyen-bileşen fallback kuralı olan sürümlenmiş bir JSON zarfını varsayılan yapın.
Gerekçe, kalıbın tüm amacıdır. Eski istemcilerde zarif şekilde geri çekilmek işin kendisidir ve gevşek-ama-sürümlü JSON, görmediği bir alanı çözmeyi reddeden katı bir şemadan daha zarif geri çekilir. Ödünleşim gerçektir: derleme zamanı güvenliğinden vazgeçer ve bileşen başına bir fallback sözleşmesi disiplinini üstlenirsiniz. Bunu, katı şemanın ucuza veremeyeceği tek özellik karşılığında kabul edersiniz: gelecekten gelen bir yükten sağ çıkmak.
Varsayılanı Ne Zaman Geçersiz Kılmalı
Protobuf veya tipli IDL geçersiz kılması; iki ucun yeterince sık birlikte yayınlandığı, dolayısıyla sürüm uyumsuzluğunun sınırlı kaldığı ve kod üretiminin değdiği performansa duyarlı, yüksek hacimli, sıkı bağlı dahili yüzeyler için geçerlidir. WebView geçersiz kılması ise native his değil, gerçekten web içeriği yeniden kullanımı gerektiğinde geçerlidir.
Bu son durum net bir ayrımı hak eder, çünkü ekipler aynı dinamizm sorununu çözmek için sıkça hem SDUI’ye hem WebView’lara uzanır. SDUI bir sunucu tarifinden native görünümler render eder; render etmenin sahibi istemcidir ve sonuç native hisseder. Bir WebView mikro-frontend’i ise web içeriğini bir native kabuğa gömer; tam web yeniden kullanımı ve anında değişim elde edersiniz ama native olmayan his, köprü karmaşıklığı ve ayrı bir çalışma zamanı bedelini ödersiniz. Yanıt gerçekten bir web ekibinin sahip olduğu web biçimli içerikse, geçersiz kılma budur ve WebView serisi onun mekaniğini kapsar.
Sık Yapılan Hatalar
En sık görülen başarısızlık zorunlu fallback kuralının olmamasıdır; bu, eski istemcileri boş ekran render eder durumda bırakır. Çözüm yapısaldır: kayıt defteri bilinmeyen tipleri atlar ve sözleşme, her bileşenin ya bir fallback beyan etmesini ya da yalnızca eklemeli olmasını zorunlu kılar.
Yakın bir ikincisi, mevcut bileşen props’larında bozucu değişikliklerdir; bu, eski istemcileri yanlış render ettirir. Props’ları yalnızca eklemeli sayın, bir alanı asla yeniden amaçlandırmayın veya kaldırmayın ve bileşeni değiştirmek yerine sürümleyin. Buna bağlı olan, yetenekleri uygulama sürümü tahminiyle üretmektir; bu kayma yaratır. Bunun yerine istemcinin her istekte kayıt defteri veya yetenek sürümünü bildirmesine ve sunucunun yükü bildirilen yeteneklere göre uyarlamasına izin verin.
İki tanesi daha kapsamla ilgilidir. Gecikmeye duyarlı veya jest yoğun ekranları SDUI’ye koymak takılgan, ağa bağımlı bir his üretir; bunları native tutun. Ve tarayıcıyı yeniden icat etmek yavaş olanıdır: bir SDUI sistemi koşullar, ifadeler ve stiller biriktirir, ta ki daha kötü, test edilmemiş bir web motoruna dönüşene kadar. Tarifi bildirimsel ve sınırlı tutun ve gerçekten web biçimli ihtiyaçları bir WebView’a yükseltin.
SDUI’yi işletirken, gösteriş sayımları yerine sözleşme sağlığını izleyin. İzlemeye değer sinyaller şunlardır: fallback isabet oranı (yüksek olması sözleşme kayması demektir), mevcut yükü render edebilen yüklü istemci sürümlerinin payı, uygulama sürümüne göre boş ekran veya render hatası oranı ve SDUI ekranlarının native ekranlara oranı (böylece neşter neşter kalır). Dinamik bir yüzey için değişim süresi, kalıbın satın alındığı hedeftir; yayın kapısına takılı günlerden sunucu dağıtımı dakikalarına geçişi, varsayılacak bir sayı değil, ölçülecek bir hedef olarak ele alın.
Kapanış
SDUI doğru araçtır; bir yüzey içerik biçimliyken, sık değişirken ve zengin etkileşime, düşük gecikmeye veya çevrimdışı davranışa bağlı değilken ve bileşen başına zorunlu fallback’i olan sürümlenmiş bir zarfa bağlanabildiğinizde. Protobuf geçersiz kılmasına yalnızca iki ucun birlikte yayınlandığı sıkı bağlı dahili yüzeylerde, WebView’a ise yalnızca gerçekten web içeriği yeniden kullanımına ihtiyaç duyduğunuzda uzanın. Kalıp asıl değerini sözleşme katmanında kazanır; bu yüzden render motorunu kurmadan önce yalnızca-eklemeli props kuralını ve bilinmeyen-bileşen fallback kuralını yazın; geri alamadığınız eski uygulama sürümlerinden sağ çıkan kısım odur.
Kaynaklar
- A Deep Dive into Airbnb’s Server-Driven UI System (Ghost Platform) - Kanonik kaynak: web/iOS/Android genelinde paylaşılan bir GraphQL şeması, arayüz ve veri birlikte taşınır
- The Journey to Server Driven UI At Lyft Bikes and Scooters - Bir ekibin SDUI’yi neden benimsediği: iş karmaşıklığı, yayın hızı ve kadro esnekliği
- Improving Development Velocity with Generic, Server-Driven UI Components (DoorDash, Facets) - Bir Facet’in bire bir bir görünüme eşlendiği bir yerleşim motoru artı bileşen kütüphanesi
- Server Driven UI: Prepared for the Unknown (REI Co-op Engineering) - İleriye dönük uyumluluk için güçlü kaynak: esnek bir JSON şeması ve backend’in göndermeye hazır olmadığı veriyle ileriye dönük uyumlu istemci
- Spotify HubFramework (kullanımdan kaldırıldı) - Ölçekte erken bir bileşen güdümlü iOS arayüz framework’ü, artık arşivli; yalnızca tarihsel öncül olarak anılır
- Server-driven UI strategies (MobileNativeFoundation Discussion #47) - Protobuf-ilkelleri yaklaşımı ve SDUI’nin çevrimdışı ile animasyon sınırlamaları
- Native UI and multiplatform Compose with Redwood (Cash App Code Blog) - Zipline ve Kotlin/JS artı WebAssembly kullanan “yalnızca veri değil, mantık gönder” sınır durumu
- How to Safely Release Server-Driven UI Updates at Scale (Digia) - İstemci ve sunucu fallback stratejileri, sürüm bildiren başlıklar ve bileşen-uygunluk haritalama
- Server-Driven UI Basics (Apollo GraphQL Docs) - Sözleşmenin GraphQL-şeması çerçevesi: alan verisi değil, arayüz ve ürün bilgisi döndür
- App Review Guidelines (Apple Developer, Guideline 2.5.2) - SDUI’yi hem mümkün kılan hem sınırlayan kısıt: veri tarifleri sorun değil, indirilmiş yürütülebilir kod değil
- Fixing Section 2.5.2 (Saagar Jha) - Veri-mantık çizgisinin pratikte neden öznel olduğu
- Shipping Mobile When You Can’t Roll Back the Client - Bu kalıbın yanıtladığı motive edici kısıt
- Server-Side Micro-Frontend Composition - Bu fikrin web karşılığı
- Mobile Micro-Frontends with React Native and Expo WebViews - Karşılaştırılan WebView alternatifi
İlgili yazılar
Async backend'le çalışan tasarımcılar için pragmatik rehber: üç etkileşim şekli, hangisi ne zaman, ve karşı durmanız gereken dört anti-pattern.
React Native ve Expo WebView'lar ile mobil micro frontend mimarisi oluşturma. Gerçek dünya örnekleri ve en iyi uygulamalar.
React Native ve WebView'lar arasında güvenli ve verimli iletişim kurma. Postmessage, bridge pattern'leri ve production deneyimleri.
Mobil binary geri alınamaz ve eski sürümler kalıcıdır; güvenlik ve hız sunucuya kayar: BFF, tüketici güdümlü sözleşmeler ve geriye dönük uyumlu sürümleme.
Sunucu tarafı micro-frontend kompozisyonu, bağımsız ekiplerin tek bir sayfanın parçalarını sahiplenmesini sağlar. Zor kısım sahiplik sınırı ve sürümlenmiş tutarlılık sözleşmesidir.