Google I/O 2016 Progresif Web Uygulaması Oluşturma

Iowa'daki ana sayfa

Özet

Web bileşenleri, Polymer ve materyal tasarımı kullanarak tek sayfalık bir uygulamayı nasıl oluşturup Google.com'da üretime sunduğumuzu öğrenin.

Sonuçlar

  • Yerel uygulamaya kıyasla daha fazla etkileşim (4:06 dakika mobil web ile Android'in 2:40 dakikası karşılaştırması).
  • Service Worker önbelleğe alma özelliği sayesinde geri gelen kullanıcılar için 450 ms daha hızlı ilk boyama
  • Ziyaretçilerin% 84'ü Hizmet Çalışanı'nı desteklemiştir
  • Ana ekrana ekleme kaydetme kayıtları 2015'e göre% 900 arttı.
  • Kullanıcıların% 3,8'i çevrimdışı olmuş ancak 11.000 sayfa görüntüleme elde etmeye devam etmiştir.
  • Oturum açmış kullanıcıların% 50'si bildirimleri etkinleştirmiştir.
  • Kullanıcılara 536 bin bildirim gönderildi (%12'si bildirim geri geldi).
  • Kullanıcıların tarayıcılarının% 99'u, web bileşenleri çoklu dolgularını destekliyordu

Genel bakış

Bu yıl, sevgiyle "IOWA" adlı Google I/O 2016 progresif web uygulaması üzerinde çalışmaktan keyif aldım. Mobil cihazlar önceliklidir, tamamen çevrimdışı çalışır ve büyük ölçüde materyal tasarımdan esinlenmiştir.

IOWA; web bileşenleri, Polymer ve Firebase kullanılarak oluşturulmuş ve App Engine (Go) ile yazılmış kapsamlı bir arka uca sahip tek sayfalı bir uygulamadır (SPA). Bir Service Worker kullanarak içeriği önbelleğe alır, yeni sayfaları dinamik olarak yükler, görünümler arasında sorunsuzca geçiş yapar ve ilk yüklemeden sonra içeriği yeniden kullanır.

Bu örnek olay incelemesinde, ön uç için aldığımız daha ilginç mimari kararlardan bazılarını ele alacağım. Kaynak kod ilginizi çekiyorsa GitHub'da bu sayfaya göz atın.

GitHub'da görüntüle

Web bileşenlerini kullanarak SPA oluşturma

Bileşen olarak her sayfa

Ön ucumuzun temel yönlerinden biri, web bileşenleri etrafında yer almasıdır. Aslında, SPA'mızdaki her sayfa bir web bileşenidir:

    <io-home-page date="2016-05-18T17:00:00Z" app="[[app]]"></io-home-page>
    <io-schedule-page date="2016-05-18T17:00:00Z" app="{ % templatetag openvariable % }app}}"></io-schedule-page>
    <io-attend-page></io-attend-page>
    <io-extended-page></io-extended-page>
    <io-faq-page></io-faq-page>

Bunu neden yaptık? İlk neden bu kodun okunabilir olmasıdır. Bu uygulamayı ilk kez okuyan bir kullanıcı olarak, uygulamamızdaki her sayfanın ne olduğu tamamen açıktır. İkinci neden, web bileşenlerinin SPA oluşturmak için kullanışlı bazı özelliklere sahip olmasıdır. <template> öğesi, Özel Öğeler ve Gölge DOM'un doğal özellikleri sayesinde birçok yaygın sorun (eyalet yönetimi, görünüm etkinleştirme, stil kapsamı) ortadan kalkar. Bunlar, tarayıcıda yerleşik olarak bulunan geliştirici araçlarıdır. Bu fırsatlardan neden yararlanmayasınız?

Her sayfa için bir Özel Öğe oluşturarak birçok şey ücretsiz kazandı:

  • Sayfa yaşam döngüsü yönetimi.
  • Sayfaya özel kapsamlı CSS/HTML.
  • Bir sayfaya özel tüm CSS/HTML/JS, gerektiği gibi gruplanır ve birlikte yüklenir.
  • Görünümler yeniden kullanılabilir. Sayfalar DOM düğümleri olduğundan, sadece bunların eklenmesi veya kaldırılması görünümü değiştirir.
  • Gelecekteki destekleyiciler, işaretlemeyi inceleyerek uygulamamızı kolayca anlayabilirler.
  • Sunucu tarafından oluşturulan işaretleme, öğe tanımları tarayıcı tarafından kaydedilip yükseltildikçe kademeli olarak geliştirilebilir.
  • Özel Öğelerin devralma modeli vardır. DRY kod iyi bir kod.
  • ...çok daha fazlasını yapabilirsiniz.

IOWA'da bu avantajlardan en iyi şekilde yararlandık. Biraz ayrıntıya inelim.

Sayfaları dinamik olarak etkinleştirme

<template> öğesi, tarayıcının standart olarak yeniden kullanılabilir işaretleme yöntemidir. <template>, SPA'ların yararlanabileceği iki özelliğe sahiptir. Öncelikle, şablonun bir örneği oluşturulana kadar <template> içindeki her şey durağandır. İkinci olarak, tarayıcı işaretlemeyi ayrıştırır ancak içeriğe ana sayfadan erişilemez. Bu, gerçek ve yeniden kullanılabilir bir işaretleme yığınıdır. Örneğin:

<template id="t">
    <div>This markup is inert and not part of the main page's DOM.</div>
    <img src="profile.png"> <!-- not loaded by the browser -->
    <video id="vid" src="vid.mp4" autoplay></video> <!-- doesn't load/start -->
    <script>alert("Not run until the template is stamped");</script>
</template>

Polimer, <template> öğelerini birkaç tür uzantısı özel öğesiyle, yani <template is="dom-if"> ve <template is="dom-repeat"> ile extends. Her ikisi de <template> öğesini ek özelliklerle genişleten özel öğelerdir. Ayrıca web bileşenlerinin bildirim temelli yapısı sayesinde her ikisi de tam olarak beklediğinizi yapar. İlk bileşen, bir koşula dayalı işaretlemeyi damgalar. İkincisi, bir listedeki her öğe için işaretlemeyi tekrarlar (veri modeli).

IOWA bu tür uzantı öğelerini nasıl kullanıyor?

Hatırlarsanız, IOWA'daki her sayfa bir web bileşenidir. Ancak ilk yüklemede her bileşeni bildirmek saçma olur. Bu, uygulama ilk yüklendiğinde her sayfanın bir örneğini oluşturmak anlamına gelir. Özellikle bazı kullanıcılar yalnızca 1 veya 2 sayfaya giderken, ilk yükleme performansımızı olumsuz etkilemek istemedik.

Çözümümüz hile yapmaktı. IOWA'da her sayfanın öğesini bir <template is="dom-if"> içine sarmalarız. Böylece, içerikler ilk başlatmada yüklenmez. Daha sonra, şablonun name özelliği URL ile eşleştiğinde sayfaları etkinleştiririz. <lazy-pages> web bileşeni bizim için tüm bu mantığı yönetir. İşaretleme şuna benzer:

<!-- Lazy pages manages the template stamping. It watches for route changes
        and sets `template.if = true` on the appropriate template. -->
<lazy-pages>
    <template is="dom-if" name="home">
    <io-home-page date="2016-05-18T17:00:00Z"></io-home-page>
    </template>

    <template is="dom-if" name="schedule">
    <io-schedule-page date="2016-05-18T17:00:00Z"></io-schedule-page>
    </template>

    <template is="dom-if" name="attend">
    <io-attend-page></io-attend-page>
    </template>
</lazy-pages>

Bu konunun hoşuma giden özelliği, her sayfanın ayrıştırılmış ve sayfa yüklendiğinde yayınlanmaya hazır olması, ancak CSS/HTML/JS'sinin yalnızca isteğe bağlı olarak yürütülmesi (<template> üst öğesi damgalandığında). Web bileşenleri FTW kullanılarak dinamik + geç görüntülemeler.

Gelecekteki iyileştirmeler

Sayfa ilk yüklendiğinde, her sayfa için tüm HTML İçe Aktarma dosyalarını tek seferde yüklüyoruz. Bariz bir iyileştirme, öğe tanımlarını yalnızca gerekli olduğunda geç yüklemektir. Polymer'in eşzamansız HTML İçe Aktarmalarını yüklemek için yararlı bir yardımcısı da vardır:

Polymer.Base.importHref('io-home-page.html', (e) => { ... });

IOWA bunu yapmıyor çünkü a) tembel durumda kaldık ve b) performansta ne kadar artış elde edeceğimizden emin değiliz. İlk boyamamız yaklaşık 1 saniyeydi.

