การสร้าง 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% ออฟไลน์ แต่ยังคงมีการดูหน้าเว็บถึง 1.1 หมื่นครั้ง
  • 50% ของผู้ใช้ที่ลงชื่อเข้าใช้เปิดใช้การแจ้งเตือน
  • มีการส่งการแจ้งเตือน 5.36 แสนครั้งไปยังผู้ใช้ (12% นำการแจ้งเตือนกลับมา)
  • 99% ของผู้ใช้ เบราว์เซอร์ที่รองรับ polyfills คอมโพเนนต์เว็บ

ภาพรวม

ปีนี้ผมรู้สึกยินดีที่ได้ได้ทำงานใน 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"></video> <!-- doesn't load/start -->
    <script>alert("Not run until the template is stamped");</script>
</template>

พอลิเมอร์ ขยาย <template> ด้วย องค์ประกอบที่กำหนดเองส่วนขยายประเภท 2-3 ประเภท คือ <template is="dom-if"> และ <template is="dom-repeat"> ทั้ง 2 แบบเป็นแบบกำหนดเอง องค์ประกอบที่ขยาย <template> ด้วยความสามารถเพิ่มเติม และขอขอบคุณ คอมโพเนนต์เว็บแบบประกาศ ทั้ง 2 อย่างนี้ทำงานตามที่คุณคาดหวังไว้จริงๆ คอมโพเนนต์แรกจะประทับมาร์กอัปตามเงื่อนไข รายการที่ 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 ที่ใช้คอมโพเนนต์เว็บ 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.
}

การใช้ประโยชน์จาก Callback เหล่านี้ใน IOWA นั้นทำได้ง่าย อย่าลืมว่าทุกหน้าเป็นโหนด DOM แบบในตัว การไปยัง "มุมมองใหม่" ใน SPA ของเราก็คือการแนบโหนดหนึ่งเข้ากับ DOM และนำอีกโหนดออก

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