mirror of
https://github.com/freeCodeCamp/devdocs
synced 2024-11-16 19:48:10 +01:00
Replace AppCache with a service worker
This commit is contained in:
parent
5edbb16c1b
commit
8ed1f4ace1
17 changed files with 144 additions and 144 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
|
55
assets/javascripts/app/serviceworker.coffee
Normal file
55
assets/javascripts/app/serviceworker.coffee
Normal 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
|
|
@ -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')
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
||||
|
|
10
lib/app.rb
10
lib/app.rb
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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:
|
||||
/ /
|
64
views/service-worker.js.erb
Normal file
64
views/service-worker.js.erb
Normal 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;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue