X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET
    June 12, 2018

JET Custom Components XXI - Minifying Your Components

Duncan Mills
Architect

 

When building Web Applications we always want to be cognisant of performance and a major part of that is the time we spend waiting for round-trips to the server to grab assets such as JavaScript and CSS files.  When you create a JET Custom Component (CCA) you will generally be dealing with at least five or six files that need to be loaded from the server, just to provide all of the bits that the component needs.  These will include the loader script, the view, the viewModel and so forth.

In this article I'm going to show you how you can use r.js to compress or minify your components to reduce that download tally from at least five to maybe only one file to download.  This is a really simple recipe to follow, so let's get started. 

About r.js

The RequireJS Optimizier or r.js is a tool supplied as part of requireJS that optimizes your applications by combining multiple JavaScript and CSS files into a single downloadable JavaScript file that contains everything.  This combination of multiple files into a single file provides the major boost to performance by reducing the number of roundtrips to the server that are needed to load a component, but it can also be used to minify or compress the actual lines of code as well making the overall size of the download smaller as well.  This all adds up to a win-win and is a recommended step for both applications as well as individual components as we discuss here. Note that other similar optimizers such as WebPack are available  which will do a similar job, however, I'll just be looking at r.js in this article.

Be Aware of the Restrictions

The requireJS optimizer takes account of the inter-file dependencies that your component declares using the define() statement and also the require() call if you are using that.  However, this only extends to cases where you provide explicit references as strings in either define() or require(). So, if you've been really clever with your components and are doing some dynamic resource loading using the require() function then it's not smart enough to work out and compress those resources into the end result.  This situation is in fact pretty rare in JET Custom Components except in the case of components with translations which I'll specifically cover later.

A second restriction relates to the case where your custom component references a CSS  file that in turn holds URL references to other resources in your component distribution (for example in a background-image definition) Again r.js cannot resolve and package up those URL references and so we have to deal with those separately. 

Scripting the Minification Process

Step 1: Create a Configuration Script

There are various ways to run r.js but the simplest repeatable way is to create a configuration script that you can use every time you want to package up your component. This configuration script can live anywhere in your project, but I'd recommend that you put it into the existing /scripts directory under your JET project root.  There will be two existing folders within /scripts already (config and hooks), so just add another one, let's call it "minification". 

For each component that you have within the project that you want to minify, create a .js file which looks a little like this. 

({
  baseUrl: '../../src/js/jet-composites',
  name: 'acme-coyote-tracker/loader',
  out: '../../src/js/jet-composites/acme-coyote-tracker/min/loader.js',
  optimize: 'uglify',
  separateCSS: false,
  generateSourceMaps: true,
  paths: {
      'text': '../../../node_modules/requirejs-text/text',
      'css': '../../../node_modules/require-css/css.min',
      'css-builder': '../../../node_modules/require-css/css-builder',
      'normalize': '../../../node_modules/require-css/normalize',
      'ojL10n': '../../../node_modules/@oracle/oraclejet/dist/js/libs/oj/ojL10n',
      'knockout': 'empty:',
      'jquery': 'empty:',
      'ojs': 'empty:'
  },
  exclude: [
      'text',
      'css',
      'css-builder',
      'normalize',
      'ojL10n'
  ]
})

This sample assumes that the overall layout of your project structure is "standard", so if you've moved things around you might have to tweak the relative paths a little. You should also change the name of the script and the component folder as approriate! For simplicity I just name the configuration script as the same name as the custom component.

Note the following important points:

  1. The out setting defines that the compressed output file is called loader.js - this is important because it's the name of the general bootstrap file that we use to load a component in the first place. Always use this name. 
  2. The out setting also specifies the target location to which the minified file should be written is to a sub-directory of the CCA folder called /min. Because the files in /min will be continually touched by the r.js process you will probably want to exclude this particular folder from your source control or write the minimised content elsewhere into an temporary area that is no under source control.  The key thing, however, is that once you package your components for distribution (i.e. as zip files) the /min folder should be copied back under the component root just to make it easy and predictable to reference (see Using the Minified Code below).
  3. The optimize setting can be set to none or uglify. Uglify will cause the overall size of the compressed file to be reduced and so is a good thing.  However, you'll note that I've also set the generateSourceMaps property to true so that I can still debug the code if needed.
  4. The paths and exclude maps define all of the dependencies that might be mentioned in your define() block that you don't want to suck into the compressed loader.js. Generally these settings will be sufficient for you, unless you have additional libraries of your own which you want to exclude as well

With this script configured we have everything set up to minify the CCA

Step 2: Run the Minification

To minify your code you need to run the r.js script. This will already be installed as a dependency of your JET project so you can issue the following command from the root of the JET project:

[shell]:node_modules/requirejs/bin/r.js -o scripts/minification/acme-coyote-tracker.js

This will produce an output a little like this


Tracing dependencies for: acme-coyote-tracker/loader

Tracing dependencies for: text

Tracing dependencies for: css

Tracing dependencies for: css-builder

Tracing dependencies for: normalize

Tracing dependencies for: ojL10n
Uglify file: /Users/mel/CustomComponents/src/js/jet-composites/acme-coyote-tracker/min/loader.js

/Users/mel/CustomComponents/src/js/jet-composites/acme-coyote-tracker/min/loader.js
----------------
text!demo-deps-singleton-1/demo-deps-singleton-1-view.html
/Users/mel/CustomComponents/src/js/jet-composites/acme-coyote-tracker/resources/nls/acme-coyote-tracker-strings.js
ojL10n!acme-coyote-tracker/resources/nls/acme-coyote-tracker-strings
//Users/mel/CustomComponents/src/js/jet-composites/acme-coyote-tracker/acme-coyote-tracker-viewModel.js
text!acme-coyote-tracker/component.json
css!acme-coyote-tracker/acme-coyote-tracker-styles
/Users/mel/CustomComponents/src/js/jet-composites/acme-coyote-tracker/loader.js

Once that has run you will find a /min subdirectory within the Custom Component folder that contains a loader.js and matching loader.js.map (a sourcemap for debugging.

Using the Minified code

All you have to do to use the minified version of the code is to change the viewModel of any module that depends on it to call the component/min/loader rather than the /component/loader file. That's easy enough to do, however we recommend that you go one step further and set things up in the consuming application so that you can simply and quickly switch between the minified and full versions of the component.

To do this you need to define a requireJS path mapping specifically for the component, we generally just use the name of the component for this. In JET 5 and above you define such path additions in the project path_mapping.json file (in the /src root of the project.
Here's a sample entry:


    "acme-coyote-tracker": {
      "debug": {
        "path": "jet-composites/acme-coyote-tracker"
      },
      "release": {
        "path": "jet-composites/acme-coyote-tracker/min"
      }
    }

What this is doing is to define the acme-coyote-tracker path root to point directly at the location of the CCA loader file. The normal version when running the debug version of the project and the minified version when you do an ojet build --release

To match this change, any modules that need to load the component will simply use the path e.g. define([...,'acme-coyote-tracker/loader'],function... Rather than naming the full jet-composites/acme-coyote-tracker location as that is now encapsulated by the requireJS path mapping. This way the consuming page can pick up either the debug or minified version of the CCA without any change to it's define block

Dealing with Special Cases

At the beginning of this article I mentioned a couple of restrictions with the r.js optimizer that need to be covered.

First of all the issue of not being able to handle dynamic loading. Well in the custom component world, by far the most common use of this is where the ojL10n plugin is being used to provide string translations to the component. If you have translation bundles then you will need to ensure that a copy of these is made into the /min subdirectory so the relative loading will still work. By convention translation bundles will all be stored under /resources/nls so you will need to duplicate that entire sub-directory into the /min folder. This is something that you can script as part of a post-build hook for the component project.

The second problem is if you have a CSS with URL() references in it. In this case you need to exclude the CSS from the r.js processing using the separateCSS property in the configuration file. You also need to ensure that the existing CSS is loaded correctly using the wrap directive int he configuration. An updated version of our configuration file would look like this:

({
  baseUrl: '../../src/js/jet-composites',
  name: 'acme-coyote-tracker/loader',
  out: '../../src/js/jet-composites/acme-coyote-tracker/min/loader.js',
  optimize: 'uglify',
  separateCSS: true,
  wrap:{
     end: "require(['css!acme-coyote-tracker/loader']);"
  }
  generateSourceMaps: true,
  paths: {
      'text': '../../../node_modules/requirejs-text/text',
      'css': '../../../node_modules/require-css/css.min',
      'css-builder': '../../../node_modules/require-css/css-builder',
      'normalize': '../../../node_modules/require-css/normalize',
      'ojL10n': '../../../node_modules/@oracle/oraclejet/dist/js/libs/oj/ojL10n',
      'knockout': 'empty:',
      'jquery': 'empty:',
      'ojs': 'empty:'
  },
  exclude: [
      'text',
      'css',
      'css-builder',
      'normalize',
      'ojL10n'
  ]
})

Setting separateCSS to true will result in the CSS being copied as loader.css into the /min directory. We then add the wrap directive to ensure that this CSS is loaded when the loader itself is called. Take note that this relies on the requireJS path that we just discussed being defined for the CCA.


JET Custom Component Series

If you've just arrived at Custom JET Components and would like to learn more, then you can access the whole series of articles on the topic from the Custom JET Component Architecture Learning Path

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha