This Starter enables you to build a basic application with PWA capabilities on Enonic XP Platform. It’s using modern tools like Webpack for the build process and Workbox for automatic generation of Service Worker file and dynamic response caching. Simple routing is powered by Enonic Router library.
1.Make sure you have Enonic XP of version 7.0.0 or later installed locally. If not, read here about how to get started
2.While using Enonic CLI to create a new project, choose PWA starter when asked what type of started you want to base your new project on.
3.Once CLI has set up your new project based on PWA Starter, go to the newly created project folder, build and deploy the application:
$ cd mytest
$ enonic project deploy
4.If the build completed without errors, you can now open your app in the browser via http://localhost:8080/webapp/com.company.myapp
(replace com.company.myapp
with whatever name you picked for your app when creating the project with CLI)
We assume that XP service is running on localhost:8000
and your app is called com.company.myapp
as in the example above.
-
Open
http://localhost:8080/webapp/com.company.myapp
in your browser. You should see this:
2.Click the burger icon in the header:
3.This menu showcases different capabilities of the PWA Starter. Read about them below.
Tip
|
Some of the features in the menu are not implemented yet but will be added in future versions. |
Click the "Offline" link in the Starter menu. That will open a new page looking like this:
This page shows that it’s possible to easily determine online/offline status in the browser and show different content on the page based on that. Go offline by unplugging network cable or turning off Wi-Fi. Now the page should change and look like this:
If you now - while staying offline - go to the main page, you will see additional note under the welcome text
As you can see, the Starter can track its online/offline status and change content of its pages accordingly.
The Starter is using Webpack to build all LESS files into one CSS bundle main.css
and all Javascript assets into one main JS bundle
app-bundle.js
(some pages are using their own JS bundles in addition to the main one). The Workbox plugin is used by Webpack to automatically generate a template for the Service Worker (sw.js
) based
on a predefined file (workbox-sw.js
). Final Service Worker file will be rendered on-the-fly by Enonic Router lib by intercepting
a call to /sw.js
file in the site root.
assets/js/app.js
is used as entry point for the Webpack builder, so make sure you add the first level of dependencies to this file (using require
).
For example, if assets/js/app.js
is using a LESS file called styles.less
, add the following line to the app.js
:
require('../css/styles.less');
Same with JS-dependencies. For example, to include a file called new.js
from the same js
folder add this line to app.js
:
require('../js/new.js');
You can then require other LESS or JS files directly from new.js
effectively building a chain of dependencies that Webpack will resolve during the build.
As mentioned before, the build process will bundle all LESS and JS assets into bundle.css
and bundle.js
files in the precache
folder which can then
be referenced directly from the main.html
page.
When the application is launched for the first time, Service Worker will attempt to precache the Application Shell - the minimum set of assets
required for the application to continue working while offline. As described above, two files - bundle.css
and bundle.js
- generated by the build
process will be precached by default. In addition, you may add any files to the assets/precache
folder and they will automatically be added
to the list of precached assets. Typically that would be images, icons, font files, 3rd-party stylesheets and Javascript libraries etc. - assets that are
considered static to current version of the application.
workbox.core.setCacheNameDetails({
prefix: 'enonic-pwa-starter',
suffix: '{{appVersion}}',
precache: 'precache',
runtime: 'runtime'
});
workbox.core.clientsClaim();
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
The last line above injects generated manifest of assets that are supposed to be precached upon application startup. The manifest itself is generated during the app build based on settings in the webpack config file.
self.__precacheManifest = (self.__precacheManifest || []).concat([
{
"revision": "9af72b212f21ac3c25ef",
"url": "bundles/css/main.css"
},
{
"revision": "532cfce048fb3cbb252e",
"url": "bundles/js/app-bundle.js"
}]
);
By default, the manifest will contain all of the files processed by Webpack (ie bundled assets), so if you want to precache additional resources you will
have to specify them via globDirectory
and globPatterns
properties of the Workbox plugin config.
new InjectManifest({
globDirectory: DST_ASSETS_DIR,
globPatterns: ['precache/**/*.*'],
swSrc: path.join(__dirname, SRC_DIR, 'templates/workbox-sw.js'),
swDest: path.join(__dirname, DST_DIR, 'templates/sw.js')
})
Sometimes you may need to cache assets outside of the precache
folder. In this case you have to explicitly specify the assets that you
need to be cached (this can be a local asset or an external URL).
Add a new asset with revision
and url
properties in the call to precacheAndRoute
method as shown below:
...
// Here we precache custom defined Urls
workbox.precaching.precacheAndRoute([{
"revision": "{{appVersion}}",
"url": "{{appUrl}}"
},{
"revision": "{{appVersion}}",
"url": "{{appUrl}}manifest.json"
}]);
Application Manifest is a file in JSON format which turns the application into a PWA. Starter comes with its own manifest.json with hardcoded
title, color scheme, display settings and favicon. Feel free to change the predefined settings: the file is located in the /resources/templates/
folder.
{
"name": "PWA Starter for Enonic XP",
"short_name": "PWA Starter",
"theme_color": "#FFF",
"background_color": "#FFF",
"display": "standalone",
"start_url": ".?source=web_app_manifest",
"icons": [
{
"src": "precache/icons/icon.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Default favicon used by the Starter is called icon.png
and located in precache/icons/
folder, so you can simply replace this icon with
your own of the same name. If you want to use a different icon file, add it to the same location and change page.html
to point to the new icon. Don’t
forget to make the same changes in manifest.json
.
<link rel="apple-touch-icon" href="{{precacheUrl}}/icons/myicon.ico">
<link rel="icon" href="{{precacheUrl}}/icons/myicon.ico">
This Starter is not a traditional site with plain HTML pages - everything is driven by a controller.
Just like resources/assets/js/app.js
is an entry point of the Starter’s client-side bundle, resources/webapp/webapp.js
is an entry point
and the main controller for the server-side execution. Setting it up is simple - just add handler of the GET request to webapp.js
file and
return response in form of rendered template or a simple string:
exports.get = function (req) {
return {
body: 'We are live'
}
};
If your application name is com.enonic.starter.pwa
and Enonic web server is launched on localhost:8000
then
http://localhost:8080/webapp/com.enonic.starter.pwa/
will open the main page of your app.
As mentioned above, main.js` is used to render pages and serve the content. In our starter we use one main template
(
templates/page.html``) and then use fragments for showing different content based on which page you’re on. This is explained below.
If your application is not a single-page app, you are going to need some routing capabilities. The Starter is using Enonic Router library which makes it incredibly simple to dynamically route a request to correct page template. First, let’s change the default page to render a proper template instead of a simple string.
var thymeleaf = require('/lib/thymeleaf');
var router = require('/lib/router');
var portalLib = require('/lib/xp/portal');
router.get('/', function (req) {
return {
body: thymeleaf.render(resolve('/templates/page.html'), {
appUrl: portalLib.url({path:'/app/' + app.name}),
pageId: 'main',
title: 'Main page'
})
}
});
exports.get = function (req) {
return router.dispatch(req);
};
Here we told the Router to respond to the "/" request (which is the app’s main page) with the rendered template from /templates/page.html
.
Now let’s create a fragment showing the content of the main page that is different from other pages:
templates/fragments/common.html:
<div data-th-fragment="fragment-page-main" data-th-remove="tag">
<div>
This is the main page!
</div>
</div>
Finally, inside the main template we should render correct fragment based on pageId
:
templates/page.html:
<main class="mdl-layout__content" id="main-content">
<div id="main-container" data-th-switch="${pageId}">
<div data-th-case="'main'" data-th-remove="tag">
<div data-th-replace="/templates/fragments/common::fragment-page-main"></div>
</div>
<div data-th-case="*" data-th-remove="tag">
<div data-th-replace="/templates/fragments/under_construction::fragment-page-under-construction"></div>
</div>
</div>
</main>
Now let’s expand this to enable routing to other pages. Let’s say, we need a new page called "About" which should open via /about
URL.
var thymeleaf = require('/lib/thymeleaf');
var router = require('/lib/router')();
router.get('/', function (req) {
...
});
router.get('/about', function (req) {
return {
body: thymeleaf.render(resolve('/templates/page.html'), {
appUrl: portalLib.url({path:'/app/' + app.name}),
pageId: 'about',
title: 'About Us'
})
}
});
exports.get = function (req) {
return router.dispatch(req);
};
Create a new fragment for the "About" page:
templates/fragments/about.html:
<div data-th-fragment="fragment-page-about" data-th-remove="tag">
<div>
This is the About Us page!
</div>
</div>
Handle new fragment inside the main template: templates/page.html:
<main class="mdl-layout__content" id="main-content">
<div id="main-container" data-th-switch="${pageId}">
<div data-th-case="'main'" data-th-remove="tag">
<div data-th-replace="/templates/fragments/common::fragment-page-main"></div>
</div>
<div data-th-case="'about'" data-th-remove="tag">
<div data-th-replace="/templates/fragments/common::fragment-page-main"></div>
</div>
<div data-th-case="*" data-th-remove="tag">
<div data-th-replace="/templates/fragments/under_construction::fragment-page-under-construction"></div>
</div>
</div>
</main>
When you’re building a PWA you typically want a user to be able to open previously visited pages even when the application is offline.
In this Starter we are using Workbox to dynamically cache URL requests for future use. Note that we are using `networkFirst
as a default
strategy but you can specify a different strategy for specific pages.
/**
* Make sure SW won't precache non-GET calls to service URLs
*/
workbox.routing.registerRoute(new RegExp('{{serviceUrl}}/*'), new workbox.strategies.NetworkOnly(), 'POST');
workbox.routing.registerRoute(new RegExp('{{serviceUrl}}/*'), new workbox.strategies.NetworkOnly(), 'PUT');
workbox.routing.registerRoute(new RegExp('{{serviceUrl}}/*'), new workbox.strategies.NetworkOnly(), 'DELETE');
/**
* Sets the default caching strategy for the client: tries contacting the network first
*/
workbox.routing.setDefaultHandler(new workbox.strategies.NetworkFirst());
workboxSW.routing.registerRoute(
'{{baseUrl}}/about',
new workboxSW.strategies.CacheFirst()
);
workboxSW.routing.registerRoute(
'//fonts.gstatic.com/s/materialicons/*',
new workboxSW.strategies.CacheFirst()
);
Here we specify default caching strategy for the entire app and then specific caching strategy for /about
URL and
requests to the 3rd-party font file on an external URL.
Tip
|
Note that we by default are using networkFirst strategy which means that Service Worker will first check for the fresh version from the network and fall back to the cached version only if the network is down. Read more about possible caching strategies here. |