Minification is an important topic in JavaScript development, because of the substantial savings in bandwidth that it brings, and the minified sizes of the various AspectJS components are given in the table you see here (minification performed using Microsoft AJAXMin). Note that the non-minified sizes include the extent of comments, and that all figures are rounded to the nearest 100 characters. Aggressive optimisation, in terms of renaming all internal symbols with one- or two-letter equivalents, has not been applied.
The least aggressive form of minification involves only the elision of whitespace, and your application will continue to operate correctly if you go no further than this when minifying the code that comprises AspectJS and any application that uses it. However, if you minify more aggressively, such that the minifier renames symbols (i.e. function and object names), then there is one consideration of which you must be aware otherwise your application will fail.
Contents |
Introduction Semantic Equivalence Controlling the Renaming Policy Pre-Processing |
Component | Non-Minified Size | Minified Size | |
AJS.js | 31.4k | 8.3k | |
AJS_Validator.js | 123.5k | 39.2k | |
AJS_Logger.js | 7.8k | 1.5k | |
AJS_ODL.js | 22.5k | 4.5k | |
AJS_DefMgr.js | 16.5k | 4.1k | |
ThrowException.js | 5.9k | 0.9k | |
ThrowException_Remote.js | 1.4k | 0.4k |
This issue pivots on the fundamental principle on which the AJS object operates, which is that there is a semantic equivalence in JavaScript between:
Obj.method ();
...and:
Obj["method"] ();
This is why you must specify method names as strings when working with the AJS object or its client components – doing so allows it to use array-style square-bracket syntax to replace the function-reference represented by Obj["method"] with a reference to a proxy function that manages execution of the method's affix(es) along with the method itself.
However, while minifiers will never change the contents of literal strings, they may rename object and method names arbitrarily. This means that the scenario illustrated in Example 1 is almost guaranteed (see below) to occur when minifying a tract of code that uses the AJS object or its clients (where you have taken no other steps).
Here, following minification, MyObj, someMethod, the AJS object and its addPrefix method have now been renamed to A, a, B and a respectively. However, the string denoting that method in the prefix-addition call still states "someMethod" as the interceptee. This will cause the code to execute incorrectly because the object called 'A' no longer has a method called someMethod.
In other words, what was someMethod (and is now a) will not acquire a prefix but will acquire an extra method called someMethod (which will be an instance of the AJS proxy-function). When your application calls what is now A, only a will execute, because the proxy and, from there, your affix(es) are never invoked.
The only time this will not happen is where you have a method called, say, 'A' and the minifier, by sheer chance, renames it to 'A' (hence the 'almost guaranteed' qualification above).
//- Example 1: Before minification ------------------------------------------- var MyObj = { someMethod : function () { } }; AJS.addPrefix (MyObj, "someMethod" function () { }); // Prefix applied - no problem. MyObj.someMethod (); // Prefix executes, then someMethod - no problem. //- After Minification ------------------------------------------------------- var A = { a : function () { } }; // Prefix will not be applied - instead, object A // will acquire an extra method called someMethod. B.a (A, "someMethod" function () { }); // Method a of A will now execute in a conventional // non-prefixed manner - this is not what we what. A.a ();
There is no way around this, as it is not imposed by the design of the AJS object, and the only solution is to use a minifier that allows control of its renaming policy. Microsoft AJAXMin supports such a feature, and the examples given hereon apply to that tool.
AJAXMin allows you to stipulate the new names that methods will have either through command-line arguments, or via an XML file (which is useful when you have a large number of objects whose re-naming must be controlled).
Example 2 shows the contents of a simple instance of such a file. It contains a 'root' element, and three 'rename' elements, each of which possess a 'from' and 'to' attribute, by which you state the name that you wish a given symbol to possess following minification.
Using such a feature on the pre-minification code in Example 1, we could preserve someMethod's name by ensuring the from and to attributes of the relevant rename-element in the XML listing could both have the value of "someMethod". This would preserve correctness in terms of using the AJS object, but it would also impinge upon the compression ratio we achieved, defeating somewhat the purpose of minifying the code in the first place.
Hence, as Example 2 shows, we can rename someMethod to simply 'aa' (or even just 'a', if you can do so without running out of one-letter symbol names), which will give us good compression.
<!-- Example 2 - - - - - - - - - - - - - - - - - - - - - - --> <root> <rename from = "someMethod" to = "aa" /> <rename from = "someOtherMethod" to = "ab" /> <rename from = "yetAnotherMethod" to = "ac" /> </root>
However, this is only half the equation. Given that we tell the minifier to rename someMethod to aa, we must also replace all instances of "someMethod" (i.e. the literal-string itself, not the method) in our code with "aa" too.
This will ensure that the AJS object is able to apply the interception mechanism to the method as if nothing had happened, and the best way to do this is to preprocess your code automatically, as a pre-minification step in your deployment process.
One way to do this is to run a preprocessor program written in, say, PHP, to which you feed your code-base, and to use regular expressions within that program to do a search-and-replace on the strings within your code. The output from the PHP program can then be fed into the minifier, and the AspectJS related code in your application will now execute as intended.
Example 3 gives a small but fully-functioning pre-processor, written in PHP. Here, $FileNameArray holds the names of the files that you wish to pre-process. This is passed to a function called Process that iterates over the array, opening each file and a corresponding output file in turn, and which passes the contents of each file to another function called ReplaceCommonNames.
That function runs a set of search-replace regular-expressions over the string that Process passes it, and returns the modified string back to the caller. Process then writes that string to the output file, and carries on to the next file-name in the array.
To use this in your own projects, you need only to modify the contents of $FileNameArray, and the regexes in ReplaceCommonNames, ensuring that the list of symbol-names in that function matches one-for-one the renaming-control list you use with the minifier. Obviously, you could beef it up somewhat to use a text file to define the list of search patterns and their corresponding replacements, and to use another file to define the list of input files.
A probably-obvious tip here: make sure the regexes search for "method-name" (i.e. with the quotes) rather than just method-name. This will ensure that no non-method-related string-literals in the code that happen to contain method-name are altered inadvertently.
<?php //- Example 3 ---------------------------------------------------------------- function ReplaceCommonNames ($Str) { $Str = preg_replace ('/\"someMethod\"/', '"aa"', $Str); $Str = preg_replace ('/\"SomeOtherMethod\"/', '"ab"', $Str); $Str = preg_replace ('/\"YetAnotherMethod\"/', '"ac"', $Str); return $Str; } function Process ($FileNameArray) { foreach ($FileNameArray as $FileName) { $InFile = fopen ($FileName . ".js", "rb"); $OutFile = fopen ($FileName . ".tmp.js", "wb"); $InSize = filesize ($FileName . ".js"); $InStr = fread ($InFile, $InSize); $OutStr = ReplaceCommonNames ($InStr); fwrite ($OutFile, $OutStr); fclose ($OutFile); fclose ($InFile); } } //============================================================================ $FileNameArray = Array ( 'File_0', 'File_1', 'File_2' ); Process ($FileNameArray); ?>