相关文章推荐
完美的充值卡  ·  ClojureScript and the ...·  7 月前    · 
完美的充值卡  ·  Closure ...·  7 月前    · 
完美的充值卡  ·  Getting Started with ...·  7 月前    · 
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-14

新しいプロジェクトで 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 のインストールと設定
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/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();
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にアクセスしたいのでリンクを張る。そして依存関係を書いたスクリプトを生成する。

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 のインストールと設定
npm install --save-dev google-closure-compiler

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

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

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
    compile $1

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

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の継承で気をつけるべきこと一覧 を読めばいいんだけども、記事が消えると困るので自分でもまとめ直しておく。

render() と decorate()

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

var component = new Component();
component.render(opt_parentElement);
// または
component.renderBefore(sibling);
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()もやっておく。
役割がバラバラの場合はプロパティに保持しておく。

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() も実装した方がやりやすい場合がある。

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する必要がある。

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が実行されているので。

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

enterDocument

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

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

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

exitDocument(){
  this.button1_.exitDocument();
  super.exitDocument();

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

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

disposeInternal

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

disposeInternal(){
  this.button1_.dispose




    
();
  this.button1_ = null;
  this.titleEl_ = null;
  this.contentEl_ = null;
  super.disposeInternal();

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

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

constructor(){
  // ....
  this.button1_ = new Button();
  this.registerDisposable(this.button1_);
disposeInternal(){
  super.disposeInternal(); // call this.button1_.dispose()
コンポーネントを組み合わせてカスタマイズした実例

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

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

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

HTMLを書く

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

<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していくというもの。

public/index.html
<script>
  appq = [];
  goog.require('app');
</script>
  <div class="button-tab-content">
    <div class="tab-area">
      <div class="tab1">Tab1</div>
      <div class="tab2">Tab2</div>
    <div class="content-area">
  <script>
  appq.push(function(){
      runTop();
  </script>
</body>

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

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

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

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() を使うと

goog.exportSymbol('app.page.runIndexPage', runIndexPage');
<script>
app.page.runIdexPage();
</script>

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

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

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

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

    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 を使う。

    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の自動化をしておく。

完成サンプル

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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0