การสร้าง Progressive Web App ของ Google I/O 2016

บ้านในไอโอวา

สรุป

มาดูว่าเราสร้างแอปหน้าเดียวโดยใช้คอมโพเนนต์ของเว็บ, Polymer และดีไซน์ Material แล้วเปิดตัวเป็นเวอร์ชันที่ใช้งานจริงใน Google.com ได้อย่างไร

ผลลัพธ์

  • ได้รับการมีส่วนร่วมมากกว่าแอปที่มาพร้อมเครื่อง (ใช้เวลา 4:06 นาทีในเว็บบนอุปกรณ์เคลื่อนที่เทียบกับเวลา 2:40 นาทีของ Android)
  • การแสดงผลครั้งแรกที่เร็วขึ้น 450 มิลลิวินาทีสำหรับผู้ใช้ที่กลับมาเนื่องจากการแคชของ Service Worker
  • 84% ของผู้เข้าชมสนับสนุน Service Worker
  • การบันทึกการเพิ่มลงในหน้าจอหลักเพิ่มขึ้น +900% เมื่อเทียบกับปี 2015
  • ผู้ใช้ 3.8% ออฟไลน์ไปแล้วแต่ยังคงสร้างการดูหน้าเว็บ 11,000 ครั้ง
  • 50% ของผู้ใช้ที่ลงชื่อเข้าใช้เปิดใช้การแจ้งเตือน
  • มีการส่งการแจ้งเตือน 536,000 รายการไปยังผู้ใช้ (12% กลับมาใช้งานอีกครั้ง)
  • 99% ของเบราว์เซอร์ของผู้ใช้รองรับ Polyfill คอมโพเนนต์ของเว็บ

ภาพรวม

ปีนี้เรามีโอกาสได้ทํางานกับ Progressive Web App ของ Google I/O 2016 ซึ่งมีชื่อเล่นว่า "IOWA" โดยออกแบบมาเพื่ออุปกรณ์เคลื่อนที่เป็นหลัก ทำงานแบบออฟไลน์ได้เต็มรูปแบบ และได้รับแรงบันดาลใจอย่างมากจากการออกแบบเชิงวัสดุ

IOWA คือแอปพลิเคชันหน้าเว็บเดียว (SPA) ที่สร้างขึ้นโดยใช้ Web Component, Polymer และ Firebase และมีแบ็กเอนด์ที่ครอบคลุมซึ่งเขียนขึ้นใน App Engine (Go) โดยจะแคชเนื้อหาไว้ล่วงหน้าโดยใช้ Service Worker, โหลดหน้าใหม่แบบไดนามิก, เปลี่ยนมุมมองอย่างราบรื่น และนำเนื้อหามาใช้ซ้ำหลังจากโหลดครั้งแรก

ในกรณีศึกษานี้ เราจะพูดถึงการตัดสินใจด้านสถาปัตยกรรมที่น่าสนใจบางอย่างที่เราทำสำหรับหน้าเว็บ หากสนใจซอร์สโค้ด โปรดดูใน GitHub

ดูใน GitHub

การสร้าง SPA โดยใช้คอมโพเนนต์เว็บ

ทุกหน้าเป็นคอมโพเนนต์

หนึ่งในแง่มุมหลักเกี่ยวกับหน้าเว็บของเราคือหน้าเว็บนั้นมุ่งเน้นที่คอมโพเนนต์เว็บ อันที่จริง ทุกหน้าใน SPA คือส่วนประกอบของเว็บ

    <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>

เหตุผลที่เราทำเช่นนี้ เหตุผลแรกคือโค้ดนี้อ่านง่าย ในฐานะผู้อ่านครั้งแรก หน้าเว็บทุกหน้าในแอปของเรานั้นชัดเจนมาก เหตุผลที่ 2 คือคอมโพเนนต์เว็บมีคุณสมบัติที่ยอดเยี่ยมในการสร้าง SPA ความไม่สบายใจที่พบได้บ่อย (การจัดการสถานะ การเปิดใช้งานมุมมอง การกำหนดขอบเขตรูปแบบ) หายไปเนื่องจากฟีเจอร์ตามธรรมชาติขององค์ประกอบ <template>, องค์ประกอบที่กำหนดเอง และ Shadow DOM เครื่องมือเหล่านี้เป็นเครื่องมือสําหรับนักพัฒนาซอฟต์แวร์ที่ฝังอยู่ในเบราว์เซอร์ เหตุใดจึงไม่ใช้ประโยชน์จากฟีเจอร์เหล่านี้