Sayfa yaşam döngüsü yönetimi

Custom Elements API, bir bileşenin durumunu yönetmek için "yaşam döngüsü geri çağırmalarını" tanımlar. Bu yöntemleri uyguladığınızda, bir bileşenin ömrüne ücretsiz olarak giriş yapabilirsiniz:

createdCallback() {
    // automatically called when an instance of the element is created.
}

attachedCallback() {
    // automatically called when the element is attached to the DOM.
}

detachedCallback() {
    // automatically called when the element is removed from the DOM.
}

attributeChangedCallback() {
    // automatically called when an HTML attribute changes.
}

IOWA'da bu geri çağırmalardan yararlanmak kolay oldu. Her sayfanın bağımsız bir DOM düğümü olduğunu unutmayın. SPA'mızda "yeni bir görünüme" gitmek, bir düğümü DOM'ye ekleyip diğerini kaldırmak anlamına gelir.

Kurulum çalışmaları için (başlatma durumu, etkinlik işleyiciler ekleme) attachedCallback kullandık. Kullanıcılar farklı bir sayfaya gittiğinde detachedCallback, temizlik yapar (işleyicileri kaldırın, paylaşılan durumu sıfırlayın). Ayrıca, yerel yaşam döngüsü geri çağırma işlevini kendimize ait birkaç geri çağırmanın kapsamını genişlettik:

