防止Google Closure Compiler重命名设置对象

时间:2011-10-19 15:29:40

标签: javascript google-closure-compiler

我正在尝试让Google Closure Compiler在作为设置或数据传递给函数时不重命名对象。通过查看jQuery中的注释,我认为这样可行:

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

然而,它最终会像这样:

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

找到的ajax函数here具有此注释,似乎可以正常工作。那么,为什么不呢?如果数据是来自外部源或设置对象的返回值,我希望能够告诉编译器不要触摸它,在我看来,使用this["escape"]技巧会侵入这样的东西。< / p>

这是一个更好的例子

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

输出:

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

success已重命名为cSuccess(带有大写字母S)已重命名为a

我现在使用jQuery 1.6 externs file编译相同的代码并获得以下输出:

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

它还会产生警告,指出属性Success未定义,正如我所料,但它无法将Success重命名为a,这仍然会破坏我的代码。我查看了ajax的注释,我找到了这个类型表达式{Object.<string,*>=},我相应地注释了我的代码,然后重新编译。仍然没有工作......

6 个答案:

答案 0 :(得分:8)

由于你的注意力集中在源头而不是输出上,所以你所关注的似乎是DRY(不要重复自己)。这是另一种DRY解决方案。

您可以使用--create_name_map_files运行Closure Compiler。这样做会发出名为_props_map.out的文件。你可以让你的JSON发射服务器端调用(ASP.Net MVC或它可能是什么)在发出它们的JSON时使用这些映射,因此它们实际上发出缩小的JSON,利用重命名Closure Compiler执行。这样,您可以在Controller和脚本上更改变量或属性的名称,添加更多等,并且缩小从脚本一直传回Controller控制器输出。您的所有来源(包括控制器)仍然是非缩小且易于阅读的。

答案 1 :(得分:6)

我认为你真正要做的是阻止它重命名从服务器上的AJAX控制器返回的对象上的属性名称,这显然会打破这个调用。

所以当你打电话

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

你想让它单独留言,对吗?

如果是这样,你通过前面提到的方式完成,但它在输出中很好地编译为.Message。以上变为:

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

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

现在缩小为:

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

使用r['Message']代替r.Message,可以防止缩小器重命名属性。这就是所谓的导出方法,正如您在Closure Compiler文档中所注意到的那样,它比externs更受欢迎。也就是说,如果你使用externs方法来做这件事,那么你就会让Google的几个人生气。他们甚至在名为“no”的标题上放了一个ID: http://code.google.com/closure/compiler/docs/api-tutorial3.html#no

也就是说,你也可以使用externs方法做到这一点,这里有一点奇怪:

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&gt; java -jar compiler.jar --externs externs.js --js jquery-1.6.js --js test.js --compilation_level ADVANCED_OPTIMIZATIONS --js_output_file output.js

出来了:

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

答案 2 :(得分:3)

不幸的是,在所有地方进行data["hello"]是阻止变量重命名的推荐(和官方)Closure方法。

我完全同意你的意见,我不喜欢这一点。但是,所有其他解决方案都会在编译时为您提供次优结果,或者可能会在不明显的情况下中断 - 如果您愿意接受次优结果,那么为什么要首先使用Closure Compiler?

但是,从服务器返回的数据实际上只需要处理,因为您应该能够安全地允许Closure重命名程序中的所有其他内容。随着时间的推移,我发现最好编写包装器来克隆从服务器返回的数据。换句话说:

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

这样,任何未编译的对象只能在从Ajax反序列化后立即生存。然后它被克隆到一个可以由Closure编译/优化的对象中。在程序中使用此克隆的所有内容,您将获得Closure优化的全部好处。

我还发现,让这样一个“处理”函数立即处理从服务器通过Ajax传输的所有东西是有用的 - 除了克隆对象之外,你可以将后处理代码放在那里,如以及验证,错误更正和安全检查等。在许多Web应用程序中,您已经有了这样的功能来首先检查返回的数据 - 您从不信任从服务器返回的数据,你呢?

答案 3 :(得分:1)

游戏有点晚了,但我只是编写了一对处理所有入站和出站ajax对象的网关函数来解决这个问题:

//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);
}

这里的方便之处在于closureToRemote dict是平的 - 即使你必须指定子属性的名称以便闭包编译器知道,你可以在同一级别指定它们。这意味着响应格式实际上可能是一个相当复杂的层次结构,它只是您将直接通过名称访问的基本密钥,需要在某处进行硬编码。

每当我要进行ajax调用时,我都会通过declosureizeData()传递我发送的数据,这意味着我将数据从闭包的命名空间中取出。当我收到数据时,我要做的第一件事就是通过closureizeData()运行它来将名称输入到闭包的名称空间。

请注意,映射字典只需要在我们的代码中占一个位置,如果你有一个结构良好的ajax代码,它总是进出相同的代码路径,那么集成它就是“do-it-曾经忘掉它的“活动类型。

答案 4 :(得分:0)

您可以尝试将其定义为记录类型

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

告诉它数据具有string类型的属性hello。

答案 5 :(得分:0)

显然注释不应该归咎于此,只需在设置对象中引入一些未使用的属性就会导致编译器重命名。

我想知道它们来自何处以及我迄今为止唯一的逻辑解释(已确认here),是编译器保留一个不会重命名的事物的全局名称表。只需拥有一个带名字的extern就会导致该名称的任何属性都被保留下来。

/** @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);

结果如下:

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);

请注意transform未重命名但elementThing是如何,即使我尝试注释此类型,也无法相应地重命名transform

但是,如果我添加以下外部源function a() {}; a.prototype.elementThing = function() {};,尽管查看代码,它也不会重命名elementThing,我可以清楚地告诉构造函数返回的类型与extern {{无关} 1}},但不知何故,这就是编译器的工作方式。我想这只是闭包编译器的一个限制,我认为这是一种耻辱。