24 Sep 2019
Native experience with Viewer - Build Viewer’s offline workflows into Progessive Web Apps
(It’s highly recommended to read Petr’s Disconnected Workflow before we go into this topic)
Quite a few candidates have been mooted as the potential heir to the traditional desktop/mobile (time indeed flies as we are labelling native apps traditional already) apps and many consider Progressive Web App (or PWA in short) to be the universal future. Today let’s take a brief look into what PWA is and explore the potential of bringing Viewer into the equation to offer users native app experience with pure browser based JavaScript.
Demystifying the Progressive Web App
Simply put, PWA is a web app that is built on modern browser based technologies, behaves like a native mobile app and can accomplish many similar tasks and in certain cases offers a lot more capability, but costs less to develop and build. PWA is easier to maintain in the long run and can be launched as traditional Web App and installed just a URL away to across all devices with the help of a modern browser. And we start off in exactly the same fashion as we do for a traditional web app with our same old JavaScript, HTML, CSS as our fundamental building blocks and go from there to leverage a mighty repertoire of modern browser APIs such as Service Worker, IndexDB, Cache, Storage, Push, Notifications, Messaging, Streams and even WebAssembly to juice up our apps.
To put things into perspective see how a PWA goes head to head with your traditional native apps:
Revisiting the Service Worker
At a PWA’s core is our “new old” friend - Service Worker. Although a PWA runs independently of a browser yet it is implemented with the technologies/capabilities of one so we can full advantage of Service Worker to perform the following important lifecycle and functionality tasks to accomplish our desired native experience:
- Cache application content - offline functionality
- Push notifications
- Background operations - threading/multitasking
Here’s how PWA typically constructs its lifecycle workflows around Service Worker as illustrated below:
But the legend goes far and beyond - we can even tap into the endless potential of WebAssembly to enrich the capacity of our PWA - see a screenshot below of a full fledged (almost) AutoCAD prototype built into a WebAssembly and running entirely in a browser - and we will talk more about taking advantage of WebAssembly to empower Viewer in our upcoming articles so stay tuned:
Sample App: Forge Digital Catalogue
Enough big talk and now let’s get down to work! Here’s our sample Viewer PWA project (basically a curatable catalogue to translate and publish models) looks like:
In our Service Worker script, we implement the Fetch event handler to intercept requests and cache them - read more on this topic in Petr’s article here - but different to Petr’s approach we cache all the contents including model files and even the Viewer library as they are being requested on the fly for the first time (and only) so you won’t need to explicitly specify any URLs to cache, making light work of maintenance and dependency upgrades. Here is a peek of the code:
// src/client/public/registerServiceWorker.js
self.addEventListener('fetch', async event => {
event.respondWith(async function(){
const cachedKeys = Object.keys(cacheKeyMap).filter(k=>loadingKeys.includes(k))
const cacheName = cachedKeys.length? [...cachedKeys, ...alwaysCache].find(k=>cacheKeyMap[k].some(m=>m==event.request.url||(m.test&&m.test(event.request.url)))) : null
if(options.debug) console.log(cacheName,cacheKeyMap,event.request.url)
return cacheName?(await caches.match(event.request) || await fetch(event.request).then(resp=>{const response = resp.clone(); caches.open(cacheName).then(cache=>cache.put(event.request.url, response));return resp})):await fetch(event.request)
}())
})
And we can achieve the on the fly caching of Viewer itself by deferring the load the Viewer’s scripts till just before the models and yes we use the dynamic script tabs for this purpose:
// src/client/components/ForgeViewer.vue
initViewer(options, forceLoad){
return Promise.all([new Promise(res=>{
if(!forceLoad&&typeof Autodesk == 'object'&&Autodesk.Viewing)res()
else{
const link = document.createElement('link')
link.rel = 'stylesheet'
link.type = 'text/css'
link.href = options.viewerCSSURL
link.onload = ()=>res()
document.head.append(link)
}
}), new Promise(res=>{
if(!forceLoad&&typeof Autodesk == 'object'&&Autodesk.Viewing)res()
else{
const script = document.createElement('script')
script.onload = ()=>res()
script.src = options.viewerScriptURL
document.head.append(script)
}
})]).then(()=>{
Autodesk.Viewing.Initializer(options, this.onInitialized)
this.$store.dispatch('setShowCatalogTree', true)
})
}
But what year it is! You can abstract cache routing and do much more with less with the help of the mighty Workbox library - with it our caching handler simply trickles down to something like:
workbox.routing.registerRoute(
/regex\/to\/cache\/path/,
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'app-content',
}),
);
On top of own script is the official Vue PWA plugin for us to happily delegate the implementation of the Service Worker lifecycle for rest of the app to:
// vue.config.js
module.exports = {
// ...other vue-cli plugin options...
pwa: {
// configure the workbox plugin
workboxPluginMode: 'InjectManifest',
workboxOptions: {
// swSrc is required in InjectManifest mode.
swSrc: 'public/service-worker.js',
// ...other Workbox options...
}
}
}
In the manifest we have meta data to browser identify our app as PWA as well as to customize our icon, splash screen etc - if you have done any mobile app dev before the below would look fairly familiar:
{
"name": "forge-digital-catalog-app",
"short_name": "forge-digital-catalog-app",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-60x60.png",
"sizes": "60x60",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-76x76.png",
"sizes": "76x76",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-120x120.png",
"sizes": "120x120",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-180x180.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "./img/icons/msapplication-icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "./img/icons/mstile-150x150.png",
"sizes": "150x150",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}
(And you can find the full source code in our official Github repo here!)
Now that we are all set, let’s install the app and throw it into action! Simply serve the content with your favorite web server and hop on in a modern browser - you will be prompted to add their Progressive Web Apps to your homescreen if you’re on mobile (or you can manually do it via Chrome’s add to homescreen option). Installing a Progressive Web App on your desktop might require you to hit the “+” sign that appears in the address bar on Chrome. Desktop users might have to visit the mobile version of a website in order to see the install prompt though. Here’s how it goes on my iPad:
That’s all we have time for today! Stay tuned for our next article with more tricks up the sleeve empowering Viewer with Service Worker. Until next time!