Skip to content
No description, website, or topics provided.
JavaScript HTML CSS
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.settings restructured lib #23 Jul 23, 2018
docs/images
gradle/wrapper Release v1.9.0 Apr 9, 2019
src/main/resources
.babelrc Upgrade node modules including babel and webpack. Use mini-css-extrac… Oct 9, 2018
.classpath
.eslintrc.js Fixed error on empty repo. Fixed double render. Debounced updates fro… Sep 20, 2018
.gitignore Code cleanup #20 Jul 6, 2018
.node-version Node 8.12.0 Oct 9, 2018
.project restructured lib #23 Jul 23, 2018
LICENSE.txt Added documentation on the Starter (#8) Oct 30, 2017
README.adoc
build.gradle Fixed dependencies May 7, 2019
gradle.properties Release v1.8.1 Apr 10, 2019
gradlew
gradlew.bat Gradle 4.10.2 Oct 9, 2018
package-lock.json
package.json Fixed dependencies May 7, 2019
settings.gradle Initial commit Oct 9, 2017
webpack.config.babel.js

README.adoc

PWA Starter for Enonic XP

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.

Installation

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)

Usage and Testing of PWA

We assume that XP service is running on localhost:8000 and your app is called com.company.myapp as in the example above.

  1. Open http://localhost:8080/webapp/com.company.myapp in your browser. You should see this:

main page

2.Click the burger icon in the header:

menu

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.

PWA Features

Tracking online/offline state

Click the "Offline" link in the Starter menu. That will open a new page looking like this:

page online

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:

page offline

If you now - while staying offline - go to the main page, you will see additional note under the welcome text

main page offline

As you can see, the Starter can track its online/offline status and change content of its pages accordingly.

Webpack Config

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.

Dependencies

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.

Auto-precaching assets

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-sw.js:
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')
})

Precaching custom assets

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:

workbox-sw.js:
...

// Here we precache custom defined Urls
workbox.precaching.precacheAndRoute([{
    "revision": "{{appVersion}}",
    "url": "{{appUrl}}"
},{
    "revision": "{{appVersion}}",
    "url": "{{appUrl}}manifest.json"
}]);

Application Manifest file

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.

manifest.json:
{
  "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"
    }
  ]
}

Changing favicon

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.

main.html:
    <link rel="apple-touch-icon" href="{{precacheUrl}}/icons/myicon.ico">
    <link rel="icon" href="{{precacheUrl}}/icons/myicon.ico">

main.js

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:

webapp.js:
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.

Page rendering

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.

Dynamic routing

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.

main.js:
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.

main.js:
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>

Runtime caching

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.

workbox-sw.js:
/**
 * 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.
You can’t perform that action at this time.