PWA Night Conf: ScrapboxServiceWorkerCache
Gyazo
1🎊

daiiz / @daizplus daiiz

Gyazo

Scrapbox
shokai masui rakusai progfay yutaro takeru tiro

Helpfeel



Scrapbox
Gyazo


Scrapbox
Wiki
Gyazo Gyazo
Socket.IO
Scrapbox 15,000
JavaScriptSPA

Scrapbox PWA
Scrapbox
💪
Gyazo

Scrapbox PWA
daiiz
Worker
#ServiceWorker

Scrapbox PWA

Google ChromeDesktop PWA
app manifest display: standalone

Agenda
ServiceWorker
CacheStorage
Scrapbox


ServiceWorker
CacheStorage


ServiceWorker

FetchEvent
UIpostMessage

ServiceWorker
UI ServiceWorker Network
Gyazo

FetchEvent
UIHTTP
URLpathname
ResponseUI
self.addEventListener('fetch', event => {
event.respondWith(async function () {
// ...
}())
})
fetchEvent.respondWith()

Response
(Scrapbox)
XMLServiceWorkerXSLTSVG


CacheStorage
UIServiceWorker
Key Value Store
key: Request objectURL
value: Response object
Cache
const cahce = caches.open(cacheKey)
response
await cache.put(request, response)
CacheStorageresponse
const response = await caches.match(request)


CacheStorage: Scrapbox
4cache
cacheName
assets-20200129-035507
UGC
image-2020-01-29
API
api-2020-01-29
prefetch
Gyazo

3
Network
Network first
Cache first



1. Network
ServiceWorker
self.addEventListener('fetch', event => {
return
})
cachenetwork
return

if (req.method !== GET) return
POST
if (new URL(req.url).pathname === "/login") return


ServiceWorker
AndroidGyazo

OS
Gyazo Gyazo

self.addEventListener('fetch', event => {
const { method, url } = event.request
if (method !== 'POST') return
if (new URL(url).pathname === '/serviceworker-upload') {
event.respondWith((async () => {
// POST
})())
}
})

{
...
"share_target": {
"action": "/serviceworker-upload",
"method": "POST",
"enctype": "multipart/form-data",
"params": { ... }
}
}


2. Network first
network
cache
self.addEventListener('fetch', event => {
event.respondWith(async function () {
const req = event.request
try {
// network
return fetch(req.clone())
} catch (err) {
// CacheStorage
return caches.match(req)
}
}())
})

API
navigator.onLine
Scrapbox
true
WiFi


3. Cache first
cache
cachenetwork
self.addEventListener('fetch', event => {
event.respondWith(async function () {
const req = event.request
const res = await caches.match(req)
if (res) return res
return fetch(req.clone())
}())
})

Resposenetwork
req.credentials , req.redirect , req.mode
const options = Object.create(null)
if (req.mode !== 'navigate') options.mode = req.mode
if (req.credentials) options.credentials = req.credentials
if (req.redirect) options.redirect = req.redirect
return fetch(req, options)
req.mode === "navigate"
URL


Quiz!! Scrapbox
Wiki使
Network
Network first
Cache first


Q1.
app assets
SPA使HTML
CSS,

Gyazo
Gyazo


A1. Cache first
寿
Gyazo

assets-cache
assetsURL
使
Gyazo
assets-version

cache.addAll()
cache
cache.add()


Q2. UGC
Scrapobox
Gyazo

Cache-Control: max-age=



A2. Network first

CacehStorageorigin
opaque
javascript
res.ok == false
res.status == 0
res.body == null
cache.addAll()
fetch API CORS mode

Request.destination - Web APIs | MDN
request.destination === "image"
img
"audio" "video"



fallback to network
request.destination === "video"

2019/2
macOS Safari 12
videomp4
self.addEventListener('fetch', event => {
event.respondWith((async function () {
// ...
// ...
// network
return fetch(req.clone())
})())
})

respondWithreturn
self.addEventListener('fetch', event => {
if (req.destination === 'video') return
event.respondWith((async function () {
// ...
})())
})
respondWithfetchRange Header

Q3.
API
&
Gyazo Gyazo

A3. Network first
Wiki
cache
Gyazo
X-Serviceworker-Cache: true cache.put()
UI使


Prefetch
postMessageServiceWorkerfetch
CacheStorage
10cache
Promise.race()timeout

UI
javascript
async function prefetch (urls) {
const { controller } = await navigator.serviceWorker
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = event => { resolve(event.data) }
controller.postMessage({
title: 'prefetch',
body: { urls }
}, [channel.port2])
})
}

ServiceWorker
MessageEvent
self.addEventListener('message', event => {
event.waitUntil(async function () {
const { urls } = event.data
// urlfetch cache.put()
Promise.all(urls.map(async url => {
const response = await fetch(url)
// cache "prefetch" "api"
})
event.ports[0].postMessage({ title: 'prefetch' })
}())
})


Cache first


Q4.
API
Gyazo

A4. Cache first
CacheStorage
X-Serviceworker-Cache
Gyazo

Slow 3G
Gyazo
CacheStorageAPI
PWA Night


cache
Scrapbox
cache keycacheresponse
cache
async function findLatestCache (req) {
const cacheNames = await caches.keys()
for (const date of cacheNames.sort().reverse()) {
const cache = await caches.open(date)
const res = await cache.match(req, {ignoreSearch: true})
if (res) return res
}
return null
}


Q5.
API
search paramsURL
GET /api/pages/daiiz/search/query?q=PWA
Gyazo

A5. Network first
search params URL
Gyazo

search params
cache.match(url, {ignoreSearch: true})
Method

Wiki
寿

寿: UGC
quota
APICacheStorage
使
const { quota, usage } = await navigator.storage.estimate()

寿: API
Wiki

cache objectquota
WorkBox

requestquota
const cache = await caches.open('images')
const reqs = await cache.keys()
// request1
for (const req of reqs) {
await cache.delete(req.url)
}
// cache object
caches.delete("images")


API
ServiceWorkersetInterval
fetch

response.ok
status 200 true
cache

status 200
response.ok
Scrapbox 200
Gyazo
cmd-shift-t

Cache-Control: no-store ServiceWorker


Cache first

Network firstfallbackCacheOK
Gyazo




Gyazo

Desktop PWA
URL
Gyazo

window.open(url) Desktop PWA
Gyazo
"noopener" "noreferrer" menu_bar


ServiceWorker CacheStorage
Scrapbox