Üretimde Hizmet Çalışanları

Dikey ekran görüntüsü

Özet

Google I/O 2015 web uygulamasını hızlı ve öncelikli olarak çevrimdışı hale getirmek için hizmet çalışanı kitaplıklarını nasıl kullandığımızı öğrenin.

Genel Bakış

Bu yılki Google I/O 2015 web uygulaması, Google'ın Geliştirici İlişkileri Ekibi tarafından, Instrument'taki arkadaşlarımız tarafından tasarlanan ve sesli/görsel denemeyi yazan tasarımlara göre yazılmıştır. Ekibimizin misyonu, I/O web uygulamasının (kod adı IOWA olarak anılacaktır) modern web'in yapabileceği her şeyi sergilemesini sağlamaktı. O halde, olması gereken özellikler listemizin başında tam olarak çevrimdışı öncelikli bir deneyim yer alıyordu.

Son zamanlarda bu sitedeki diğer makalelerden herhangi birini okuduysanız hizmet işçileriyle kesinlikle karşılaşmışsınızdır. IOWA'nın çevrimdışı desteğinin büyük ölçüde bunlara bağlı olduğunu duymak sizi şaşırtmayacaktır. IOWA'nın gerçek ihtiyaçlarından yola çıkarak iki farklı çevrimdışı kullanım alanını ele alacak iki kitaplık geliştirdik: Statik kaynakların ön önbelleğe alınmasını otomatikleştirmek için sw-precache ve çalışma zamanında önbelleğe alma ve yedek stratejileri yönetmek için sw-toolbox.

Kitaplıklar birbirini iyi bir şekilde tamamlıyor ve IOWA'nın statik içerik "kabuk"unun her zaman doğrudan önbellekten, dinamik veya uzak kaynakların ise ağdan sunulduğu, gerektiğinde önbelleğe alınmış veya statik yanıtlara yedekleme olanağı sunan performanslı bir strateji uygulamamıza olanak tanıdı.

sw-precache ile önceden önbelleğe alınıyor

IOWA'nın statik kaynakları (HTML, JavaScript, CSS ve resimleri) web uygulamasının temel kabuğunu sağlar. Bu kaynakları önbelleğe alma konusunda dikkate almamız gereken iki önemli şart vardı: Çoğu statik kaynağın önbelleğe alındığından ve güncel tutulduğundan emin olmak istedik. sw-precache, bu gereksinimler göz önünde bulundurularak tasarlanmıştır.

Derleme zamanı entegrasyonu

sw-precache ile IOWA'nın gulp tabanlı derleme sürecini kullanırız ve IOWA'nın kullandığı tüm statik kaynakların eksiksiz bir listesini oluşturmak için bir dizi glob kalıbından yararlanırız.

staticFileGlobs: [
    rootDir + '/bower_components/**/*.{html,js,css}',
    rootDir + '/elements/**',
    rootDir + '/fonts/**',
    rootDir + '/images/**',
    rootDir + '/scripts/**',
    rootDir + '/styles/**/*.css',
    rootDir + '/data-worker-scripts.js'
]

Alternatif yaklaşımlar (özellikle de kodu kontrol eden birden fazla ekip üyemizin olduğu göz önünde bulundurulduğunda, dosya adları listesini bir diziye sabit kodlamak ve bu dosyalarda her değişiklik yapıldığında bir önbellek sürüm numarasını dokundurmayı hatırlamak gibi) Hiç kimse manuel olarak yönetilen bir dizide yeni bir dosya bırakarak çevrimdışı desteği kesmek istemez. Derleme zamanı entegrasyonu, bu endişeleri yaşamadan mevcut dosyalarda değişiklik yapabilmemizi ve yeni dosya ekleyebilmemizi sağladı.

Önbelleğe Alınan Kaynakları Güncelleme

sw-precache, önceden önbelleğe alınan her kaynak için benzersiz bir MD5 karması içeren temel bir hizmet çalışanı komut dosyası oluşturur. Mevcut bir kaynak her değiştiğinde veya yeni bir kaynak eklendiğinde hizmet çalışanı komut dosyası yeniden oluşturulur. Bu işlem, yeni kaynakların önbelleğe alındığı ve güncel olmayan kaynakların temizlendiği hizmet çalışanı güncelleme akışını otomatik olarak tetikler. Aynı MD5 karma oluşturma değerine sahip mevcut kaynaklar olduğu gibi bırakılır. Yani siteyi daha önce ziyaret etmiş kullanıcılar yalnızca değiştirilen kaynakların minimum kümesini indirir. Bu da, önbelleğin tamamının toplu olarak süresi dolduğunda sağlanan deneyimden çok daha verimli bir deneyim sağlar.

Glob kalıplarından biriyle eşleşen her dosya, bir kullanıcı IOWA'yı ilk kez ziyaret ettiğinde indirilir ve önbelleğe alınır. Yalnızca sayfayı oluşturmak için gereken kritik kaynakların önbelleğe alındığından emin olmaya çalıştık. İşitsel/görsel denemede kullanılan medya veya oturumların konuşmacılarının profil resimleri gibi ikincil içerikler kasıtlı olarak önceden önbelleğe alınmadı. Bunun yerine, bu kaynaklara yönelik çevrimdışı istekleri işlemek için sw-toolbox kitaplığını kullandık.

sw-toolbox, for All Our Dynamic Needs

Daha önce de belirtildiği gibi, bir sitenin çevrimdışı çalışması için gereken her kaynağı önbelleğe almak uygun değildir. Bazı kaynaklar, bu işlemin faydalı olması için çok büyüktür veya sık kullanılmaz. Diğer kaynaklar ise uzak bir API'den veya hizmetten gelen yanıtlar gibi dinamiktir. Ancak bir isteğin önceden önbelleğe alınmamış olması, NetworkError ile sonuçlanması gerektiği anlamına gelmez. sw-toolbox, bazı kaynaklar için çalışma zamanında önbelleğe alma işlemini ve diğer kaynaklar için özel yedekleri yöneten istek işleyicileri uygulama esnekliği sağladı. Ayrıca, push bildirimleri için daha önce önbelleğe alınmış kaynaklarımızı güncellemek amacıyla da bu özelliği kullandık.

Aşağıda, sw-toolbox'ı temel alarak oluşturduğumuz özel istek işleyicilere ilişkin birkaç örnek verilmiştir. Bağımsız JavaScript dosyalarını hizmet çalışanının kapsamına alan sw-precache'ın importScripts parameter özelliği sayesinde bu dosyaları temel hizmet çalışanı komut dosyasıyla entegre etmek kolaydı.

Ses/Görüntü Denemesi

Ses/görsel denemesi için sw-toolbox'nin networkFirst önbelleğe alma stratejisini kullandık. Denemenin URL kalıbıyla eşleşen tüm HTTP istekleri önce ağa gönderilir ve başarılı bir yanıt döndürülürse bu yanıt Cache Storage API kullanılarak saklanır. Ağ kullanılamadığında sonraki bir istek yapılırsa daha önce önbelleğe alınmış yanıt kullanılır.

Başarılı bir ağ yanıtı her geldiğinde önbellek otomatik olarak güncellendiğinden, kaynakların sürümünü belirtmemiz veya girişlerin süresini sonlandırmamız gerekmiyordu.

toolbox.router.get('/experiment/(.+)', toolbox.networkFirst);

Hoparlör profil resimleri

Konuşmacı profil resimleri için amacımız, belirli bir konuşmacının görüntüsünün varsa önceden önbelleğe alınmış bir sürümünü görüntülemek, böyle bir resim yoksa görüntü almak için ağa geri dönmekti. Bu ağ isteği başarısız olursa son yedek olarak, önceden önbelleğe alınmış (ve bu nedenle her zaman kullanılabilecek) genel bir yer tutucu resim kullandık. Bu, genel bir yer tutucuyla değiştirilebilir resimlerle çalışırken yaygın olarak kullanılan bir stratejidir ve sw-toolbox cacheFirst ile cacheOnly işleyicileri zincirlenerek kolayca uygulanabilir.

var DEFAULT_PROFILE_IMAGE = 'images/touch/homescreen96.png';

function profileImageRequest(request) {
    return toolbox.cacheFirst(request).catch(function() {
    return toolbox.cacheOnly(new Request(DEFAULT_PROFILE_IMAGE));
    });
}

toolbox.precache([DEFAULT_PROFILE_IMAGE]);
toolbox.router.get('/(.+)/images/speakers/(.*)',
                    profileImageRequest,
                    {origin: /.*\.googleapis\.com/});
Oturum sayfasından profil resimleri
Oturum sayfasından alınan profil resimleri.

Kullanıcı Programlarındaki Güncellemeler

IOWA'nın önemli özelliklerinden biri, oturum açmış kullanıcıların katılmayı planladıkları oturumların programını oluşturmasına ve sürdürmesine olanak tanımasıydı. Beklediğiniz gibi, oturum güncellemeleri bir arka uç sunucuya HTTP POST istekleri aracılığıyla yapıldı ve kullanıcı çevrimdışıyken bu durum değiştiren istekleri işlemenin en iyi yolunu bulmak için biraz zaman harcadık. Başarısız isteklerin IndexedDB'de sıraya alınmasını sağlayan bir çözümle birlikte, ana web sayfasındaki mantıkla IndexedDB'de sıraya alınmış istekleri kontrol edip bulduğu tüm istekleri yeniden deneyen bir çözüm geliştirdik.

var DB_NAME = 'shed-offline-session-updates';

function queueFailedSessionUpdateRequest(request) {
    simpleDB.open(DB_NAME).then(function(db) {
    db.set(request.url, request.method);
    });
}

function handleSessionUpdateRequest(request) {
    return global.fetch(request).then(function(response) {
    if (response.status >= 500) {
        return Response.error();
    }
    return response;
    }).catch(function() {
    queueFailedSessionUpdateRequest(request);
    });
}

toolbox.router.put('/(.+)api/v1/user/schedule/(.+)',
                    handleSessionUpdateRequest);
toolbox.router.delete('/(.+)api/v1/user/schedule/(.+)',
                        handleSessionUpdateRequest);

Yeniden denemeler ana sayfa bağlamında yapıldığından, yeni bir dizi kullanıcı kimlik bilgisi içerdiklerinden emin olabiliriz. Yeniden denemeler başarılı olduktan sonra, kullanıcıya daha önce sıraya alınan güncellemelerinin uygulandığını bildiren bir mesaj görüntüledik.

simpleDB.open(QUEUED_SESSION_UPDATES_DB_NAME).then(function(db) {
    var replayPromises = [];
    return db.forEach(function(url, method) {
    var promise = IOWA.Request.xhrPromise(method, url, true).then(function() {
        return db.delete(url).then(function() {
        return true;
        });
    });
    replayPromises.push(promise);
    }).then(function() {
    if (replayPromises.length) {
        return Promise.all(replayPromises).then(function() {
        IOWA.Elements.Toast.showMessage(
            'My Schedule was updated with offline changes.');
        });
    }
    });
}).catch(function() {
    IOWA.Elements.Toast.showMessage(
    'Offline changes could not be applied to My Schedule.');
});

Çevrimdışı Google Analytics

Benzer şekilde, başarısız Google Analytics isteklerini sıraya sokmak ve daha sonra ağın kullanılabilir olması beklendiğinde tekrar oynatmaya çalışmak için bir işleyici devreye soktuk. Bu yaklaşımla, çevrimdışı olmak Google Analytics'in sunduğu analizlerden ödün vermek anlamına gelmez. Google Analytics arka ucuna doğru bir etkinlik ilişkilendirme süresinin ulaşmasını sağlamak için, sıraya eklenen her isteğe qt parametresini ekledik. Bu parametre, istek ilk kez denendikten sonra geçen süreye ayarlanır. Google Analytics, qt değerlerini yalnızca 4 saate kadar resmi olarak desteklemektedir. Bu nedenle, Service Worker'ın çalışmaya başladığı her seferde bu istekleri mümkün olan en kısa sürede tekrar yürütmek için elimizden geleni yaptık.

var DB_NAME = 'offline-analytics';
var EXPIRATION_TIME_DELTA = 86400000;
var ORIGIN = /https?:\/\/((www|ssl)\.)?google-analytics\.com/;

function replayQueuedAnalyticsRequests() {
    simpleDB.open(DB_NAME).then(function(db) {
    db.forEach(function(url, originalTimestamp) {
        var timeDelta = Date.now() - originalTimestamp;
        var replayUrl = url + '&qt=' + timeDelta;
        fetch(replayUrl).then(function(response) {
        if (response.status >= 500) {
            return Response.error();
        }
        db.delete(url);
        }).catch(function(error) {
        if (timeDelta > EXPIRATION_TIME_DELTA) {
            db.delete(url);
        }
        });
    });
    });
}

function queueFailedAnalyticsRequest(request) {
    simpleDB.open(DB_NAME).then(function(db) {
    db.set(request.url, Date.now());
    });
}

function handleAnalyticsCollectionRequest(request) {
    return global.fetch(request).then(function(response) {
    if (response.status >= 500) {
        return Response.error();
    }
    return response;
    }).catch(function() {
    queueFailedAnalyticsRequest(request);
    });
}

toolbox.router.get('/collect',
                    handleAnalyticsCollectionRequest,
                    {origin: ORIGIN});
toolbox.router.get('/analytics.js',
                    toolbox.networkFirst,
                    {origin: ORIGIN});

replayQueuedAnalyticsRequests();

Push Bildirimi Açılış Sayfaları

Hizmet çalışanları, IOWA'nın çevrimdışı işlevini yönetmenin yanı sıra kullanıcıları yer işareti eklenmiş oturumlarındaki güncellemeler hakkında bilgilendirmek için kullandığımız push bildirimlerini de destekledi. Bu bildirimlerle ilişkili açılış sayfasında güncellenen oturum ayrıntıları gösteriliyordu. Bu açılış sayfaları, genel sitenin bir parçası olarak zaten önbelleğe alınıyordu. Bu nedenle, çevrimdışı olarak görüntülendiğinde bile bu sayfadaki oturum ayrıntılarının güncel olduğundan emin olmamız gerekiyordu. Bunu yapmak için daha önce önbelleğe alınmış oturum meta verilerini, push bildirimini tetikleyen güncellemelerle değiştirdik ve sonucu önbellekte sakladık. Bu güncel bilgiler, oturum ayrıntıları sayfası bir sonraki kez açıldığında (online veya çevrimdışı) kullanılır.

caches.open(toolbox.options.cacheName).then(function(cache) {
    cache.match('api/v1/schedule').then(function(response) {
    if (response) {
        parseResponseJSON(response).then(function(schedule) {
        sessions.forEach(function(session) {
            schedule.sessions[session.id] = session;
        });
        cache.put('api/v1/schedule',
                    new Response(JSON.stringify(schedule)));
        });
    } else {
        toolbox.cache('api/v1/schedule');
    }
    });
});

Dikkat edilmesi gereken noktalar

Elbette, IOWA ölçeğindeki bir projede çalışan hiç kimse birkaç sorunla karşılaşmadan çalışmaz. Karşılaştığımız sorunlardan ve bunların üstesinden gelmek için neler yaptığımızdan bahsedelim.

Eski İçerik

Bir önbelleğe alma stratejisi planlarken, hizmet işçileri aracılığıyla veya standart tarayıcı önbelleğiyle uygulansın, kaynakları mümkün olduğunca hızlı bir şekilde sunma ile en güncel kaynakları sunma arasında bir denge vardır. sw-precache aracılığıyla, uygulamamızın kabuğu için agresif bir önbellek öncelikli strateji uyguladık. Bu, hizmet işleyicimizin sayfadaki HTML, JavaScript ve CSS'yi döndürmeden önce ağda güncelleme olup olmadığını kontrol etmeyeceği anlamına geliyor.