การสร้างองค์ประกอบที่กำหนดเองสำหรับแต่ละหน้าทำให้เราได้รับสิ่งดีๆ มากมายฟรี:

  • การจัดการวงจรของหน้าเว็บ
  • CSS/HTML ที่กําหนดขอบเขตเฉพาะหน้านั้นๆ
  • CSS/HTML/JS ทั้งหมดที่เกี่ยวข้องกับหน้าเว็บจะรวมกลุ่มไว้และโหลดเข้าด้วยกันตามที่จำเป็น
  • มุมมองนํามาใช้ซ้ำได้ เนื่องจากหน้าเว็บเป็นโหนด DOM การเพิ่มหรือนําหน้าเว็บออกจึงเปลี่ยนมุมมอง
  • ผู้ดูแลระบบในอนาคตจะเข้าใจแอปของเราได้ง่ายๆ เพียงทำความเข้าใจมาร์กอัป
  • มาร์กอัปที่แสดงผลโดยเซิร์ฟเวอร์จะเพิ่มประสิทธิภาพได้อย่างต่อเนื่องเมื่อเบราว์เซอร์บันทึกการกำหนดองค์ประกอบและอัปเกรดโดยเบราว์เซอร์
  • องค์ประกอบที่กําหนดเองมีรูปแบบการสืบทอด โค้ด DRY คือโค้ดที่ดี
  • …และอีกมากมาย

เราใช้ประโยชน์จากสิทธิประโยชน์เหล่านี้อย่างเต็มที่ในไอโอวา มาเจาะลึกรายละเอียดกัน

การเปิดใช้งานหน้าเว็บแบบไดนามิก

องค์ประกอบ <template> เป็นวิธีที่เบราว์เซอร์ใช้สร้างมาร์กอัปที่นํากลับมาใช้ซ้ำได้ <template> มีลักษณะ 2 อย่างที่สปาใช้ประโยชน์ได้ ขั้นแรก ทุกอย่างภายใน <template> จะหยุดทำงานจนกว่าจะมีการสร้างอินสแตนซ์ของเทมเพลต ประการที่ 2 เบราว์เซอร์จะแยกวิเคราะห์มาร์กอัป แต่เข้าถึงเนื้อหาจากหน้าหลักไม่ได้ นั่นคือกลุ่มมาร์กอัปที่นำมาใช้ซ้ำได้จริง เช่น

<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"></video> <!-- doesn't load/start -->
    <script>alert("Not run until the template is stamped");</script>
</template>

Polymer ขยาย <template> ด้วยองค์ประกอบที่กำหนดเองของส่วนขยายประเภท 2-3 รายการ ได้แก่ <template is="dom-if"> และ <template is="dom-repeat"> ทั้ง 2 รายการเป็นองค์ประกอบที่กําหนดเองซึ่งขยาย <template> ด้วยความสามารถเพิ่มเติม และด้วยลักษณะการประกาศของคอมโพเนนต์เว็บ ทั้งสองอย่างจึงทํางานตามที่คาดไว้ คอมโพเนนต์แรกประทับมาร์กอัปตามเงื่อนไข รายการที่ 2 จะมาร์กอัปซ้ำสำหรับทุกรายการในรายการ (โมเดลข้อมูล)

IOWA ใช้องค์ประกอบส่วนขยายประเภทเหล่านี้อย่างไร

