新しいプロジェクトで Closure Compiler を使おうと思ったら、Javaのバージョン古くて使えなかった。そしてClosure Libraryの
clocure-libaray/closure/goog/deps.js
が無くなった関係で
depswriter.py
が使えなくなっていた。
一旦は古いバージョンでやり過ごしたけど、せっかくなので新しいバージョンを入れて設定してみる。
だいたいは
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が実行されているので。
大抵はcreateDom
かdecorateInternal
のどちらかを実装していれば足りる。
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();
render
(createDom
)で生成したコンポーネントは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
のスクリプトを実行すればすぐに試せる。
decorate
とcreateDom
をちゃんぽんした。(個人的に)よくある感じのやつ。サーバーサイドである程度の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年後に読み返して分かるかな?
正直、他にハマりポイントとかセオリーとか重要なことがあったような気がしなくもない。
あとで気付いたら付け足すかも。