简体   繁体   中英

Prevent Google Closure Compiler from renaming settings objects

I'm trying to get the Google Closure Compiler to not rename objects when passed as settings or data to a function. By looking at the annotations present in jQuery, I thought this would work:

/** @param {Object.<string,*>} data */
window.hello = function(data) {
    alert(data.hello);
};
hello({ hello: "World" });

However, it ends up like this:

window.a = function(b) {
  alert(b.a)
};
hello({a:"World"});

The ajax function found here has this annotation and it appears to work. So, why won't this? If data is the return value from an external source or a settings object I'd like to be able to tell the compiler to not touch it, using the this["escape"] trick is to intrusive for something like this in my opinion.

Here's a better example

function ajax(success) {
      // do AJAX call
    $.ajax({ success: success });
}
ajax(function(data) {
    alert(data.Success);
});

Output:

$.b({c:function(a){alert(a.a)}});

success has been renamed to c and Success (with a capital S) has been renamed to a .

I now compile the same code with the jQuery 1.6 externs file and get the following output:

$.ajax({success:function(a){alert(a.a)}});

It also produces a warning that the property Success is not defined, as I would expect, but it cannot rename Success to simply a , that will still break my code. I look at the annotation present for the ajax and I find this type expression {Object.<string,*>=} , I annotate my code accordingly, and recompile. Still not working...

Since your focus seems to be on the source rather than the output, it seems like what you're focused on is DRY (Don't Repeat Yourself). Here's an alternative DRY solution.

You can run the Closure Compiler with --create_name_map_files . Doing so emits a file named _props_map.out . You can have your JSON-emitting server-side calls (ASP.Net MVC or whatever it might be) use these maps when emitting their JSON, so they're actually emitting minified JSON that leverages the renames the Closure Compiler performed. This way you can change the name of a variable or property on your Controller and your scripts, add more, etc, and the minification carries through from the scripts all the way back to the Controller output. All of your source, including the Controller, continues to be non-minified and easy to read.

I think what you're really trying to do is stop it from renaming property names on the object coming back from an AJAX controller on the server, which obviously would break the call.

So when you call

$.ajax({
    data: { joe: 'hello' },
    success: function(r) {
        alert(r.Message);
    }
});

You want it to leave Message alone, correct?

If so that's done by the way you mentioned earlier, but it's compiled nicely to .Message in the output. The above becomes:

var data = {};
data['joe'] = 'hello';

$.ajax({
    data: data,
    /**
    @param Object.<string> r
    */
    success: function (r) {
        alert(r['Message']);
    }
});

Minifies now to:

$.ajax({data:{joe:"hello"},success:function(a){alert(a.Message)}});

By using r['Message'] instead of r.Message , you prevent the property rename by the minifier. That's called the export method, which as you'll notice in the Closure Compiler documentation is preferred over externs. That is, if you use the externs method to do this instead, you're going to make several people at Google angry. They even put an ID on the heading named, "no": http://code.google.com/closure/compiler/docs/api-tutorial3.html#no

That said, you can also do this using the externs method, and here it is in all its weirdness:

externs.js

/** @constructor */
function Server() { };

/** @type {string} */
Server.prototype.Message;

test.js

$.ajax({
    data: { joe: 'hello' },
    /**
    @param {Server} r
    */
    success: function (r) {
        alert(r.Message);
    }
});

C:\\java\\closure>java -jar compiler.jar --externs externs.js --js jquery-1.6.js --js test.js --compilation_level ADVANCED_OPTIMIZATIONS --js_output_file output.js

And out comes:

$.ajax({data:{a:"hello"},success:function(a){alert(a.Message)}});

Unfortunately, doing data["hello"] all over the place is the recommended (and official) Closure way of preventing variable renaming.

I agree totally with you that I do not like this a single bit. However, all other solutions will give you sub-optimal results with the compilation or may break in obscure situations -- and if you're willing to live with sub-optimal results, then why use the Closure Compiler in the first place?

However, data returned from a server is really all you need to handle, because you should be able to safely allow Closure to rename everything else in your program. Over the time, I've found that it is best to write wrappers that will clone data coming back from a server. In other words:

var data1 = { hello:data["hello"] };
// Then use data1.hello anywhere else in your program

