google 闭包库中的命名空间问题

Namespace issue in google closure library

教程中提到here,模块提供的命名空间是:

goog.provide('tutorial.notepad.Note');

但我想知道为什么不这样做:

goog.provide('tutorial.notepad');

因为,根据下面提到的规则:

tutorial = tutorial || {};
tutorial.notepad = tutorial.notepad || {};
tutorial.notepad.Note = tutorial.notepad.Note || {};

如果我们刚刚提供:

goog.provide('tutorial.notepad'); 那么,我们已经有:

tutorial = tutorial || {};
tutorial.notepad = tutorial.notepad || {};

我们可以添加 属性 Note

tutorial.notepad.Note = function() {};

因此,我的问题是:

为什么不直接声明 goog.provide('tutorial.notepad') 然后用它来包含顶级 Classes,而是建议对每个 Class 使用 goog.provide('tutorial.notepad.Note') 这对我来说是多余的.

goog.provide('tutorial.notepad'); 在 "tree of dependencies" 中为该命名空间创建一个条目,但它不会为 class tutorial.notepad.Note 创建一个条目。如果您像在示例代码中那样手动创建 tutorial.notepad.Note,那么您不会激活闭包编译器机制以将 class tutorial.notepad.Note 包含到闭包编译器使用的命名空间依赖项树中。

原因是 goog.provide 被闭包编译器用来设置依赖关系树,用于确定要加载的名称空间以及加载顺序。

通过不使用 goog.provide,而是用您显示的代码模拟其效果,编译器不会了解 class Note 以及它如何适应树名称空间和 classes 及其依赖项。

运行 基于闭包编译器的代码有两种方法:已编译和未编译。这些中的每一个都以不同的方式构建和使用名称空间依赖关系树:

  • UNCOMPILED 闭包编译器的一大优点是您可以 运行 所有代码都未编译。该过程中的一个必要步骤是使用 depswriter.py,一个 Python 程序,它读取所有源文件(寻找 goog.providegoog.require 调用)并生成一个文件 deps.js。那个deps.js文件就是命名空间依赖树的体现。这是我项目 deps.js 文件中的一个示例行(共 333 行):

    goog.addDependency('../../../src/lab/app/ViewPanner.js',
      ['myphysicslab.lab.app.ViewPanner'], ['myphysicslab.lab.util.DoubleRect',
       'myphysicslab.lab.util.UtilityCore', 'myphysicslab.lab.util.Vector',
       'myphysicslab.lab.view.CoordMap', 'myphysicslab.lab.view.LabView'], false);
    

当我 运行 我的代码处于未编译状态时,有一个 <script> 标记 运行 是 deps.js 脚本。这样做会导致创建命名空间依赖树的内存版本,goog.require 在 运行 时访问它,以加载特定 class 所需的任何其他文件。

  • COMPILED 编译器(Java 程序)作为编译过程的一部分,执行与上述相同的事情。不同之处在于,生成的名称空间依赖树仅在编译期间用于确定定义 class 的顺序,确定需要什么等。编译完成后,名称空间依赖树将被丢弃.

参考文献:

https://github.com/google/closure-compiler/wiki/Managing-Dependencies

https://github.com/google/closure-compiler/wiki/Debugging-Uncompiled-Source-Code


回复您的评论:

Why not just declare goog.provide('tutorial.notepad') and then use that to include top level Classes, instead its recommended to use goog.provide('tutorial.notepad.Note') for each Class which feels redundant to me.

我认为这涉及闭包编译器的目标和设计问题。正如@Technetium 指出的那样,使用闭包编译器 "is extremely verbose" - 它需要用注释注释您的 Java 脚本代码,以说明每个方法(函数)的输入和输出类型以及每个 属性 对象 (class).

(我不是编译器专家,但是)我认为按照您的建议进行操作需要编译器 "understand" 您的代码并猜测您认为什么是 class,以及什么您认为是 class 的构造函数和方法或其他属性。这将是一个 比闭包编译器设计者到达的问题更难的问题 - 特别是因为 JavaScript 是一种 "loose" 语言,它允许你几乎任何你能想到的。

实际上我发现 goog.provide 一点也不麻烦。我通常只为每个文件定义一个 class。我发现更麻烦的是所有 goog.require 语句。我通常可以在一个文件中包含 20 或 30 个这样的文件,并且这个文件列表经常以类似的方式重复 class。我的代码中出现了 3870 次 goog.require

即使这样也可以,但更糟糕的是闭包编译器有一个 goog.scope 机制,可以让你使用更短的名字,就像我可以说 Vector 而不是 new myphysicslab.lab.util.Vector。这很好,但问题是每个 class 你已经 goog.required 然后你必须在 goog.scope 中创建一个短变量,像这样:

var Vector = myphysicslab.lab.util.Vector;

无论如何,我的观点是:是的,闭包编译器需要比原始 JavaScript 更多的代码。但是 goog.provide 是这方面最少的问题。

还有一件事:用户@Technetium 说

The real reason to use it is to run your Google Closure code through the javascript-to-javascript Closure Compiler that removes dead/unused code while minimizing and obfuscating the pieces you do use.

虽然这是一个非常有用的功能,但使用闭包编译器还有另一个非常重要的原因:类型检查。如果您花时间为您的函数添加注释,那么编译器将 "have your back" 捕获错误。这对任何项目都有很大的帮助,但当您有多个开发人员在一个项目上工作时就变得至关重要,这也是 Google 开发闭包编译器的主要原因之一。

这里有几件事在起作用:

  1. 每个命名空间只能调用一次goog.provide()

您目前可能在单个文件中定义了 "class",例如 Note.js,现在 goog.provide('tutorial.notepad');。但是,如果您添加另一个文件,比如 Tab.js,其中包含 "class" tutorial.notepad.Tab,当 Tab.js 也调用 goog.provide('tutorial.nodepad').

  1. 调用 goog.provide('tutorial.notepad') 不会告诉闭包编译器有关 "class" tutorial.notepad.Note

Google 原始库形式的闭包代码极其冗长。使用它的真正原因是 运行 您的 Google 闭包代码通过 javascript-to-javascript 闭包编译器删除 dead/unused 代码,同时最小化和混淆您 使用的作品。虽然您的示例在调试模式下工作,因为它没有利用闭包编译器,但一旦闭包编译器 运行 并尝试构建依赖关系图,它将无法找到 tutorial.notepad.Note class 时某些东西试图通过 goog.requires('tutorial.notepad.Note') 引用它。如果您想了解有关此依赖关系图如何工作的更多信息,owler 的回答是一个很好的起点。

顺便说一句,请注意我在引号中使用 "class",而且是故意的。虽然 Google Closure 通过其 @constructor 注释在许多方面提供了面向对象编程的外观和感觉,并且通过 goog.provide/goog.require 语法粗略地模拟了 package/import,但它仍然是 JavaScript 在一天结束时。