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

16

我正在尝试让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函数有这个注释,而且似乎可以工作。那么为什么这样不行呢?如果数据是来自外部源或一个设置对象的返回值,我希望能够告诉编译器不要改动它,使用this["escape"]技巧对于像这样的事情来说过于麻烦。

下面是更好的例子

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

输出:

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

success已更名为c,而Success(大写S)已更名为a

我现在使用jQuery 1.6外部文件编译相同的代码,得到以下输出:

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

它还会产生一个警告,指出未定义属性Success,这是我所期望的,但它无法将Success重命名为简单的a,这仍然会破坏我的代码。我查看了ajax的注释,并找到了这个类型表达式{Object.<string,*>=},我相应地注释了我的代码并重新编译。仍然不起作用...

1
为了更好地理解,任何未来阅读此内容的人:链接的JS是一个外部文件。它仅与编译代码一起使用,以防止重命名“外部化”的变量、属性和函数/方法。其中的注释仅指示编译时类型检查的正确使用方式。它们绝不指示编译器不要重命名jQuery的方法和参数。 - Kiruse
6个回答

8
由于你的关注点似乎在源代码而不是输出上,因此看起来你关注的是DRY(不要重复自己)。以下是一种替代DRY解决方案。
您可以使用--create_name_map_files运行Closure编译器。这样做会生成一个名为_props_map.out的文件。您可以让服务器端调用(例如ASP.Net MVC)在发出JSON时使用这些映射,以便它们实际上发出利用Closure编译器执行的重命名的缩小版JSON。这样,您可以更改控制器上变量或属性的名称,添加更多内容等,而缩小则从脚本一直到控制器输出中进行。您的所有源代码,包括控制器,都将保持非缩小且易于阅读。

那是我愿意采用的解决方案。需要一些设置工作,但那是一个非常好的主意。 - John Leidegren
_props_map.out文件里究竟是什么?我已经构建了一个,但是对我来说毫无意义... - John Leidegren
1
抱歉,我之前在运行编译器时甚至没有开启高级优化。 - John Leidegren

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

因此当你调用

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

你希望让它单独留下“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编译器文档中,您会注意到它比外部方法更受欢迎。也就是说,如果您使用外部方法来完成这项工作,您将会让Google的一些人感到生气。他们甚至在标题上放了一个ID,名为“no”:http://code.google.com/closure/compiler/docs/api-tutorial3.html#no 话虽如此,您也可以使用外部方法来完成这个操作,以下是所有奇怪的内容:
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

然后输出:

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

这正是我不想做的事情,这并不是因为编译器将 this["abc"] 转换为 this.abc 更短,而是在这种情况下我对输出并不关心,我关心的是输入,因为那是我要编写的内容。我不想到处写带括号的代码,这太啰嗦了。但是使用一个包含我的数据模式的 externs.js 实际上是我可能会采取的措施,但它会使你远离使用动态对象的一些好处,这就是我在这里遇到的问题。 - John Leidegren
我可能最终不会编译这些动态脚本,现在还不确定是否有更好的方法。 - John Leidegren
如果您采用以上的Externs方法,您可以为每个(我假设)发出JSON的MVC控制器添加一个条目,这实际上非常好 - 您会从控制器中获得始于数据的类型安全。我将发布第二个答案,重点是DRY(不要重复自己)。 - Chris Moschini

3

很不幸,在 Closure 中,推荐(并且是官方的)防止变量重命名的方法就是在所有地方使用 data["hello"]。我完全同意你的看法,我一点也不喜欢这种方法。然而,所有其他解决方案都会导致编译结果次优或在不常见的情况下出现故障——如果你愿意接受次优结果,那么使用 Closure 编译器又有何必呢?

不过,从服务器返回的数据才是真正需要处理的内容,因为你应该能够安全地允许 Closure 重命名程序中的其余部分。随着时间的推移,我发现编写包装器来克隆从服务器返回的数据是最好的方法。换句话说:

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

这样,任何未被破坏的对象在从Ajax反序列化后只能短暂存在。然后它会被克隆成一个可以被Closure编译/优化的对象。在程序中使用这个克隆体,您将获得Closure优化的全部好处。
我还发现,对于通过服务器传输的所有内容立即使用这样的“处理”函数进行处理非常有用。除了克隆对象之外,您还可以在其中放置后处理代码,以及验证、错误更正和安全检查等。在许多Web应用程序中,您已经有这样的函数来对返回的数据进行检查 - 您绝对不会信任从服务器返回的数据,是吧?

我开始意识到试图避免“全不带引号”方式的徒劳无功。 - John Leidegren
欢迎来到这个俱乐部... 我花了一段时间才意识到这种无用也是徒劳无益的... 有点让我想起那部古老的黑客电影《战争游戏》——一个奇怪的游戏,唯一的获胜策略就是不参与。对于Closure来说,唯一的方法就是按照它们的方式去做,否则就不要使用它。 :-( - Stephen Chung

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字典是扁平的 - 也就是说,尽管您必须指定子属性的名称以便closure编译器知道,但您可以在同一级别上指定它们。这意味着响应格式实际上可以是相当复杂的层次结构,只是基本键需要通过名称直接访问的那些键需要在某个地方进行硬编码。

每当我要进行ajax调用时,我会通过declosureizeData()传递要发送的数据,这意味着我正在将数据从closure的命名空间中取出。当我接收到数据时,我首先运行它通过closureizeData()将名称带入closure的命名空间。

请注意,映射字典只需要在我们的代码中出现一次,如果您有良好结构化的ajax代码,始终进入和离开相同的代码路径,那么集成它就是一种“一次性完成并忘记”的活动。


0
你可以尝试将其定义为记录类型,
/**
  @param {{hello: string}} data
*/

这告诉它数据具有字符串类型的 hello 属性。


即使那样做可以起作用,但我没有静态记录,它是一个具有许多属性、设置和数据的对象。我不知道模式,所以无法定义它。我的工作理论是,闭包编译器应该从类型注释中获取它,但似乎并没有起作用。 - John Leidegren

0

显然,注解并不是这里的问题,只要在设置对象中引入一些未使用的属性,编译器就会重命名代码。

我想知道这些来自哪里,到目前为止我得出的唯一合乎逻辑的解释(在这里得到证实),是编译器保留了一个全局名称表,其中列出了它不会重命名的东西。仅仅有一个带有名称的外部变量就足以使该名称的任何属性被保留下来。

/** @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,尽管看代码,我可以清楚地知道构造函数返回的类型与外部 a 不相关,但不知何故,编译器就是这样做的。我想这只是闭包编译器的一个限制,这真是太遗憾了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接