今から始める Google Closure Tools 2022-08 (Closure Library + Closure Compiler)

新しいプロジェクトで Closure Compiler を使おうと思ったら、Javaのバージョン古くて使えなかった。そしてClosure Libraryの `clocure-libaray/closure/goog/deps.js` が無くなった関係で `depswriter.py` が使えなくなっていた。
一旦は古いバージョンでやり過ごしたけど、せっかくなので新しいバージョンを入れて設定してみる。


# Closure Library と Closure Compiler のインストール

だいたいは
https://google.github.io/closure-library/develop/get-started
https://www.npmjs.com/package/google-closure-compiler
ここの通りに進める。

プロジェクトのディレクトリを作ってnpmで設定。nodejsとnpmは、昔独自にnodebrewで入れたやつは使わずにDebian10のパッケージを入れ直した。

## Closure Library のインストールと設定

```bash
npm init -y

npm install google-closure-library
npm install --save-dev google-closure-deps

mkdir js
mkdir public
```

npm でインストールした場合は `clocure-libaray/closure/goog/deps.js` があった。あれ？GitHubから持ってきた場合は無かったんだけども。

とりあえずサンプルをそのまま持ってくる。

```js:js/hello.js
/**
 * @fileoverview Closure getting started tutorial code example.
 */
goog.module('hello');

const {TagName, createDom} = goog.require('goog.dom');

/**
 * Appends an `h1` tag to the body with the message "Hello world!".
 */
function sayHi() {
  const newHeader = createDom(TagName.H1, {'style': 'background-color:#EEE'},
    'Hello world!');
  document.body.appendChild(newHeader);
}

sayHi();
```

```html:public/index.html
<!DOCTYPE html>
<meta charset="UTF-8">
<script src="google-closure-library/closure/goog/base.js"></script>
<script src="js/deps.js"></script>
<div>Content to be rendered below</div>
<script>
  goog.require('hello');
</script>
```

`public` が公開ディレクトリ。この中から`js`にアクセスしたいのでリンクを張る。そして依存関係を書いたスクリプトを生成する。


```bash:bin/deps-js.sh
#!/bin/bash

set -eux

cd `dirname $0`/../
cd public

ln -f -s ../node_modules/google-closure-library .
ln -f -s ../js .

../node_modules/.bin/closure-make-deps \
    --root js \
    -f google-closure-library/closure/goog/deps.js \
    --closure-path google-closure-library/closure/goog \
    > deps.js
```

`-f` じゃなくて `--root` でディレクトリ指定。
これでブラウザでアクセスすれば`Hello world!`が表示される。


## Closure Compiler のインストールと設定

```bash
npm install --save-dev google-closure-compiler
```

Linuxで実行すると、`google-closure-compiler-linux`がインストールされてデフォルトではネイティブの実行ファイルになるようだ。
Javaじゃなくて実行ファイルだ。マジで！？
`GraalVM` を使って実行ファイルを作っているようだ。そういうツールがあったのね。これでJavaの環境云々に煩わされることがないね。


コンパイル用のシェルスクリプトを書く。

```bash:bin/compile-js.sh
#!/bin/bash

set -eux

cd `dirname $0`/../

#
# Closure Compiler
#
function compile(){
    node_modules/.bin/google-closure-compiler \
         --compilation_level=ADVANCED_OPTIMIZATIONS \
         --define 'goog.LOCALE=ja' \
         --define 'goog.DEBUG=false' \
         --define 'CONSOLE_OUTPUT_ENABLED=false' \
         --dependency_mode=PRUNE \
         --entry_point=goog:myapp.$1 \
         'node_modules/google-closure-library/closure/**.js' \
         '!node_modules/google-closure-library/closure/**_test.js' \
         'node_modules/google-closure-library/third_party/closure/**.js' \
         '!node_modules/google-closure-library/third_party/closure/**_test.js' \
         "js/**.js" \
         '!js/**_test.js' \
         --js_output_file public/$1.js
}

if [ "$#" == "0" ]; then
    compile "main" &
    PID1=$!
    wait $PID1
else 
    compile $1
fi
```

`wait` とかやってるのは、複数のエントリーポイントを作るときに並列実行する用。

