【问题标题】:Prevent Google Closure Compiler from renaming settings objects阻止 Google Closure Compiler 重命名设置对象
【发布时间】:2011-10-19 15:29:40
【问题描述】:

我试图让 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"});

找到hereajax 函数有这个注释并且它似乎可以工作。那么,为什么不呢?如果数据是来自外部源或设置对象的返回值,我希望能够告诉编译器不要触摸它,在我看来,使用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 已重命名为 cSuccess(带有大写字母 S)已重命名为 a

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

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

它还会产生一个警告,指出属性Success 未定义,正如我所料,但它不能将Success 重命名为简单的a,这仍然会破坏我的代码。我查看了为ajax 提供的注释,我找到了这个类型表达式{Object.&lt;string,*&gt;=},我相应地注释了我的代码,然后重新编译。还是不行……

【问题讨论】:

  • 为了让以后阅读本文的人更好地理解:链接的 JS 是一个外部文件。它仅与要编译的代码一起使用,以防止重命名“外部化”变量、属性和函数/方法。其中的注释仅表明编译时类型检查的正确用途。他们绝不会指示编译器不要重命名 jQuery 的方法和参数。

标签: javascript google-closure-compiler


【解决方案1】:

由于您的关注点似乎是源而不是输出,因此您关注的似乎是 DRY(不要重复自己)。这是另一种 DRY 解决方案。

您可以使用--create_name_map_files 运行闭包编译器。这样做会发出一个名为_props_map.out 的文件。你可以让你的 JSON 发射服务器端调用(ASP.Net MVC 或其他任何可能的东西)在发射它们的 JSON 时使用这些映射,因此它们实际上发射的是利用闭包编译器执行的重命名的缩小 JSON。通过这种方式,您可以更改控制器和脚本上的变量或属性的名称,添加更多等,并且缩小从脚本一直返回到控制器输出。您的所有源代码(包括控制器)都不会缩小且易于阅读。

【讨论】:

  • 这是我愿意采用的解决方案。需要做一些设置工作,但这是个好主意。
  • _props_map.out 文件中到底有什么内容?我已经建造了一个,但对我来说一点意义都没有......
  • NVM,我在运行编译器时甚至没有开启高级优化。
【解决方案2】:

我认为您真正想做的是阻止它重命名从服务器上的 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 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>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 实际上是我最终可能会做的事情,但它会让你远离使用动态对象的一些好处,这就是我在这里遇到的问题。
  • 我可能最终不会编译这些动态脚本,现在不确定是否有更好的方法。
  • 如果您采用上面的 Externs 方法,您可以为每个(我假设)发出 JSON 的 MVC 控制器添加一个条目,这实际上相当不错 - 您可以获得从来自的数据开始的类型安全控制器。我将发布第二个答案,重点是 DRY(不要重复自己)。
【解决方案3】:

不幸的是,到处都是data["hello"] 是推荐的(也是官方的)封闭方式来防止变量重命名。

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

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

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

这样,任何未损坏的对象都只会在从 Ajax 反序列化后短暂存活。然后它被克隆到一个可以由 Closure 编译/优化的对象中。在你的程序中使用这个克隆所有东西,你就可以从 Closure 的优化中获得全部好处。

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

【讨论】:

  • 我开始意识到试图避免“所有未引用”的方法是徒劳的......
  • 欢迎来到俱乐部...花了我一段时间才意识到徒劳...有点让我想起了老黑客电影“战争游戏”——一个奇怪的游戏,唯一的赢家是不玩。使用 Closure,唯一的方法就是按照他们的方式去做,或者根本不使用它。 :-(
【解决方案4】:

游戏有点晚了,但我只是通过编写一对处理我所有入站和出站 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 字典是扁平的——也就是说,即使您必须指定子属性的名称以便闭包编译器知道,您也可以在同一级别上指定它们。这意味着响应格式实际上可以是一个相当复杂的层次结构,它只是您将通过名称直接访问的基本键,需要在某处进行硬编码。

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

请注意,映射字典只需要在我们的代码中的一个位置,如果你有结构良好的 ajax 代码总是进出相同的代码路径,那么集成它就是“做它”一劳永逸”的活动。

【讨论】:

    【解决方案5】:

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

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

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

    【讨论】:

    • 即使这样有效,我也没有静态记录,它是一个包含很多属性、设置和数据的对象。我不知道架构,所以我无法定义它。我的工作理论是闭包编译器应该从该类型注释中获取它,但它似乎不起作用。
    【解决方案6】:

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

    我想知道这些是从哪里来的,到目前为止我唯一合乎逻辑的解释(确认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 尽管查看代码,我可以清楚地看出构造函数返回的类型与外部 a 无关,但不知何故,这就是编译器的做法。我想这只是闭包编译器的一个限制,我认为这是一种耻辱。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-10-15
      • 1970-01-01
      • 1970-01-01
      • 2011-05-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-03
      相关资源
      最近更新 更多