Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gatsby-plugin-offline): replace no-cache detection with dynamic path whitelist #9907

Merged
merged 26 commits into from Nov 20, 2018
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6f53bb1
Remove all no-cache code
Nov 5, 2018
41a932a
Remove references to no-cache in offline plugin
Nov 5, 2018
a191ffe
Merge upstream/master into origin/master
Nov 12, 2018
a1cecf4
Initial work on hybrid navigation handler
Nov 12, 2018
1807752
Refactor whitelist code to allow it to support onPostPrefetchPathname
Nov 12, 2018
8f3cd4b
Fix service worker detection
Nov 12, 2018
8d826c6
Fix IndexedDB race condition
Nov 12, 2018
e8224ea
Prevent race conditions + reset whitelist on SW update
Nov 12, 2018
c60bb5c
Remove unnecessary API handler (onPostPrefetchPathname is called anyway)
Nov 12, 2018
465a4d2
Add debugging statements + fix some minor problems
Nov 13, 2018
c62c378
Fix back/forward not working after 404
Nov 13, 2018
4d47124
Remove unneeded debugging statements
Nov 13, 2018
b233cf6
Bundle idb-keyval instead of using an external CDN
Nov 13, 2018
7c4a2b6
Update README
Nov 13, 2018
0923c67
Merge upstream/master into origin/hybrid-offline-shell
Nov 13, 2018
25e4ecf
Fix excessive file caching (e.g. GA tracking gif)
Nov 14, 2018
23599eb
Backport fixes from #9907
Nov 14, 2018
e02f9d5
minor fixes for things I copy-pasted wrong
Nov 14, 2018
ffc5868
Merge origin/backport-9415-fix into origin/hybrid-offline-shell
Nov 14, 2018
fbeee6d
Merge upstream/master into origin/hybrid-offline-shell
Nov 14, 2018
0d0b001
Fetch resources the same way in enqueue to getResourcesForPathname
Nov 15, 2018
b9857c6
Revert "Fetch resources the same way in enqueue to getResourcesForPat…
Nov 15, 2018
6d6762c
Refactor prefetching so we can detect success
Nov 15, 2018
b7f8b83
Move catch to prevent onPostPrefetchPathname after failure
Nov 15, 2018
37034bc
Revert "Move catch to prevent onPostPrefetchPathname after failure"
Nov 15, 2018
0dcface
Merge upstream/master into hybrid-offline-shell
Nov 15, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 39 additions & 13 deletions packages/gatsby-plugin-offline/src/gatsby-browser.js
@@ -1,23 +1,12 @@
exports.registerServiceWorker = () => true

let swNotInstalled = true
const prefetchedPathnames = []

exports.onPostPrefetchPathname = ({ pathname }) => {
// if SW is not installed, we need to record any prefetches
// that happen so we can then add them to SW cache once installed
if (swNotInstalled && `serviceWorker` in navigator) {
prefetchedPathnames.push(pathname)
}
}
const whitelistedPathnames = []

