- Your dev setup is on Linux, or other Bash-compatible. (Build script written for Bash.)
- You use Git for SCM. (The build script will use git for some operations. Feel free to alter the script, and get rid of this dependency.)
- The app is structured as recommended by the angular-seed project. (Build script expects certain folders to be present at specific locations. You can adapt it to your project structure.)
Go grab the following:
- YUI Compressor
- Gzip (Installed by default on most *NIX systems. Pull from distro repos otherwise.)
- Node.js (For testing stuff on your localhost. Not required if you have/prefer some other server for delivering static content.)
- Stomach for shell scripts
I wanted to split my web application into two distinct components:
- A client-side, JS-driven presentation layer.
- A lightweight, REST-based backend.
I’ve had to sort out a lot of issues to get both to cooperate while running on different servers on different domains, use digest-based authentication instead of cookies (REST is stateless), and so on, but that’s another post. This one focuses on efficiently delivering the UI portion – HTML + CSS + JS + Media – which from a server POV is static content.
Preparing AngularJS Scripts for Minification
The AngularJS docs provide some information on how to prepare controllers for minification here. Quoting from the page:
PhoneListCtrlcontroller, all of its function arguments would be minified as well, and the dependency injector would not be able to identify services correctly.
PhoneListCtrl is part of the angular-phonecat application, used for driving the on-site tutorial.
Basically, every controller defined by your application needs to be explicitly injected with whatever dependencies it has. For the example above, it looks something like:
PhoneListCtrl.$inject = ['$scope', '$http'];
There is one more way defined on the site, but I prefer the method above.
However, this is not enough to get minified scripts working right. YUI Compressor changes closure parameter names, and this doesn’t go down well with Angular. You need to use inline annotations in defining custom services. You can find a usage example here.
Additionally, you can collate all content from controllers.js, directives.js, services.js and filters.js into app.js to reduce the number of calls made to the server.
Don’t forget to modify your index.html / index-async.html to reflect this change.
The Build Script
If you’re sticking to the folder structure provided by angular-seed, you’ll have an app folder in your project root. Adjacent to this, create a build folder to contain the minified and compressed output files generated by the build script. You can tell git to ignore this folder by adding the following line to .gitignore:
You can put your build script anywhere you like, and run it from anywhere in the project folder. I have put it inside the conveniently provided scripts folder.
#!/bin/bash ccred=$(echo -e "\033[0;31m") ccyellow=$(echo -e "\033[0;33m") ccgreen=$(echo -e "\033[0;32m") ccend=$(echo -e "\033[0m") exit_code=0 cd "$(git rev-parse --show-toplevel)" #Minify echo -e "$ccyellow========Minify========$ccend" for ext in 'css' 'js' do for infile in `find ./app -name *.$ext |grep -v min` do outfile="$(echo $infile |sed 's/\(.*\)\..*/\1/').min.$ext" echo -n -e "\nMinifying $infile to $outfile: " if [ ! -f "$outfile" ] || [ "$infile" -nt "$outfile" ] then yuicompressor "$infile" > "$outfile" if [ `echo $?` != 0 ] then exit_code=1 echo -e "\n$ccred========Failed minification of $infile to $outfile . Reverting========$ccend\n" >&2 git checkout -- "$outfile" || rm -f "$outfile" else echo $ccgreen Success.$ccend fi else echo $ccgreen Not modified.$ccend fi done done #Compress / Copy echo -e "\n\n$ccyellow========Compress / Copy========$ccend\n" for infile in `find ./app -type f -not -empty` do filetype="$(grep -r -m 1 "^" "$infile" |grep '^Binary file')" outfile="./build/$(echo $infile |cut -c7-)" mkdir -p $(dirname "$outfile") if [ ! -f "$outfile" ] || [ "$infile" -nt "$outfile" ] then if [ "$filetype" = "" ] #Compress text files then echo -n -e "\nCompressing $infile to $outfile: " gzip -c "$infile" > "$outfile" else #Copy binary files as is echo -e -n "\nCopying $infile to $outfile: " cp "$infile" "$outfile" fi if [ `echo $?` != 0 ] then exit_code=2 echo -e "\n$ccred========Failed compress / copy of $infile to $outfile . Reverting========$ccend\n" >&2 else echo $ccgreen Success.$ccend fi else echo -e "$infile -> $outfile: $ccgreen Not modified.$ccend\n" fi done echo -e "\n$ccyellow========Finished========$ccend" exit $exit_code
Once you run this script, every app/file.[css | js] would have a working copy at build/file.min.[css | js]. Every other file in the app folder will be either:
- compressed and copied (name unchanged) into the build folder if it is a text file, or
- simply copied into the build folder if it is a binary file (like an image).
Your CSS and JS references need to be updated to their corresponding min versions in index.html / index-async.html.
Now that you’ve got a compressed, minified version of your app in the build folder, you can deploy it to any static server. But you do need to set your HTTP response headers properly, or the browser WILL show garbage. Most importantly, any compressed content must be served with the HTTP response header:
Additionally, for every file that is static content, it makes sense to set a far future date using an Expires header similar to the following:
Expires: Thu, 31 Dec 2037 20:00:00 GMT
The NodeJS web-server.js Script
The contents of the build folder are technically ready to be uploaded to any web server, but you will probably want to run the app from your localhost to first check if everything works fine. The built-in web-server.js is very useful to quickly launch and test your app, but it needs a few mods in order to serve the compressed content from the build folder correctly. The Content-Encoding header is sufficient to render the page correctly, but if you’re a stickler for good YSlow grades even on your localhost, you will want to add the Expires headers as well. Search for the following response codes in your web-server.js and add the lines listed below:
- 200 (writeDirectoryIndex), 500, 404, 403, 301:
'Expires': 'Thu, 31 Dec 2037 20:00:00 GMT',
- 200 (sendFile) (After var file = fs.createReadStream(path);):
That’s it! Now when you run the web-server.js, all content from the build folder will be correctly served with the ‘gzip’ header (unless it is a binary).