Mimari hakkında konuşalım mı?
Önemli ancak yanlış anlaşılmaya müsait bir konuyu ele alacağım: Web uygulamanız için kullandığınız mimari ve özellikle de progresif web uygulaması oluştururken mimari kararlarınızın nasıl devreye girdiği.
"Mimari" kelimesi belirsiz gelebilir ve bunun neden önemli olduğu hemen anlaşılmayabilir. Mimariyi düşünmenin bir yolu, kendinize şu soruları sormaktır: Bir kullanıcı sitemdeki bir sayfayı ziyaret ettiğinde hangi HTML yüklenir? Peki, başka bir sayfayı ziyaret ettiklerinde ne yüklenir?
Bu soruların yanıtları her zaman basit değildir ve progresif web uygulamaları hakkında düşünmeye başladığınızda daha da karmaşık hale gelebilir. Bu nedenle amacım, etkili bulduğum olası bir mimariyi size anlatmak. Bu makale boyunca, progresif web uygulaması oluştururken verdiğim kararları "benim yaklaşımım" olarak etiketleyeceğim.
Kendi PWA'nızı oluştururken benim yaklaşımımı kullanabilirsiniz ancak her zaman başka geçerli alternatifler de vardır. Tüm parçaların bir araya gelişini görmenin size ilham vereceğini ve bunu ihtiyaçlarınıza göre özelleştirebileceğinizi umuyorum.
Stack Overflow PWA
Bu makaleye eşlik etmesi için bir Stack Overflow PWA oluşturdum. Stack Overflow'da çok zaman geçiriyorum, okuyorum ve katkıda bulunuyorum. Belirli bir konuyla ilgili sık sorulan sorulara göz atmayı kolaylaştıracak bir web uygulaması geliştirmek istedim. Herkese açık Stack Exchange API üzerine kurulmuştur. Açık kaynaklı bir projedir. GitHub projesini ziyaret ederek daha fazla bilgi edinebilirsiniz.
Çok sayfalı uygulamalar (MPA'lar)
Ayrıntılara girmeden önce bazı terimleri tanımlayalım ve temel teknolojinin parçalarını açıklayalım. İlk olarak, "Çok Sayfalı Uygulamalar " veya"MPA'lar" olarak adlandırdığım konuyu ele alacağım.
MPA, web'in başlangıcından beri kullanılan geleneksel mimarinin havalı bir adıdır. Kullanıcı her yeni bir URL'ye gittiğinde tarayıcı, o sayfaya özel HTML'yi aşamalı olarak oluşturur. Gezinmeler arasında sayfanın durumunu veya içeriğini koruma girişimi yoktur. Yeni bir sayfayı her ziyaret ettiğinizde yeni bir başlangıç yaparsınız.
Bu, web uygulamaları oluşturmaya yönelik tek sayfalık uygulama (SPA) modelinden farklıdır. SPA modelinde tarayıcı, kullanıcı yeni bir bölümü ziyaret ettiğinde mevcut sayfayı güncellemek için JavaScript kodunu çalıştırır. Hem SPA'lar hem de MPA'lar eşit derecede geçerli modellerdir ancak bu yayında PWA kavramlarını çok sayfalı bir uygulama bağlamında incelemek istedim.
Güvenilir ve hızlı
Benim (ve sayısız kişinin) "progresif web uygulaması" veya PWA ifadesini kullandığını duymuşsunuzdur. Bu sitedeki diğer sayfalarda yer alan arka plan bilgilerinin bir kısmını zaten biliyor olabilirsiniz.
PWA'yı, birinci sınıf bir kullanıcı deneyimi sunan ve kullanıcının ana ekranında gerçekten yer edinmeyi hak eden bir web uygulaması olarak düşünebilirsiniz. "FIRE" kısaltması, Fast (Hızlı), Integrated (Entegre), Reliable (Güvenilir) ve Engaging (Etkileşimli) kelimelerinin baş harflerinden oluşur ve PWA oluştururken dikkate alınması gereken tüm özellikleri özetler.
Bu makalede, bu özelliklerin bir alt kümesine odaklanacağım: Hızlı ve Güvenilir.
Hızlı: "Hızlı" kelimesi farklı bağlamlarda farklı anlamlara gelse de ağdan mümkün olduğunca az veri yüklemenin hız açısından avantajlarını ele alacağım.
Güvenilir: Ancak hız tek başına yeterli değildir. PWA gibi hissettirmesi için web uygulamanızın güvenilir olması gerekir. Ağın durumu ne olursa olsun, her zaman bir şey (yalnızca özelleştirilmiş bir hata sayfası olsa bile) yükleyecek kadar esnek olması gerekir.
Güvenilir bir şekilde hızlı: Son olarak, PWA tanımını biraz değiştirerek güvenilir bir şekilde hızlı bir şey oluşturmanın ne anlama geldiğine bakacağım. Yalnızca düşük gecikmeli bir ağdayken hızlı ve güvenilir olmak yeterli değildir. Güvenilir bir şekilde hızlı olmak, web uygulamanızın hızının, temel ağ koşullarından bağımsız olarak tutarlı olduğu anlamına gelir.
Etkinleştirme Teknolojileri: Service Worker'lar + Cache Storage API
PWA'lar, hız ve esneklik açısından çıtayı yükseltir. Neyse ki web platformu, bu tür bir performansı gerçeğe dönüştürmek için bazı yapı taşları sunuyor. Hizmet çalışanlarından ve Cache Storage API'den bahsediyorum.
Gelen istekleri dinleyen, bazılarını ağa ileten ve yanıtın bir kopyasını gelecekte kullanılmak üzere Cache Storage API aracılığıyla depolayan bir hizmet çalışanı oluşturabilirsiniz.
Web uygulaması aynı isteği bir sonraki sefer yaptığında, hizmet çalışanı önbelleklerini kontrol edebilir ve daha önce önbelleğe alınmış yanıtı döndürebilir.
Mümkün olduğunca ağdan kaçınmak, güvenilir ve hızlı performans sunmanın çok önemli bir parçasıdır.
"İzomorfik" JavaScript
Ele almak istediğim bir diğer kavram da bazen "izomorfik" veya "evrensel" JavaScript olarak adlandırılan kavram. Basitçe ifade etmek gerekirse aynı JavaScript kodunun farklı çalışma zamanı ortamları arasında paylaşılabileceği fikridir. PWA'mı oluştururken arka uç sunucum ile hizmet çalışanı arasında JavaScript kodu paylaşmak istedim.
Kodu bu şekilde paylaşmanın birçok geçerli yaklaşımı vardır ancak benim yaklaşımım, kesin kaynak kodu olarak ES modüllerini kullanmaktı. Ardından, Babel ve Rollup'ı birlikte kullanarak bu modülleri sunucu ve hizmet çalışanı için çevirip paketledim. Projemde, .mjs dosya uzantısına sahip dosyalar, bir ES modülünde bulunan kodlardır.
Sunucu
Bu kavramları ve terminolojiyi aklımızda tutarak Stack Overflow PWA'mı nasıl oluşturduğuma bakalım. Arka uç sunucumuzla başlayıp genel mimariye nasıl uyduğunu açıklayacağım.
Statik barındırmayla birlikte dinamik bir arka uç kombinasyonu arıyordum ve Firebase platformunu kullanmaya karar verdim.
Firebase Cloud Functions, gelen bir istek olduğunda otomatik olarak Node tabanlı bir ortam oluşturur ve zaten aşina olduğum popüler Express HTTP çerçevesi ile entegre olur. Ayrıca sitemin tüm statik kaynakları için kullanıma hazır barındırma hizmeti de sunuyor. Sunucunun istekleri nasıl işlediğine bakalım.
Bir tarayıcı, sunucumuza gezinme isteği gönderdiğinde aşağıdaki akıştan geçer:
Sunucu, isteği URL'ye göre yönlendirir ve eksiksiz bir HTML belgesi oluşturmak için şablon oluşturma mantığını kullanır. Stack Exchange API'den gelen verilerin yanı sıra sunucunun yerel olarak depoladığı kısmi HTML parçalarını kullanıyorum. Hizmet çalışanımız nasıl yanıt vereceğini öğrendikten sonra HTML'yi web uygulamamıza geri aktarmaya başlayabilir.
Bu resimde daha ayrıntılı olarak incelenmeye değer iki parça vardır: yönlendirme ve şablon oluşturma.
Güzergah Oluşturma/Planlama
Yönlendirme için Express çerçevesinin yerel yönlendirme söz dizimini kullanmayı tercih ettim. Basit URL ön eklerinin yanı sıra yolun bir parçası olarak parametre içeren URL'lerle eşleşecek kadar esnektir. Burada, rota adları ile eşleştirilecek temel Express kalıbı arasında bir eşleme oluşturuyorum.
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index', '/'],
]);
export default routes;
Ardından bu eşlemeye doğrudan sunucunun kodundan referans verebilirim. Belirli bir Express kalıbı için eşleşme olduğunda uygun işleyici, eşleşen rotaya özgü şablon oluşturma mantığıyla yanıt verir.
import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
// Templating logic.
});
Sunucu tarafı şablon oluşturma
Peki bu şablon mantığı nasıl görünüyor? Ben de kısmi HTML parçalarını sırayla bir araya getiren bir yaklaşım kullandım. Bu model, canlı yayın için idealdir.
Sunucu, hemen bazı ilk HTML şablonlarını geri gönderir ve tarayıcı bu kısmi sayfayı hemen oluşturabilir. Sunucu, veri kaynaklarının geri kalanını bir araya getirirken belge tamamlanana kadar bunları tarayıcıya aktarır.
Ne demek istediğimi anlamak için rotalarımızdan birinin Express koduna göz atın:
app.get(routes.get('index'), async (req, res) => {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end();
});
response nesnesinin write() yöntemini kullanarak ve yerel olarak depolanan kısmi şablonlara referans vererek, harici veri kaynaklarını engellemeden yanıt akışını hemen başlatabiliyorum. Tarayıcı bu ilk HTML'yi alır ve hemen anlamlı bir arayüz ve yükleme mesajı oluşturur.
Sayfamızın sonraki bölümünde Stack Exchange API'den alınan veriler kullanılır. Bu verileri almak için sunucumuzun bir ağ isteği göndermesi gerekir. Web uygulaması, yanıt alıp işleyene kadar başka bir şey oluşturamaz ancak en azından kullanıcılar beklerken boş bir ekrana bakmaz.
Web uygulaması, Stack Exchange API'den yanıt aldıktan sonra API'den gelen verileri karşılık gelen HTML'ye çevirmek için özel bir şablon oluşturma işlevini çağırır.
Şablon oluşturma dili
Şablon oluşturma, şaşırtıcı bir şekilde tartışmalı bir konu olabilir ve benim seçtiğim yaklaşım, birçok yaklaşımdan yalnızca biridir. Özellikle mevcut bir şablon çerçevesiyle eski bağlarınız varsa kendi çözümünüzü kullanmak isteyebilirsiniz.
Benim kullanım alanım için en mantıklı olan, JavaScript'in şablon değişmezlerine güvenmek ve bazı mantıkları yardımcı işlevlere ayırmaktı. Çok sayfalı uygulama oluşturmanın güzel yanlarından biri, durum güncellemelerini takip etmeniz ve HTML'nizi yeniden oluşturmanız gerekmemesidir. Bu nedenle, statik HTML üreten temel bir yaklaşım benim için işe yaradı.
Web uygulamamın dizininin dinamik HTML bölümünü nasıl şablonlaştırdığıma dair bir örnek: Rotalarımda olduğu gibi, şablon oluşturma mantığı hem sunucuya hem de hizmet çalışanına aktarılabilen bir ES modülünde depolanır.
export function index(tag, items) {
const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
const form = `<form method="GET">...</form>`;
const questionCards = items
.map(item =>
questionCard({
id: item.question_id,
title: item.title,
})
)
.join('');
const questions = `<div id="questions">${questionCards}</div>`;
return title + form + questions;
}
Bu şablon işlevleri saf JavaScript'tir ve mantığı uygun olduğunda daha küçük, yardımcı işlevlere ayırmak faydalıdır. Burada, API yanıtında döndürülen öğelerin her birini bu tür bir işleve aktarıyorum. Bu işlev, tüm uygun özellikler ayarlanmış standart bir HTML öğesi oluşturur.
function questionCard({id, title}) {
return `<a class="card"
href="/questions/${id}"
data-cache-url="${questionUrl(id)}">${title}</a>`;
}
Özellikle dikkat edilmesi gereken nokta, her bağlantıya eklediğim veri özelliğidir. data-cache-url, ilgili soruyu görüntülemek için ihtiyacım olan Stack Exchange API URL'sine ayarlanır. Bunu aklınızda bulundurun. Daha sonra tekrar bakacağım.
Rota işleyicime geri dönersek şablon oluşturma işlemi tamamlandıktan sonra sayfamın HTML'sinin son bölümünü tarayıcıya aktarır ve akışı sonlandırırım. Bu, tarayıcıya aşamalı oluşturmanın tamamlandığını gösteren işarettir.
app.get(routes.get('index'), async (req, res) => {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end();
});
Sunucu kurulumumun kısa bir turu bu şekilde. Web uygulamamı ilk kez ziyaret eden kullanıcılar her zaman sunucudan yanıt alır ancak ziyaretçi web uygulamama geri döndüğünde hizmet çalışanım yanıt vermeye başlar. Şimdi bu konuyu ayrıntılı olarak inceleyelim.
Hizmet çalışanı
Bu diyagram size tanıdık gelebilir. Daha önce ele aldığım parçaların çoğu burada biraz farklı bir düzende yer alıyor. Hizmet çalışanını hesaba katarak istek akışını inceleyelim.
Hizmet çalışanımız, belirli bir URL için gelen gezinme isteğini işler ve tıpkı sunucumun yaptığı gibi, nasıl yanıt vereceğini belirlemek için yönlendirme ve şablon oluşturma mantığının bir kombinasyonunu kullanır.
Yaklaşım daha öncekiyle aynıdır ancak fetch() ve Cache Storage API gibi farklı alt düzey temel öğeler kullanılır. Bu veri kaynaklarını kullanarak HTML yanıtını oluşturur ve hizmet çalışanına iletir. Hizmet çalışanı da bu yanıtı web uygulamasına geri gönderir.
Workbox
Düşük seviyeli temel öğelerle sıfırdan başlamak yerine hizmet çalışanımı Workbox adlı bir dizi üst düzey kitaplığın üzerine inşa edeceğim. Herhangi bir hizmet çalışanının önbelleğe alma, yönlendirme ve yanıt oluşturma mantığı için sağlam bir temel sağlar.
Güzergah Oluşturma/Planlama
Sunucu tarafı kodumda olduğu gibi, hizmet çalışanımın da gelen bir isteği uygun yanıt mantığıyla nasıl eşleştireceğini bilmesi gerekir.
Benim yaklaşımım, her Express rotasını karşılık gelen bir normal ifadeye çevirmek ve bunu yaparken regexparam adlı faydalı bir kitaplıktan yararlanmaktı. Bu çeviri yapıldıktan sonra Workbox'ın normal ifade yönlendirme için yerleşik desteğinden yararlanabilirim.
Normal ifadeleri içeren modülü içe aktardıktan sonra her normal ifadeyi Workbox'ın yönlendiricisine kaydederim. Her rotada yanıt oluşturmak için özel şablon mantığı sağlayabilirim. Hizmet çalışanında şablon oluşturma, arka uç sunucumda olduğundan biraz daha karmaşık ancak Workbox, zorlu işlerin çoğunda yardımcı oluyor.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
Statik öğe önbelleğe alma
Şablon oluşturma sürecinin önemli bir parçası, kısmi HTML şablonlarımın Cache Storage API aracılığıyla yerel olarak kullanılabilir olmasını ve web uygulamasında değişiklikler yaptığımda güncel tutulmasını sağlamaktır. Önbellek bakımı manuel olarak yapıldığında hataya açık olabilir. Bu nedenle, derleme sürecimin bir parçası olarak önceden önbelleğe alma işlemini gerçekleştirmek için Workbox'ı kullanıyorum.
Workbox'a hangi URL'lerin önbelleğe alınacağını bir yapılandırma dosyası kullanarak bildiriyorum. Bu dosya, tüm yerel öğelerimi içeren dizini ve eşleştirilecek kalıpları gösteriyor. Bu dosya, siteyi her yeniden oluşturduğumda çalıştırılan Workbox'ın CLI'si tarafından otomatik olarak okunur.
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Other options...
};
Workbox, her dosyanın içeriğinin anlık görüntüsünü alır ve bu URL ve düzeltme listesini nihai hizmet çalışanı dosyama otomatik olarak ekler. Workbox artık önceden önbelleğe alınmış dosyaları her zaman kullanılabilir ve güncel tutmak için gereken her şeye sahiptir. Sonuç, aşağıdakine benzer bir içerik barındıran bir service-worker.js dosyasıdır:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746a9ecc6',
},
// etc.
]);
Daha karmaşık bir derleme süreci kullananlar için Workbox'ta komut satırı arayüzünün yanı sıra hem webpack eklentisi hem de genel bir düğüm modülü bulunur.
Canlı Yayın
Ardından, hizmet çalışanının önceden önbelleğe alınmış kısmi HTML'yi hemen web uygulamasına geri aktarmasını istiyorum. Bu, "güvenilir bir şekilde hızlı" olmanın çok önemli bir parçasıdır. Ekranda her zaman hemen anlamlı bir şey görürüm. Neyse ki hizmet çalışanımızda Streams API'yi kullanarak bunu yapabiliriz.
Streams API'yi daha önce duymuş olabilirsiniz. Meslektaşım Jake Archibald, yıllardır bu özelliği övüyor. 2016'nın web akışlarının yılı olacağı yönünde cesur bir tahminde bulunmuştu. Streams API, iki yıl önce olduğu gibi bugün de harika bir API ancak önemli bir fark var.
O zamanlar yalnızca Chrome, Streams API'yi destekliyordu ancak artık daha fazla tarayıcı tarafından destekleniyor. Genel olarak olumlu bir tablo söz konusu. Uygun yedek kodla, hizmet çalışanınızda akışları kullanmanızı engelleyecek hiçbir şey yok.
Ancak Streams API'nin nasıl çalıştığını anlamak sizi biraz zorlayabilir. Çok güçlü bir temel öğe kümesi sunar ve bu aracı kullanma konusunda rahat olan geliştiriciler, aşağıdaki gibi karmaşık veri akışları oluşturabilir:
const stream = new ReadableStream({
pull(controller) {
return sources[0]
.then(r => r.read())
.then(result => {
if (result.done) {
sources.shift();
if (sources.length === 0) return controller.close();
return this.pull(controller);
} else {
controller.enqueue(result.value);
}
});
},
});
Ancak bu kodun tüm etkilerini anlamak herkes için kolay olmayabilir. Bu mantığı ayrıştırmak yerine, hizmet çalışanı akışına yaklaşımım hakkında konuşalım.
Yepyeni, üst düzey bir sarmalayıcı olan workbox-streams kullanıyorum.
Bu sayede, hem önbelleklerden hem de ağdan gelebilecek çalışma zamanı verilerinden gelen akış kaynaklarının bir karışımını iletebilirim. Workbox, bağımsız kaynakları koordine etme ve bunları tek bir akış yanıtında birleştirme işlemlerini gerçekleştirir.
Ayrıca Workbox, Streams API'nin desteklenip desteklenmediğini otomatik olarak algılar ve desteklenmediğinde eşdeğer, akış dışı bir yanıt oluşturur. Bu sayede, akışlar %100 tarayıcı desteğine yaklaştıkça yedek yazma konusunda endişelenmeniz gerekmez.
Çalışma zamanı önbelleğe alma
Şimdi hizmet çalışanımın Stack Exchange API'den gelen çalışma zamanı verilerini nasıl işlediğine bakalım. Web uygulamasının depolama alanının sınırsız şekilde büyümemesi için Workbox'ın stale-while-revalidate önbelleğe alma stratejisi için yerleşik desteğinden ve süre sonu özelliğinden yararlanıyorum.
Workbox'ta, akış yanıtını oluşturacak farklı kaynakları işlemek için iki strateji ayarladım. Workbox, birkaç işlev çağrısı ve yapılandırmayla, normalde yüzlerce satır kod yazarak yapabileceğimiz işlemleri yapmamızı sağlar.
const cacheStrategy = workbox.strategies.cacheFirst({
cacheName: workbox.core.cacheNames.precache,
});
const apiStrategy = workbox.strategies.staleWhileRevalidate({
cacheName: API_CACHE_NAME,
plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});
İlk strateji, kısmi HTML şablonlarımız gibi önceden önbelleğe alınmış verileri okur.
Diğer strateji, stale-while-revalidate önbelleğe alma mantığını ve 50 girişe ulaştığımızda en son kullanılan önbellek süresinin dolmasını uygular.
Bu stratejileri belirledikten sonra yapmam gereken tek şey, Workbox'a bunları kullanarak eksiksiz bir akış yanıtı oluşturmasını söylemek. Kaynak dizisini işlev olarak ilettiğimde bu işlevlerin her biri hemen yürütülür. Workbox, her kaynaktan gelen sonucu alır ve sırayla web uygulamasına aktarır. Yalnızca dizideki bir sonraki işlev henüz tamamlanmadıysa gecikme olur.
workbox.streams.strategy([
() => cacheStrategy.makeRequest({request: '/head.html'}),
() => cacheStrategy.makeRequest({request: '/navbar.html'}),
async ({event, url}) => {
const tag = url.searchParams.get('tag') || DEFAULT_TAG;
const listResponse = await apiStrategy.makeRequest(...);
const data = await listResponse.json();
return templates.index(tag, data.items);
},
() => cacheStrategy.makeRequest({request: '/foot.html'}),
]);
İlk iki kaynak, doğrudan Cache Storage API'den okunan önceden önbelleğe alınmış kısmi şablonlardır. Bu nedenle, her zaman hemen kullanılabilirler. Bu sayede, hizmet çalışanımızın istekleri yanıtlama hızı, sunucu tarafı kodum gibi güvenilir bir şekilde hızlı olur.
Bir sonraki kaynak işlevimiz, Stack Exchange API'den veri getirir ve yanıtı web uygulamasının beklediği HTML'ye dönüştürür.
stale-while-revalidate stratejisi, bu API çağrısı için daha önce önbelleğe alınmış bir yanıtım varsa bunu hemen sayfaya aktarabileceğim ve bir sonraki istek için önbellek girişini "arka planda" güncelleyebileceğim anlamına gelir.
Son olarak, altbilgimin önbelleğe alınmış bir kopyasını aktarır ve yanıtı tamamlamak için son HTML etiketlerini kapatırım.
Kod paylaşımı, her şeyin senkronize kalmasını sağlar
Hizmet çalışanı kodunun bazı bölümlerinin tanıdık olduğunu fark edeceksiniz. Hizmet çalışanım tarafından kullanılan kısmi HTML ve şablon oluşturma mantığı, sunucu tarafı işleyicimin kullandığı mantıkla aynıdır. Bu kod paylaşımı, kullanıcıların web uygulamamı ilk kez ziyaret ederken veya hizmet çalışanı tarafından oluşturulan bir sayfaya geri dönerken tutarlı bir deneyim yaşamasını sağlar. İzomorfik JavaScript'in güzelliği de burada.
Dinamik, progresif geliştirmeler
PWA'm için hem sunucuyu hem de hizmet çalışanını inceledim ancak ele alınması gereken son bir mantık var: Sayfalarımın her birinde, tamamen akışa alındıktan sonra çalışan küçük bir JavaScript miktarı var.
Bu kod, kullanıcı deneyimini aşamalı olarak iyileştirir ancak çok önemli değildir. Çalıştırılmasa bile web uygulaması çalışmaya devam eder.
Sayfa meta verisi
Uygulamam, API yanıtına göre bir sayfanın meta verilerini güncellemek için istemci tarafı JavaScript kullanıyor. Her sayfa için önbelleğe alınmış HTML'nin aynı ilk bitini kullandığım için web uygulaması, dokümanımın başlığında genel etiketler kullanıyor. Ancak şablon oluşturma ve istemci tarafı kodum arasındaki koordinasyon sayesinde, sayfaya özgü meta verileri kullanarak pencerenin başlığını güncelleyebilirim.
Şablon oluşturma kodunun bir parçası olarak, yaklaşımım, düzgün şekilde kodlanmış dizeyi içeren bir komut dosyası etiketi eklemektir.
const metadataScript = `<script>
self._title = '${escape(item.title)}';
</script>`;
Ardından, sayfam yüklendikten sonra bu dizeyi okuyup doküman başlığını güncelliyorum.
if (self._title) {
document.title = unescape(self._title);
}
Kendi web uygulamanızda güncellemek istediğiniz sayfaya özgü başka meta veriler varsa aynı yaklaşımı uygulayabilirsiniz.
Çevrimdışı kullanıcı deneyimi
Eklediğim diğer progresif geliştirme, çevrimdışı özelliklerimize dikkat çekmek için kullanılıyor. Güvenilir bir PWA oluşturdum ve kullanıcıların, çevrimdışıyken daha önce ziyaret ettikleri sayfaları yükleyebileceklerini bilmelerini istiyorum.
Öncelikle, daha önce önbelleğe alınmış tüm API isteklerinin listesini almak için Cache Storage API'yi kullanıyorum ve bunu bir URL listesine dönüştürüyorum.
Daha önce bahsettiğim, her biri bir soruyu görüntülemek için gereken API isteğinin URL'sini içeren özel veri özelliklerini hatırlıyor musunuz? Bu veri özelliklerini önbelleğe alınmış URL'ler listesiyle karşılaştırabilir ve eşleşmeyen tüm soru bağlantılarının bir dizisini oluşturabilirim.
Tarayıcı çevrimdışı duruma geçtiğinde önbelleğe alınmamış bağlantılar listesinde döngü oluşturup çalışmayacak olanları devre dışı bırakıyorum. Bu, kullanıcılara bu sayfalardan ne beklemeleri gerektiği konusunda görsel bir ipucu verir. Bağlantıları devre dışı bırakmaz veya kullanıcıların gezinmesini engellemez.
const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);
const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
return !cachedUrls.includes(card.dataset.cacheUrl);
});
const offlineHandler = () => {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '0.3';
}
};
const onlineHandler = () => {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '1.0';
}
};
window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);
Sık karşılaşılan sorunlar
Çok sayfalı bir PWA oluşturma yaklaşımımı anlattım. Kendi yaklaşımınızı oluştururken göz önünde bulundurmanız gereken birçok faktör vardır ve benim yaptıklarımdan farklı seçimler yapabilirsiniz. Bu esneklik, web için geliştirme yapmanın en güzel yanlarından biridir.
Kendi mimari kararlarınızı verirken karşılaşabileceğiniz birkaç yaygın sorun vardır ve sizi bu sorunlardan kurtarmak istiyorum.
HTML'nin tamamını önbelleğe almayın
Önbelleğinizde tam HTML belgeleri depolamanızı önermiyorum. Birincisi, yer kaplar. Web uygulamanız her sayfası için aynı temel HTML yapısını kullanıyorsa aynı işaretlemenin kopyalarını tekrar tekrar depolarsınız.
Daha da önemlisi, sitenizin paylaşılan HTML yapısında bir değişiklik yaparsanız önceden önbelleğe alınmış sayfaların her biri eski düzeninizde kalır. Geri gelen bir ziyaretçinin eski ve yeni sayfaların bir karışımını gördüğünde yaşayacağı hayal kırıklığını düşünün.
Sunucu/hizmet çalışanı sapması
Kaçınılması gereken diğer bir sorun ise sunucunuz ile hizmet çalışanınızın senkronizasyonunun bozulmasıdır. Benim yaklaşımım, izomorfik JavaScript kullanarak aynı kodun her iki yerde de çalıştırılmasını sağlamaktı. Mevcut sunucu mimarinize bağlı olarak bu her zaman mümkün olmayabilir.
Hangi mimari kararları verirseniz verin, sunucunuzda ve hizmet çalışanınızda eşdeğer yönlendirme ve şablon oluşturma kodunu çalıştırmak için bir stratejiniz olmalıdır.
En kötü durum senaryoları
Tutarsız düzen/tasarım
Bu tuzakları görmezden gelirseniz ne olur? Her türlü hata mümkündür ancak en kötü senaryo, geri gelen bir kullanıcının çok eski bir düzene sahip, önbelleğe alınmış bir sayfayı ziyaret etmesidir. Bu sayfa, güncel olmayan başlık metni içeren veya artık geçerli olmayan CSS sınıf adlarını kullanan bir sayfa olabilir.
En kötü senaryo: Bozuk yönlendirme
Alternatif olarak, kullanıcı sunucunuz tarafından işlenen ancak hizmet çalışanınız tarafından işlenmeyen bir URL ile karşılaşabilir. Zombi düzenler ve çıkmazlarla dolu bir site, güvenilir bir PWA değildir.
Başarı için ipuçları
Ancak bu süreçte yalnız değilsiniz. Aşağıdaki ipuçları bu tuzaklardan kaçınmanıza yardımcı olabilir:
Çok dilli uygulamaları olan şablon oluşturma ve yönlendirme kitaplıklarını kullanma
JavaScript uygulamaları olan şablon oluşturma ve yönlendirme kitaplıklarını kullanmayı deneyin. Her geliştiricinin mevcut web sunucusundan ve şablon dilinden geçiş yapma lüksüne sahip olmadığını biliyorum.
Ancak popüler şablon oluşturma ve yönlendirme çerçevelerinin birçoğunun birden fazla dilde uygulamaları vardır. Hem JavaScript hem de mevcut sunucunuzun diliyle çalışan bir kitaplık bulabilirseniz hizmet çalışanınızı ve sunucunuzu senkronize etmeye bir adım daha yaklaşmış olursunuz.
İç içe yerleştirilmiş şablonlar yerine sıralı şablonları tercih edin
Ardından, birbiri ardına yayınlanabilecek bir dizi sıralı şablon kullanmanızı öneririm. HTML'nizin ilk bölümünü mümkün olduğunca hızlı bir şekilde aktarabildiğiniz sürece, sayfanızın sonraki bölümlerinde daha karmaşık şablon oluşturma mantığı kullanılabilir.
Hizmet çalışanınızda hem statik hem de dinamik içeriği önbelleğe alma
En iyi performans için sitenizin tüm kritik statik kaynaklarını önceden önbelleğe almanız gerekir. Ayrıca, API istekleri gibi dinamik içerikleri işlemek için çalışma zamanı önbelleğe alma mantığını da ayarlamanız gerekir. Workbox'ı kullanmak, her şeyi sıfırdan uygulamak yerine iyi test edilmiş, üretime hazır stratejiler üzerine inşa edebileceğiniz anlamına gelir.
Ağda yalnızca kesinlikle gerekli olduğunda engelleme yapın
Bununla ilgili olarak, yalnızca önbellekten yanıt akışı yapmanın mümkün olmadığı durumlarda ağda engelleme yapmanız gerekir. Önbelleğe alınmış bir API yanıtını hemen göstermek, genellikle yeni verileri beklemekten daha iyi bir kullanıcı deneyimi sağlar.