reveal

the Rising

the Rising

mei

Profile

mei

  • Front End engineer
  • HTML / JavaScript / CSS
  • work @ Verizon Media
  • facebook: mei.studio.li
  • #DoRightThings

Agenda

Agenda

  • - What is PWA ?
  • - How to Build it ?

What is PWA ?

What is PWA ?

PWA

Progressive Web APP

  • - APP Like
  • - Offline capability
  • - Notification
  • - Work on all device
10:21

tw.mall.yahoo.com

How to Build it ?

How to it ?

checklist

Checklist

  • - mobile web
  • - manifest
  • - Cache API
  • - Web Push

Wonder AMP

Wonder AMP

Yahoo Mall

Yahoo Mall

Build the most effective mobile web through AMP.

10:21

tw.mall.yahoo.com

Manifest

manifest

Manifest

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

Cache API

service-worker

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

10:21

tw.mall.yahoo.com

Workbox

Workbox

Workbox is a set of libraries that make it easy to cache assets and take full advantage of features used to build PWA.

  • - Precaching
  • - Runtime caching
  • - Strategies
  • - Request routing
  • - Background sync
10:21

developers.google.com

Workbox

unfold
※ 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();

              
10:21

tw.mall.yahoo.com

Web Push

Web Push

Web Push

unfold
※ 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);
    })()
  );
});

                
10:21

tw.bid.yahoo.com

Lighthouse

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.

10:21

developers.google.com

Thinking

We can do better

fullscreen design

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


                

thankful

Thanks, all of you.