หากคุณจำได้ ทุกหน้าใน IOWA คือคอมโพเนนต์เว็บ อย่างไรก็ตาม การประกาศคอมโพเนนต์ทุกรายการในการโหลดครั้งแรกนั้นไม่ฉลาดนัก ซึ่งหมายความว่าจะต้องสร้างอินสแตนซ์ของทุกหน้าเมื่อแอปโหลดเป็นครั้งแรก เราไม่อยากส่งผลเสียต่อประสิทธิภาพการโหลดครั้งแรก โดยเฉพาะอย่างยิ่งเนื่องจากผู้ใช้บางรายไปที่หน้าเพียง 1 หรือ 2 หน้าเท่านั้น

ทางออกของเราคือการโกง ใน IOWA เราจะตัดองค์ประกอบของหน้าแต่ละหน้าไว้ใน <template is="dom-if"> เพื่อไม่ให้เนื้อหาของหน้านั้นโหลดในการบูตครั้งแรก จากนั้นเราจะเปิดใช้งานหน้าเว็บเมื่อแอตทริบิวต์ name ของเทมเพลตตรงกับ URL คอมโพเนนต์เว็บ <lazy-pages> จะจัดการตรรกะทั้งหมดนี้ให้เรา มาร์กอัปจะมีลักษณะดังนี้

<!-- 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>

สิ่งที่ฉันชอบเกี่ยวกับวิธีนี้คือทุกหน้าเว็บได้รับการแยกวิเคราะห์และพร้อมใช้งานเมื่อโหลดหน้าเว็บ แต่ CSS/HTML/JS ของหน้าเว็บจะทำงานเฉพาะเมื่อมีการเรียกใช้ (เมื่อมีการประทับ <template> ของหน้าเว็บหลัก) มุมมองแบบไดนามิก + การโหลดแบบเลื่อนดูทีละหน้าโดยใช้คอมโพเนนต์ของเว็บ

การปรับปรุงในอนาคต

เมื่อหน้าเว็บโหลดเป็นครั้งแรก เราจะโหลดการนําเข้า HTML ทั้งหมดสําหรับแต่ละหน้าพร้อมกัน การปรับปรุงที่เห็นได้ชัดคือการโหลดการกำหนดองค์ประกอบแบบ Lazy Loading เมื่อจำเป็นเท่านั้น นอกจากนี้ Polymer ยังมีตัวช่วยที่ยอดเยี่ยมสำหรับการโหลดการนําเข้า HTML แบบแอซิงค์ด้วย

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

IOWA ไม่ทำแบบนี้เนื่องจาก ก) เราขี้เกียจ และ ข) เรายังไม่แน่ใจว่าควรเพิ่มประสิทธิภาพได้มากน้อยเพียงใด การลงสีครั้งแรกของเราไปแล้วประมาณ 1 วินาที

การจัดการวงจรของหน้าเว็บ

Custom Elements API จะกำหนด "การเรียกกลับเกี่ยวกับวงจร" เพื่อจัดการสถานะของคอมโพเนนต์ เมื่อใช้เมธอดเหล่านี้ คุณจะได้รับฮุกฟรีสำหรับวงจรชีวิตของคอมโพเนนต์ ดังนี้

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 ได้อย่างง่ายดาย โปรดทราบว่าหน้าเว็บแต่ละหน้าคือโหนด DOM ที่แยกต่างหาก การไปยัง "มุมมองใหม่" ใน SPA ของเราเป็นเรื่องของการแนบโหนดหนึ่งกับ DOM และนำอีกโหนดออก

เราใช้ attachedCallback เพื่อทํางานตั้งค่า (สถานะเริ่มต้น แนบโปรแกรมรับฟังเหตุการณ์) เมื่อผู้ใช้ไปยังหน้าอื่น detachedCallback จะล้างข้อมูล (นํา Listener ออก รีเซ็ตสถานะการแชร์) นอกจากนี้ เรายังได้ขยายการเรียกกลับวงจรชีวิตของเนทีฟด้วยฟีเจอร์ของเราเองอีกหลายรายการ ดังนี้

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

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

การดำเนินการเหล่านี้มีประโยชน์ในการเลื่อนเวลาทำงานและลดการกระตุกระหว่างการเปลี่ยนหน้า เราจะแจ้งข้อมูลเพิ่มเติมในภายหลัง

