Replace AppCache with a service worker

This commit is contained in:
Jasper van Merle 2019-07-07 00:55:58 +02:00
parent 5edbb16c1b
commit 8ed1f4ace1
17 changed files with 144 additions and 144 deletions

View file

@ -156,7 +156,7 @@ Contributions are welcome. Please read the [contributing guidelines](./.github/C
* [Doc Browser](https://github.com/qwfy/doc-browser) is a native Linux app that supports DevDocs docsets
* [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated in headerbar
* [macOS Application](https://github.com/dteoh/devdocs-macos)
* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView with AppCache enabled
* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView
## Copyright / License

View file

@ -13,7 +13,7 @@
@el = $('._app')
@localStorage = new LocalStorageStore
@appCache = new app.AppCache if app.AppCache.isEnabled()
@serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled()
@settings = new app.Settings
@db = new app.DB()
@ -149,7 +149,7 @@
saveDocs: ->
@settings.setDocs(doc.slug for doc in @docs.all())
@db.migrate()
@appCache?.updateInBackground()
@serviceWorker?.updateInBackground()
welcomeBack: ->
visitCount = @settings.get('count')
@ -169,14 +169,14 @@
reload: ->
@docs.clearCache()
@disabledDocs.clearCache()
if @appCache then @appCache.reload() else @reboot()
if @serviceWorker then @serviceWorker.reload() else @reboot()
return
reset: ->
@localStorage.reset()
@settings.reset()
@db?.reset()
@appCache?.update()
@serviceWorker?.update()
window.location = '/'
return
@ -195,9 +195,9 @@
return
indexHost: ->
# Can't load the index files from the host/CDN when applicationCache is
# Can't load the index files from the host/CDN when service worker is
# enabled because it doesn't support caching URLs that use CORS.
@config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_origin']
@config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin']
onBootError: (args...) ->
@trigger 'bootError'

View file

@ -1,42 +0,0 @@
class app.AppCache
$.extend @prototype, Events
@isEnabled: ->
try
applicationCache and applicationCache.status isnt applicationCache.UNCACHED
catch
constructor: ->
@cache = applicationCache
@notifyUpdate = true
@onUpdateReady() if @cache.status is @cache.UPDATEREADY
$.on @cache, 'progress', @onProgress
$.on @cache, 'updateready', @onUpdateReady
update: ->
@notifyUpdate = true
@notifyProgress = true
try @cache.update() catch
return
updateInBackground: ->
@notifyUpdate = false
@notifyProgress = false
try @cache.update() catch
return
reload: ->
$.on @cache, 'updateready noupdate error', -> app.reboot()
@notifyUpdate = false
@notifyProgress = true
try @cache.update() catch
return
onProgress: (event) =>
@trigger 'progress', event if @notifyProgress
return
onUpdateReady: =>
@trigger 'updateready' if @notifyUpdate
return

View file

@ -13,3 +13,4 @@ app.config =
version: <%= Time.now.to_i %>
release: <%= Time.now.utc.httpdate.to_json %>
mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css'
service_worker_path: '/service-worker.js'

View file

@ -0,0 +1,55 @@
class app.ServiceWorker
$.extend @prototype, Events
@isEnabled: ->
!!navigator.serviceWorker
constructor: ->
@registration = null
@installingRegistration = null
@notifyUpdate = true
navigator.serviceWorker.register(app.config.service_worker_path, {scope: '/'})
.then((registration) => @updateRegistration(registration))
.catch((error) -> console.error 'Could not register service worker:', error)
update: ->
return unless @registration
@notifyUpdate = true
return @doUpdate()
updateInBackground: ->
return unless @registration
@notifyUpdate = false
return @doUpdate()
reload: ->
return @updateInBackground().then(() -> app.reboot())
doUpdate: ->
return @registration.update().catch(->)
updateRegistration: (registration) ->
$.off @registration, 'updatefound', @onUpdateFound if @registration
$.off @installingRegistration, 'statechange', @onStateChange if @installingRegistration
@registration = registration
@installingRegistration = null
$.on @registration, 'updatefound', @onUpdateFound
return
onUpdateFound: () =>
@installingRegistration = @registration.installing
$.on @installingRegistration, 'statechange', @onStateChange
return
onStateChange: () =>
if @installingRegistration.state == 'installed' and navigator.serviceWorker.controller
@updateRegistration(@installingRegistration)
@onUpdateReady()
return
onUpdateReady: ->
@trigger 'updateready' if @notifyUpdate
return

View file

@ -3,13 +3,13 @@ class app.UpdateChecker
@lastCheck = Date.now()
$.on window, 'focus', @onFocus
app.appCache.on 'updateready', @onUpdateReady if app.appCache
app.serviceWorker.on 'updateready', @onUpdateReady if app.serviceWorker
setTimeout @checkDocs, 0
check: ->
if app.appCache
app.appCache.update()
if app.serviceWorker
app.serviceWorker.update()
else
ajax
url: $('script[src*="application"]').getAttribute('src')

View file

@ -26,7 +26,7 @@ app.templates.offlinePage = (docs) -> """
<dl>
<dt>How does this work?
<dd>Each page is cached as a key-value pair in <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> (downloaded from a single file).<br>
The app also uses <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache">AppCache</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API">localStorage</a> to cache the assets and index files.
The app also uses <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers">Service Workers</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API">localStorage</a> to cache the assets and index files.
<dt>Can I close the tab/browser?
<dd>#{canICloseTheTab()}
<dt>What if I don't update a documentation?
@ -41,10 +41,10 @@ app.templates.offlinePage = (docs) -> """
"""
canICloseTheTab = ->
if app.AppCache.isEnabled()
if app.ServiceWorker.isEnabled()
""" Yes! Even offline, you can open a new tab, go to <a href="//devdocs.io">devdocs.io</a>, and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). """
else
""" No. AppCache isn't available in your browser (or is disabled), so loading <a href="//devdocs.io">devdocs.io</a> offline won't work.<br>
""" No. Service Workers aren't available in your browser (or are disabled), so loading <a href="//devdocs.io">devdocs.io</a> offline won't work.<br>
The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """
app.templates.offlineDoc = (doc, status) ->

View file

@ -123,7 +123,7 @@ class app.views.EntryPage extends app.View
@render @tmpl('pageLoadError')
@resetClass()
@addClass @constructor.errorClass
app.appCache?.update()
app.serviceWorker?.update()
return
cache: ->

View file

@ -22,12 +22,10 @@ class app.views.SettingsPage extends app.View
toggleDark: (enable) ->
app.settings.set('dark', !!enable)
app.appCache?.updateInBackground()
return
toggleLayout: (layout, enable) ->
app.settings.setLayout(layout, enable)
app.appCache?.updateInBackground()
return
toggleSmoothScroll: (enable) ->

View file

@ -26,9 +26,7 @@ class app.views.Resizer extends app.View
newSize = "#{value}px"
@style.innerHTML = @style.innerHTML.replace(new RegExp(@size, 'g'), newSize)
@size = newSize
if save
app.settings.setSize(value)
app.appCache?.updateInBackground()
app.settings.setSize(value) if save
return
onDragStart: (event) =>

View file

@ -25,7 +25,6 @@ class app.views.Settings extends app.View
if super
@render()
document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)
app.appCache?.on 'progress', @onAppCacheProgress
return
deactivate: ->
@ -33,7 +32,6 @@ class app.views.Settings extends app.View
@resetClass()
@docPicker.detach()
document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)
app.appCache?.off 'progress', @onAppCacheProgress
return
render: ->
@ -52,7 +50,7 @@ class app.views.Settings extends app.View
docs = @docPicker.getSelectedDocs()
app.settings.setDocs(docs)
@saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'
@saveBtn.textContent = 'Saving\u2026'
disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
disabledDocs.uninstall ->
app.db.migrate()
@ -83,9 +81,3 @@ class app.views.Settings extends app.View
$.stopEvent(event)
app.router.show '/'
return
onAppCacheProgress: (event) =>
if event.lengthComputable
percentage = Math.round event.loaded * 100 / event.total
@saveBtn.textContent = "Downloading\u2026 (#{percentage}%)"
return

View file

@ -220,7 +220,7 @@ class App < Sinatra::Application
app_theme == 'dark'
end
def redirect_via_js(path) # courtesy of HTML5 App Cache
def redirect_via_js(path)
response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'
redirect '/', 302
end
@ -243,15 +243,15 @@ class App < Sinatra::Application
end
end
get '/manifest.appcache' do
content_type 'text/cache-manifest'
get '/service-worker.js' do
content_type 'application/javascript'
expires 0, :'no-cache'
erb :manifest
erb :'service-worker.js'
end
get '/' do
return redirect "/#q=#{params[:q]}" if params[:q]
return redirect '/' unless request.query_string.empty? # courtesy of HTML5 App Cache
return redirect '/' unless request.query_string.empty?
response.headers['Content-Security-Policy'] = settings.csp if settings.csp
erb :index
end

View file

@ -106,58 +106,6 @@ class AppTest < MiniTest::Spec
end
end
describe "/manifest.appcache" do
it "works" do
get '/manifest.appcache'
assert last_response.ok?
end
it "works with cookie" do
set_cookie('docs=css/html~5')
get '/manifest.appcache'
assert last_response.ok?
assert_includes last_response.body, '/css/index.json?1420139788'
assert_includes last_response.body, '/html~5/index.json?1420139791'
end
it "ignores invalid docs in the cookie" do
set_cookie('docs=foo')
get '/manifest.appcache'
assert last_response.ok?
refute_includes last_response.body, 'foo'
end
it "has the word 'default' when no 'dark' cookie is set" do
get '/manifest.appcache'
assert_includes last_response.body, '# default'
refute_includes last_response.body, '# dark'
end
it "has the word 'dark' when the cookie is set" do
set_cookie('dark=1')
get '/manifest.appcache'
assert_includes last_response.body, '# dark'
refute_includes last_response.body, '# default'
end
it "sets default size" do
get '/manifest.appcache'
assert_includes last_response.body, '20rem'
end
it "sets size from cookie" do
set_cookie('size=42')
get '/manifest.appcache'
assert_includes last_response.body, '42px'
end
it "sets layout from cookie" do
set_cookie('layout=foo_layout')
get '/manifest.appcache'
assert_includes last_response.body, 'foo_layout'
end
end
describe "/[doc]" do
it "renders when the doc exists and isn't enabled" do
set_cookie('docs=html~5')

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html<%= ' manifest="/manifest.appcache"' if App.production? %> prefix="og: http://ogp.me/ns#" lang="en" class="_booting _theme-<%= app_theme %>">
<html prefix="og: http://ogp.me/ns#" lang="en" class="_booting _theme-<%= app_theme %>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no">

View file

@ -1,14 +0,0 @@
CACHE MANIFEST
# <%= app_theme %> <%= app_size %> <%= app_layout %>
CACHE:
/
<%= manifest_asset_urls.join "\n" %>
<%= doc_index_urls.join "\n" %>
NETWORK:
/s/
*
FALLBACK:
/ /

View file

@ -0,0 +1,64 @@
<%# Use the hash of the application.js file as cache name, or 'app' if not running in production %>
<%# This ensures that the cache is always updated if the hash of the application.js file changes %>
const cacheName = '<%= javascript_path('application', asset_host: false).scan(/application-([^\.]+)\.js/).last&.first || 'app' %>';
<%# Paths to cache when the service worker is installed %>
const cachePaths = [
'/',
'/favicon.ico',
'/manifest.json',
'/images/webapp-icon-32.png',
'/images/webapp-icon-60.png',
'/images/webapp-icon-80.png',
'/images/webapp-icon-128.png',
'/images/webapp-icon-256.png',
'/images/webapp-icon-512.png',
'<%= manifest_asset_urls.join "',\n '" %>',
'<%= doc_index_urls.join "',\n '" %>',
];
<%# Set-up the cache %>
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
caches.open(cacheName).then(cache => cache.addAll(cachePaths)),
);
});
<%# Remove old caches %>
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.map(key => {
if (key !== cacheName) {
return caches.delete(key);
}
})
))
);
});
<%# Handle HTTP requests %>
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response;
}
return fetch(event.request)
.catch(err => {
const url = new URL(event.request.url);
<%# Return the index page from the cache if the user is visiting a url like devdocs.io/javascript/global_objects/array/find %>
<%# The index page will make sure the correct documentation or a proper offline page is shown %>
if (url.origin === location.origin && !url.pathname.includes('.')) {
return caches.match('/').then(response => response || err);
}
return err;
});
})
);
});

View file

@ -11,9 +11,9 @@
<p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p>
<ul class="_fail-list">
<li>Recent versions of Firefox, Chrome, or Opera</li>
<li>Safari 9.1+</li>
<li>Safari 11.1+</li>
<li>Edge 16+</li>
<li>iOS 10+</li>
<li>iOS 11.3+</li>
</ul>
<p class="_fail-text">
If you're unable to upgrade, we apologize.