History

Replace AppCache with a service worker

Jasper van Merle committed 7 years ago

Commit 8ed1f4a1 parent 5edbb1617 files · +144 −144Browse files

Unified · Split

17 files
  1. README.md +1 −1
  2. assets/javascripts/app/app.coffee +6 −6
  3. assets/javascripts/app/appcache.coffee +0 −42
  4. assets/javascripts/app/config.coffee.erb +1 −0
  5. assets/javascripts/app/serviceworker.coffee +55 −0
  6. assets/javascripts/app/update_checker.coffee +3 −3
  7. assets/javascripts/templates/pages/offline_tmpl.coffee +3 −3
  8. assets/javascripts/views/content/entry_page.coffee +1 −1
  9. assets/javascripts/views/content/settings_page.coffee +0 −2
  10. assets/javascripts/views/layout/resizer.coffee +1 −3
  11. assets/javascripts/views/layout/settings.coffee +1 −9
  12. lib/app.rb +5 −5
  13. test/app_test.rb +0 −52
  14. views/index.erb +1 −1
  15. views/manifest.erb +0 −14
  16. views/service-worker.js.erb +64 −0
  17. views/unsupported.erb +2 −2
README.md+1−1
@@ -156,7 +156,7 @@ Contributions are welcome. Please read the [contributing guidelines](./.github/C · 155 unmodified lines
156156 * [Doc Browser](https://github.com/qwfy/doc-browser) is a native Linux app that supports DevDocs docsets
157157 * [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated in headerbar
158158 * [macOS Application](https://github.com/dteoh/devdocs-macos)
159−* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView with AppCache enabled
159+* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView
160160
161161 ## Copyright / License
162162
assets/javascripts/app/app.coffee+6−6
@@ -13,7 +13,7 @@ · 12 unmodified lines
1313
1414 @el = $('._app')
1515 @localStorage = new LocalStorageStore
16− @appCache = new app.AppCache if app.AppCache.isEnabled()
16+ @serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled()
1717 @settings = new app.Settings
1818 @db = new app.DB()
1919
@@ -149,7 +149,7 @@ · 129 unmodified lines
149149 saveDocs: ->
150150 @settings.setDocs(doc.slug for doc in @docs.all())
151151 @db.migrate()
152− @appCache?.updateInBackground()
152+ @serviceWorker?.updateInBackground()
153153
154154 welcomeBack: ->
155155 visitCount = @settings.get('count')
@@ -169,14 +169,14 @@ · 13 unmodified lines
169169 reload: ->
170170 @docs.clearCache()
171171 @disabledDocs.clearCache()
172− if @appCache then @appCache.reload() else @reboot()
172+ if @serviceWorker then @serviceWorker.reload() else @reboot()
173173 return
174174
175175 reset: ->
176176 @localStorage.reset()
177177 @settings.reset()
178178 @db?.reset()
179− @appCache?.update()
179+ @serviceWorker?.update()
180180 window.location = '/'
181181 return
182182
@@ -195,9 +195,9 @@ · 12 unmodified lines
195195 return
196196
197197 indexHost: ->
198− # Can't load the index files from the host/CDN when applicationCache is
198+ # Can't load the index files from the host/CDN when service worker is
199199 # enabled because it doesn't support caching URLs that use CORS.
200− @config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_origin']
200+ @config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin']
201201
202202 onBootError: (args...) ->
203203 @trigger 'bootError'
assets/javascripts/app/appcache.coffee+0−42
@@ -1,42 +0,0 @@
1−class app.AppCache
2− $.extend @prototype, Events
3−
4− @isEnabled: ->
5− try
6− applicationCache and applicationCache.status isnt applicationCache.UNCACHED
7− catch
8−
9− constructor: ->
10− @cache = applicationCache
11− @notifyUpdate = true
12− @onUpdateReady() if @cache.status is @cache.UPDATEREADY
13−
14− $.on @cache, 'progress', @onProgress
15− $.on @cache, 'updateready', @onUpdateReady
16−
17− update: ->
18− @notifyUpdate = true
19− @notifyProgress = true
20− try @cache.update() catch
21− return
22−
23− updateInBackground: ->
24− @notifyUpdate = false
25− @notifyProgress = false
26− try @cache.update() catch
27− return
28−
29− reload: ->
30− $.on @cache, 'updateready noupdate error', -> app.reboot()
31− @notifyUpdate = false
32− @notifyProgress = true
33− try @cache.update() catch
34− return
35−
36− onProgress: (event) =>
37− @trigger 'progress', event if @notifyProgress
38− return
39−
40− onUpdateReady: =>
41− @trigger 'updateready' if @notifyUpdate
42− return
assets/javascripts/app/config.coffee.erb+1−0
@@ -13,3 +13,4 @@ app.config = · 12 unmodified lines
1313 version: <%= Time.now.to_i %>
1414 release: <%= Time.now.utc.httpdate.to_json %>
1515 mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css'
16+ service_worker_path: '/service-worker.js'
assets/javascripts/app/serviceworker.coffee+55−0
@@ -0,0 +1,55 @@
1+class app.ServiceWorker
2+ $.extend @prototype, Events
3+
4+ @isEnabled: ->
5+ !!navigator.serviceWorker
6+
7+ constructor: ->
8+ @registration = null
9+ @installingRegistration = null
10+ @notifyUpdate = true
11+
12+ navigator.serviceWorker.register(app.config.service_worker_path, {scope: '/'})
13+ .then((registration) => @updateRegistration(registration))
14+ .catch((error) -> console.error 'Could not register service worker:', error)
15+
16+ update: ->
17+ return unless @registration
18+ @notifyUpdate = true
19+ return @doUpdate()
20+
21+ updateInBackground: ->
22+ return unless @registration
23+ @notifyUpdate = false
24+ return @doUpdate()
25+
26+ reload: ->
27+ return @updateInBackground().then(() -> app.reboot())
28+
29+ doUpdate: ->
30+ return @registration.update().catch(->)
31+
32+ updateRegistration: (registration) ->
33+ $.off @registration, 'updatefound', @onUpdateFound if @registration
34+ $.off @installingRegistration, 'statechange', @onStateChange if @installingRegistration
35+
36+ @registration = registration
37+ @installingRegistration = null
38+
39+ $.on @registration, 'updatefound', @onUpdateFound
40+ return
41+
42+ onUpdateFound: () =>
43+ @installingRegistration = @registration.installing
44+ $.on @installingRegistration, 'statechange', @onStateChange
45+ return
46+
47+ onStateChange: () =>
48+ if @installingRegistration.state == 'installed' and navigator.serviceWorker.controller
49+ @updateRegistration(@installingRegistration)
50+ @onUpdateReady()
51+ return
52+
53+ onUpdateReady: ->
54+ @trigger 'updateready' if @notifyUpdate
55+ return
assets/javascripts/app/update_checker.coffee+3−3
@@ -3,13 +3,13 @@ class app.UpdateChecker · 2 unmodified lines
33 @lastCheck = Date.now()
44
55 $.on window, 'focus', @onFocus
6− app.appCache.on 'updateready', @onUpdateReady if app.appCache
6+ app.serviceWorker.on 'updateready', @onUpdateReady if app.serviceWorker
77
88 setTimeout @checkDocs, 0
99
1010 check: ->
11− if app.appCache
12− app.appCache.update()
11+ if app.serviceWorker
12+ app.serviceWorker.update()
1313 else
1414 ajax
1515 url: $('script[src*="application"]').getAttribute('src')
assets/javascripts/templates/pages/offline_tmpl.coffee+3−3
@@ -26,7 +26,7 @@ app.templates.offlinePage = (docs) -> """ · 25 unmodified lines
2626 <dl>
2727 <dt>How does this work?
2828 <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>
29− 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.
29+ 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.
3030 <dt>Can I close the tab/browser?
3131 <dd>#{canICloseTheTab()}
3232 <dt>What if I don't update a documentation?
@@ -41,10 +41,10 @@ app.templates.offlinePage = (docs) -> """ · 8 unmodified lines
4141 """
4242
4343 canICloseTheTab = ->
44− if app.AppCache.isEnabled()
44+ if app.ServiceWorker.isEnabled()
4545 """ 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). """
4646 else
47− """ 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>
47+ """ 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>
4848 The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """
4949
5050 app.templates.offlineDoc = (doc, status) ->
assets/javascripts/views/content/entry_page.coffee+1−1
@@ -123,7 +123,7 @@ class app.views.EntryPage extends app.View · 122 unmodified lines
123123 @render @tmpl('pageLoadError')
124124 @resetClass()
125125 @addClass @constructor.errorClass
126− app.appCache?.update()
126+ app.serviceWorker?.update()
127127 return
128128
129129 cache: ->
assets/javascripts/views/content/settings_page.coffee+0−2
@@ -22,12 +22,10 @@ class app.views.SettingsPage extends app.View · 21 unmodified lines
2222
2323 toggleDark: (enable) ->
2424 app.settings.set('dark', !!enable)
25− app.appCache?.updateInBackground()
2625 return
2726
2827 toggleLayout: (layout, enable) ->
2928 app.settings.setLayout(layout, enable)
30− app.appCache?.updateInBackground()
3129 return
3230
3331 toggleSmoothScroll: (enable) ->
assets/javascripts/views/layout/resizer.coffee+1−3
@@ -26,9 +26,7 @@ class app.views.Resizer extends app.View · 25 unmodified lines
2626 newSize = "#{value}px"
2727 @style.innerHTML = @style.innerHTML.replace(new RegExp(@size, 'g'), newSize)
2828 @size = newSize
29− if save
30− app.settings.setSize(value)
31− app.appCache?.updateInBackground()
29+ app.settings.setSize(value) if save
3230 return
3331
3432 onDragStart: (event) =>
assets/javascripts/views/layout/settings.coffee+1−9
@@ -25,7 +25,6 @@ class app.views.Settings extends app.View · 24 unmodified lines
2525 if super
2626 @render()
2727 document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)
28− app.appCache?.on 'progress', @onAppCacheProgress
2928 return
3029
3130 deactivate: ->
@@ -33,7 +32,6 @@ class app.views.Settings extends app.View · 1 unmodified line
3332 @resetClass()
3433 @docPicker.detach()
3534 document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)
36− app.appCache?.off 'progress', @onAppCacheProgress
3735 return
3836
3937 render: ->
@@ -52,7 +50,7 @@ class app.views.Settings extends app.View · 12 unmodified lines
5250 docs = @docPicker.getSelectedDocs()
5351 app.settings.setDocs(docs)
5452
55− @saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'
53+ @saveBtn.textContent = 'Saving\u2026'
5654 disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
5755 disabledDocs.uninstall ->
5856 app.db.migrate()
@@ -83,9 +81,3 @@ class app.views.Settings extends app.View · 24 unmodified lines
8381 $.stopEvent(event)
8482 app.router.show '/'
8583 return
86−
87− onAppCacheProgress: (event) =>
88− if event.lengthComputable
89− percentage = Math.round event.loaded * 100 / event.total
90− @saveBtn.textContent = "Downloading\u2026 (#{percentage}%)"
91− return
lib/app.rb+5−5
@@ -220,7 +220,7 @@ class App < Sinatra::Application · 219 unmodified lines
220220 app_theme == 'dark'
221221 end
222222
223− def redirect_via_js(path) # courtesy of HTML5 App Cache
223+ def redirect_via_js(path)
224224 response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'
225225 redirect '/', 302
226226 end
@@ -243,15 +243,15 @@ class App < Sinatra::Application · 16 unmodified lines
243243 end
244244 end
245245
246− get '/manifest.appcache' do
247− content_type 'text/cache-manifest'
246+ get '/service-worker.js' do
247+ content_type 'application/javascript'
248248 expires 0, :'no-cache'
249− erb :manifest
249+ erb :'service-worker.js'
250250 end
251251
252252 get '/' do
253253 return redirect "/#q=#{params[:q]}" if params[:q]
254− return redirect '/' unless request.query_string.empty? # courtesy of HTML5 App Cache
254+ return redirect '/' unless request.query_string.empty?
255255 response.headers['Content-Security-Policy'] = settings.csp if settings.csp
256256 erb :index
257257 end
test/app_test.rb+0−52
@@ -106,58 +106,6 @@ class AppTest < MiniTest::Spec · 105 unmodified lines
106106 end
107107 end
108108
109− describe "/manifest.appcache" do
110− it "works" do
111− get '/manifest.appcache'
112− assert last_response.ok?
113− end
114−
115− it "works with cookie" do
116− set_cookie('docs=css/html~5')
117− get '/manifest.appcache'
118− assert last_response.ok?
119− assert_includes last_response.body, '/css/index.json?1420139788'
120− assert_includes last_response.body, '/html~5/index.json?1420139791'
121− end
122−
123− it "ignores invalid docs in the cookie" do
124− set_cookie('docs=foo')
125− get '/manifest.appcache'
126− assert last_response.ok?
127− refute_includes last_response.body, 'foo'
128− end
129−
130− it "has the word 'default' when no 'dark' cookie is set" do
131− get '/manifest.appcache'
132− assert_includes last_response.body, '# default'
133− refute_includes last_response.body, '# dark'
134− end
135−
136− it "has the word 'dark' when the cookie is set" do
137− set_cookie('dark=1')
138− get '/manifest.appcache'
139− assert_includes last_response.body, '# dark'
140− refute_includes last_response.body, '# default'
141− end
142−
143− it "sets default size" do
144− get '/manifest.appcache'
145− assert_includes last_response.body, '20rem'
146− end
147−
148− it "sets size from cookie" do
149− set_cookie('size=42')
150− get '/manifest.appcache'
151− assert_includes last_response.body, '42px'
152− end
153−
154− it "sets layout from cookie" do
155− set_cookie('layout=foo_layout')
156− get '/manifest.appcache'
157− assert_includes last_response.body, 'foo_layout'
158− end
159− end
160−
161109 describe "/[doc]" do
162110 it "renders when the doc exists and isn't enabled" do
163111 set_cookie('docs=html~5')
views/index.erb+1−1
@@ -1,5 +1,5 @@
11 <!DOCTYPE html>
2−<html<%= ' manifest="/manifest.appcache"' if App.production? %> prefix="og: http://ogp.me/ns#" lang="en" class="_booting _theme-<%= app_theme %>">
2+<html prefix="og: http://ogp.me/ns#" lang="en" class="_booting _theme-<%= app_theme %>">
33 <head>
44 <meta charset="utf-8">
55 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no">
views/manifest.erb+0−14
@@ -1,14 +0,0 @@
1−CACHE MANIFEST
2−# <%= app_theme %> <%= app_size %> <%= app_layout %>
3−
4−CACHE:
5−/
6−<%= manifest_asset_urls.join "\n" %>
7−<%= doc_index_urls.join "\n" %>
8−
9−NETWORK:
10−/s/
11−*
12−
13−FALLBACK:
14−/ /
views/service-worker.js.erb+64−0
@@ -0,0 +1,64 @@
1+<%# Use the hash of the application.js file as cache name, or 'app' if not running in production %>
2+<%# This ensures that the cache is always updated if the hash of the application.js file changes %>
3+const cacheName = '<%= javascript_path('application', asset_host: false).scan(/application-([^\.]+)\.js/).last&.first || 'app' %>';
4+
5+<%# Paths to cache when the service worker is installed %>
6+const cachePaths = [
7+ '/',
8+ '/favicon.ico',
9+ '/manifest.json',
10+ '/images/webapp-icon-32.png',
11+ '/images/webapp-icon-60.png',
12+ '/images/webapp-icon-80.png',
13+ '/images/webapp-icon-128.png',
14+ '/images/webapp-icon-256.png',
15+ '/images/webapp-icon-512.png',
16+ '<%= manifest_asset_urls.join "',\n '" %>',
17+ '<%= doc_index_urls.join "',\n '" %>',
18+];
19+
20+<%# Set-up the cache %>
21+self.addEventListener('install', event => {
22+ self.skipWaiting();
23+
24+ event.waitUntil(
25+ caches.open(cacheName).then(cache => cache.addAll(cachePaths)),
26+ );
27+});
28+
29+<%# Remove old caches %>
30+self.addEventListener('activate', event => {
31+ event.waitUntil(
32+ caches.keys().then(keys => Promise.all(
33+ keys.map(key => {
34+ if (key !== cacheName) {
35+ return caches.delete(key);
36+ }
37+ })
38+ ))
39+ );
40+});
41+
42+<%# Handle HTTP requests %>
43+self.addEventListener('fetch', event => {
44+ event.respondWith(
45+ caches.match(event.request).then(response => {
46+ if (response) {
47+ return response;
48+ }
49+
50+ return fetch(event.request)
51+ .catch(err => {
52+ const url = new URL(event.request.url);
53+
54+ <%# Return the index page from the cache if the user is visiting a url like devdocs.io/javascript/global_objects/array/find %>
55+ <%# The index page will make sure the correct documentation or a proper offline page is shown %>
56+ if (url.origin === location.origin && !url.pathname.includes('.')) {
57+ return caches.match('/').then(response => response || err);
58+ }
59+
60+ return err;
61+ });
62+ })
63+ );
64+});
views/unsupported.erb+2−2
@@ -11,9 +11,9 @@ · 10 unmodified lines
1111 <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p>
1212 <ul class="_fail-list">
1313 <li>Recent versions of Firefox, Chrome, or Opera</li>
14− <li>Safari 9.1+</li>
14+ <li>Safari 11.1+</li>
1515 <li>Edge 16+</li>
16− <li>iOS 10+</li>
16+ <li>iOS 11.3+</li>
1717 </ul>
1818 <p class="_fail-text">
1919 If you're unable to upgrade, we apologize.