การใช้ฟังก์ชันการทำงานทั่วไปซ้ำๆ ในหน้าต่างๆ

การสืบทอดเป็นฟีเจอร์ที่มีประสิทธิภาพขององค์ประกอบที่กําหนดเอง ซึ่งให้บริการรูปแบบการสืบทอดมาตรฐานสําหรับเว็บ

ขออภัยที่ Polymer 1.0 ยังไม่ได้ใช้งานการสืบทอดองค์ประกอบในขณะที่เขียนบทความนี้ ในระหว่างนี้ ฟีเจอร์ลักษณะการทำงานของ Polymer ก็มีประโยชน์ไม่แพ้กัน ลักษณะการทํางานเป็นเพียงมิกซ์อิน

แทนที่จะสร้างแพลตฟอร์ม API เดียวกันในทุกหน้า เราจึงควรทำให้โค้ดฐานซ้ำกันน้อยที่สุดด้วยการสร้างมิกซ์อินที่แชร์ ตัวอย่างเช่น PageBehavior จะกำหนดพร็อพเพอร์ตี้/เมธอดทั่วไปที่หน้าเว็บทั้งหมดในแอปของเราต้องการ

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};

ดังที่คุณเห็น PageBehavior ดําเนินการงานทั่วไปที่ทํางานเมื่อมีการเข้าชมหน้าใหม่ เช่น การอัปเดต document.title, รีเซ็ตตำแหน่งการเลื่อน และการตั้งค่า Listener เหตุการณ์สำหรับเอฟเฟกต์การเลื่อนและการนำทางย่อย

หน้าเว็บแต่ละหน้าใช้ PageBehavior โดยการโหลดเป็นทรัพยากร Dependency และใช้ behaviors นอกจากนี้ ผู้ใช้ยังสามารถลบล้างพร็อพเพอร์ตี้/เมธอดพื้นฐานได้หากจําเป็น ตัวอย่างเช่น ต่อไปนี้คือสิ่งที่ "คลาสย่อย" ของหน้าแรกลบล้าง

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>

รูปแบบการแชร์

เราใช้โมดูลสไตล์ที่แชร์ของ Polymer เพื่อแชร์สไตล์ในคอมโพเนนต์ต่างๆ ในแอป โมดูลสไตล์ช่วยให้คุณกำหนด CSS ส่วนหนึ่งเพียงครั้งเดียวแล้วนำไปใช้ซ้ำในที่ต่างๆ ทั่วทั้งแอปได้ "ที่ต่างๆ" ในที่นี้หมายถึงคอมโพเนนต์ต่างๆ

ใน IOWA เราได้สร้าง shared-app-styles เพื่อแชร์ระดับสี การพิมพ์ และเลย์เอาต์ในหน้าเว็บและคอมโพเนนต์อื่นๆ ที่เราสร้างขึ้น

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>

ในที่นี้ <style include="shared-app-styles"></style> คือไวยากรณ์ของ Polymer สำหรับ "รวมสไตล์ในโมดูลชื่อ "shared-app-styles"

สถานะแอปพลิเคชันการแชร์

ตอนนี้คุณก็ทราบแล้วว่าหน้าทุกหน้าในแอปของเราเป็นองค์ประกอบที่กำหนดเอง เราพูดเรื่องนี้ไปหลายล้านครั้งแล้ว โอเค แต่หากหน้าเว็บทุกหน้าเป็นคอมโพเนนต์เว็บแบบสแตนด์อโลน คุณอาจสงสัยว่าเราแชร์สถานะในแอปได้อย่างไร

IOWA ใช้เทคนิคที่คล้ายกับการฉีดข้อมูล Dependency (Angular) หรือ Redux (React) ในการแชร์สถานะ เราได้สร้างพร็อพเพอร์ตี้ app ส่วนกลางและได้แขวนพร็อพเพอร์ตี้ย่อยที่แชร์ไว้ app ระบบจะส่ง app ไปทั่วแอปพลิเคชันโดยการแทรกลงในคอมโพเนนต์ทุกรายการที่ต้องการข้อมูล การใช้ฟีเจอร์การเชื่อมโยงข้อมูลของ Polymer ทำให้เรื่องนี้ง่ายขึ้นเนื่องจากเราต่อสายไฟได้โดยไม่ต้องเขียนโค้ดใดๆ เลย ดังนี้

<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>

องค์ประกอบ <google-signin> จะอัปเดตพร็อพเพอร์ตี้ user เมื่อผู้ใช้เข้าสู่ระบบแอปของเรา เนื่องจากพร็อพเพอร์ตี้ดังกล่าวเชื่อมโยงกับ app.currentUser หน้าเว็บใดๆ ที่ต้องการเข้าถึงผู้ใช้ปัจจุบันเพียงต้องเชื่อมโยงกับ app และอ่านพร็อพเพอร์ตี้ย่อย currentUser เทคนิคนี้มีประโยชน์สำหรับการแชร์สถานะในแอปอยู่แล้ว แต่ประโยชน์อีกอย่างหนึ่งคือเราสร้างองค์ประกอบการลงชื่อเพียงครั้งเดียวและนําผลลัพธ์ไปใช้ซ้ำในเว็บไซต์ เช่นเดียวกับคิวรีสื่อ การที่ทุกหน้าต้องลงชื่อเข้าใช้ซ้ำหรือสร้างชุดการค้นหาสื่อของตัวเองนั้นเป็นเรื่องที่สิ้นเปลือง แต่คอมโพเนนต์ที่รับผิดชอบฟังก์ชันการทำงาน/ข้อมูลทั่วทั้งแอปจะอยู่ที่ระดับแอป

ทรานซิชันหน้าเว็บ

ขณะไปยังส่วนต่างๆ ของเว็บแอป Google I/O คุณจะสังเกตเห็นการเปลี่ยนหน้าอย่างราบรื่น (à la ดีไซน์ Material)

ภาพการเปลี่ยนหน้าเว็บของ IOWA
การทํางานของการเปลี่ยนหน้าของ IOWA

เมื่อผู้ใช้ไปยังหน้าใหม่ เหตุการณ์ต่างๆ จะเกิดขึ้นตามลําดับดังนี้

  1. แถบนำทางด้านบนจะเลื่อนแถบการเลือกไปยังลิงก์ใหม่
  2. บรรทัดแรกของหน้าจะค่อยๆ เลือนหายไป
  3. เนื้อหาของหน้าจะเลื่อนลงแล้วค่อยๆ เลือนหายไป
  4. เมื่อเล่นภาพเคลื่อนไหวเหล่านั้นย้อนกลับ หัวเรื่องและเนื้อหาของหน้าใหม่จะปรากฏขึ้น
  5. (ไม่บังคับ) หน้าใหม่จะทํางานเริ่มต้นเพิ่มเติม

หนึ่งในความท้าทายของเราคือการหาวิธีสร้างการเปลี่ยนภาพอย่างราบรื่นนี้โดยไม่ลดประสิทธิภาพ เรามีงานจำนวนมากที่ต้องทำอย่างต่อเนื่อง และข้อบกพร่องไม่อยู่ในงานของเรา โซลูชันของเราคือ Web Animations API และ Promises รวมกัน การใช้ทั้ง 2 อย่างนี้ร่วมกันทำให้เรามีความหลากหลาย ระบบภาพเคลื่อนไหวแบบปลั๊กแอนด์เพลย์ และการควบคุมแบบละเอียดเพื่อลดอาการกระตุกของ das

วิธีการทำงาน

เมื่อผู้ใช้คลิกไปยังหน้าใหม่ (หรือกดย้อนกลับ/ไปข้างหน้า) runPageTransition() ของเราจะทํางานอย่างน่าอัศจรรย์โดยเรียกใช้ชุด Promise การใช้ Promises ช่วยให้เราจัดระเบียบภาพเคลื่อนไหวอย่างรอบคอบและช่วยอธิบายเหตุผลของ "การทำงานแบบอะซิงโครนัส" ของภาพเคลื่อนไหว CSS และเนื้อหาที่โหลดแบบไดนามิก

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));
    }

}