```html:public/index.html
<!DOCTYPE html>
<meta charset="UTF-8">
<div>Content to be rendered below</div>
<script src="main.js?t=<?= filemtime('main.js') ?>"></script>
```

なくてもいいけど、ブラウザキャッシュ更新用に時刻を入れた。


あとはjsファイルの更新を検知して、上記のシェルスクリプトを走らせれば手間が減る。
自分は `auto-shell-command.el` を使ってるけれども、一般的にはこれも `node` でやるべきだろうか。


Java環境を整えなくてもいいしPATH系が楽になったので、だいぶ早く設定できるようになった。シェルスクリプトを書いている箇所は、node＋jsでも良い。


# Closure Library goog.ui.Component の使い方

基本的には「goog.ui.Component のはぐれかた」（消えかけ？）と [goog.ui.Componentの継承で気をつけるべきこと一覧](https://blog.kengo-toda.jp/entry/2014/10/24/100557) を読めばいいんだけども、記事が消えると困るので自分でもまとめ直しておく。

## render() と decorate()

`goog.ui.Component` のオブジェクトを表示するのは `render()` でHTMLを生成するか `decorate()` で既存HTMLに割り当てるかのどちらかになる。
自作コンポーネントの場合は、自分は面倒なのでどちらかしか対応していない。サーバーサイドでHTMLを生成しつつ、JSでも生成できるようなパーツの場合はどちらも対応できるように書いたりする。

```js
var component = new Component();
component.render(opt_parentElement);
// または
component.renderBefore(sibling);
```

```js
var el = dom.getElement('component-area');
var component = new Component();
component.decorate(el);
```

`render()` に対応してoverrideする必要がある関数は `createDom()`、`enterDocument()`。
`decorate()` に対応してoverrideする必要がある関数は `canDecorate()`、`decorateInternal()`、`enterDocument()`。

自分は1画面の1箇所ごとに専用のコンポーネントを作ることが多い＆多数の開発メンバーが居るわけでもないので、 `decorate()` を使う場合でも `canDecorate()` は手抜きして実装しないことが多い。


## 処理の流れ

### コンストラクタ

子コンポーネントがある場合は、ここで作っておく。同じ種類の子コンポーネントを並べる場合は`addChild()`もやっておく。
役割がバラバラの場合はプロパティに保持しておく。

```js
constructor(opt_domHelper){
  super(opt_domHelper);

  var content1 = new Content1(opt_domHelper);
  var content2 = new Content2(opt_domHelper);

  this.tabBar_ = new TabBar([content1, content2], opt_domHelper);

  this.addChild(content1);
  this.addChild(content2);

  this.someFlag_ = false;
  this.specialEl_ = null;
}
```

それから、後で使うプロパティはnullで初期化しておく。丁寧にやるなら、ここに `/** @type */` を書く。


### createDom, decorateInternal

利用する全てのDOMをここで生成する、またはHTMLから取得して保持する。
ヘッダーエリアとコンテンツエリアに分かれているような場合は `getContentElement()` も実装した方がやりやすい場合がある。


```js
createDom(){
  var dh = this.getDomHelper();
  var el = dh.createDom(TagName.DIV, 'my-conponent');
  this.setElementInternal(el);
  // または空のdivでいいなら super.createDom()

  this.titleEl_ = dh.createDom(TagName.DIV, 'my-component-title', dh.createTextNode('タイトル'));
  this.contentEl_ = dh.createDom(TagName.DIV, 'my-component-content');
  dh.append(el, this.title_);
  dh.append(el, this.contentEl_);

  this.forEachChild(function(child){
    child.createDom();
    dh.append(this.contentEl_, child.getElement());
  }, this);
}

getContentElement(){
  return this.contentEl_;
}
```

子コンポーネントの`createDom`は自動では呼び出されないので、コンストラクタで`addChild`したコンポーネントの`createDom`を呼び出して`append`する必要がある。



```js
decorateInternal(el){
  super.decorateInternal(el);

  var dh = this.getDomHelper();

  this.titleEl_ = this.getElementByClass('my-component-title');
  if (!this.titleEl_){
    this.titleEl_ = dh.createDom(TagName.DIV, 'my-component-title');
    dh.append(el, this.titleEl_);
  }
  this.contentEl_ = this.getElementByClass('my-component-content');
  if (!this.contentEl_){
    this.contentEl_ = dh.createDom(TagName.DIV, 'my-component-content');
    dh.append(el, this.contentEl_);
  }

  this.forEachChild(function(child){
    child.render(this.contentEl_);
  }, this);

}
```

`decorateInternal`は普段はここまで丁寧に作っていない。タグやクラスのチェックは無しにしておき、もし正しくないならどこかでエラーになる。

`createDom()` のときは `dh.append()` と `enterDocument()` をそれぞれ別々に実行する。createDomが実行された瞬間はまだenterDocumentが実行されていないので。
`decorateInternal()` のときは`child.render()`でDOM生成とenterDocumentを同時に実行する。`super.decorateInternal()` で enterDocumentが実行されているので。

大抵は`createDom`か`decorateInternal`のどちらかを実装していれば足りる。




### enterDocument

主にイベントを登録する。

```js
enterDocument(){
  super.enterDocument();

  var eh = this.getHandler();
  eh.listen(this.button_, EventType.CLICK, function(e){
    console.log(e);
  });
}
```

イベントの登録は `goog.events` ではなく `this.getHandler()`を使う。これは`exitDocument`時に自動で`unlisten`される。
child の enterDocumentは `super.enterDocument()` から自動で実行される。

childでないコンポーネントはそれぞれ`enterDocument()`を実行する必要がある。これをしないと、該当コンポーネントのイベントが動かない。


### exitDocument

継承して実装を書くことになっているけれど、特にこれといってやることがない。

```js
exitDocument(){
  this.button1_.exitDocument();

  super.exitDocument();
}
```

`addChild` していないコンポーネントのexitDocumentを呼び出すぐらいか。大抵の場合はすぐに`dispose`するので忘れていてもあまり不都合は無い。

`exitDocument()` 後に `dispose` せずに、同じオブジェクトを使いまわして`enterDocument()`する場合は正しく実装する必要がある（やってないので分からない。disposeしてメモリ解放してしまって良い気がする）。

### disposeInternal

コンストラクタやこれまでのコードで設定した変数を全て開放する必要がある。
小さい処理なら無くても困らないけど、SPAでずっとリロード無しだとメモリリークが溜まっていく。

```js
disposeInternal(){
  this.button1_.dispose();
  this.button1_ = null;

  this.titleEl_ = null;
  this.contentEl_ = null;

  super.disposeInternal();
}
```

`render`（`createDom`）で生成したコンポーネントは`dispose`時にHTMLから除去される。`decorate`で割り当てた場合は`dispose`してもHTMLはそのまま。それも消したければ`disposeInternal` の中で `dh.removeNode(this.getElement())` を実行する。


`addChild` していないコンポーネントはそれぞれ`dispose`を呼び出す必要があるが、先に`registerDisposable`で登録しておくと`dispose`が自動で実行される。

```js
constructor(){
  // ....

  this.button1_ = new Button();
  this.registerDisposable(this.button1_);
}

disposeInternal(){
  super.disposeInternal(); // call this.button1_.dispose()
}
```





# コンポーネントを組み合わせてカスタマイズした実例

練習として、ボタンを押すと特定エリアのコンテンツ表示を切り替える仕組みを作る。


`npm ci` した後に `bin` のスクリプトを実行すればすぐに試せる。
`decorate`と`createDom`をちゃんぽんした。（個人的に）よくある感じのやつ。サーバーサイドである程度のHTMLは生成しつつ、JavaScriptで残りを生成する。

メンテを考えるならどちらかに統一した方が良いけど、説明もあるので両方使う。

## HTMLを書く

Closure Library の`Button` などのコンポーネントをそのまま使うには、合わせてcssを読み込む必要がある。
ここでは直接書く。最終的には結合してまとめることになる。

```html
<script src="google-closure-library/closure/goog/base.js"></script>
<script src="deps.js"></script>
<link rel="stylesheet" href="google-closure-library/closure/goog/css/common.css">
<link rel="stylesheet" href="google-closure-library/closure/goog/css/css3button.css">
```

`base.js` や 生成した `deps.js` も読み込む。これもClosure Compilerでコンパイルしたら、そのファイルだけ読み込むことになる。

jsを割り当てるHTMLと起動用のJavaScriptを書く。
個人的なjs起動方法のお気に入りは、Google Analytics のように配列に処理をpushしていくというもの。

```html:public/index.html
<script>
  appq = [];
  goog.require('app');
</script>

<body>
  <div class="button-tab-content">
    <div class="tab-area">
      <div class="tab1">Tab1</div>
      <div class="tab2">Tab2</div>
    </div>
    <div class="content-area">
    </div>
  </div>

  <script>
  appq.push(function(){
      runTop();
  });
  </script>
</body>
```

`appq.push()` で呼び出したい処理を順次書いていく。


## JavaScriptのエントリーポイントを書く

最初に呼び出されるjsを書く。

```javascript:js/app.js
goog.module('app');

const top = goog.require('app.top');

function init(){
  window['runTop'] = top;
}
function runQ(q){
  for (var i = 0; i < q.length; i++){
    q[i]();
  }
  window['appq'] = {
    'push': (callback) => {
      callback();
    }
  };
}
init();

window['appq'] = window['appq'] || [];
runQ(window['appq']);
```

`window['appq']` は配列で、その配列の関数が全て実行されたら `push` メソッドを持つオブジェクトに入れ替える。
`deps.js`利用時では意味がないけれど、コンパイルされたjsをasyncで読み込んだ場合は最速のタイミングでjsの処理が動き出すようになる。

ここでは window の直接操作で`runTop`を入れちゃってるけれど、`goog.exportSymbol()` を使うべきかもしれない。
`goog.exportSymbol()` を使うと

```javascript
goog.exportSymbol('app.page.runIndexPage', runIndexPage');
```

```html
<script>
app.page.runIdexPage();
</script>
```

のようにしてネームスペースが簡単に使えるようになる。


## goog.ui.Button を radioボタンのように動作させる

`goog.ui.Button` などの control系コンポーネントは、表示と機能が一体になっている。（タグ生成はrendererという形で分かれているが）
デフォルトでは、クリックすると `Component.State.CHECKED` の状態が切り替わるという動作になっている。

まずはその初期動作を変更する。

```javascript
    var b = new Button(null, renderer, opt_domHelper);
    b.setSupportedState(Component.State.CHECKED, false);
    b.setSupportedState(Component.State.SELECTED, true);
    b.setAutoStates(Component.State.SELECTED, false);
```

`CHECKED` ではなく `SELECTED` にする。
そうすることによって、`goog.ui.SelectionModel` と連携してひとつだけ選択するという動作が簡単に実装できる。
また、クリックすると状態が変わるという動作は設定から外す。
これは、ある一つのデータを参照して表示を切り替える場合に、オンオフがクリックと連動していると不便だから。連動させるのはモデル（データ）にする。その代わりひと手間（ふた手間？）増えて「クリック => モデル変更 => 変更検知 => ボタンの表示状態変更」という流れになる。

複数のコンポーネントのイベントをまとめて検知したい、でもchildにはしたくない、というときは `goog.events.EventTarget` を使う。

```javascript
    this.buttonParent_ = new EventTarget();
    for (let i = 0; i < buttons.length; i++){
      var b = buttons[i];
      b.setParentEventTarget(this.buttonParent_);
      this.registerDisposable(b);
    }
```

parentEventTarget を listen すれば複数の `buttons` のイベントを拾えるようになった。
ついでに `registerDisposable` を登録してdisposeの自動化をしておく。

## 完成サンプル

各コンポーネントを生成したり各イベントごとに処理を書いて、組み合わせて完成。

https://github.com/nishimura/npm-closure-library-example

肝心の`SelectionModel`の説明を書いてないけれども、まあそれは説明よりコード見た方が早いでしょう。

1年後に読み返して分かるかな？
正直、他にハマりポイントとかセオリーとか重要なことがあったような気がしなくもない。
あとで気付いたら付け足すかも。