onPageTransitionDone() {
    // page transition animations are complete.
},

onSubpageTransitionDone() {
    // sub nav/tab page transitions are complete.
}

Bunlar, çalışmaları geciktirmek ve sayfa geçişleri arasındaki duraklamayı en aza indirmek için yararlı eklemelerdi. Bu konuyla ilgili daha fazla bilgi vereceğiz.

Sayfalar arasında ortak işlevleri kurutma

Devralma, Özel Öğeler'in güçlü bir özelliğidir. Web için standart bir devralma modeli sağlar.

Maalesef Polymer 1.0, yazma sırasında öğe devralmayı henüz uygulamadı. Bu sırada Polymer'in Behaviors (Davranışlar) özelliği de aynı derecede yararlı oldu. Davranışlar tamamen birbirine karışmış.

Tüm sayfalarda aynı API yüzeyini oluşturmak yerine, paylaşılan mix'ler oluşturarak kod tabanını rafine etmek mantıklıdır. Örneğin PageBehavior, uygulamamızdaki tüm sayfaların ihtiyaç duyduğu ortak özellikleri/yöntemleri tanımlar:

PageBehavior.html

let PageBehavior = {

    // Common properties all pages need.
    properties: {
    name: { type: String }, // Slug name of the page.
    ...
    },

    attached() {
    // If the page defines a `onPageTransitionDone`, call it when the router
    // fires 'page-transition-done'.
    if (this.onPageTransitionDone) {
        this.listen(document.body, 'page-transition-done', 'onPageTransitionDone');
    }

    // Update page meta data when new page is navigated to.
    document.body.id = `page-${this.name}`;
    document.title = this.title || 'Google I/O 2016';

    // Scroll to top of new page.
    if (IOWA.Elements.Scroller) {
        IOWA.Elements.Scroller.scrollTop = 0;
    }

    this.setupSubnavEffects();
    },

    detached() {
    this.unlisten(document.body, 'page-transition-done', 'onPageTransitionDone');
    this.teardownSubnavEffects();
    }
};

IOWA.IOBehaviors = IOWA.IOBehaviors || {PageBehavior: PageBehavior};

Gördüğünüz gibi PageBehavior, yeni bir sayfa ziyaret edildiğinde çalıştırılan yaygın görevleri gerçekleştirir. Örneğin, document.title öğesini güncelleme, kaydırma konumunu sıfırlama, kaydırma ve alt gezinme efektleri için etkinlik işleyicileri ayarlama.

Bağımsız sayfalar PageBehavior uygulamasını, bağımlılık olarak yükleyip behaviors kullanarak kullanır. Ayrıca, gerekirse temel özelliklerini/yöntemlerini geçersiz kılabilirler. Örneğin, ana sayfamızdaki "subclass" sınıfı şu şekilde geçersiz kılınır:

io-home-page.html

<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="PageBehavior.html">
<!-- rest of the import dependencies used by the page. -->

<dom-module id="io-home-page">
    <template>
    <!-- PAGE'S MARKUP -->
    </template>
    <script>
    Polymer({
        is: 'io-home-page',

        behaviors: [IOBehaviors.PageBehavior], // All pages have common functionality.

        // Pages define their own title and slug for the router.
        title: 'Schedule - Google I/O 2016',
        name: 'home',

        // The home page has custom setup work when it's added navigated to.
        // Note: PageBehavior's attached also gets called.
        attached() {
        if (this.app.isPhoneSize) {
            this.listen(IOWA.Elements.ScrollContainer, 'scroll', '_onPageScroll');
        }
        },

        // The home page does its own cleanup when a new page is navigated to.
        // Note: PageBehavior's detached also gets called.
        detached() {
        this.unlisten(IOWA.Elements.ScrollContainer, 'scroll', '_onPageScroll');
        },

        // The home page can define onPageTransitionDone to do extra work
        // when page transitions are done, and thus preventing janky animations.
        onPageTransitionDone() {
        ...
        }
    });
    </script>
</dom-module>

Paylaşım stilleri

Uygulamamızdaki farklı bileşenlerde stilleri paylaşmak için Polymer'in paylaşılan stil modüllerini kullandık. Stil modülleri, bir CSS parçasını bir kez tanımlamanıza ve bunu bir uygulama genelinde farklı yerlerde yeniden kullanmanıza olanak tanır. Bizim için "farklı yerler" farklı bileşenler demekti.

IOWA'da, sayfalar ve oluşturduğumuz diğer bileşenler arasında renkler, yazı tipi ve düzen sınıflarını paylaşmak için shared-app-styles özelliğini oluşturduk.

shared-app-styles.html

<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="../bower_components/paper-styles/color.html">

<dom-module id="shared-app-styles">
    <template>
    <style>
        [layout] {
        @apply(--layout);
        }
        [layout][horizontal] {
        @apply(--layout-horizontal);
        }
        .scrollable {
        @apply(--layout-scroll);
        }
        .noscroll {
        overflow: hidden;
        }
        /* Style radio buttons and tabs the same throughout the app */
        paper-tabs {
        --paper-tabs-selection-bar-color: currentcolor;
        }
        paper-radio-button {
        --paper-radio-button-checked-color: var(--paper-cyan-600);
        --paper-radio-button-checked-ink-color: var(--paper-cyan-600);
        }
        ...
    </style>
    </template>
</dom-module>

io-home-page.html

<link rel="import" href="shared-app-styles.html">
<!-- Rest of import dependencies used by the page. -->

<dom-module id="io-home-page">
    <template>
    <style include="shared-app-styles">
        :host { display: block} /* Other element styles can go here. */
    </style>
    <!-- PAGE'S MARKUP -->
    </template>
    <script>Polymer({...});</script>
</dom-module>

Burada <style include="shared-app-styles"></style>, Polymer'in ""shared-app-styles" adlı modüle stilleri dahil et" anlamına gelen söz dizimini göstermektedir.

Paylaşım uygulaması durumu

Artık uygulamamızdaki her sayfanın bir Özel Öğe olduğunu biliyorsunuz. Bunu bir milyon kez söyledim. Peki, her sayfa bağımsız bir web bileşeniyse kendinize durumu uygulama genelinde nasıl paylaştığımızı soruyor olabilirsiniz.

IOWA, paylaşım durumu için bağımlılık ekleme (Açısal) veya redux (React) benzeri bir teknik kullanır. Global bir app mülkü oluşturduk ve paylaşılan alt mülkleri bu mülkün içine askıya aldık. app, verilerine ihtiyaç duyan her bileşene enjekte ederek uygulamamızda iletilmesini sağlar. Kablo kurulumunu herhangi bir kod yazmadan yapabildiğimizden, Polymer'in veri bağlama özelliklerini kullanmak bunu kolaylaştırır:

<lazy-pages>
    <template is="dom-if" name="home">
    <io-home-page date="2016-05-18T17:00:00Z" app="[[app]]"></io-home-page>
    </template>

    <template is="dom-if" name="schedule">
    <io-schedule-page date="2016-05-18T17:00:00Z" app="{ % templatetag openvariable % }app}}"></io-schedule-page>
    </template>
    ...
</lazy-pages>

<google-signin client-id="..." scopes="profile email"
                            user="{ % templatetag openvariable % }app.currentUser}}"></google-signin>

<iron-media-query query="(min-width:320px) and (max-width:768px)"
                                query-matches="{ % templatetag openvariable % }app.isPhoneSize}}"></iron-media-query>

Kullanıcılar uygulamamıza giriş yaptığında <google-signin> öğesi, user özelliğini günceller. Bu mülk app.currentUser öğesine bağlı olduğundan, geçerli kullanıcıya erişmek isteyen tüm sayfaların app öğesine bağlanması ve currentUser alt mülkünü okuması gerekir. Bu teknik tek başına, uygulama genelinde durum paylaşımı için kullanışlıdır. Ancak bir diğer avantajı da tek bir oturum açma öğesi oluşturup sonuçlarını site genelinde yeniden kullanmamız oldu. Medya sorguları için de aynı şey. Her sayfanın yine aynı oturum açma işlemini yinelemesi veya kendi medya sorguları kümesini oluşturması boşa giderdi. Bunun yerine, uygulama genelindeki işlevlerden/verilerden sorumlu olan bileşenler uygulama düzeyinde bulunur.

Sayfa geçişleri

Google I/O web uygulamasında gezinirken uygulamanın şık sayfa geçişlerini (à la material tasarım) fark edeceksiniz.

IOWA&#39;nın sayfa geçişlerini uygulama
IOWA'nın sayfa geçişlerini uygulama.

Kullanıcılar yeni bir sayfaya gittiğinde bir dizi işlem gerçekleşir:

  1. Üst gezinme, bir seçim çubuğunu yeni bağlantıya kaydırır.
  2. Sayfa başlığı yavaş yavaş kaybolur.
  3. Sayfanın içeriği aşağıya doğru kayarak kaybolur.
  4. Bu animasyonlar tersine çevrildiğinde yeni sayfanın başlığı ve içeriği görünür.
  5. (İsteğe bağlı) Yeni sayfa ek başlatma işlemleri yapar.

Karşılaştığımız zorluklardan biri, performanstan ödün vermeden bu muhteşem geçişi nasıl yapabileceğinizi bulmaktı. Pek çok dinamik çalışma yürütülüyor ve jank partimizde hoş görülmedi. Sunduğumuz çözüm Web Animasyonları API'si ve Promises'in birleşiminden oluşuyordu. Bu ikisini birlikte kullanmak bize çok yönlü kullanım olanağı, bir tak-çalıştır animasyon sistemi ve das olumsuzluğunu en aza indirmek için ayrıntılı kontrol sağladı.

İşleyiş şekli

Kullanıcılar yeni bir sayfayı tıkladıklarında (veya geri-ileri tıkladıklarında) yönlendiricimizin runPageTransition() işlevi, bir dizi Söz'den yararlanarak büyüsünü gerçekleştirir. Promise'i kullanmak, animasyonları dikkatli bir şekilde düzenlememizi sağladı ve CSS Animasyonlarının "eş zamansızlığını" ve dinamik olarak yüklenen içeriği uygun hale getirmemize yardımcı oldu.

class Router {

    init() {
    window.addEventListener('popstate', e => this.runPageTransition());
    }

    runPageTransition() {
    let endPage = this.state.end.page;

    this.fire('page-transition-start');              // 1. Let current page know it's starting.

    IOWA.PageAnimation.runExitAnimation()            // 2. Play exist animation sequence.
        .then(() => {
        IOWA.Elements.LazyPages.selected = endPage;  // 3. Activate new page in <lazy-pages>.
        this.state.current = this.parseUrl(this.state.end.href);
        })
        .then(() => IOWA.PageAnimation.runEnterAnimation())  // 4. Play entry animation sequence.
        .then(() => this.fire('page-transition-done')) // 5. Tell new page transitions are done.
        .catch(e => IOWA.Util.reportError(e));
    }

}