การเรียกคืนจากส่วน "การคงสิ่งต่างๆ ให้คงที่: ฟังก์ชันทั่วไปในหน้าเว็บต่างๆ" หน้าเว็บจะคอยฟังเหตุการณ์ DOM page-transition-start และ page-transition-done ตอนนี้คุณจะเห็นตําแหน่งที่เหตุการณ์เหล่านั้นเริ่มทํางาน

เราใช้ Web Animations API แทนตัวช่วย runEnterAnimation/runExitAnimation ในกรณีของ runExitAnimation เราจะจับโหนด DOM 2 โหนด (โฆษณา Masthead และพื้นที่เนื้อหาหลัก) ประกาศจุดเริ่มต้น/สิ้นสุดของภาพเคลื่อนไหวแต่ละรายการ และสร้าง GroupEffect เพื่อเรียกใช้ทั้ง 2 พร้อมกัน

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)
    ]);
}

เพียงแก้ไขอาร์เรย์เพื่อทำให้การเปลี่ยนมุมมองซับซ้อนขึ้น (หรือน้อยลง)

เอฟเฟกต์การเลื่อน

IOWA มีเอฟเฟกต์ที่น่าสนใจ 2-3 อย่างเมื่อคุณเลื่อนหน้าเว็บ รายการแรกคือปุ่มการทำงานแบบลอย (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>

การใช้การเลื่อนที่ราบรื่นทำได้โดยใช้องค์ประกอบเลย์เอาต์แอปของ Polymer โดยมีเอฟเฟกต์การเลื่อนที่พร้อมใช้งานทันที เช่น การนำทางด้านบนแบบติดหนึบ/แบบย้อนกลับ เงาตกกระทบ การเปลี่ยนสีและพื้นหลัง เอฟเฟกต์พารัลแลกซ์ และการเลื่อนอย่างราบรื่น

    // 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> คือสำหรับการนำทางแบบติดหนึบ อย่างที่เห็นในวิดีโอว่า ปุ่มจะหายไปเมื่อผู้ใช้เลื่อนหน้าเว็บลงและกลับมาอีกครั้งเมื่อเลื่อนกลับขึ้นไป

แถบนำทางการเลื่อนแบบติดหนึบ
การไปยังส่วนต่างๆ ของหน้าเว็บแบบเลื่อนติดหนึบโดยใช้

เราใช้องค์ประกอบ <app-header> ตามเดิม ใส่เอฟเฟกต์การเลื่อนได้อย่างง่ายๆ ในแอป ก็ใช้ได้เลย แต่การที่เราเขียนโค้ดรายละเอียดไว้แล้วในคอมโพเนนต์แบบใช้ซ้ำได้นั้นช่วยประหยัดเวลาได้มาก

ประกาศองค์ประกอบ ปรับแต่งด้วยแอตทริบิวต์ เท่านี้ก็เรียบร้อย

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

บทสรุป

สำหรับ Progressive Web App ของ I/O เราสามารถสร้างฟรอนท์เอนด์ทั้งหมดได้ในไม่กี่สัปดาห์ด้วยคอมโพเนนต์ของเว็บและวิดเจ็ตดีไซน์ Material ของ Polymer ที่สร้างไว้ล่วงหน้า ฟีเจอร์ของ API เดิม (องค์ประกอบที่กําหนดเอง, Shadow DOM, <template>) เหมาะสําหรับความยืดหยุ่นของ SPA อย่างเป็นธรรมชาติ ความสามารถในการนํากลับมาใช้ใหม่ช่วยประหยัดเวลาได้เป็นอย่างมาก

หากสนใจสร้าง Progressive Web App ของคุณเอง โปรดดูกล่องเครื่องมือสำหรับแอป กล่องเครื่องมือแอปของ Polymer คือคอลเล็กชันคอมโพเนนต์ เครื่องมือ และเทมเพลตสําหรับสร้าง PWA ด้วย Polymer ซึ่งช่วยให้เริ่มต้นใช้งานได้ง่าย