Progressive Web Apps have opened us a lot of new ways to deliver amazing user experiences on the web. They combine the best of web and the best of apps, regardless of any operating system and browser choice. Once installed they feel and look like native applications without requiring special developing skills. Modern business applications should work on desktop and mobile systems, which is no issue thanks to the responsiveness of PWAs. To top it all: It works completely offline.
SAPUI5 gives us the opportunity to build fast, reliable, and responsive user interfaces for the web. There was already an article about
Building a PWA with OpenUI5, but how about converting an
existing OpenUI5 Application into a Progressive Web App?
Example: Icon Explorer
The icon explorer is a small tool that allows you to find and preview icons by browsing through categories and tags or just by searching. It loads the SAP icons font, gives an overview of the icons, and shows them in different use cases with UI5 controls. It is a great base for testing the PWA compatibility of UI5.
What do we need?
A default PWA needs:
- Special meta tags in index.html to tell the browser that this is a web app and to load some icons and to make other modifications.
- Splash screen and icons to beautify the user interface because Progressive Web Apps need some static content that is shown immediately
- A Web App Manifest to describe our application. UI5 already requires a manifest, meaning that we can merge them by adding PWA-related terms in the existing manifest.json file
- A service worker to guarantee offline compatibility
For more details, you can look up the
PWA Checklist
After having made all these changes one big problem appears:
We have a lot of
synchronous XMLHttpRequests but the service worker is not able to see any of them. To check this just open the Web Application with Chrome, start
Developer tools, click on
Network to see which files are loaded by the service worker: When you first start the page, the service worker will be registered and installed. When you now reload it, the service worker will start serving the files.
This gives us an overview of all files requested by the icon explorer. You will see that there are already a lot of files loaded by the service worker, but there are still various files not coming from there.
So, what else do we need?
1. Component-Preload
A component preload will combine all needed components in one file. This will help us to get fewer XHRs and to improve the start-up performance for delivering a better user experience. To create a
Component-Preload.js file you can either use the
SAPUI5 Web IDE or you can do it with
Grunt. In this case, we will use
Grunt.
We need to create a
Gruntfile.js:
module.exports = function(grunt) {
grunt.initConfig({
dir:{
webapp: 'IconExplorer/src',
dist: 'dist'
},
clean: {
"preload": ["src/Component-preload.js"],
"openui5": ['src/openui5']
},
"openui5_preload": {
component: {
options: {
compress: false,
resources: {
cwd: "src",
prefix: "sap/ui/demo/iconexplorer",
src: [
"Component.js",
"**/*.js",
"**/*.fragment.xml",
"**/*.view.xml",
"**/*.properties",
"manifest.json",
"!Component-preload.js",
"!test/**",
"!openui5/**"
]
},
dest: "src"
},
components: "sap/ui/demo/iconexplorer"
}
}
});
grunt.loadNpmTasks("grunt-contrib-clean");
grunt.loadNpmTasks("grunt-openui5");
grunt.registerTask('build', ['clean', 'openui5_preload']);
grunt.registerTask('default', ['build']);
};
The
openui5_preload task will generate our
Component-preload.js file which contains our
Component.js,
manifest.json and all other
.js, .xmland
.properties files.
2. Load components asynchronously
We created the
Component-preload.js file to reduce the
XMLHttpRequests, but the components are still loaded
synchronously. Luckily UI5 offers us several opportunities to make things
asynchronous.
To fix this, we need to modify the
index.html file:
<script>
sap.ui.getCore().attachInit(function () {
sap.ui.require([
"sap/m/Shell",
"sap/ui/core/ComponentContainer"
], function (Shell, ComponentContainer) {
var oCompContainer = new ComponentContainer({
height: "100%"
});
// initialize the UI component with async property
var oComponent = sap.ui.component({
async: true,
manifestFirst: true,
name: "sap.ui.demo.iconexplorer"
}).then(function(oComponent){
oCompContainer.setComponent(oComponent);
});
new Shell({
showLogout:false,
app: oCompContainer
}).placeAt("content");
});
});
</script>
3. Load libraries asynchronously
After having all components loaded asynchronously, we also need to do this with the libraries: Just add this to the bootstrapping part of
index.html:
data-sap-ui-preload="async"
This way the libraries are loaded asynchronously, but you have to check, if there are still synchronous requests for libraries. In our case, there are synchronous requests for
Avatar.js and
library.js file from
sap.f-library. This means we must preload the
sap.f library.
4. Preload libraries
To preload libraries, we just have to add them to
data-sap-ui-libs in
bootstrap of
index.html:
data-sap-ui-libs="sap.m, sap.ui.layout, sap.ui.core, sap.f"
Now, there are two library requests left: The
library-parameters.json files. They are requested, because the browser tries to render the UI before the
theme is fully loaded. To solve this, we have to tell the browser in bootstrapping of UI5 in
index.html, that it has to wait for the theme:
data-sap-ui-xx-waitForTheme="true"
5. Messagebundles preload
(We do not need this step, when we use the
OpenUI5 Nightlies, because after version
1.52 of
OpenUI5 the messagebundles are loaded asynchronously. But for older versions we need to preload these files to prevent requests.)
After fixing this, we only have six XHRs. Three of them are asynchronous, so there are just three synchronous requests left:
These three files are the
messagebundle files. They are for standard texts in Components, for example the
“Search” text of a
Search field.
To make them load asynchronously, there is unfortunately no property or tag. We need a workaround:
Firstly, we have to download packaged versions of our libraries with
Bower to reach the messagebundle.properties files, by creating this
bower.json file:
{
"name": "iconexplorer",
"dependencies": {
"openui5-sap.ui.core": "openui5/packaged-sap.ui.core",
"openui5-sap.m": "openui5/packaged-sap.m",
"openui5-sap.f": "openui5/packaged-sap.f",
"openui5-sap.ui.layout": "openui5/packaged-sap.ui.layout"
}
}
Secondly, we need to run this command in
terminal
$ bower install
Bower will now save our libraries in
bower_components directory.
Thirdly, we must must extend our existing
Gruntfile.js with one more task called
concat:
concat: {
"sap-ui-messagebundle-preload.js": {
options: {
process: function(src, filepath) {
var moduleName = filepath.substr(filepath.indexOf("resources/") + "resources/".length);
var preloadName = moduleName.replace(/\//g, ".").replace(/\.js$/, "-preload");
var preload = {
"version": "2.0",
"name": preloadName,
"modules": {}
};
preload.modules[moduleName] = src;
return "jQuery.sap.registerPreloadedModules(" + JSON.stringify(preload) + ");";
}
},
src: [
"bower_components/openui5-sap.ui.core/resources/sap/ui/core/*.properties",
"bower_components/openui5-sap.m/resources/sap/m/*.properties",
"bower_components/openui5-sap.ui.layout/resources/sap/ui/layout/*.properties",
"bower_components/openui5-sap.f/resources/sap/f/*.properties"
],
dest: "src/openui5/resources/sap-ui-messagebundle-preload.js"
}
}
This task will compress all
messagebundle files in one JavaScript file and register each file as preloaded, so it does not need to be requested.
The generated
sap-ui-messagebundle-preload.js file by Grunt needs to be called in
index.html to be recognized:
<!-- Preload Messagebundle files -->
<script src="openui5/resources/sap-ui-messagebundle-preload.js"></script>
At last, we also have to add this to bootstrapping of UI5 in
index.html:
data-sap-ui-xx-supportedLanguages="default"
If we do not define a language for our application (e.g with
sap-ui-language), UI5 tries to load the messagebundle file according to the browser language. This can lead to requests for very specific language files like
de_DE, but UI5 is “only” translated into ~38 languages with one version of German (*de*), causing a language loading fallback. Our preload also contains only these languages, so we have to tell UI5, that it has to load just
supported languages. Setting the supported languages to
default will avoid any request for languages not existing in UI5.
Test
Finally, our application should now load everything from the service worker. This means that our Progressive Web App is finished and works completely offline. Since Chrome 59, a PWA will also appear in the App Drawer on Android devices, when added to the homescreen, delivering an even more native-like user experience.
Conclusion
This example showed us that Progressive Web Apps are indeed no limitation for OpenUI5. In summary we can say, that it is enough to create a
Gruntfile.js, which preloads all components and libraries, to convert our existing OpenUI5 application into a Progressive Web App. But we also have to point out that the icon explorer is a standalone application with no synchronization. For the future we should also consider to examine how OpenUI5 is performing as PWA when it comes to synchronization and data exchange.