İçeriğe atla

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.

Ayhan Sipahi Ayhan Sipahi

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.

Sürümlenmiş bileşen sözleşmesi, SDUI’nin production’da işe yarayıp yaramayacağını belirler. 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’ı belirtir, props veya veri taşır ve isteğe bağlı olarak children içerir. Örnek bir taslak:

{
  "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 geldiğinde ne olacağıdır. Bu, geriye dönük uyumluluğun bel kemiğidir; dolayısıyla sonraya bırakılamaz.

İş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 çökmez; hiçbir şey render etmeden geçer.
    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 işi, bilinmeyen bir type geldiğinde uygulamanın çökmemesini ve tüm ekranın boş kalmamasını sağlamaktır. Beyan edilmiş fallback’i render eder veya hiçbir şey render etmez ve ağacın geri kalanıyla devam eder.

Sürümlenmiş Bileşen Sözleşmesi#

İ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 normal durum olarak varsaymalıdır.

evet

hayır, fallback var

hayır, fallback yok

Sunucu spec'i oluşturur

Ağ üzerinden JSON ağacı

İstemci ağacı dolaşır

type kayıt defterinde mi?

Native görünüm kur

Fallback render et

Düğümü atla, ekranı koru

Render edilmiş ekran

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 gösterimlerini 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 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.

Eski istemci

Yeni yük: fallback ile render

Eski yük: tam render

Yeni istemci

Yeni yük: tam render

Eski yük: tam render

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şeni bir fallback ile birlikte gönderin. 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; tarif ile davranış arasındaki bu çizginin pratikte öznel olduğu kabul edilir.

Spec Biçimi Seçenekleri#

Ekipler, ağdan gelen yüke ne kadar güvendikleri konusunda keskin biçimde ayrışır ve seçtikleri format bunu yansıtır.

Sürümlenmiş JSON zarfı esnek ve insan tarafından okunabilir, ama zayıf tiplidir; REI’nin şeması ve DoorDash’in Facets framework’ü (bir Facet’i bire bir bir görünüme eşleyen) bu yolu izler. GraphQL, tipleme yelpazesinin diğer ucundadır: Airbnb’nin Ghost Platform’u web, iOS ve Android’i tek bir paylaşılan şemadan besler ve Apollo bunu, API’nin alan verisinden ayrı olarak ürün ve arayüz bilgisi döndürmesi şeklinde belgeler. Protobuf veya tipli bir IDL ise bu esnekliğin bir kısmından vazgeçip iki uç için güçlü tipleme ve kod üretimi kazandırır; MobileNativeFoundation katkıcıları, düğme ve yerleşim gibi ilkelleri protobuf’ta tanımlayıp her iki tarafta native ve web render ediciler kullanmayı 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, farklı bir risk duruşu taşır; onu aşağıdaki varsayılan tavsiyenin ötesinde bir sınır durum olarak ele alın.

Tarihsel bağlam için, Spotify’ın HubFramework’ü iOS’ta geniş ölçekte çalışan en erken bileşen güdümlü arayüz sistemlerindendi; artık arşivlenmiş ve kullanımdan kaldırılmış durumda.

Ö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 tutun. Sözleşme için, 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; tam tipli protobuf IDL’i aşağıdaki geçersiz kılma durumlarına saklayın.

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. Trade-off gerçektir: derleme zamanı güvenliğinden vazgeçer ve bileşen başına bir fallback sözleşmesi disiplinini üstlenirsiniz.

Varsayılanı Ne Zaman Geçersiz Kılmalı#

hayır

evet

evet

evet

hayır

hayır, web içeriği yeniden kullanılacak ya da sahibi bir web ekibi

Ekran yayın olmadan değişmeli mi?

Native kurulu ekran

İçerik biçimli, yeni etkileşim kalıbı yok mu?

Jest zengini, gecikmeye duyarlı veya çevrimdışı mı?

SDUI: sürümlenmiş zarf artı fallback

WebView parçası

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 yerine gerçekten web içeriği yeniden kullanımına ihtiyaç duyduğunuzda 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.

Başarısızlık Biçimleri ve İzlenecek Sinyaller#

En sık görülen hata, önceki bölümdeki fallback kuralını atlamaktır: eski bir istemci, geri çekilecek hiçbir şeyi olmayan bilinmeyen bir type ile karşılaşır ve ekran içerik yerine boş render edilir. Kayıt defteri kodu bunu zaten mekanik olarak çözer; disiplin, kuralı riskli görünmeyenler dahil her yeni bileşene uygulamaktan ibarettir.

Yakın bir ikincisi kod incelemesinde ortaya çıkar: bir prop eklenmek yerine yeniden amaçlandırılır veya kaldırılır, orijinal alanı hâlâ okuyan eski istemciler yanlış render eder. Aynı kaymanın daha sessiz bir biçimi, istemci yeteneklerini uygulama sürüm numarasından tahmin etmekten gelir; bunun yerine istemcinin her istekte kayıt defteri veya yetenek sürümünü bildirmesine izin verin ve yükü bildirilenlere göre uyarlayın.

İki hata daha sözleşmenin dışında, kapsam kararlarında yaşar. Jest yoğun veya gecikmeye duyarlı ekranlar SDUI üzerinden geçirildiğinde kasan, ağa bağımlı bir hisse dönüşür; bunları native tutun. Ve koşullar, ifadeler ve stil seçenekleri biriktirmeye devam eden bir tarif, JSON’a esneklik eklemeyi bırakıp bir sonraki isteği bir WebView’a yönlendirme zamanının geldiğine işarettir.

SDUI’yi işletirken 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ı. Dinamik bir yüzey için değişim süresi, kalıbın satın alındığı ölçüttür: bir içerik değişikliğinin yayın olmadan canlıya çıkması ne kadar sürdüğüdür.

Kapanış#

Varsayılan, içerik biçimli ve sık değişen, zengin etkileşime, düşük gecikmeye veya çevrimdışı desteğe ihtiyaç duymayan yüzeylerde geçerlidir. Geçersiz kılmayı yalnızca yukarıdaki durumlarda kullanın: kod üretimini haklı çıkaracak kadar sıkı birlikte yayınlanan uçlar için protobuf, içeriğin gerçekten bir web ekibine ait olduğu durumlar için WebView.

Kaynaklar#

İlgili yazılar