Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
0 Kudos
1,143

Introduction


If best practices of UI5 like extending of base controllers and the usage of fragments are applied, the amount of files can be become quite high. Normally you will have used speaking variables and functions like camelCase and added JSDoc and other comments in the code. As a result the user has to download many files which are bigger than they need to be. This downloading will than lead to a long waiting time and a bad user experience.

The answer to this challenge is compression and minification. In a non UI5 project you would normally compress and concat static files like JavaScript and CSS files into one file each and compress the code by removing comments. Before compressing JavaScript is typically uglyfied. This means speaking variables are altered to a, A and so on.
This techniques can be used for UI5 applications as well and on top of it a Component-preload.js can be created.
After such a compression the project will likely only contain one HTML, one JavaScript, one CSS, one i18n and compressed JSON files. The overall loading time will significantly decrease.

In the following the JavaScript task runner Gulp will be used to achieve this goal. As a side note it is worth mentioning that a good alternative is the task runner GruntJS or none at all, if you are comfortable to write your own JavaScript, Bash or Batch script around the used NodeJS tools.

The outcome of the Gulp tasks will be a dist folder which contains the compressed files. The contents of this folder should be the only files deployed to a productive systems. All tests and source files should be omitted. Optionally there will be a reports folder containing the results of the code linting.

Prerequisites


All the tools are JavaScript tools which are based on NodeJS. So a prerequisite is a LTS or an up to date NodeJS installation with its package manager npm. In addition a UI5 project with the recommended folder structure is required:
- test
- ...
- webapp
- controller
- i18n
- model
- view
- ...
- Component.js
- index.html

Links



NPM


The dev dependencies will be managed with a package.json. It will allow to have a dedicated node_modules folder with possibly pinned versions per project.

Initializing NPM


If no package.json exists yet the project can be started by navigating to the root folder of the project and executing the init command. The init command will prompt for the project specifics and create the package.json accordingly.
cd path/to/project_root/
npm init

Gulp and Plugins


The majority of the plugins will be used only in the project. If a tool is needed globally install without the `--save-dev` parameter. Examples could be gulp-cli.
npm install --global gulp-cli

Clean up, compression and minification will be done with the following plugins. In case there is no CSS or JSON file to compress, the plugins are not needed.
npm install --save-dev del fs gulp gulp-clean-css gulp-htmlmin gulp-if gulp-json-editor gulp-json-minify gulp-pretty-data gulp-rename gulp-replace gulp-uglify gulp-ui5-preload gulp-util

In case JavaScript should be checked with ESLint the plugin is required which will install ESLint as well.
npm install --save-dev gulp-eslint

If CSS should be linted install stylelint support.
npm install --save-dev gulp-stylelint gulp-stylelint-checkstyle-reporter

gulpfile.js


Per default Gulp will expect a gulpfile.js containing the individual tasks. If not specified otherwise the tasks will run in parallel.

Loading Plugins


As a first step load the plugins, which will be used on top of the file. Depending on the plugins you are going to use at you tasks include more.
const del = require('del');
const gulp = require('gulp');

Basic Task


After the plugins are loaded a task can be created. A clean up of previous lint reports could look like:
gulp.task('cleanDist', () => {
return del(['dist/*']);
});

Running Tasks


If no task is specified, the task named default will be executed. If specified only the giving task will be executed:
gulp cleanDist

Wait For Tasks


If one task should start only after another has finished, use the second parameter of the task definition. Here you can specify the list of tasks the tasks should wait for.
gulp.task('minify-css', ['cleanDist'], () => {
return gulp.src('webapp/css/*.css')
.pipe(cleanCSS({compatibility: 'ie8'}))
.pipe(gulp.dest('dist/css'));
});

The same parameter can be used for grouping N tasks into one. It will give the ability to structure the individual tasks into steps. The grouped tasks will be run in parallel.
gulp.task('create-dist', ['ui5preload','minify-css','minify-html']);

index.html


The index.html might contain properties or configurations including the path to the webapp folder. This string needs to be replaced. Afterwards the output will be run through a minification pipe.
// Compress the HTML index.html
gulp.task('minify-html', () => {
return gulp.src('webapp/index.html')
.pipe(replace('/webapp', './'))
.pipe(htmlmin({
caseSensitive: true,
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true
}))
.pipe(gulp.dest('dist'));
});

Component-preload.js


Specific to UI5 is the creation of the Component-preload.js. This file is concat_all.min.js equivalent you might know from non UI5 web projects. Make sure the plugin is loaded and define the task like:
gulp.task('ui5preload', ['cleanDist'], () => {

// Only the front end UI5 related files are relevant
return gulp.src(['webapp/**/**.+(js|xml)','!webapp/thirdparty/**'])
// only pass .js files to uglify
.pipe(gulpif('**/*.js',uglify()))
// only pass .xml to prettydata
.pipe(gulpif('**/*.xml',prettydata({type:'minify'})))
// Define base path and namespace
.pipe(ui5preload({base:'webapp',namespace:'my.project.ui'}))
// Output the Component-preload.js to the dist folder
.pipe(gulp.dest('dist'));
});

neo-app.json


The UI5 app might be deployed as HCP HTML5 app. In this case a neo-app.json will be used, where the folders and index.html are specified. While this configuration will work on your development system, the paths will no longer be valid in case of deploying the dist folders content. As a solution create a task that copies an adapted neo-app.json into the dist folder.
gulp.task('neo-app-json', ['cleanDist'], () => {
return gulp.src('neo-app.json')
.pipe(jsonEditor({
'welcomeFile': '/index.html',
'logoutPage': '/logout/logout.html'
}))
.pipe(gulp.dest('dist'))
});