exports.onServiceWorkerActive = ({
getResourceURLsForPathname,
serviceWorker,
}) => {
// stop recording prefetch events
swNotInstalled = false

// grab nodes from head of document
const nodes = document.querySelectorAll(`
head > script[src],
Expand All @@ -41,7 +30,44 @@ exports.onServiceWorkerActive = ({
)

serviceWorker.active.postMessage({
api: `gatsby-runtime-cache`,
gatsbyApi: `runtimeCache`,
resources: [...resources, ...prefetchedResources],
})
serviceWorker.active.postMessage({
gatsbyApi: `whitelistPathnames`,
pathnames: whitelistedPathnames,
})
}

function whitelistPathname(pathname, includesPrefix) {
if (`serviceWorker` in navigator) {
const { serviceWorker } = navigator

if (serviceWorker.controller !== null) {
serviceWorker.controller.postMessage({
gatsbyApi: `whitelistPathnames`,
pathnames: [{ pathname, includesPrefix }],
})
} else {
whitelistedPathnames.push({ pathname, includesPrefix })
}
}
}

exports.onPostPrefetchPathname = ({ pathname }) => {
console.log(`onPostPrefetchPathname ${pathname}`)
whitelistPathname(pathname, false)

// if SW is not installed, we need to record any prefetches
// that happen so we can then add them to SW cache once installed
if (
`serviceWorker` in navigator &&
!(
navigator.serviceWorker.controller !== null &&
navigator.serviceWorker.controller.state === `activated`
)
) {
console.log(`SERVICE WORKER NOT INSTALLED`)
prefetchedPathnames.push(pathname)
}
}
22 changes: 4 additions & 18 deletions packages/gatsby-plugin-offline/src/gatsby-node.js
Expand Up @@ -79,17 +79,6 @@ exports.onPostBuild = (args, pluginOptions) => {
// the default prefix with `pathPrefix`.
"/": `${pathPrefix}/`,
},
navigateFallback: `${pathPrefix}/offline-plugin-app-shell-fallback/index.html`,
// Only match URLs without extensions or the query `no-cache=1`.
// So example.com/about/ will pass but
// example.com/about/?no-cache=1 and
// example.com/cheeseburger.jpg will not.
// We only want the service worker to handle our "clean"
// URLs and not any files hosted on the site.
//
// Regex based on http://stackoverflow.com/a/18017805
navigateFallbackWhitelist: [/^([^.?]*|[^?]*\.([^.?]{5,}|html))(\?.*)?$/],
navigateFallbackBlacklist: [/\?(.+&)?no-cache=1$/],
cacheId: `gatsby-plugin-offline`,
// Don't cache-bust JS or CSS files, and anything in the static directory
dontCacheBustUrlsMatching: /(.*\.js$|.*\.css$|\/static\/)/,
Expand All @@ -99,11 +88,6 @@ exports.onPostBuild = (args, pluginOptions) => {
urlPattern: /\.(?:png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/,
handler: `staleWhileRevalidate`,
},
{
// Use the Network First handler for external resources
urlPattern: /^https?:/,
handler: `networkFirst`,
},
],
skipWaiting: true,
clientsClaim: true,
Expand All @@ -121,9 +105,11 @@ exports.onPostBuild = (args, pluginOptions) => {
.then(({ count, size, warnings }) => {
if (warnings) warnings.forEach(warning => console.warn(warning))

const swAppend = fs.readFileSync(`${__dirname}/sw-append.js`)
fs.appendFileSync(`public/sw.js`, swAppend)
const swAppend = fs
.readFileSync(`${__dirname}/sw-append.js`, `utf8`)
.replace(/%pathPrefix%/g, pathPrefix)

fs.appendFileSync(`public/sw.js`, swAppend)
console.log(
`Generated ${swDest}, which will precache ${count} files, totaling ${size} bytes.`
)
Expand Down
105 changes: 100 additions & 5 deletions packages/gatsby-plugin-offline/src/sw-append.js
@@ -1,8 +1,79 @@
/* global workbox */
/* global importScripts, workbox, idbKeyval */

self.addEventListener(`message`, event => {
const { api } = event.data
if (api === `gatsby-runtime-cache`) {
importScripts(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we were gonna go a configurable whitelist approach instead of the indexeddb approach? I'm fine with either, just want to set expectations so I'm aware.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that at first (with the first two commits of this PR) but realised it wouldn't be a great solution given that some sites might depend on the existing functionality - this is a much more reliable solution since it only whitelists pages whose resources have successfully downloaded, so doesn't e.g. prevent Netlify CMS from working.

If we make assumptions about the default whitelist then it would make things worse for people who put Gatsby pages in places like /admin (maybe they're making a custom admin panel), since these pages wouldn't work offline.

Also, just to clarify in case there's any confusion - the original idea was to use the IDB to replace ?no-cache=1 by temporarily blacklisting pages which are detected to not be Gatsby. While this approach uses IDB, it works in the opposite way by only whitelisting pages on which Gatsby is detected, since this is easier to do and safer. (Sorry if you were already aware of that!)

`https://cdn.jsdelivr.net/npm/idb-keyval@3/dist/idb-keyval-iife.min.js`
)

const WHITELIST_KEY = `custom-navigation-whitelist`

const navigationRoute = new workbox.routing.NavigationRoute(({ event }) => {
const { pathname } = new URL(event.request.url)
console.log(`handling ${pathname}`)

return idbKeyval.get(WHITELIST_KEY).then((customWhitelist = []) => {
// Respond with the offline shell if we match the custom whitelist
if (customWhitelist.includes(pathname)) {
const offlineShell = `%pathPrefix%/offline-plugin-app-shell-fallback/index.html`
const cacheName = workbox.core.cacheNames.precache

console.log(`serving ${offlineShell} for ${pathname}`)
return caches.match(offlineShell, { cacheName })
}

console.log(`fetching ${pathname} from network`)
return fetch(event.request)
})
})

workbox.routing.registerRoute(navigationRoute)

// Handle any other requests with Network First, e.g. 3rd party resources.
// This needs to be done last, otherwise it will prevent the custom navigation
// route from working (and any other rules).
workbox.routing.registerRoute(
/^https?:/,
workbox.strategies.networkFirst(),
`GET`
)

let updatingWhitelist = null

function rawWhitelistPathnames(pathnames) {
if (updatingWhitelist !== null) {
// Prevent the whitelist from being updated twice at the same time
return updatingWhitelist.then(() => rawWhitelistPathnames(pathnames))
}

updatingWhitelist = idbKeyval
.get(WHITELIST_KEY)
.then((customWhitelist = []) => {
pathnames.forEach(pathname => {
if (!customWhitelist.includes(pathname)) customWhitelist.push(pathname)
})

return idbKeyval.set(WHITELIST_KEY, customWhitelist)
})
.then(() => {
updatingWhitelist = null
})

return updatingWhitelist
}

function rawResetWhitelist() {
if (updatingWhitelist !== null) {
return updatingWhitelist.then(() => rawResetWhitelist())
}

updatingWhitelist = idbKeyval.set(WHITELIST_KEY, []).then(() => {
updatingWhitelist = null
})

return updatingWhitelist
}

const messageApi = {
runtimeCache(event) {
const { resources } = event.data
const cacheName = workbox.core.cacheNames.runtime

Expand All @@ -25,5 +96,29 @@ self.addEventListener(`message`, event => {
)
)
)
}
},

whitelistPathnames(event) {
let { pathnames } = event.data

pathnames = pathnames.map(({ pathname, includesPrefix }) => {
if (!includesPrefix) {
return `%pathPrefix%${pathname}`
} else {
return pathname
}
})

console.log(`setting whitelist`)
event.waitUntil(rawWhitelistPathnames(pathnames))
},

resetWhitelist(event) {
event.waitUntil(rawResetWhitelist())
},
}

self.addEventListener(`message`, event => {
const { gatsbyApi } = event.data
if (gatsbyApi) messageApi[gatsbyApi](event)
})
8 changes: 1 addition & 7 deletions packages/gatsby/cache-dir/ensure-resources.js
Expand Up @@ -2,7 +2,6 @@ import React from "react"
import PropTypes from "prop-types"
import loader from "./loader"
import shallowCompare from "shallow-compare"
import { getRedirectUrl } from "./load-directly-or-404"

// Pass pathname in as prop.
// component will try fetching resources. If they exist,
Expand Down Expand Up @@ -94,16 +93,11 @@ class EnsureResources extends React.Component {
}

render() {
// This should only occur if there's no custom 404 page
if (
process.env.NODE_ENV === `production` &&
!(this.state.pageResources && this.state.pageResources.json)
) {
// This should only occur if there's no custom 404 page
const url = getRedirectUrl(this.state.location.href)
if (url) {
window.location.replace(url)
}

return null
}

Expand Down
72 changes: 0 additions & 72 deletions packages/gatsby/cache-dir/load-directly-or-404.js

This file was deleted.

18 changes: 9 additions & 9 deletions packages/gatsby/cache-dir/navigation.js
Expand Up @@ -6,7 +6,6 @@ import { apiRunner } from "./api-runner-browser"
import emitter from "./emitter"
import { navigate as reachNavigate } from "@reach/router"
import parsePath from "./parse-path"
import loadDirectlyOr404 from "./load-directly-or-404"

// Convert to a map for faster lookup in maybeRedirect()
const redirectMap = redirects.reduce((map, redirect) => {
Expand Down Expand Up @@ -66,8 +65,12 @@ const navigate = (to, options = {}) => {
pathname = parsePath(to).pathname
}

// If we had a service worker update, no matter the path, reload window
// If we had a service worker update, no matter the path, reload window and
// reset the pathname whitelist
if (window.GATSBY_SW_UPDATED) {
const { controller } = navigator.serviceWorker
controller.postMessage({ gatsbyApi: `resetWhitelist` })

window.location = pathname
return
}
Expand All @@ -82,14 +85,11 @@ const navigate = (to, options = {}) => {
}, 1000)

loader.getResourcesForPathname(pathname).then(pageResources => {
if (
(!pageResources || pageResources.page.path === `/404.html`) &&
process.env.NODE_ENV === `production`
) {
clearTimeout(timeoutId)
loadDirectlyOr404(pageResources, to).then(() =>
if (!pageResources && process.env.NODE_ENV === `production`) {
loader.getResourcesForPathname(`/404.html`).then(() => {
clearTimeout(timeoutId)
reachNavigate(to, options)
)
})
} else {
reachNavigate(to, options)
clearTimeout(timeoutId)
Expand Down