エクスターンとエクスポート

エクスターンの目的

エクスターンは、高度なコンパイル中に名前を変更すべきでないシンボルの名前を Closure Compiler に伝える宣言です。これらのシンボルは、多くの場合、ネイティブ コード、サードパーティ ライブラリなど、コンパイル以外のコードによって定義されるため、外部と呼ばれます。このため、extern には型アノテーションもあり、Closure Compiler はそれらの記号の使用を型チェックできます。

一般に、外部要素は、コンパイルされたコードの一部の実装者と使用者の間の API コントラクトと考えることをおすすめします。外部接続は、実装者が想定する内容と、コンシューマが使用できる機能を定義します。双方に契約のコピーが必要です。

エクスターンは、他の言語のヘッダー ファイルに似ています。

Externs の構文

エクスターンは、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 コマンドライン フラグを使用して通常のソースファイルとして含めることができます。

ただし、別の方法として、外部ファイルを指定する方法があります。--externs コマンドライン フラグを使用して、外部ファイルを明示的に渡すことができます。この方法はおすすめしません。

エクスターンの使用

上記の外部変数は次のように使用できます。

/**
 * @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 に Externs を含める方法

Closure Compiler アプリと Closure Compiler Service API ではどちらも外部宣言が可能です。ただし、Closure Compiler サービスの UI には、外部ファイルを指定するためのインターフェース要素がありません。

Closure Compiler サービスに外部宣言を送信する方法は 3 つあります。

  • @externs アノテーションを含むファイルをソースファイルとして渡します。
  • js_externs パラメータで、JavaScript を Closure Compiler サービスに渡します。
  • JavaScript ファイルの URL を externs_url パラメータで Closure Compiler サービスに渡します。

js_externs を使用する場合と externs_url を使用する場合の唯一の違いは、JavaScript が Closure Compiler サービスに通知される方法です。

エクスポートの目的

エクスポートは、コンパイル後にシンボルに一貫した名前を付けるためのメカニズムでもあります。外部に比べると有用性が低く、混乱を招くことが多いため、シンプルなケースを除くすべてのケースで、この方法は避けることをおすすめします。

エクスポートは、Closure Compiler が文字列リテラルを変更しないことに依存します。リテラルを使用して名前を付けたプロパティにオブジェクトを割り当てると、コンパイル後のプロパティ名でもそのオブジェクトを利用できるようになります。

簡単な例を次に示します。

/**
 * @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 Library を使用している場合は、goog.exportSymbol 関数と goog.exportProperty 関数を使ってエクスポートを宣言することもできます。

これらの関数について詳しくは、Closure Library のドキュメントをご覧ください。ただし、特別なコンパイラ サポートがあるため、コンパイル済み出力で完全に変換されることに注意してください。

エクスポートに関する問題

外部参照とは異なり、エクスポートでは、参照用の公開エイリアスのみが作成されます。コンパイル済みコード内では、エクスポートされたシンボルの名前が変更されます。このため、エクスポートされたシンボルは一定である必要があります。コード内で再割り当てすると、公開されるエイリアスが間違ったものを指してしまうためです。

名前の変更に関する細かな部分は、エクスポートされたインスタンス プロパティに関して特に複雑です。

理論上、エクスポートではコード内での長い名前の変更が可能なため、外部と比べてコードサイズを小さくすることができます。実際には、これらの改善は多くの場合、非常に軽微であり、エクスポートで作成される混乱を正当化するものではありません。

また、エクスポートには、外部とは異なり API を提供しません。エクスポートと比較して、外部は公開するシンボルとその型を文書化し、使用状況に関する情報を追加する場所を提供します。また、コンシューマーが Closure Compiler も使用している場合は、コンパイル先の外部外部デバイスが必要になります。