Geri çağırma ("İşin sırrını koruma: sayfalar arasında ortak işlevler" bölümünden ibaret), sayfalar page-transition-start ve page-transition-done DOM etkinliklerini dinler. Şimdi bu olayların nereden tetiklendiğini görüyorsunuz.

runEnterAnimation/runExitAnimation yardımcılarının yerine Web Animations API'yi kullandık. runExitAnimation durumunda, birkaç DOM düğümü (manşet ve ana içerik alanı) alırız, her bir animasyonun başlangıcını/sonunu belirtir ve ikisini paralel olarak çalıştırmak için bir GroupEffect oluştururuz:

function runExitAnimation(section) {
    let main = section.querySelector('.slide-up');
    let masthead = section.querySelector('.masthead');

    let start = {transform: 'translate(0,0)', opacity: 1};
    let end = {transform: 'translate(0,-100px)', opacity: 0};
    let opts = {duration: 400, easing: 'cubic-bezier(.4, 0, .2, 1)'};
    let opts_delay = {duration: 400, delay: 200};

    return new GroupEffect([
    new KeyframeEffect(masthead, [start, end], opts),
    new KeyframeEffect(main, [{opacity: 1}, {opacity: 0}], opts_delay)
    ]);
}

Görünüm geçişlerini daha fazla (veya daha az) ayrıntılı hale getirmek için diziyi değiştirmeniz yeterlidir!

Kaydırma efektleri

IOWA'nın sayfayı kaydırdığınızda birkaç ilginç etkisi olur. İlki, kullanıcıları tekrar sayfanın başına döndüren kayan işlem düğmemizdir (FAB):

    <a href="#" tabindex="-1" aria-hidden="true" aria-label="back to top" onclick="backToTop">
      <paper-fab icon="io:expand-less" noink tabindex="-1"></paper-fab>
    </a>

Kesintisiz kaydırma, Polymer'in uygulama düzeni öğeleri kullanılarak uygulanır. Yapışkan/geri dönen üst gezinme, gölge, renk ve arka plan geçişleri, paralaks efektleri ve yumuşak kaydırma gibi kullanıma hazır kaydırma efektlerini sağlarlar.

    // Smooth scrolling the back to top FAB.
    function backToTop(e) {
      e.preventDefault();

      Polymer.AppLayout.scroll({top: 0, behavior: 'smooth',
                                target: document.documentElement});

      e.target.blur();  // Kick focus back to the page so user starts from the top of the doc.
    }

<app-layout> öğelerini kullandığımız bir başka yer de yapışkan gezinme içindi. Videoda görebileceğiniz gibi, kullanıcılar sayfayı aşağı kaydırdığında kayboluyor, tekrar yukarı kaydırdığında ise görüntüye dönüyor.

Yapışkan gezinme gezinmeleri
ile yapılan yapışkan kaydırmalı gezinmeler.

<app-header> öğesini olduğu gibi kullandık. Uygulamada içine girmek ve süslü kaydırma efektleri oluşturmak çok kolay oldu. Bunları elbette kendimiz de uygulayabilirdik, ancak ayrıntıların yeniden kullanılabilir bir bileşende önceden kodlanmış olması zamandan tasarruf etmemizi sağladı.

Öğeyi tanımlayın. Özelliklerle özelleştirin. Hepsi bu kadar!

    <app-header reveals condenses effects="fade-background waterfall"></app-header>

Sonuç

I/O progresif web uygulaması için web bileşenleri ve Polymer'in önceden hazırlanmış materyal tasarım widget'ları sayesinde birkaç haftada bir ön ucun tamamını oluşturmayı başardık. Yerel API'lerin özellikleri (Özel Öğeler, Gölge DOM, <template>) doğal bir şekilde SPA'nın dinamik havasına katkıda bulunur. Yeniden kullanılabilirlik, zamandan tasarruf etmenizi sağlar.

Kendi progresif web uygulaması oluşturmakla ilgileniyorsanız Uygulama Araç Kutusu'na göz atın. Polymer'in Uygulama Araç Kutusu, Polymer ile PWA oluşturmak için kullanılan bileşen, araç ve şablonlardan oluşan bir koleksiyondur. Kampanyanızı kullanmaya başlamanın kolay bir yoludur.