This way, any unmangled object only lives briefly right after being deserialized from Ajax. Then it gets cloned into an object which can be compiled/optimized by Closure. Use this clone everything in your program, and you get the full benefits of Closure's optimizations.

I've also found that it is useful to have such a "processing" function immediately processing everything that comes via Ajax from a server -- in addition to cloning the object, you can put post-processing code in there, as well as validations, error corrections and security checks etc. In many web apps, you already have such functions to do such checking on returned data in the first place -- you NEVER trust data returned from a server, now do you?

A little late to the game, but I got around this just by writing a pair of gateway functions that process all of my inbound and outbound ajax objects:

//This is a dict containing all of the attributes that we might see in remote
//responses that we use by name in code.  Due to the way closure works, this
//is how it has to be.
var closureToRemote = {
  status: 'status', payload: 'payload', bit1: 'bit1', ...
};
var closureToLocal = {};
for (var i in closureToRemote) {
  closureToLocal[closureToRemote[i]] = i;
}
function _closureTranslate(mapping, data) {
  //Creates a new version of data, which is recursively mapped to work with
  //closure.
  //mapping is one of closureToRemote or closureToLocal
  var ndata;
  if (data === null || data === undefined) {
    //Special handling for null since it is technically an object, and we
    //throw in undefined since they're related
    ndata = data;
  }
  else if ($.isArray(data)) {
    ndata = []
    for (var i = 0, m = data.length; i < m; i++) {
      ndata.push(_closureTranslate(mapping, data[i]));
    }
  }
  else if (typeof data === 'object') {
    ndata = {};
    for (var i in data) {
      ndata[mapping[i] || i] = _closureTranslate(mapping, data[i]);
    }
  }
  else {
    ndata = data;
  }
  return ndata;
}

function closureizeData(data) {
  return _closureTranslate(closureToLocal, data);
}
function declosureizeData(data) {
  return _closureTranslate(closureToRemote, data);
}

The handy thing here is that the closureToRemote dict is flat - that is, even though you have to specify the names of child attributes so that the closure compiler knows, you can specify them all on the same level. This means that the response format can actually be a fairly intricate hierarchy, it's just the base keys that you will be accessing directly by name that need to be hard coded somewhere.

Whenever I am about to make an ajax call, I pass the data I'm sending through declosureizeData(), implying that I am taking the data out of closure's namespacing. When I receive data, the first thing I do is run it through closureizeData() to get the names into closure's namespace.

Note please that the mapping dictionary only needs to be one place in our code, and if you have well-structured ajax code that always comes into and out of the same code path, then integrating it is a "do-it-once-and-forget-about-it" type of activity.

You could try defining it as a record type,

/**
  @param {{hello: string}} data
*/

That tells it data has property hello of type string.

Apparently annotations are not to blame here, simply by introducing some unused properties to the settings object will result in the compiler renaming stuff.

I'd like to know where these came from and the only logical explanation I have so far (confirmed here ), is that the compiler keeps a global name table of things it won't rename. Simply having a extern with a name will result in any property of that name to be keept around.

/** @type {Object.<string,*>} */
var t = window["t"] = {
  transform: function(m, e) {
    e.transform = m;
  },
  skew: function(m, e) {
    e.skew = m;
  }
}

/** 
 * @constructor
 */
function b() {
  this.transform = [];
  this.elementThing = document.createElement("DIV");
}

t.transform(new b().transform, new b().elementThing);

Results in the following output:

function c() {
    this.transform = [];
    this.a = document.createElement("DIV")
}(window.t = {
    transform: function (a, b) {
        b.transform = a
    },
    b: function (a, b) {
        b.b = a
    }
}).transform((new c).transform, (new c).a);

Notice how transform isn't renamed but elementThing is, even if I try to annotate this type I can't get it to rename transform accordingly.

But if I add the following extern source function a() {}; a.prototype.elementThing = function() {}; function a() {}; a.prototype.elementThing = function() {}; it won't rename elementThing despite looking at the code, I can clearly tell that the type returned by the constructor is unrelated to the extern a , yet somehow, this is how the compiler does it. I guess this is just a limitation of the closure compiler, which I think is a darn shame.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM