the Rising
mei
the Rising
mei
mei
Agenda
What is PWA ?
Progressive Web APP
tw.mall.yahoo.com
How to Build it ?
Checklist
Wonder AMP
Yahoo Mall
Build the most effective mobile web through AMP.
tw.mall.yahoo.com
manifest
※ HTML Structure: <meta name="apple-mobile-web-app-title" content="超級商城"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <link rel="apple-touch-icon" href="icon-192x192.png"> <link rel="apple-touch-icon" sizes="192x192" href="icon-192x192.png"> <link rel="apple-touch-icon" sizes="256x256" href="icon-256x256.png"> <link rel="apple-touch-icon" sizes="384x384" href="icon-384x384.png"> <link rel="apple-touch-icon" sizes="512x512" href="icon-512x512.png"> <link rel="apple-touch-startup-image" href="launch-screen-640x1136.png"> <link rel="apple-touch-startup-image" href="launch-screen-640x1136.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"> <link rel="apple-touch-startup-image" href="launch-screen-750x1294.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"> <link rel="apple-touch-startup-image" href="launch-screen-1242x2148.png" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"> <link rel="manifest" href="manifest.json"> ※ manifest content: { "dir": "ltr", "lang": "zh-Hant-TW", "short_name": "超級商城", "name": "Yahoo奇摩超級商城", "start_url": "yahoo_mall_fp_amp.html?launcher=true", "display": "fullscreen", "background_color": "#00b3ff", "theme_color": "#00b3ff", "description": "Yahoo 雅虎超級商城, 擁有超過百萬商品評價推薦的 NO. 1 購物第一站。流行名店,運動休閒,生活量販,生鮮食品,寵物用品,3C電器,送禮小物,文青用品 通通一網打盡你的購物慾望。每天都有最優惠的折價券/ 超贈點回饋讓你買的划算有保障!", "orientation": "portrait", "icons": [ { "src": "icon-192x192.png", "type": "image/png", "sizes": "192x192" }, { "src": "icon-256x256.png", "type": "image/png", "sizes": "256x256" }, { "src": "icon-384x384.png", "type": "image/png", "sizes": "384x384" }, { "src": "icon-512x512.png", "type": "image/png", "sizes": "512x512" } ], "related_applications": [ { "platform": "play", "url": "https://play.google.com/store/apps/details?id=com.yahoo.mobile.client.android.ecstore" }, { "platform": "itunes", "url": "https://itunes.apple.com/tw/app/yahoo%E5%A5%87%E6%91%A9%E8%B6%85%E7%B4%9A%E5%95%86%E5%9F%8E/id778296354?mt=8" } ] }
Cache API
※ main.js: if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js') .then(function(registration) { console.log('Registration successful, scope is:', registration.scope); }) .catch(function(error) { console.log('Service worker registration failed, error:', error); }); } ※ service-worker.js: self.addEventListener('install', function(event) { event.waitUntil( caches.open(cacheName).then(function(cache) { return cache.addAll( [ 'img/yau/ico-search-black.svg', 'img/yau/ico-cart.svg', 'img/yau/ico-my.svg', 'img/yau/ico-hammer.svg', 'img/yau/ico-like-item.svg' ] ); }) ); }); self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request).then(function(response) { return response || fetchAndCache(event.request); }) ); }); function fetchAndCache(url) { return fetch(url) .then(function(response) { if (!response.ok) { throw Error(response.statusText); } return caches.open(cacheName) .then(function(cache) { cache.put(url, response.clone()); return response; }); }) .catch(function(error) { console.log('Request failed:', error); }); }
tw.mall.yahoo.com
Workbox
Workbox is a set of libraries that make it easy to cache assets and take full advantage of features used to build PWA.
※ service-worker.js: importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js'); workbox.precaching.precacheAndRoute([ 'img/yau/ico-search-black.svg', ... ... ... 'img/yau/ico-like-item.svg' ]); // AMP runtime workbox.routing.registerRoute( /(?:https:\/\/.*)?cdn\.ampproject\.org\/.*\.js/, workbox.strategies.staleWhileRevalidate({ cacheName: 'amp-runtime-0.1', plugins: [ new workbox.expiration.Plugin({ maxAgeSeconds: 60 * 60 * 24 }) ] }) ); // ws - 1day workbox.routing.registerRoute( /.*\?.*cacheMode=1day.*/, workbox.strategies.cacheFirst({ cacheName: 'yauc-ws-1day-0.1', plugins: [ new workbox.expiration.Plugin({ maxEntries: 20, maxAgeSeconds: 60 * 60 * 24 }) ] }) ); ... ... ... // ws - 1hour workbox.routing.registerRoute( /.*\?.*cacheMode=1hour.*/, workbox.strategies.cacheFirst({ cacheName: 'yauc-ws-1hour-0.1', plugins: [ new workbox.expiration.Plugin({ maxEntries: 20, maxAgeSeconds: 60 * 60 * 1 }) ] }) ); // material workbox.routing.registerRoute( /(?:https:\/\/.*)?s.yimg.com.*/, workbox.strategies.networkFirst({ cacheName: 'yec-material-0.1', plugins: [ new workbox.expiration.Plugin({ maxEntries: 300, maxAgeSeconds: 60 * 60 * 6 }) ] }) ); // shell page cache workbox.routing.registerRoute( /.*(yahoo_mall_fp_amp|yahoo_mall_item_amp).*/, workbox.strategies.staleWhileRevalidate({ cacheName: 'yec-shell-page-0.1', plugins: [ new workbox.expiration.Plugin({ maxEntries: 50, maxAgeSeconds: 60 * 60 * 1 }) ] }) ); // Enable Offline Google Analytics workbox.googleAnalytics.initialize();
tw.mall.yahoo.com
Web Push
※ HTML Structure: <amp-web-push id="amp-web-push" layout="nodisplay" helper-iframe-url="web-push-helper.html" permission-dialog-url="web-push-dialog.html" service-worker-url="service-worker.js" ></amp-web-push> <div class="switch-notification-wrap"> <amp-web-push-widget visibility="unsubscribed" layout="fixed" width="42" height="28"> <a class="switch-notification" on="tap:amp-web-push.subscribe"></a> </amp-web-push-widget> <amp-web-push-widget visibility="subscribed" layout="fixed" width="42" height="28"> <a class="switch-notification" on="tap:amp-web-push.unsubscribe"></a> </amp-web-push-widget> <amp-web-push-widget visibility="blocked" layout="fixed" width="42" height="28"> <a class="switch-notification blocked"></a> </amp-web-push-widget> </div> ※ service-worker.js: ... ... ... function updateSubscriptionOnServer(subscription) { let url, data; url = conf.uri.updateSubscription; data = subscription || {}; if (subscription) { console.log('subscription:', JSON.stringify(subscription)); } fetch(url, { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), cache: 'no-cache', credentials: 'include', method: 'POST', mode: 'cors', }) .catch( (err) => console.log('updateSubscription fail.') ); } function sendBeacon({ ea = '', el = '' }) { const beacon = { v: 1, t: 'event', tid: 'UA-74300583-2', cid: '9fcb3f85-d792-41fa-b7a4-b6214e2d8ddc', dp: '/', ec: 'notification', ea, el }; const query = Object.keys(beacon).reduce( (acc, cur) => { return acc.concat(`${encodeURIComponent(cur)}=${encodeURIComponent(beacon[cur])}`); } , []).join('&'); const url = `https://www.google-analytics.com/collect?${query}`; fetch(url, { cache: 'no-cache', method: 'GET' }) .catch( (err) => console.log('sendBeacon fail.') ); } self.addEventListener('push', event => { const data = { ...event.data.json() }; const options = { body: data.content, icon: data.icon, badge: data.badge, vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: 1, link: data.link, id: data.id } }; event.waitUntil( (() => { sendBeacon({ ea:'viewd', el:data.id }); self.registration.showNotification(data.title, options); })() ); }); self.addEventListener('notificationclick', event => { const { data } = event.notification; event.notification.close(); event.waitUntil( (() => { sendBeacon({ ea:'clicked', el:data.id }); clients.openWindow(data.link); })() ); });
tw.bid.yahoo.com
Lighthouse
Lighthouse is an open-source, automated tool for improving the quality of web pages. It has audits for performance, accessibility, progressive web apps, and more.
We can do better
※ HTML Structure: <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1,viewport-fit=cover"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> ※ manifest: { ... "display": "fullscreen", "orientation": "portrait", ... } ※ CSS: :root { --left: 0; --top: 0; --bottom: 0; --main-pdding-horizontal: calc(var(--left) + 1em); --main-padding-top: calc(var(--top) + 4px); --main-padding-bottom: calc(var(--bottom) + 20px); } main { padding: 0 var(--main-pdding-horizontal); } @supports (bottom:env(safe-area-inset-top)) { :root { --left: env(safe-area-inset-left); --top: env(safe-area-inset-top); --bottom: env(safe-area-inset-bottom); } @supports (bottom:max(.75em,1px)) { --main-pdding-horizontal: max(1em, var(--left)); } }
Thanks, all of you.