Js/css Build Optimize Flow

I changed the my work frontend module build optimize flow few day ago. The original build flow is use wro4j plugin and my buildhelper. The cons is that there’re many trivial steps when adding a new page. I decide to update it with new way. I study the YEOMAN build flow and apply it in my module. The result is good.

YEOMAN uses nodejs and Grunt to do optimize. But the company module system is using Maven. The first step is to find a maven plugin to run grunt. I choose grunt-maven-plugin and it’s easy to integrate.

See my snippet code from pom.xml. I copy all files from src/main/webapp to ${project.build.directory}/original and run grunt in prepare package phase. I don’t want to let grunt access src/main/webapp so the ${project.build.directory}/original will be my grunt project source folder.

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-resources-plugin</artifactId>
  <version>2.6</version>
  <executions>
    <!-- copy webapp to target folder -->
    <execution>
      <id>copy-webapp</id>
      <phase>process-resources</phase>
      <goals>
        <goal>copy-resources</goal>
      </goals>
      <configuration>
        <outputDirectory>${project.build.directory}/original</outputDirectory>
        <resources>
          <resource>
            <directory>src/main/webapp/</directory>
          </resource>
        </resources>
      </configuration>
    </execution>
  </executions>
</plugin>

<plugin>
  <groupId>pl.allegro</groupId>
  <artifactId>grunt-maven-plugin</artifactId>
  <version>1.2.1</version>
  <configuration>
    <showColors>true</showColors>
  </configuration>
  <executions>
    <execution>
      <id>grunt-build</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>npm</goal>
        <goal>grunt</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Here i will skip the installation of yeoman. Let’s see how yeoman build flow work. I set the gamenow.app to target/original that is the folder copy from src/main/webapp. gamenow.dist will be target/storefront and this folder will be packaged to WAR file in maven build.

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = function (grunt) {
  // Load grunt tasks automatically
  require('load-grunt-tasks')(grunt);

  // Time how long tasks take. Can help when optimizing build times
  require('time-grunt')(grunt);

  grunt.initConfig({
    gamenow: {
      // Configurable paths
      app: 'target/original',
      dist: 'target/storefront'
    },

    ....

  });

YEOMAN uses usemin grunt plugin to create concat,minify,rewrite. In useminPrepare setting i set the html sources in <%= gamenow.app %>/*.html. It means usemin plugin will search all .html files and find the build block description to create configuration automatically. You will see how to set build block description in html later. The flow.steps enables us to add/modify step ourselves. Here i add a new step copy and it only executes concat. The js and css steps are default.
In usemin setting i add a copy function definition in blockReplacements. The function is quite simple. We need to set html and css folders too because usemin plugin needs to find files to modify in these folders.

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  useminPrepare: {
    options: {
      dest: '<%= gamenow.dist %>',
      flow: {
        steps: {
          js: ['concat','uglifyjs'],
          css: ['concat','cssmin'],
          copy: ['concat']
        },
        post: {}
      }
    },
    html: '<%= gamenow.app %>/*.html'
  },

  usemin: {
    options: {
      assetsDirs: ['<%= gamenow.dist %>'],
      blockReplacements: {
        copy: function(block) {
          return '<script src="' + block.dest + '"></script>';
        }
      }
    },
    html: ['<%= gamenow.dist %>/*.html'],
  },

Let’s take a look at sample.html. Use <!-- build:step path --> to wrap your css. Here the step is css and path is css/all.css. The usemin plugin will use concat to merge all css files to css/all.css and cssmin plugin to minify it.
Use <!-- build:js path --> to optimize your JS. The js step will do concat and ugilfy in defult. Use <!-- build:copy --> to only concat file without ugilfy. Because i want config file could be modified manually. Obfuscated file makes hard to modify.

sample.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  <!-- build:css css/all.css -->
  <link rel="stylesheet" href="css/basics/normalize.css"  />
  <link rel="stylesheet" href="css/desktop/1.css"  />
  <link rel="stylesheet" href="css/desktop/2.css"  />
  <!-- endbuild -->

  <!-- build:js js/build/vendor.js -->
  <script src="js/vendor/jquery-1.9.1.min.js"></script>
  <script src="js/vendor/jquery.scrollTo-1.4.3.1.js"></script>
  <script src="js/vendor/handlebars-v1.3.0.js"></script>
  <!-- endbuild -->

  <!-- build:copy js/config.js -->
  <script src="js/settings/config.js"></script>
  <!-- endbuild -->

If you want to overwrite the default cssmin and concat settings you could just add the block in config. I overwrite the original concat process function. Because the css url path will be wrong if your source css and merged css are in different folder. See my comment inside function.

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  cssmin: {
    options: {
      noAdvanced: true
    }
  },

  concat: {
    options: {
      separator: '\n',
      process: function(src, filepath) {
        var cssPatt = new RegExp('(\/.*\/).*\.css$');
        var file = cssPatt.exec(filepath);

        if (file) {
          // rewrite the url in css file.
          // url(../../img/bluebg.jpg) -> url(../img/bluebg.jpg)
          // url(../../../img/bluebg2.jpg) -> url(../img/bluebg.2jpg)

          var urlPatt = /url\((\.\.\/){1,}(.*)\)/g;
          return src.replace(urlPatt, function(match, p1, p2) {
            var replacedUrl = 'url(../' + p2 + ')';
            return replacedUrl;
          });
        }

        return src;
      }
    }
  }

To prevent browser cache the js/css files need to change file name when the content are different. We use rev plugin to add md5 in file name. Don’t worry about the filename changed because usemin is smart enough to search files in assetsDirs: ['<%= gamenow.dist %>'].

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
  rev: {
    dist: {
      files: {
        src: [
          '<%= gamenow.dist %>/js/*.js',
          '<%= gamenow.dist %>/js/build/*.js',
          '<%= gamenow.dist %>/css/*.css',
        ]
      }
    }
  },

In my module i also want to keep the originl .html file for debug. I add a task debugHtml to get it. I copy .html files to dist folder with -dev filename. Task nonOptimizeFiles is to copy the rest files.

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
  copy: {
    filesNeedRewrite: {
      files: [{
        expand: true,
        dot: true,
        cwd: '<%= gamenow.app %>',
        dest: '<%= gamenow.dist %>',
        src: ['*.html']
      }]
    },
    debugHtml: {
      files: [{
        expand: true,
        src: '<%= gamenow.app %>/*.html',
        dest: '<%= gamenow.dist %>/',
        rename: function(dest, src) {
          //rename *.html to *-dev.html
          var filename = path.basename(src, '.html');
          var extname = path.extname(src);
          return dest + filename + "-dev" + extname;
        }
      }]
    },
    nonOptimizeFiles: {
      files: [{
        expand: true,
        dot: true,
        cwd: '<%= gamenow.app %>',
        dest: '<%= gamenow.dist %>',
        src: [
          '*.{ico,png,xml,jsp,json}',
          '.htaccess',
          'WEB-INF/**',
          'img/**',

          //copy original js,css files for debug
          'js/*/**',
          'css/*/**'
        ]
      }]
    }
  }

The final optimized sample.html would be like

sample.html
1
2
3
  <link rel="stylesheet" href="css/46d81h87.all.css"  />
  <script src="js/build/xh64yr8d.vendor.js"></script>
  <script src="js/2sd4fgcv.config.js"></script>

The grunt build task sequence.

sample.html
1
2
3
4
5
6
7
8
9
10
11
  grunt.registerTask('build', [
    'useminPrepare',
    'concat',
    'cssmin',
    'uglify',
    'copy:filesNeedRewrite',
    'rev',
    'usemin',
    'copy:debugHtml',
    'copy:nonOptimizeFiles'
  ]);

My package.json

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "name": "ugamenow",
  "version": "0.0.0",
  "devDependencies": {
    "grunt": "~0.4.1",
    "load-grunt-tasks": "~0.2.0",
    "time-grunt": "~0.2.0",

    "grunt-contrib-clean": "~0.5.0",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-cssmin": "~0.10.0",
    "grunt-contrib-copy": "~0.5.0",
    "grunt-contrib-uglify": "~0.2.0",
    "grunt-rev": "~0.1.0",
    "grunt-usemin": "~2.3.0"
  }
}

Comments