Extern 和 Exports

Extern 的用途

Extern 是向 Closure Compiler 告知在高级编译期间不应重命名的符号名称的声明。之所以称为“外部”,是因为它们通常由编译之外的代码(如原生代码或第三方库)定义。因此,extern 通常还有类型注释,以便 Closure 编译器对这些符号的使用情况进行类型检查。

通常,最好将 extern 视为实现者与某段编译代码的使用方之间的 API 协定。exex 定义了实现者承诺提供的内容以及使用方可以依赖的内容。双方都需要合同副本。

Extern 与其他语言的头文件类似。

Externs 语法

Extern 是与为 Closure Compiler 注释的普通 JavaScript 非常相似的文件。主要区别在于,它们的内容绝不会作为编译输出的一部分输出,因此没有任何值有意义,只有名称和类型。

下面是一个简单库的 externs 文件的示例。

// The `@externs` annotation is the best way to indicate a file contains externs.

/**
 * @fileoverview Public API of my_math.js.
 * @externs
 */

// Externs often declare global namespaces.

const myMath = {};

// Externs can declare functions, most importantly their names.

/**
 * @param {number} x
 * @param {number} y
 * @return {!myMath.DivResult}
 */
myMath.div = function(x, y) {};  // Note the empty body.

// Externs can contain type declarations, such as classes and interfaces.

/** The result of an integer division. */
myMath.DivResult = class {

  // Constructors are special; member fields can be declared in their bodies.

  constructor() {
    /** @type {number} */
    this.quotient;
    /** @type {number} */
    this.remainder;
  }

  // Methods can be declared as usual; their bodies are meaningless though.

  /** @return {!Array<number>} */
  toPair() {}

};

// Fields and methods can also be declared using prototype notation.

/**
 * @override
 * @param {number=} radix
 */
myMath.DivResult.prototype.toString = function(radix) {};
    

--externs 标志

通常,@externs 注解是告知编译器文件包含 extern 的最佳方式。您可以使用 --js 命令行 flag 将此类文件添加为普通源文件,

不过,还有一种方法可以指定 extern 文件。--externs 命令行标记可用于明确传递 extern 文件。不建议使用此方法。

使用 Extern

上面的 Exexents 可以如下使用。

/**
 * @fileoverview Do some math.
 */

/**
 * @param {number} x
 * @param {number} y
 * @return {number}
 */
export function greatestCommonDivisor(x, y) {
  while (y != 0) {
    const temp = y;
    // `myMath` is a global, it and `myMath.div` are never renamed.
    const result = myMath.div(x, y);
    // `remainder` is also never renamed on instances of `DivResult`.
    y = result.remainder;
    x = temp;
  }
  return x;
}
    

如何使用 Closure Compiler Service API 添加 Extern

Closure Compiler 应用和 Closure Compiler Service API 都允许执行外部声明。不过,Closure Compiler 服务界面不提供指定 extern 文件的界面元素。

您可以通过以下三种方式向 Closure Compiler 服务发送 extern 声明:

  • 将包含 @externs 注解的文件作为源文件传递。
  • 将 JavaScript 传递给 js_externs 参数中的 Closure Compiler 服务。
  • 将 JavaScript 文件的网址传递给 externs_url 参数中的 Closure Compiler 服务。

使用 js_externs 和使用 externs_url 之间的唯一区别在于 JavaScript 如何传达给 Closure 编译器服务。

导出目的

导出是另一种用于在编译后提供符号一致名称的机制。它们的使用不如 Exex,通常也让人感到困惑。除简单情况外,最好避免这种情况。

导出所依赖的事实是,Closure 编译器不会修改字符串字面量。通过将对象分配给使用字面量命名的属性,即使在编译后,仍可通过该属性名称使用该对象。

下面是一个简单示例。

/**
 * @fileoverview Do some math.
 */

// Note that the concept of module exports is totally unrelated.

/** @return {number} */
export function myFunction() {
  return 5;
}

// This assignment ensures `myFunctionAlias` will be a global alias exposing `myFunction`,
// even after compilation.

window['myFunctionAlias'] = myFunction;
    

如果您使用的是 Closure 库,还可以使用 goog.exportSymbolgoog.exportProperty 函数声明导出。

如需了解详情,请参阅这些函数的 Closure 库文档。不过请注意,它们具有特殊的编译器支持,并且会在已编译的输出中完全转换。

与导出有关的问题

导出与外部不同,它们仅创建公开的别名供使用方引用。在编译的代码中,导出的符号仍会重命名。因此,导出的符号必须是常量,因为在您的代码中重新分配符号会导致公开的别名指向错误的内容。

对于重命名的实例属性而言,重命名的这种细微差别特别复杂。

从理论上讲,与 extern 相比,导出所允许的代码大小可以更小,因为长名称仍可以更改为代码中的短名称。实际上,这些改进通常非常小,并且无法证明导出会造成混淆。

此外,导出功能也不会为消费者提供可按照 EXED 方式使用的 API。与导出功能相比,导出功能会记录您打算公开的符号及其类型,并为您提供添加使用情况信息的地方。此外,如果您的消费者还在使用 Closure 编译器,则需要借助 Exexsure 编译器进行编译。