Complete Example


const del = require('del');
const fs = require('fs');
const gulp = require('gulp');
const eslint = require('gulp-eslint');
const gulpStylelint = require('gulp-stylelint');
const checkstyleReporter = require('gulp-stylelint-checkstyle-reporter').default;
const ui5preload = require('gulp-ui5-preload');
const uglify = require('gulp-uglify');
const prettydata = require('gulp-pretty-data');
const gulpif = require('gulp-if');
const cleanCSS = require('gulp-clean-css');
const replace = require('gulp-replace');
const htmlmin = require('gulp-htmlmin');
const rename = require("gulp-rename");
const jsonMinify = require('gulp-json-minify');
const jsonEditor = require('gulp-json-editor');

// Remove all previous files and logs
gulp.task('cleanReports', () => {
return del(['reports/*']);
});

// Remove all previous files and logs
gulp.task('cleanDist', () => {
return del(['dist/*']);
});

// Ensure there is no Component-preload.js in the src folder
gulp.task('removePreload', () => {
return del(['webapp/Component-preload.js']);
});

gulp.task('clean', ['cleanReports', 'cleanDist', 'removePreload'], () => {

// Create directories if not defined
fs.stat('reports', function(error) {
if (error && error.code === 'ENOENT') {
fs.mkdir('reports');
}
});
fs.stat('reports/checkstyle', function(error) {
if (error && error.code === 'ENOENT') {
fs.mkdir('reports/checkstyle');
}
});
});

// After clean up is done, check the source files with lint tools
gulp.task('eslint', () => {
// ESLint ignores files with 'node_modules' paths.
// So, it's best to have gulp ignore the directory as well.
// Also, Be sure to return the stream from the task;
// Otherwise, the task may end before the stream has finished.
return gulp.src(['{webapp,test}/**/*.js'])
// eslint() attaches the lint output to the 'eslint' property
// of the file object so it can be used by other modules.
.pipe(eslint())
// Format all results at once, at the end
.pipe(eslint.format())
// eslint.format() outputs the lint results to the console.
// Alternatively use eslint.formatEach() (see Docs).
.pipe(eslint.format('checkstyle', fs.createWriteStream('reports/checkstyle/js.xml')))
// To have the process exit with an error code (1) on
// lint error, return the stream and pipe to failAfterError last.
.pipe(eslint.failAfterError());
});

// CSS Linting
gulp.task('stylelint', () => {
return gulp.src('webapp/css/*.css')
.pipe(gulpStylelint({
failAfterError: true,
reporters: [
{formatter: checkstyleReporter({output: 'reports/checkstyle/css.xml'}), console: false}
],
debug: false
}));
});

// Combine all type of linting into one task.
// All linters will run in parallel
gulp.task('lint', ['eslint', 'stylelint']);

// Create the Component-preload.js task, which will be depended on the lint tasks.
// If one lint tasks exits with an error, the ui5preload task will not be executed
gulp.task('ui5preload', () => {

// Only the front end UI5 related files are relevant
return gulp.src(['webapp/**/**.+(js|xml)'])
// only pass .js files to uglify
.pipe(gulpif('**/*.js',uglify()))
// only pass .xml to prettydata
.pipe(gulpif('**/*.xml',prettydata({type:'minify'})))
// Define base path and namespace
.pipe(ui5preload({base:'webapp',namespace:'sap.hcp.analytics.mco'}))
// Output the Component-preload.js to the dist folder
.pipe(gulp.dest('dist'));
});

// Compress the css file(s)
gulp.task('minify-css', () => {
return gulp.src('webapp/css/*.css')
.pipe(cleanCSS({compatibility: 'ie8'}))
.pipe(gulp.dest('dist/css'));
});

// Compress the HTML index.html
gulp.task('minify-html', () => {
return gulp.src('webapp/index.html')
.pipe(replace('/webapp', './'))
.pipe(htmlmin({
caseSensitive: true,
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true
}))
.pipe(gulp.dest('dist'));
});

// Compress the json file(s)
gulp.task('minify-json', () => {
return gulp.src('webapp/model/*.json')
.pipe(jsonMinify())
.pipe(gulp.dest('dist/model'))
});

// Change the neo-app.json to productive mode
gulp.task('neo-app-json', () => {
return gulp.src('neo-app.json')
.pipe(jsonEditor({
'welcomeFile': '/index.html',
'logoutPage': '/logout/logout.html'
}))
.pipe(gulp.dest('dist'))
});

// Copy the image files to the dist folder
gulp.task('copy-images', () => {
gulp.src('webapp/img/*')
.pipe(gulp.dest('./dist/img'));
});

//Copy the image files to the dist folder
gulp.task('copy-favicon', () => {
gulp.src('webapp/favicon.ico')
.pipe(gulp.dest('./dist'));
});

// Copy the i18n files to the dist folder
gulp.task('copy-i18n', () => {
gulp.src('webapp/i18n/*.properties')
.pipe(gulp.dest('./dist/i18n'));
});

// Combine compress and copy tasks
gulp.task('create-dist', ['ui5preload','minify-css','minify-html','minify-json','neo-app-json','copy-images','copy-favicon','copy-i18n']);

// Default task which will execute them all in sequence
gulp.task('default', ['create-dist']);