The term Progressive Web App refers to a group of technologies, such as service workers, and push notifications, that can bring native-like performance and user experience to web apps. The term got coined back in 2015 by designer Frances Berriman and Google Chrome engineer Alex Russell. Some of the characteristics of PWAs are:
- Progressive – Work for every user, regardless of browser choice because they’re built with progressive enhancement as a core tenet.
- Responsive – Fit any form factor: desktop, mobile, tablet, or forms yet to emerge.
- Connectivity independent – Service workers allow work offline, or on low-quality networks.
- App-like – Feel like an app to the user with app-style interactions and navigation.
- Fresh – Always up-to-date thanks to the service worker update process.
- Safe – Served via HTTPS to prevent snooping and ensure content hasn’t been tampered with.
- Discoverable – Are identifiable as “applications” thanks to W3C manifests[6] and service worker registration scope, allowing search engines to find them.
- Re-engageable – Make re-engagement easy through features like push notifications.
- Installable – Allow users to “keep” apps they find most useful on their home screen without the hassle of an app store.
- Linkable – Easily shared via a URL and do not require complex installation.
Not long ago, having an application in an app store was considered the holy grail. To this day, a lot of companies still religiously follow the path to app store publishing, however, according to recent studies, just 26.4 percent of users who visit a page in an app store today will install an app. The other 73.6 percent is lost to the developer and don’t even try the app. Among the ones who install it, an average of 80 percent is lost in the following 90 days.
As a consequence, the same people who declared the web dead years ago are now shouting, “Apps are dying” and “Wait! The web isn’t dead after all!” At the recent Google I/O 2017 conference, there were a lot of talks showcasing how modern mobile web development is being re-defined through PWAs such as Twitter Lite, Forbes or Wego.
PWA Pre-Requisites
All of us working in web development know that JavaScript frameworks have greatly evolved in the past years. PWAs are essentially JavaScript applications with a few added features, such as app manifests and service workers for offline-mode and push notifications.
The preferred JavaScript frameworks for developing mobile PWAs are currently AngularJS and ReactJS. They both have their advantages. AngularJS can be combined with Ionic, which offers a set of ready to use components. React is very intuitive and easy to work with and benefits from the existence of create-react-app, a boilerplate project which contains everything you need, from development to productions tasks. In create-react-app, “the production build is a fully functional, offline-first Progressive Web App.”
No matter the framework choice, ES6 is a must, bringing powerful features such as classes, arrow-functions, block-scoping, promises, and many others. Although ES6 is not completely supported by all browsers, transpilers such as Babel are available, so we don’t have to worry about browser compatibility.
Every PWA needs an API for retrieving content from the server. NodeJS has become hugely popular as a platform for implementing custom APIs, but we can also use a CMS as a backend.
WordPress currently powers 27 percent of the entire web. At the end of 2015, Matt Mullenweg has encouraged all WordPress developers to “Learn JavaScript, Deeply.” The shift in tech-stack has not become mainstream yet, and the majority of WordPress developers are still heavily relying on PHP to build responsive themes. Since I have started as a PHP developer myself, I understand its attractiveness and its progress from last years. PHP is structured and easy to learn, especially compared to JavaScript, where things can get out of control pretty quickly. However, there’s no denying that JavaScript is now the go-to solution for building wonderful user experiences on the web.
WordPress stakeholders already know that the change is coming, so REST API was included in the core starting from version 4.5. Although more than 50 percent of WordPress sites use the latest CMS version, 30 percent of WordPress installs are stuck with version 4.4 or below. This number can only decrease in the future, and we can safely say that the REST API is available for the majority of WordPress sites. I should also mention that the WordPress REST API is beautifully documented and is available by default at a path relative to the website’s domain.
Next, I’m going to show you how to build a basic PWA with AngularJS and Ionic 1.x, on top of the WordPress REST API.
Environment Setup
I’m going to skip over the setup of a simple WordPress website for exposing the REST API. Even if you don’t know how to do it yet, there are countless good tutorials out there that will show you how to accomplish this.
Instead, I’m going to focus on the setup for the Angular JS / Ionic 1.x app. After developing many applications on this framework, we created a boilerplate, which includes:
- Documentation on how to set up the environment and structure the app.
- Tasks for compiling, production and testing the app.
- Coding standards and linters.
- Unit and end-to-end tests.
It might seem unimportant at first, but having a structured work environment has allowed us to write consistent, maintainable code.
Our boilerplate also includes Gulp tasks for watching changes, linters, transpiling, generating documentation with ngdoc, testing (unit and end-to-end) and production. The project assumes that NodeJS, NPM, Bower, Gulp and IonicCLI are globally installed.
You can find the full documentation on how to set up the environment here, just follow the steps to get started.
Structuring the application
We have used a folders-by-feature structure, as recommended by John Papa’s style guide. However, we have added our own twist by using ES6.
All the application’s files are located in as src folder, which contains:
- app – a folder with the .js files (controllers, modules), html templates, and scss files for each module.
- assets – a folder for static assets such as images, json files, and the app’s global styling (scss files).
- index.html – the starting point of the app
All files from src are compiled, transpiled, and copied to a www folder by running the default “gulp” task. The “bower install” command will add the Ionic library to the same folder.
The app will be automatically loaded in your default browser when using “ionic serve”.
We have also added a global configuration file (src/app/config.json), from where the API endpoints can be changed and paths to other assets (such as the app’s logo, icon, etc.) can be added. The configuration properties are attached to the window object from a src/app/main.js file that is run at init:
const fetchConfig = () => { const $initInjector = angular.injector(['ng']); const $http = $initInjector.get('$http'); const $window = $initInjector.get('$window'); return $http.get(`${$window.__APPTICLES_BOOTSTRAP_DATA__.CONFIG_PATH}`); }; ... fetchConfig() .then(initializeConfiguration) .then(boostrapApplication) .catch(errorHandler);
Retrieving data the REST API
The first thing that we added to the app was a service for communicating to the API. As I mentioned in the previous section, we have mapped the paths to the API in a global configuration file. For the sake of simplicity, I have included here only the endpoints for fetching categories and posts, but of course, you can easily add other endpoints for pages, comments, media, etc.
{ "export": { "categories": { "find": "//pwathemes.com/demo-api/wp-json/wp/v2/categories", "findOne": "//pwathemes.com/demo-api/wp-json/wp/v2/categories" }, "posts": { "find": "//pwathemes.com/demo-api/wp-json/wp/v2/posts", "findOne": "//pwathemes.com/demo-api/wp-json/wp/v2/posts" } } }
Having this setup, we can generate the methods for connecting to the backend API in our service:
'use strict'; angular.module('appticles.api').factory('AppticlesAPI', AppticlesAPI); AppticlesAPI.$inject = ['$log', '$http', 'configuration']; /** * @ngdoc service * @name appticles.api.AppticlesAPI * * @description Creates a programmatic API that wraps around the export endpoints provided via * the configuration service. */ function AppticlesAPI($log, $http, configuration) { var API = {}; var exportApiEndpoints = configuration.export; Object.keys(exportApiEndpoints).forEach(function (endpoint) { var methods = exportApiEndpoints[endpoint]; Object.keys(methods).forEach(function (method) { API[camelCase([method, endpoint])] = function () { var params = arguments.length > 0 && angular.isDefined(arguments[0]) ? arguments[0] : {}; params._jsonp = 'JSON_CALLBACK'; var url = methods[method]; if (params && params.id) { url = url + '/' + String(params.id); delete params.id; } return $http.jsonp(url, { method: 'GET', params: params }); }; }); }); return API; }
Since the REST API uses the same endpoint for reading single and multiple items, we pass an “id” param that will be concatenated to the API URL. Also, please note that the REST API uses a “_jsonp” param instead of the usual “callback”.
After adding this service, all we need is to do is inject it in a controller and parse the result. We start by creating a categories module that will load our controller and template:
angular.module('appticles.categories', [ 'ui.router', 'appticles.api', 'appticles.configuration' ]) .config(['$stateProvider', '$urlRouterProvider', ($stateProvider, $urlRouterProvider) => { $stateProvider .state('categories', { url: '/categories', controller: 'CategoriesController as categoriesVm', templateUrl: 'app/categories/categories.template.html' }); $urlRouterProvider.otherwise('/categories'); }]);
In the controller, we’re using the API service to read the posts categories:
class Categories { constructor($log, AppticlesAPI) { const populateCategories = (result) => { this.categories = result; }; AppticlesAPI.findCategories({hide_empty: 1}) .then(populateCategories) .catch($log.error); } } Categories.$inject = ['$log', 'AppticlesAPI']; angular.module('appticles.categories') .controller('CategoriesController', Categories);
In the template, we just iterate over the data and display the categories names:
<ion-view view-title="Progressive Web App Sample" cache-view="false"> <ion-content> <ion-list> <div ng-repeat="category in categoriesVm.categories"> <p data-ng-bind-html="category.name | TrustHtmlFilter"></p> </div> </ion-list> </ion-content> </ion-view>
Validating data
In our example, we are blindingly trusting the API that it will give us the data that we need, and in the format that we need. In real life, things can quickly go sour with such an approach. And while we can’t make the API give us the information that we want, we can ensure that the app doesn’t crash in a disastrous way when things don’t work as expected.
For this purpose, we have added a validation service to our app. The purpose of this service is to take the raw data from the API, check it and return an error if a field fails the required or format validation. Here is how we are checking the post categories:
angular.module('appticles.validation') .factory('AppticlesValidation', AppticlesValidation); /** * @ngdoc service * @name appticles.validation.AppticlesValidation * * @description Service for validating data coming from the API. */ function AppticlesValidation() { let service = { validateCategories: validateCategories, ... }; return service; /** * @ngdoc function * @name appticles.validation.AppticlesValidation#validateCategories * @methodOf appticles.validation.AppticlesValidation * @description Validate categories data * * @return {Array} An array of category objects * * {@link https://developer.wordpress.org/rest-api/reference/categories/#list-categorys} */ function validateCategories(input) { if(angular.isObject(input) && !angular.isArray(input)) { if (angular.isDefined(input.data)) { if (input.data.length === 0) { return []; } let validatedCategories = input.data.map(_checkOneCategories); if (validatedCategories.indexOf(false) === -1) { return validatedCategories; } } } return { 'error': 'Invalid data' }; } /** * @ngdoc function * @name appticles.validation.AppticlesValidation#checkOneCategories * @methodOf appticles.validation.AppticlesValidation * @description Validate a single category object. * * @return {Boolean|Object} Return the category if it's valid, false otherwise. * * {@link https://developer.wordpress.org/rest-api/reference/categories/#schema} */ function _checkOneCategories(category) { if (angular.isDefined(category.id) && /^[a-z0-9]+$/i.test(category.id) && angular.isDefined(category.name) && angular.isString(category.name) && angular.isDefined(category.slug) && angular.isString(category.slug) && angular.isDefined(category.link) && angular.isString(category.link) && (angular.isUndefined(category.parent) || /^[a-z0-9]+$/i.test(category.parent) )) { return { id: category.id, name: category.name, slug: category.slug, link: category.link, parent: category.parent }; } return false; } }
As you can see in the example above, we are validating only the data that we’re going to use in the app. We are also returning only those particular fields. This allows us to completely decouple the backend logic from the front end logic – it doesn’t matter what properties are used in the API, we can transform those properties into whatever we want. In case we want to connect our app to a different content source, we could update only the validation service, without worrying that we’ll need to go through the controllers and views and update the theme as well.
Here is how our categories controller will look after adding the validation service:
class Categories { constructor($log, $q, AppticlesAPI, AppticlesValidation) { const validateCategories = (result) => { let validatedCategories = AppticlesValidation.validateCategories(result); return $q.when(validatedCategories); }; const populateCategories = (result) => { if (angular.isUndefined(result.error)) { this.categories = result; } }; AppticlesAPI.findCategories({hide_empty: 1}) .then(validateCategories) .then(populateCategories) .catch($log.error); } } Categories.$inject = ['$log', '$q', 'AppticlesAPI', 'AppticlesValidation']; angular.module('appticles.categories') .controller('CategoriesController', Categories);
Don’t forget to add this new service to the categories module.
In a similar matter, we can create modules for retrieving the posts from a category and a post’s content. You’ll find the full example in our Github repository.
Integrating the PWA in WordPress
Once you are done implementing the PWA, you can generate the production files by using a series of gulp tasks. First, make sure that your www folder contains the latest source files by running the default “gulp” task. Then, you can use “gulp production:js” and “gulp production:css.” These commands will create a www/dist folder where you’ll find a single .css file (bundle.css) and a single .js file (bundle.js). These are the production files for your app.
The PWA can be used independently of WordPress. It doesn’t require a web server to run, so you can even host it on a storage system like AWS S3. If you want it to be used as a WordPress theme, you’ll first need to create a theme folder with the static files and a single index.php that will load the app.
<!DOCTYPE HTML> <html manifest="" <?php language_attributes(); ?>> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-touch-fullscreen" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black" /> <meta name="mobile-web-app-capable" content="yes" /> <meta name="theme-color" content="#ffffff"> <title><?php echo get_bloginfo("name");?></title> <!-- load css --> <link rel="stylesheet" href="<?php echo get_template_directory_uri();?>/style.css"> <!-- load js --> <script type="text/javascript" pagespeed_no_defer=""> window.__APPTICLES_BOOTSTRAP_DATA__ = { CONFIG_PATH: '<?php echo get_template_directory_uri();?>/config.json' }; </script> <script src="<?php echo get_template_directory_uri();?>/js/bundle.js" type="text/javascript"></script> </head> <body> <ion-nav-view></ion-nav-view> </body> </html>
As you can see, this is a very simple setup that loads the .css, .js files and the configuration of the theme. You’ll also want to transform the config.json file into a .php file, for dynamically mapping the paths to the API depending on the website’s address.
To load the PWA only for mobile visitors, you can create a plugin that handles mobile detection and changes the default theme folder by using the add_filter() method, whenever a supported device is detected.
Just a heads up – be careful when adding wp_head(), wp_footer() and other WordPress methods to your PWA theme. All WordPress plugins and widgets use these as hooks to add their own data, and content included in such a way can conflict with your code, especially when talking about JavaScript based widgets.
Web Push Notifications, Offline Mode and other PWA features
You can obtain a decent PWA score on Google Chrome’s Lighthouse plugin (around 50-60 percent) with just the JavaScript app and a couple of tweaks. One of these tweaks is the manifest file, from where you can add links to your app icon and change some simple things like the browser’s navigation bar color.
If you want to take things further, you’ll need to add service workers for offline-mode and web push notifications. Fortunately, web push notifications are made much easier these days. You can find free WordPress plugins that will handle the notifications for you, the most popular being the One Signal plugin. It will add a manifest file to your theme and prompt users for permissions before sending the notifications. Just be careful to merge this manifest file with your existing one, as browsers will ignore the second manifest file.
For implementing offline mode, we recommend that you take a look at:
- sw-precache – offline precaching for static assets/application shells.
- sw-toolbox – offline caching for dynamic/runtime requests.
Conclusion
In this article, I have presented only a basic PWA to get you started. The code samples are a greatly simplified version of our own PWAs. You can see a complete PWA here. The repo includes services for inserting ads in the content, translating the app’s texts and, last but not leats – unit tests with Karma and Jasmine and end-to-end tests with Protractor.
Building a “progressive” future for the mobile web starts with us, web developers and designers, looking to go beyond responsiveness. Since the App Store model is beginning to fail, publishers and businesses are realizing that Apps are not the Holy Grail. We need to give the mobile web a second chance, a chance to evolve and progress beyond format (i.e. fitting the screen) into actual app-like functionality.
WordPress has what it takes to be at the forefront of this trend and it’s up to the community to accept the challenge and steer both WordPress and the web in a “progressive” direction.
1 Comment