Neyse ki sayfa yüklendikten sonra yeni içeriğin ne zaman kullanılabileceğini tespit etmek için hizmet çalışanı yaşam döngüsü etkinliklerinden yararlanabildik. Güncellenmiş bir hizmet çalışanı algılandığında, kullanıcıya en yeni içeriği görmek için sayfasını yeniden yüklemesi gerektiğini bildiren bir kısa mesaj mesajı gösteririz.

if (navigator.serviceWorker && navigator.serviceWorker.controller) {
    navigator.serviceWorker.controller.onstatechange = function(e) {
    if (e.target.state === 'redundant') {
        var tapHandler = function() {
        window.location.reload();
        };
        IOWA.Elements.Toast.showMessage(
        'Tap here or refresh the page for the latest content.',
        tapHandler);
    }
    };
}
Son içerik bildirimi
"En son içerik" kısa mesajı.

Statik İçeriğin Statik Olmasına Dikkat Edin

sw-precache, yerel dosya içeriklerinin MD5 karma değerini kullanır ve yalnızca karması değişen kaynakları getirir. Bu, kaynakların sayfaya neredeyse hemen erişilebilir olduğu anlamına gelir ancak bir öğe önbelleğe alındıktan sonra, güncellenmiş bir hizmet çalışanı komut dosyasında yeni bir karma oluşturma işlemi uygulanana kadar önbelleğe alınmış olarak kalır.

Arka uç sistemimizin, konferansın her günü için canlı yayın YouTube video kimliklerini dinamik olarak güncellemesi gerektiğinden, I/O sırasında bu davranışla ilgili bir sorunla karşılaştık. Temel şablon dosyası sabit olduğu ve değişmediği için Service Worker güncelleme akışımız tetiklenmedi. YouTube videolarının güncellenmesiyle sunucudan dinamik olarak verilmesi planlanan yanıt birçok kullanıcı için önbelleğe alınmış yanıt oldu.

Web uygulamanızın, kabuğun her zaman statik ve güvenli bir şekilde önbelleğe alınabileceği, kabuğunu değiştiren dinamik kaynakların ise bağımsız olarak yükleneceği şekilde yapılandırıldığından emin olarak bu tür sorunları önleyebilirsiniz.

Önbelleğe Alma İsteklerinizi Önbelleği Bozma

sw-precache, önbelleğe alınacak kaynaklar için istek gönderdiğinde, dosyanın MD5 karmasının değişmediğine karar verdiği sürece bu yanıtları süresiz olarak kullanır. Bu nedenle, önbelleğe alma isteğine verilen yanıtın yeni bir yanıt olduğundan ve tarayıcının HTTP önbelleğinden döndürülmediğinden emin olmanız özellikle önemlidir. (Evet, bir hizmet çalışanında yapılan fetch() istekleri, tarayıcıdaki HTTP önbelleğindeki verilerle yanıt verebilir.)

Önbelleğe aldığımız yanıtların tarayıcının HTTP önbelleğinden değil, doğrudan ağdan alındığından emin olmak için sw-precache, istediği her URL'ye otomatik olarak önbelleği bozan bir sorgu parametresi ekler. sw-precache kullanmıyorsanız ve önbelleğe öncelikli yanıt stratejisi kullanıyorsanız kendi kodunuzda benzer bir şey yaptığınızdan emin olun.

Ön önbelleğe alma için kullanılan her Request öğesinin önbelleğe alma modunu reload olarak ayarlamak, önbelleği bozma sorununa daha temiz bir çözümdür. Bu ayar, yanıtın ağdan gelmesini sağlar. Ancak bu yazı hazırlandığı sırada, önbelleğe alma modu seçeneği Chrome'da desteklenmiyor.

Oturum açma ve kapama desteği

IOWA, kullanıcıların Google Hesaplarını kullanarak giriş yapıp özelleştirilmiş etkinlik programlarını güncellemelerine olanak tanıyordu. Ancak bu, kullanıcıların daha sonra çıkış yapabilecekleri anlamına da geliyordu. Kişiselleştirilmiş yanıt verilerini önbelleğe almak, hassas bir konudur ve her zaman tek bir doğru yaklaşım yoktur.

Kişisel programınızı çevrimdışıyken bile görüntülemek IOWA deneyiminin temelinde yer aldığından, önbelleğe alınan verileri kullanmanın uygun olacağına karar verdik. Bir kullanıcı oturumu kapattığında, önceden önbelleğe alınmış oturum verilerini temizlediğimizden emin olduk.

    self.addEventListener('message', function(event) {
      if (event.data === 'clear-cached-user-data') {
        caches.open(toolbox.options.cacheName).then(function(cache) {
          cache.keys().then(function(requests) {
            return requests.filter(function(request) {
              return request.url.indexOf('api/v1/user/') !== -1;
            });
          }).then(function(userDataRequests) {
            userDataRequests.forEach(function(userDataRequest) {
              cache.delete(userDataRequest);
            });
          });
        });
      }
    });

Ekstra Sorgu Parametrelerine Dikkat Edin

Bir hizmet çalışanı, önbelleğe alınmış bir yanıt olup olmadığını kontrol ederken anahtar olarak bir istek URL'si kullanır. Varsayılan olarak istek URL'si, URL'nin arama bölümündeki tüm sorgu parametreleri dahil olmak üzere önbelleğe alınan yanıtı depolamak için kullanılan URL ile tam olarak eşleşmelidir.

Bu durum, geliştirme sırasında trafiğimizin kaynağını izlemek için URL parametrelerini kullanmaya başladığımızda bizim açımızdan bir soruna neden oldu. Örneğin, bildirimlerimizden biri tıklandığında açılan URL'lere utm_source=notification parametresini ekledik ve web uygulaması manifestimiz için start_url içinde utm_source=web_app_manifest kullandık. Daha önce önbelleğe alınmış yanıtlarla eşleşen URL'ler, bu parametreler eklendiğinde gözden kaçmış olarak gösteriliyordu.

Bu sorun, Cache.match() çağrısı yapılırken kullanılabilen ignoreSearch seçeneğiyle kısmen giderilebilir. Maalesef Chrome ignoreSearch'yi henüz desteklemiyor. Desteklemiş olsa bile bu davranış ya tamamen ya da hiç uygulanır. İhtiyacımız olan, bazı URL sorgu parametrelerini yoksayırken anlamlı olan diğer parametreleri de hesaba katmaktı.

sw-precache'ü, önbelleğe eşleşme olup olmadığını kontrol etmeden önce bazı sorgu parametrelerini çıkarmak ve geliştiricilerin ignoreUrlParametersMatching seçeneği aracılığıyla hangi parametrelerin yoksayıldığını özelleştirmelerine olanak tanımak için genişlettik. Temel uygulama şu şekildedir:

function stripIgnoredUrlParameters(originalUrl, ignoredRegexes) {
    var url = new URL(originalUrl);

    url.search = url.search.slice(1)
    .split('&')
    .map(function(kv) {
        return kv.split('=');
    })
    .filter(function(kv) {
        return ignoredRegexes.every(function(ignoredRegex) {
        return !ignoredRegex.test(kv[0]);
        });
    })
    .map(function(kv) {
        return kv.join('=');
    })
    .join('&');

    return url.toString();
}

Bu durum sizin için ne anlama geliyor?

Google I/O web uygulamasındaki hizmet çalışanı entegrasyonu, muhtemelen bugüne kadar kullanıma sunulan en karmaşık gerçek kullanım alanıdır. Kendi web uygulamalarınızı destekleyecek, sw-precache ve sw-toolbox oluşturduğumuz araçların yanı sıra, açıkladığımız teknikleri kullanan web geliştiricisi topluluğunu heyecanla bekliyoruz. Service Worker'lar, hemen kullanmaya başlayabileceğiniz progresif bir geliştirmedir. Düzgün yapılandırılmış bir web uygulamasının bir parçası olarak kullanıldığında hız ve çevrimdışı avantajlar kullanıcılarınız açısından önemlidir.