Making GitLab a PWA

Or, how I learned to stop worrying and embrace the boring solution.

Sam Beckham
@samdbeckham

3G

WiFi

1s

3s

3G

WiFi

0.3s

0.3s

Offline

3G

WiFi

0.3s

0.3s

0.3s

It's a
website
that acts like a
native app

Install to
 your device

Load offline

Just a
website

It's a
progressive enhancement

The

boring
solution

Lighthouse

Non-Javascript content

✅ https

✅ https redirect

✅ Fast enough on 3g

✅ <meta viewport> tag

✅ Sized for the viewport

 Custom splash screen

❌ Themed Address Bar

❌ Service Worker

❌ 200 When offline

 Prompted to install the web app

Lighthouse

// TODO: Insert hilarious slide that makes a pun about there being a break right now.

Custom Splash Screen

Themed Address bar

{
  "name": "GitLab",
  "short_name": "GitLab",
  "display": "standalone",
  "scope": "/",
  "start_url": "/",
  "theme_color": "#474D57",
  "background_color": "#380D75",
  "icons": [
    {
      "src": "/pwa_icon_512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
./manifest.json
<head>
  
  <!-- other head stuff -->
  
  <link rel="manifest" href="/manifest.json">

</head>
./yourpage.html

Lighthouse

✅ Custom splash screen

✅ Themed Address Bar

❌ Service Worker

❌ 200 When offline

❌ Prompted to install the web app

Service worker

💻

🌐

Browser

Server

request

response

💻

🌐

Browser

Server

🐲

Service

Worker

💻

🌐

Browser

Server

🐲

Service

Worker

cat.png

cat.png

💵

Cache

💻

🌐

Browser

Server

🐲

Service

Worker

cat.png

cat.png

💵

Cache

💤

200 when offline

 

 

The

boring
solution

200 when offline

 

 

💻

🌐

Browser

Server

🐲

Service

Worker

offline.html

💵

Cache

💻

🌐

Browser

Server

🐲

Service

Worker

issue

offline

💵

Cache

issue

💧

 

 

// Register a service worker if our
// browser allows it

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register(
    '/service_worker.js',
    { scope: '/'}
  );
}

Register

const CURRENT_CACHE = '<%= Gitlab.version %>_<%= Gitlab.revision %>';
const OFFLINE_PAGE = '/-/offline';

self.addEventListener('install', event => {
  event.waitUntil(caches.open(CURRENT_CACHE)
    .then(cache => cache.add(OFFLINE_PAGE)));
});

self.addEventListener('activate', event => {
  event.waitUntil(
    caches
      .keys()
      .then(cacheNames =>
        Promise.all(
          cacheNames.map(cache =>
            cache !== CURRENT_CACHE ? caches.delete(cache) : Promise.resolve(),
          ),
        ),
      ),
  );
});

self.addEventListener('fetch', event => {
  const { request } = event;
  const { method, mode } = request;

  if (method === 'GET' && mode === 'navigate') {
    event.respondWith(fetch(request).catch(() => caches.match(OFFLINE_PAGE)));
  }
});
./service_worker.js
const CURRENT_CACHE = '<%= Gitlab.version %>_<%= Gitlab.revision %>';
const OFFLINE_PAGE = '/offline';

self.addEventListener('install', event => {
  event.waitUntil(
    caches
      .open(CURRENT_CACHE)
      .then(cache => 
        cache.add(OFFLINE_PAGE))
      )
});

Install

./service_worker.js
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames =>
        Promise.all(
          cacheNames.map(cache =>
            cache !== CURRENT_CACHE ?
              caches.delete(cache) :
              Promise.resolve(),
    ))));
});

Activate

./service_worker.js
self.addEventListener('fetch', event => {
  const { request } = event;
  const { method, mode } = request;
  if (
    method === 'GET' &&
    mode === 'navigate') {
      event.respondWith(
        fetch(request).catch(() => (
          caches.match(OFFLINE_PAGE)));
      )
  }
});

Fetch

./service_worker.js
const CURRENT_CACHE = '<%= Gitlab.version %>_<%= Gitlab.revision %>';
const OFFLINE_PAGE = '/-/offline';

self.addEventListener('install', event => {
  event.waitUntil(caches.open(CURRENT_CACHE)
    .then(cache => cache.add(OFFLINE_PAGE)));
});

self.addEventListener('activate', event => {
  event.waitUntil(
    caches
      .keys()
      .then(cacheNames =>
        Promise.all(
          cacheNames.map(cache =>
            cache !== CURRENT_CACHE ? caches.delete(cache) : Promise.resolve(),
          ),
        ),
      ),
  );
});

self.addEventListener('fetch', event => {
  const { request } = event;
  const { method, mode } = request;

  if (method === 'GET' && mode === 'navigate') {
    event.respondWith(fetch(request).catch(() => caches.match(OFFLINE_PAGE)));
  }
});
./service_worker.js

/offline

/-/offline

Lighthouse

❌ Prompted to install the web app

✅ Service worker

✅ 200 when offline

✅ https

✅ http => https

✅ Custom Splash Screen

✅ Themed Address bar

✅ non-javascript content

✅ Fast enough on 3g

 <meta viewport> tag

✅ Sized correctly for the viewport

Prompted to install the web app

Do literally nothing else

✅ Service worker

Lighthouse

✅ 200 when offline

✅ https

✅ http => https

✅ Prompted to install the web app

✅ Custom Splash Screen

✅ Themed Address bar

✅ non-javascript content

✅ Fast enough on 3g

 <meta viewport> tag

✅ Sized correctly for the viewport

Basic PWA Support

Vulnerability Caching

July 22nd 2019

v12.1.0

 

Thanks!

Sam Beckham
@samdbeckham

Making GitLab a PWA (FENE May 2019)

By Sam Beckham

Making GitLab a PWA (FENE May 2019)

  • 1,228