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

บ้านของไอโอวา

สรุป

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

ผลลัพธ์

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

ภาพรวม

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

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

ในกรณีศึกษานี้ ผมจะพูดถึงการตัดสินใจด้านสถาปัตยกรรมที่น่าสนใจซึ่งเราทำขึ้นสำหรับฟรอนท์เอนด์ หากคุณสนใจซอร์สโค้ด โปรดดูใน 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 เป็นโค้ดที่ดี
  • ...เพิ่มเติมอีกมากมาย

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

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

องค์ประกอบ <template> เป็นวิธีมาตรฐานของเบราว์เซอร์ในการสร้างมาร์กอัปที่นำมาใช้ใหม่ได้ <template> มี 2 คุณสมบัติที่ SPA ใช้ประโยชน์ได้ อย่างแรกคือ ทุกอย่างภายใน <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" autoplay></video> <!-- doesn't load/start -->
    <script>alert("Not run until the template is stamped");</script>
</template>

พอลิเมอร์extends <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> ระดับบนสุด) มุมมองแบบไดนามิก + แบบ Lazy Loading โดยใช้ FTW สำหรับคอมโพเนนต์เว็บ

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

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

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

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

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

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

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 เพื่อดำเนินการตั้งค่า (init State, Attach Listener event) เมื่อผู้ใช้ไปยังหน้าอื่น 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 สำหรับการพูดว่า "รวมสไตล์ไว้ในโมดูลที่ชื่อ "สไตล์แอปที่แชร์"

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

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

IOWA ใช้เทคนิคที่คล้ายกับการแทรกทรัพยากร Dependency (Angular) หรือ Redux (React) เพื่อเรียกสถานะการแชร์ เราได้สร้างพร็อพเพอร์ตี้ 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 Design)

การทำงานของการเปลี่ยนหน้าของ IOWA
การทํางานของการเปลี่ยนหน้าของ IOWA

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

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

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

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

เมื่อผู้ใช้คลิกไปยังหน้าใหม่ (หรือกดย้อนกลับ) runPageTransition() ของเราเตอร์จะทำงานอย่างมีประสิทธิภาพด้วยการเรียกใช้ผ่านชุดคำสัญญา การใช้ 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));
    }

}

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

เราใช้ 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 มีเอฟเฟกต์ที่น่าสนใจหลายอย่างเมื่อคุณเลื่อนดูหน้า อย่างแรกคือปุ่มการทำงานแบบลอย (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 ของคุณเอง โปรดดูกล่องเครื่องมือ App App Toolbox ของ Polymer คือคอลเล็กชันคอมโพเนนต์ เครื่องมือ และเทมเพลตสำหรับการสร้าง PWA ด้วย Polymer ซึ่งเป็นวิธีง่ายๆ ในการเริ่มต้นใช้งาน