Google Closure Library を使ったJavaScriptをブラウザ無しでテストする
試行錯誤して長くかかったのでメモ。
まず、Closure Library は最初の頃はJSのファイルを羅列したhtmlを読み込んでテストする方式だったと思う。
JsUnit がその形だったかな?
CIとかが手軽に出来なかった頃はそれでも問題なかった。今ではテストのハードルがぐっと上がるので、ブラウザを自分で開いて確認するのはよろしくない。
jsunit test report not showing tests · Issue #869 · google/closure-library · GitHub
昔から使っていた人も他のツールに移っていっている模様。
今では(というか数年前?)ブラウザをコマンドで立ち上げてブラウザ機能を使いつつ、コンソールに結果を表示するという方法が取られるようだ。
blockly/run_mocha_tests_in_browser.js at master · google/blockly · GitHub
Googleのプロジェクトでサンプルを見れる。
このプロジェクトだとES5で書かれているから問題ないようだけど、ES6のmoduleを使うと、file:// で読み込めない問題がある。別途Webサーバーを立ち上げないといけない。
E2Eテストが遅いからJavaScriptだけでテストしたいのに、こんなに色々な準備が必要なら本末転倒である。見た目は綺麗で良い感じなんだけど。
かと言ってシンプルにmocha だけ使っても、documentが無いのでクライアントのテストができない。
Mocha - the fun, simple, flexible JavaScript test framework
MVC的にうまく切り分けられているならModelだけテストできそうだけれども、Closure LibraryはControl系コンポーネント以外はViewもゴチャ混ぜになっている。
そういうわけで色々試した結果、mocha と jsdom を使うことにした。
mochaはテストフレームワーク、jsdomはdomを提供してくれる。
(mocha-jsdomというパッケージもあるが、更新されていないようだった)
npm install --save-dev mocha npm install --save-dev jsdom
mochaディレクトリを作り、初期化用ファイルを置く。
mkdir mocha vi mocha/init.js vi mocha/accordion_test.js
init.js
require('../lib/closure-library/closure/goog/bootstrap/nodejs.js'); // Polyfill for encoding which isn't present globally in jsdom var util = require('util'); global.TextEncoder = util.TextEncoder; global.TextDecoder = util.TextDecoder; goog.nodeGlobalRequire('build/mocha_deps.js');
jsdomに無い関数を割り当てたりしながら、deps.jsも読み込む。
nodeを使いつつモジュール以外のファイルを読み込む場合は goog.nodeGlobalRequire を使うようだ。
mocha_deps.js は依存関係が書かれたファイル。
pathの扱いがややこしく、普段Closure Libraryで使っているものをそのまま使えなかった。base.jsとnodejs.jsはpathが一段深くなるので、deps.jsを生成するときのroot_with_prefixの指定を一段深くする必要があった。
これは別のファイルを生成することに。
mocha_js.sh
#!/bin/bash set -eux cd `dirname $0`/../ ./lib/closure-library/closure/bin/build/depswriter.py \ --root_with_prefix="js ../../../../js" \ > ./build/mocha_deps.js
実際のテストのファイルはこんな感じ。
accordion_test.js
var JSDOM = require('jsdom').JSDOM; var assert = require('assert'); var FakeTimers = require("@sinonjs/fake-timers"); goog.require('app.anim.Accordion'); var Accordion = goog.module.get('app.anim.Accordion'); goog.require('goog.style'); var clock; describe('app.anim.Accordion', function() { before(function(){ var dom = new JSDOM( '<html><body><form name="form">' + '<input type="checkbox" name="check">' + '<div id="area" style="display:none">test</div>' + '</form></body></html>'); global.window = dom.window; global.document = window.document; this.check_ = document.forms.form.elements.check; this.area_ = document.getElementById('area'); clock = FakeTimers.install(); this.nowOrig_ = goog.now; goog.now = (function() { return +new Date(); }); }); after(function(){ this.check_ = null; this.area_ = null; clock.uninstall(); goog.now = this.nowOrig_; }); describe('#decorate()', function() { it('can decorate Accordion', function() { var target = new Accordion(this.check_, 100); target.decorate(this.area_); assert.equal(target.isInDocument(), true); }); it('should change shown and running animation', function() { var target = new Accordion(this.check_, 100); target.decorate(this.area_); assert.equal(goog.style.isElementShown(this.area_), false, 'element is not shown before starred'); this.check_.dispatchEvent(new window.Event('change')); assert(target.anim_, "exists anim object"); assert.equal(target.anim_.isPlaying(), true, 'anim is started'); assert.equal(goog.style.isElementShown(this.area_), true, "element is shown just after clicked"); clock.tick(20); // start delay assert.equal(target.anim_.isPlaying(), true, 'anim is still started'); assert.equal(goog.style.isElementShown(this.area_), true, "element is shown"); clock.tick(80); assert.equal(target.anim_.isPlaying(), false, 'anim is stopped'); assert.equal(goog.style.isElementShown(this.area_), true, "element is shown"); }); }); });
チェックボックスがオンされるとアコーディオンがアニメーションで開く、というコンポーネントのテスト。
ハマりポイントが結構あった。
最初は init.js でjsdomを読み込んでいたけれど、テストごとに別のhtmlを使うと思ってtest.jsに移動した。逆に言うと、htmlが同じなら*test.jsを同じファイルにまとめられる。
ES6を読み込む場合は goog.require と goog.module.get の両方が必要なことに注意する。
Closure Libraryの時刻系処理は全てgoog.nowを使っていて、それは読み込み時に既にnew Date()が割り当てられているので、goog.nowも置き換える必要がある。めっちゃライブラリのソース読んだ。
domの幅は取れなかった。画面が無いもんね。なのでstyleを確認。
イベントはjsdomで作ったwindowのEventをnewすればOK。
最後に、GitLab用のCIファイルを書く。
.gitlab-ci.yml
mocha_test: image: registry.gitlab.example.com/app/nodejs-python:12.0 stage: test before_script: - npm install script: - ./bin/mocha_js.sh - ./node_modules/mocha/bin/mocha --file mocha/init.js "mocha/**/*test.js" except: - schedules
nodejs-python は自分でビルドしてGitLabのレジストリに登録した。
Dockerfile
FROM node:12-buster-slim RUN apt-get -y update && apt-get -y upgrade && apt-get -y install python
バージョンが古いのは、今使っているdebianに合わせるため。普通にbuster-slimからapt install nodejs でも良かったかも。
キャッシュが無い状態で1分半、キャッシュがあれば53秒だった。
なかなか良い感じではないかな。
これでやっとJavaScriptのテストが書けるという最低限の状態にできた。
Googleアナリティクス4プロパティで使われているフロントエンドのツール
メインのプログラムは<script id=base-js ・・・となっている場所にある。
あとはHTMLやJSのcopyrightなどを見ていく。
依存関係は知らないので(angularを使えばlessとmaterialは勝手に付いてくるよ!など)見つけた順に書いていく。
- Closure Library
- Closure Compiler
- Less 1.7.5 https://lesscss.org/
- Material Design https://material.io/
- jQuery 2.2.5-pre
- AnglarJS 1.6.4 https://angular.io/
- d3 https://github.com/d3/d3
- Papa Parse 4.1.2
- RxJS
- MochiKit (Closure Library)
- Polymer https://github.com/Polymer/polymer (?)
- Math.uuid.js 本家消滅? https://gist.github.com/Wind4/3baa40b26b89b686e4f2
- https://fonts.google.com/
今までと大きな違いは無い感じ。
Angular用の細かいライブラリは省略。
Google Closure Library の UI と調べもの
What UI rendering systems are used by Google with closure library, and why are these not open source?
goog.ui.* は古いまま更新されていない。
最近のGoogleのサービスのUIはオープンソース化されていない。
Touch support for goog.ui.menu?
goog.uiは現在活発に開発されていないので他のライブラリを使った方が良い。
html5history and query parameters
html5history のクエリパラメーターが正しく処理されない。
goog.HTML5History with query params
ちょっとしたコードで簡単に解決はできる。
UI系は自分たちでやってくれってことかな。
ちなみにGoogle Groups はGWTで作られているっぽい。
Google Search、Gmail は
・Closure Compiler
・Closure Library
・MochiKit
・JsActionっぽいフレームワーク
などが使われているようだ。
例えば Gmail の /scs/mail-static/_/js/k=gmail.main.ja.〜〜〜 のJavaScriptには Closure Library の debug/debug.js にある $googDebugFname という文字列があるし、
function(b,c,d,e,f,g){kq(d)||(d&&(j2a[0]=d.toString()),d=j2a);for(var k=0;k<d.length;k++){var l=Cp(c,d[k],e||b.handleEvent,f||!1,g||b.Lb||b);if(!l)break;b.Ce[l.key]=l}return b};
というコードは goog.events.EventHandler.prototype.listen_ 関数のコンパイル結果に見える。
UI以外は普通にClosure Libraryが使われているようである。
他に Lovefield というのものあった。SQLっぽくデータを操作するライブラリらしい。
Is Lovefield production quality?
Yes. As of May 2016, Inbox by GMail heavily relies on Lovefield to perform complex client-side structural data queries. Google Play Movies & TV has shipped with Lovefield for almost two years.
https://github.com/google/lovefield/blob/master/docs/FAQ.md
LovefieldがClosure Libraryを使っているので、良いサンプルになるかもしれない。
と言っても利用しているライブラリは
goog.require('goog.Promise'); goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.iter'); goog.require('goog.labs.userAgent.browser'); goog.require('goog.math'); goog.require('goog.structs.Map'); goog.require('goog.structs.Set'); goog.require('goog.userAgent'); goog.require('goog.userAgent.platform'); goog.require('goog.userAgent.product');
が全てだった。
Hasteを試したログ
GHCJSの重さが気になっていたところで、こんな記事を見かけた。
qiita.com
Hasteが良さそうなので試してみる。
Hasteのプロジェクトの作り方は stack new した後に
stackoverflow.com
該当箇所をここの設定に書き換える。
% stack exec haste-boot ... Installed haste-prim-0.5.4.2 Warning: The package list for 'hackage.haskell.org' does not exist. Run 'cabal update' to download it. Resolving dependencies... haste-cabal.bin: Could not resolve dependencies: trying: time-1.5.0.1 (user goal) next goal: deepseq (dependency of time-1.5.0.1) Dependency tree exhaustively searched.
エラーになった。
再度トライ。
% stack exec haste-boot -- --local Downloading haste-cabal from GitHub Downloading Google Closure compiler... `include' is not a directory
エラー。
% mkdir include % stack exec haste-boot -- --local Downloading haste-cabal from GitHub Downloading Google Closure compiler... utils/unlit: changeWorkingDirectory: does not exist (No such file or directory)
エラーが素っ気ないのでよく分からないけど、--localでhasteのソースを探しているのか…。
https://github.com/valderman/haste-compiler/issues/395
この辺を見ると解決しているような気がする。
stack.yaml を変更してもう一度トライ。
extra-deps: - haste-compiler-0.5.5.1 - ghc-simple-0.3 - shellmate-0.2.3 - HTTP-4000.2.23
stack build stack install haste-compiler stack exec haste-boot
同じエラーになった。
どうにもならないのでソースから入れる。
github.com
git clone https://github.com/valderman/haste-compiler.git cd haste-compiler stack build stack install stack exec haste-boot
何の問題もなく入った。
バージョンが0.6.0.0になっていたので、stack.yamlのバージョンを変えたら入るんだろうか。
resolver: lts-6.14 extra-deps: - haste-compiler-0.6.0.0 - ghc-simple-0.3 - shellmate-0.3.4.2 - shellmate-extras-0.3.4.1 - http-conduit-2.2.4 - http-client-0.5.14 - http-client-tls-0.3.5.3 - tagged-0.8.6
stackoverflow.com
うーん、stack build --copy-compiler-tool haste-compiler は何か違うっぽい。
取り敢えずここまでのソースから入れたあとの設定を最初からやる。
stack new haste-project cd haste-project
compilerは使わないのでhaste-libだけ書く。
.cabal
build-depends: base >=4.7 && <5 , haste-lib
stack.yaml
resolver: lts-6.14 packages: - . extra-deps: - haste-lib-0.6.0.0 - binary-0.7.6.1 - containers-0.5.6.3 - haste-prim-0.6.0.0 - time-1.5.0.1
これでstack build出来るし、import Hasteできた。
GHCのバージョンが古くてinteroが動かなくなったので intero.el を修正する。
(defun intero-copy-compiler-tool-auto-install (source-buffer targets buffer) "Automatically install Intero appropriately for BUFFER. Use the given TARGETS, SOURCE-BUFFER and STACK-YAML." (let ((ghc-version (intero-ghc-version-raw))) (insert (format " Installing intero-%s for GHC %s ... " intero-package-version ghc-version)) (redisplay) (cl-case (let ((default-directory (make-temp-file "intero" t))) (intero-call-stack nil (current-buffer) t nil "build" "--copy-compiler-tool" (concat "intero-" intero-package-version) "--flag" "haskeline:-terminfo" "--resolver" (concat "ghc-" ghc-version) ;; "haskeline-0.7.5.0" "haskeline-0.7.4.0" "ghc-paths-0.1.0.9" "mtl-2.2.2" "network-2.7.0.0" "random-1.1" "syb-0.7")) (0 (message "Installed successfully! Starting Intero in a moment ...") (bury-buffer buffer) (switch-to-buffer source-buffer) (intero-start-process-in-buffer buffer targets source-buffer))
haskeline のバージョンを下げる。
intero自体のバージョンはdefcustomで変更できるようになってるんだけど、この依存ライブラリは固定なので直接編集するしかない。
編集したらbytecompileして起動確認。
初期のファイルを
module Lib ( someFunc ) where import Haste someFunc :: IO () -- someFunc = putStrLn "someFunc" someFunc = writeLog $ toJSString "someFunc"
console.log出力に変えてコンパイル。
#!/bin/bash TARGET=compiled.js set -eux mkdir -p build hastec -O2 -fglasgow-exts \ --opt-minify --opt-minify-flag='--jscomp_off=checkVars' \ -o build/compiled.js \ --outdir=build \ app/*.hs src/*.hs
24KBになった。
自分でclosure compilerを実行したときより誤差程度軽い。
最初のURLの比較のように3KBには遠い。
バージョンが上がって機能が増えて重くなったんだろうか。
とりあえず一旦ここまで。
GHCJSを使えるようにしたログ
ソースからのインストールは出来たんだけど、それをstackで使う方法が分からなかった。
エラーがややこしいので元々入っているcabalを消したりhaskell-platformを消したりしてstackだけ動いている状態にして試した。
コンパイルが1時間近くかかる。長い。
取り敢えず動いたのはいいんだけど、emacsで不備が出る。stackで動かしたい。
https://docs.haskellstack.org/en/stable/ghcjs/#ghcjs
ここからのリンクの
https://github.com/matchwood/ghcjs-stack-dist
この設定をコピペして使う。
jsのビルドはスムーズになった。
だがやっぱり普通のHaskellのソースを書くようには行かない。
どうやらemacsのinteroが動かない。
github.com
closeになっているけどghcjs-base-stubもよく分からなかった。ghcjs --interactiveが無理だと厳しい?
よく考えたら素のHaskellが動くし
http://hackage.haskell.org/package/ghcjs-dom
この辺のGHCJS用のライブラリは普通のGHCで動くんだから、普通のGHCでコンパイルし直してプログラミング中はHaskellプログラムとしておけば良さそうに思える。
コンパイル…コンパイル…コンパイル…長い。
github.com
そしてエラーが出る。
jsaddleのコンパイルでメモリ8GB以上持っていかれる。富豪的過ぎる…。
メモリを15GBにしてもスワップも持って行かれた。でも何とかコンパイル成功。
適当にsrcディレクトリをシンクしてGHCJS用のstackファイルとGHC用のstackファイルを用意して、ソースを書くときはGHC用のディレクトリで作業するようにすれば、何とか出来そうだった。
全体的に参考にしたのはこの辺。
qiita.com
labs.spookies.co.jp
それでstackの初期状態 "someFunc" を出力するプログラムをコンパイルしたらjsが1MBある。Closure Compilerで圧縮しても240Kある。
まあHaskellのbaseとかの環境が全部入っているとそうなるのか。Closure Compilerで未使用関数だと判別できない部分が多かったっぽい。
ちなみにClosure Compilerは
java -jar $DIR/compiler.jar \ --compilation_level=ADVANCED_OPTIMIZATIONS \ --define 'goog.LOCALE=ja' \ --define 'goog.DEBUG=false' \ --jscomp_off=checkVars \ $GHCJSOUT \ > $COMPILEDJS
こんな感じで、 --jscomp_off が必要。普通にやると関数の多重定義でエラーになる。
Closure Compiler の出力するJavaScriptを ES5(旧)とES6(新)で比べる
ふと、ES6よりES5で書いた方が軽くなるかと思ったので疑問を解消するために確かめる。
※ ES5、ES6と書きつつ、単に書き方の違いの調査になってきたので新・旧とする。
Closure Compiler と Closure Libraryをダウンロードする。
wget https://dl.google.com/closure-compiler/compiler-latest.zip unar compiler-latest.zip ln -s compiler-latest/closure-compiler-v20190325.jar closure-compiler.jar https://github.com/google/closure-library/archive/v20190325.tar.gz unar v20190325.tar.gz ln -s closure-library-20190325 closure-library
プログラムのディレクトリを作る。
mkdir js mkdir js/es5 mkdir js/es6
Closure Compiler は何度も実行するのでコマンドを書いておく。
build.sh
#!/bin/bash set -eux function compile(){ java -jar compiler.jar \ --compilation_level=ADVANCED_OPTIMIZATIONS \ --define 'goog.LOCALE=ja' \ --define 'goog.DEBUG=false' \ --create_source_map $1.js.map \ --dependency_mode=STRICT \ --entry_point=goog:entrypoint \ 'closure-library/closure/**.js' \ '!closure-library/closure/**test.js' \ "js/$1/**.js" \ > $1.js echo >> $1.js echo "//@ sourceMappingURL=$1.js.map" >> $1.js } compile $1
まずは簡単なHello Worldから。
旧
goog.provide('entrypoint'); function hello(v){ console.log('hello ' + v); } function startApp(){ hello('es5'); } startApp();
新
goog.module('entrypoint'); const hello = (v) => console.log('hello ' + v); const startApp = () => { hello('es6'); } startApp();
まあこれぐらいなら基本なので全て削除されるかなと予想しつつコンパイル。
chmld +x build.sh ./build.sh es5 ./build.sh es6
結果。
app.js
旧
console.log("hello es5");
新
console.log("hello es6");
ファイルを分けてみる。
旧
// app.js goog.provide('entrypoint'); goog.require('app.Foo'); window['startApp'] = app.Foo.startApp; // foo.js goog.provide('app.Foo'); app.Foo.hello = function(v){ console.log('hello ' + v); } app.Foo.startApp = function(){ app.Foo.hello('es5'); };
新
// app.js goog.module('entrypoint'); const app = goog.require('app'); window['startApp'] = app.startApp; // foo.js goog.module('app'); const hello = (v) => console.log('hello ' + v); const startApp = () => { hello('es6'); } exports = { startApp };
結果
旧
window.startApp=function(){console.log("hello es5")};
新
window.startApp=function(){console.log("hello es6")};
どちらも無駄がない。
HTMLを作って呼び出す形にする。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Closure Test</title> <script src="es5.js"></script> <!-- <script src="es6.js"></script> --> </head> <body> <h1>Closure Test</h1> <div id="contents"> <div class="list"></div> <div class="list"></div> <div class="list"></div> </div> <script> startApp5(); //startApp6(); </script> </body> </html>
旧
// app.js goog.provide('entrypoint'); goog.require('app.Foo'); window['startApp5'] = app.Foo.startApp; // foo.js goog.provide('app.Foo'); goog.provide('app.Bar'); goog.require('goog.dom'); app.Foo.hello = function(v){ console.log('hello ' + v); var parentEl = goog.dom.getElement('contents'); var children = goog.dom.getElementsByClass('list', parentEl); for (var i = 0; i < children.length; i++){ goog.dom.setTextContent(children[i], 'list ' + i); } } app.Foo.startApp = function(){ app.Foo.hello('es5'); };
新
// app.js goog.module('entrypoint'); const app = goog.require('app'); window['startApp6'] = app.startApp; // foo.js goog.module('app'); const dom = goog.require('goog.dom'); const hello = (v) => { console.log('hello ' + v); const parentEl = dom.getElement('contents'); const children = dom.getElementsByClass('list', parentEl); for (let i = 0; i < children.length; i++){ dom.setTextContent(children[i], 'list ' + i); } }; const startApp = () => hello('es6'); exports = { startApp };
結果
var h = Array.prototype.indexOf ? function(a, b) { return Array.prototype.indexOf.call(a, b, void 0) } : function(a, b) { if ("string" == typeof a) return "string" == typeof b && 1 == b.length ? a.indexOf(b, 0) : -1; for (var c = 0; c < a.length; c++) if (c in a && a[c] === b) return c; return -1 } ; window.startApp5 = function() { console.log("hello es5"); var a, b = (a = document.getElementById("contents5")) || document; if (b.querySelectorAll && b.querySelector) a = b.querySelectorAll(".list"); else { var c; b = document; a = a || b; if (a.querySelectorAll && a.querySelector) a = a.querySelectorAll(".list"); else if (a.getElementsByClassName) { var d = a.getElementsByClassName("list"); a = d } else { d = a.getElementsByTagName("*"); var e = {}; for (b = c = 0; a = d[b]; b++) { var g = a.className, f; if (f = "function" == typeof g.split) f = 0 <= h(g.split(/\s+/), "list"); f && (e[c++] = a) } e.length = c; a = e } } for (b = 0; b < a.length; b++) a[b].innerHTML = "list " + b } ;
これもどちらも同じなので片方は省略。
goog.domを使ったのでindexOfやquerySelectorが無い場合の処理が入った。
querySelectorがある場合の処理だけ抜き出すと
window.startApp6 = function() { console.log("hello es6"); var a, b = (a = document.getElementById("contents6")) || document; a = b.querySelectorAll(".list"); } for (b = 0; b < a.length; b++) a[b].innerHTML = "list " + b };
こうなる。
変数が再利用されていてvarとかletとかのレベルではなかった。
次にクラス。
旧
// app.js goog.provide('entrypoint'); goog.require('app.Bar'); goog.require('app.Baz'); var bar = new app.Bar('test'); bar.hello(); var baz = new app.Baz('test 2'); baz.hello(); // foo.js goog.provide('app.Foo'); goog.provide('app.Bar'); goog.provide('app.Baz'); /** * @constructor */ app.Foo = function(value){ this.value_ = value; }; app.Foo.prototype.hello = function(){ console.log('Foo > ' + this.value_); }; /** * @constructor * @extends {app.Foo} */ app.Bar = function(value){ app.Bar.base(this, 'constructor', value); }; goog.inherits(app.Bar, app.Foo); app.Foo.prototype.hello = function(){ console.log('Bar > ' + this.value_); }; /** * @constructor * @extends {app.Bar} */ app.Baz = function(value){ app.Baz.base(this, 'constructor', value); }; goog.inherits(app.Baz, app.Bar); app.Foo.prototype.hello = function(){ console.log('Baz > ' + this.value_); };
新
// app.js goog.module('entrypoint'); const app = goog.require('app'); const bar = new app.Bar('test'); bar.hello(); const baz = new app.Baz('test 2'); baz.hello(); // foo.js goog.module('app'); class Foo { constructor(value){ this.value_ = value; } hello(){ console.log('Foo > ' + this.value_); } } class Bar extends Foo { constructor(value){ super(value); } hello(){ console.log('Bar > ' + this.value_); } } class Baz extends Bar { constructor(value){ super(value); } hello(){ console.log('Baz > ' + this.value_); } } exports = { Foo, Bar, Baz }
結果
旧
function c(a, d) { function g() {} g.prototype = d.prototype; a.f = d.prototype; a.prototype = new g; a.prototype.constructor = a; a.c = function(l, m, n) { for (var h = Array(arguments.length - 2), b = 2; b < arguments.length; b++) h[b - 2] = arguments[b]; return d.prototype[m].apply(l, h) } } ;function e(a) { this.a = a } e.prototype.b = function() { console.log("Foo > " + this.a) } ; function f(a) { this.a = a } c(f, e); e.prototype.b = function() { console.log("Bar > " + this.a) } ; function k(a) { this.a = a } c(k, f); e.prototype.b = function() { console.log("Baz > " + this.a) } ; (new f("test")).b(); (new k("test 2")).b();
新
var d = "function" == typeof Object.create ? Object.create : function(a) { function b() {} b.prototype = a; return new b } , e; if ("function" == typeof Object.setPrototypeOf) e = Object.setPrototypeOf; else { var f; a: { var g = { c: !0 } , h = {}; try { h.__proto__ = g; f = h.c; break a } catch (a) {} f = !1 } e = f ? function(a, b) { a.__proto__ = b; if (a.__proto__ !== b) throw new TypeError(a + " is not extensible"); return a } : null } var k = e; function l(a, b) { a.prototype = d(b.prototype); a.prototype.constructor = a; if (k) k(a, b); else for (var c in b) if ("prototype" != c) if (Object.defineProperties) { var m = Object.getOwnPropertyDescriptor(b, c); m && Object.defineProperty(a, c, m) } else a[c] = b[c]; a.f = b.prototype } ;function n(a) { this.a = a } n.prototype.b = function() { console.log("Foo > " + this.a) } ; function p(a) { this.a = a } l(p, n); p.prototype.b = function() { console.log("Bar > " + this.a) } ; function q(a) { this.a = a } l(q, p); q.prototype.b = function() { console.log("Baz > " + this.a) } ; (new p("test")).b(); (new q("test 2")).b();
class構文の方はトランスパイル後の共通コードが若干複雑になっているような。
ただし本体のコードはやはり同じだった。
その他、細かい動作等。
よくあるスコープミスのコード
function app() { var a = [1, 2, 3, 5, 8]; var b = []; for (var i = 0; i < a.length; i++){ b.push(function(){ console.log(i); }); } for (var j = 0; j < b.length; j++){ b[j](); } } app();
// compile (function() { for (var a = [1, 2, 3, 5, 8], b = [], c = 0; c < a.length; c++) b.push(function() { console.log(c) }); for (a = 0; a < b.length; a++) b[a]() } )();
単純なコードのままだけれどバグっている。
letを使う
function app() { var a = [1, 2, 3, 5, 8]; var b = []; for (let i = 0; i < a.length; i++){ b.push(function(){ console.log(i); }); } for (let i = 0; i < b.length; i++){ b[i](); } } app();
// compile (function() { for (var a = [1, 2, 3, 5, 8], c = [], b = { a: 0 }; b.a < a.length; b = { a: b.a }, b.a++) c.push(function(d) { return function() { console.log(d.a) } }(b)); for (a = 0; a < c.length; a++) c[a]() } )();
forEachを使うと安全。
goog.require('goog.array'); function app() { var a = [1, 2, 3, 5, 8]; var b = []; goog.array.forEach(a, function(a, i){ b.push(function(){ console.log(i); }); }); for (var j = 0; j < b.length; j++){ b[j](); } } app();
// compile var f = Array.prototype.forEach ? function(a, b) { Array.prototype.forEach.call(a, b, void 0) } : function(a, b) { for (var e = a.length, d = "string" == typeof a ? a.split("") : a, c = 0; c < e; c++) c in d && b.call(void 0, d[c], c, a) } ; (function() { var a = []; f([1, 2, 3, 5, 8], function(e, d) { a.push(function() { console.log(d) }) }); for (var b = 0; b < a.length; b++) a[b]() } )();
ただし素のJSで使おうとすると危険。
function app() { var a = [1, 2, 3, 5, 8]; var b = []; a.forEach(function(a, i){ b.push(function(){ console.log(i); }); }); for (let i = 0; i < b.length; i++){ b[i](); } } app();
// compile (function() { var a = []; [1, 2, 3, 5, 8].forEach(function(d, c) { a.push(function() { console.log(c) }) }); for (var b = 0; b < a.length; b++) a[b]() } )();
[].forEach を直接書くと、それはトランスパイル後もそのままになっている。
a.forEach(function(a, i){
の箇所を
a.myforEach(function(a, i){
にするとコンパイルエラーになるから判別は出来ているんだけど。
直接arrayの関数を呼び出す場合はブラウザ対応を開発者に任せるということかな。
クラスやthisが入ると長くなるのかなと思いきや、そうでもない。
const PREFIX = 'prefix_'; const SUFFIX = '_suffix'; const plusPrefix = (v) => PREFIX + v; const plusSuffix = function(v){ return v + SUFFIX; }; class Foo { constructor(val){ this.val_ = val; } setValue(val){ this.val_ = val; } getValue(){ const v1 = plusPrefix(this.val_); return plusSuffix(v1); } } const foo = new Foo('val'); console.log(foo.getValue());
// compile console.log("prefix_val_suffix");
あれ?クラスは?
驚愕の圧縮率。
何度も使うと結果が変わる。
const PREFIX = 'prefix_'; const SUFFIX = '_suffix'; const plusPrefix = (v) => PREFIX + v; const plusSuffix = function(v){ return v + SUFFIX; }; class Foo { constructor(val){ this.val_ = val; } setValue(val){ this.val_ = val; } getValue(){ const v1 = plusPrefix(this.val_); return plusSuffix(v1); } } const foo = new Foo('val'); console.log(foo.getValue()); const foo2 = new Foo('val 2'); console.log(foo2.getValue()); const foo3 = new Foo('val 3'); console.log(foo3.getValue());
// compile function a(b) { this.a = b } console.log("prefix_" + (new a("val")).a + "_suffix"); console.log("prefix_" + (new a("val 2")).a + "_suffix"); console.log("prefix_" + (new a("val 3")).a + "_suffix");
それでも動的な部分以外は定数になっている。
setValueを呼び出しても結果が変わる。
const PREFIX = 'prefix_'; const SUFFIX = '_suffix'; const plusPrefix = (v) => PREFIX + v; const plusSuffix = function(v){ return v + SUFFIX; }; class Foo { constructor(val){ this.val_ = val; } setValue(val){ this.val_ = val; } getValue(){ const v1 = plusPrefix(this.val_); return plusSuffix(v1); } } const foo = new Foo('val'); foo.setValue('val2'); console.log(foo.getValue());
// compile var a = new function() { this.a = "val" } ; a.a = "val2"; console.log("prefix_" + a.a + "_suffix");
なんかこのコンパイル結果を再びコンパイルすると綺麗に消せそうだけれども。
一度しか利用されていない変数やメソッドはインライン展開されるということかな。
クラス定数はどうか。
const PREFIX = 'prefix_'; const SUFFIX = '_suffix'; const plusPrefix = (v) => PREFIX + v; const plusSuffix = function(v){ return v + SUFFIX; }; class Foo { constructor(val){ this.val_ = val; } setValue(val){ this.val_ = val; } getValue(){ const v1 = plusPrefix(this.val_); const v2 = plusSuffix(v1); const v3 = this.plusPrefix(v2); return Foo.plusSuffix(v3); } plusPrefix(v) { return this.CLASS_PREFIX + v; } get CLASS_PREFIX() { return 'classprefix_'; } static plusSuffix(v) { return v + Foo.CLASS_SUFFIX; }; static get CLASS_SUFFIX() { return '_classsuffix'; } } const foo = new Foo('val'); console.log(foo.getValue());
// compile var a = "undefined" != typeof window && window === this ? this : "undefined" != typeof global && null != global ? global : this; function b(d) { this.b = d } a.Object.defineProperties(b.prototype, { a: { configurable: !0, enumerable: !0, get: function() { return "classprefix_" } } }); a.Object.defineProperties(b, { a: { configurable: !0, enumerable: !0, get: function() { return "_classsuffix" } } }); var c = new b("val"); console.log(c.a + ("prefix_" + c.b + "_suffix") + b.a);
なんか増えた。
素直にクラス外で定数にした方が良さそう。
効率良いトランスパイル結果を得ようと思ったら、 goog.module でモジュールにしつつthisを使わないシンプルな関数や定数の組み合わせでコードを書くのが良さそうだ。
まあそもそもthisを使うためにnewするのだから、thisを使わないのにクラスメソッドにするのは無駄な設計とも言えるね。
Google Closure Tools難しい
http://closure-library.googlecode.com/svn/trunk/closure/goog/demos/dialog.html
サンプルのソースを見たら分かるけど、JavaScriptの中にHTMLを書いてる。
Google Sitesでもそうだった。しかも日本語文字列はUnicodeでエンコードしてあった。これはClosure Compilerを使ってるからか…。
DBからデータを取ってきてそれをダイアログで表示、とかはどうするんだろう。
最初は単純にテンプレートをAjaxで呼び出してダイアログを表示するのがいいかな。
方向性が合ってるか微妙なところだが。
Debian lenny でClosure Compilerを使うにはまずJavaをインストールする。
java --version java version "1.5.0" gij (GNU libgcj) version 4.3.2 Copyright (C) 2007 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. aptitude install sun-java6-bin update-alternatives --config java java-6-sunのjavaを選択 java -version java version "1.6.0_12" Java(TM) SE Runtime Environment (build 1.6.0_12-b04) Java HotSpot(TM) Client VM (build 11.2-b01, mixed mode, sharing) wget http://closure-compiler.googlecode.com/files/compiler-latest.zip 展開…
goog.requireでの依存関係を解決しながら最適化レベル最大でコンパイル。
DIR=google-closure-library-path $DIR/closure/bin/calcdeps.py -i input.js -p $DIR -o compiled \ -c closure-compiler-path/compiler.jar -f "--compilation_level=ADVANCED_OPTIMIZATIONS" \ > out.js
コンパイラの警告が地味に有り難い。
参考:
http://itpro.nikkeibp.co.jp/article/COLUMN/20091112/340473/
http://code.google.com/intl/ja/closure/library/docs/calcdeps.html