Closure Compiler による最適化を更に押し進める方法
$.css.hooks
から $.css.patches
にプロパティ名を変更。推敲と追記。(2024/04/16)
クロスブラウザを切り口に、アプリケーションが使用しないコードをコンパイル時に除去する方法を示します。
やや手間が増えますが、使用しないコードは確実に除去されるので、クロスブラウザ対策をモリモリに盛り込んで、ソースコードをブラウザの(黒)歴史博物館にしちゃいましょう。
ファイルを細かく分ける
この為に、tagName
毎にファイルを分けることにします。ファイル数は(爆発的に)増えますが、関心毎にファイルを分離できるメリットもあります。例えば、IFRAME はご存じのように黒魔術(patch)の宝庫ですね。
また patch の無さそうな DIV や SPAN は一つのファイルにまとめてしまって良いでしょう。とはいえアプリケーション側は将来の変化に強くするために、愚直に dom.tagName.DIV
を使うこととします。例えば次のようなコードになります。
goog.require( 'dom.tagName.DIV' );
var div = dom.createElement( dom.tagName.DIV );
この記事では tagName
ではなく、CSS のプロパティ毎にファイルを分割したサンプルを示し、コンパイル結果を比較してみます。パッと思いつくクロスブラウザの例が opacity
だった由です。
opacity
を設定するだけのサンプルアプリ
プロジェクトのファイル構成
+- src/ | | | +- main1.js | +- main2.js | | | +- $/ | | | | | +- $.js | | +- $.css.patches.js | | +- $.css.opacity.js | | | *- closure-primitives/ | | | +- base.js <- closure-tools からコピーして配置 | +- dist/ | +- package.json +- gulpfile.js
src/main1.js(dist/app1.js のソース)
次の通り、jQuery っぽい DOM 操作ライブラリ($
)を使うアプリがあるとします。
goog.provide( 'app1' );
goog.require( '$' );
goog.scope(
function(){
var $elm = new $( 'app' );
$elm[ 'setStyle' ]( 'opacity', 0.5 );
}
);
src/main2.js(dist/app2.js のソース)
以上(main1.js)を一般的な書き方とするならば、今回検証する書き方は次になります。
goog.provide( 'app2' );
goog.require( '$' );
goog.require( '$.css.opacity' ); // <-
goog.scope(
function(){
var $elm = new $( 'app' );
$elm[ 'setStyle' ]( $.css.opacity, 0.5 ); // <-
}
);
goog.require( '$.css.opacity' );
が追加されていて、'opacity'
とせずに、$.css.opacity
を使用しています。$.css.opacity
は文字列定数とし、多くの場合で == 'opacity'
です。
$/ のコード
$ 以下の実装を次に示します。
- src / $ / $.js
- src / $ / $.css.patches.js
- src / $ / $.css.opacity.js
src / $ / $.js
setStyle
というメソッドだけを持つ DOM I/F 抽象化クラスです。
メソッドの追加にドット記法ではなくブラケット記法(角括弧)を使うのは、本記事用にアグレッシブな最適化を一部で避けるためです。ドット記法に書き換えると、より最適化されて原型を留めないファイルを出力します。これは本解説記事には適しません。
goog.provide( '$' );
goog.provide( '$.css' );
/**
* @constructor
* @param {!Element|string} elmOrID
*/
var $ = function( elmOrID ){
if( typeof elmOrID === 'string' ){
this.element = document.getElementById( elmOrID );
} else {
this.element = elmOrID;
};
};
/**
* @param {string} prop
* @param {string|number} value
*/
$.prototype[ 'setStyle' ] = function( prop, value ){
if( !prop ) return;
if( $.css.patches ) var patch = $.css.patches[ prop ];
if( patch ){
patch( this.element, value );
} else {
this.element.style[ prop ] = '' + value;
};
};
src / $ / $.css.patches.js
setStyle
について、環境毎に異なる処理を格納する patches
を $.css
に生やします。それだけですが最適化の為にファイルにします。
なお、css
, patches
といったオブジェクトは goog.provide()
コール内で生成されていると考えてください。開発者が $.css = {}, $.css.patches = {}
と書くことはありません。
goog.provide( '$.css.patches' );
goog.require( '$.css' );
/**
* @package
* @const
*/
var p_style = document.body.style;
src / $ / $.css.opacity.js
定数 $.css.opacity
は実行時にブラウザ毎に設定されます。opacity
に非対応の環境では if($.css.opacity)
で処理を切り替える使い方もできますね。
更に filter
で patch する IE8 以下の場合 $.css.patches.opacity
に関数を登録します。
goog.provide( '$.css.opacity' );
goog.require( '$.css.patches' );
/** @const {string} */
$.css.opacity = void 0 !== p_style.opacity || document.documentMode < 9
? 'opacity'
: void 0 !== p_style.MozOpacity
? 'MozOpacity'
: void 0 !== p_style[ '-khtml-opacity' ]
? '-khtml-opacity'
: '';
if( document.documentMode < 9 ){
/**
* IE ~8
* @const {!function(!Element, (string|number))}
*/
$.css.patches[ $.css.opacity ] = function( element, value ){
element.style.filter = 'alpha(opacity=' + (+ value) * 100 + ')';
};
};
ビルド用ファイル
次のコマンドで dist/ 以下に、2パターンのアプリを書き出します。
npm install gulp app
package.json
{
"devDependencies": {
"google-closure-compiler": "^20240317.0.0",
"gulp": "^4.0.2"
}
}
gulpfile.js
const gulp = require('gulp'),
ClosureCompiler = require('google-closure-compiler').gulp(),
gulp.task('app', gulp.series(
function () {
return gulp
.src(
['./src/closure-primitives/base.js', './src/**/*.js']
).pipe(
ClosureCompiler(
{
dependency_mode : 'PRUNE',
entry_point : 'goog:app1',
compilation_level : 'ADVANCED',
formatting : 'PRETTY_PRINT',
warning_level : 'VERBOSE',
language_in : 'ECMASCRIPT3',
language_out : 'ECMASCRIPT3',
js_output_file : 'app1.js'
}
)
).pipe(
gulp.dest('dist')
);
},
function () {
return gulp
.src(
['./src/closure-primitives/base.js', './src/**/*.js']
).pipe(
ClosureCompiler(
{
dependency_mode : 'PRUNE',
entry_point : 'goog:app2',
compilation_level : 'ADVANCED',
formatting : 'PRETTY_PRINT',
warning_level : 'VERBOSE',
language_in : 'ECMASCRIPT3',
language_out : 'ECMASCRIPT3',
js_output_file : 'app2.js'
}
)
).pipe(
gulp.dest('dist')
);
}
));
コンパイル結果を確認する
dist / app2.js
IE8 以下用の patch も組み込まれているのが分かります。
7行で e(== $.css.patches)
の存在確認をしているのが冗長ですが、残念ながら、これを最適化する方法は見つかりませんでした。
function d() {
this.g = document.getElementById("app");
}
d.prototype.setStyle = function(a, b) {
if (a) {
var c;
e && (c = e[a]);
c ? c(this.g, b) : this.g.style[a] = "" + b;
}
};
var e = {}, f = document.body.style;
var g = void 0 !== f.opacity || 9 > document.documentMode ? "opacity" : void 0 !== f.MozOpacity ? "MozOpacity" : void 0 !== f["-khtml-opacity"] ? "-khtml-opacity" : "";
9 > document.documentMode && (e[g] = function(a, b) {
a.style.filter = "alpha(opacity=" + 100 * +b + ")";
});
(new d()).setStyle(g, 0.5);
dist / app1.js goog.require()
しない場合のコンパイル結果を確認する
このコードは、opacity
のクロスブラウザ対応を開発者が失念したものではなく、結果的に patch が不要な CSS プロパティだけで実現したアプリケーションと考えてください。
opacity
の patch 関連が組み込まれず小さくなっていますが、c.h(= $.css.patches)
が undefined
にも拘わらず、7~9行に patch 用の処理が残ってしまうのが気がかりです。しかし以降で、黒魔術的なコードを追加して解決しますのでご安心を。
var c = {};
function d() {
this.g = document.getElementById("app");
}
d.prototype.setStyle = function(a, e) {
if (a) {
var b;
c.h && (b = c.h[a]);
b ? b(this.g, e) : this.g.style[a] = "" + e;
}
};
(new d()).setStyle("opacity", 0.5);
$.css.patches
周りの更なる最適化をするには?
src / $ / $.js へ追記する
$
の宣言の後に $.css.patches
に Falthy な値を代入してみました。@suppress は Closure Compiler の警告から解決できないものを非表示にする指示です。
var $ = function( elmOrID ){...};
/** @suppress {checkTypes} */
$.css.patches = 0;
それではコンパイル結果を見ていきます。
dist / app1.js($.js に追記してコンパイル)
狙い通り setStyle
メソッドが最適化されています!
function a() {
this.g = document.getElementById("app");
}
a.prototype.setStyle = function(b, c) {
b && (this.g.style[b] = "" + c);
};
(new a()).setStyle("opacity", 0.5);
dist / app2.js($.js に追記してコンパイル)
4行(var e = 0;
)と12行(e = {};
)で冗長になってしまったのが気になりますが、上の結果が得られたので、この程度はいったん良しとします。
function b() {
this.g = document.getElementById("app");
}
var e = 0;
b.prototype.setStyle = function(a, c) {
if (a) {
var d;
e && (d = e[a]);
d ? d(this.g, c) : this.g.style[a] = "" + c;
}
};
e = {};
var f = document.body.style;
var g = void 0 !== f.opacity || 9 > document.documentMode ? "opacity" : void 0 !== f.MozOpacity ? "MozOpacity" : void 0 !== f["-khtml-opacity"] ? "-khtml-opacity" : "";
9 > document.documentMode && (e[g] = function(a, c) {
a.style.filter = "alpha(opacity=" + 100 * +c + ")";
});
(new b()).setStyle(g, 0.5);
さいごに
正に Closure Library が goog.require('goog.dom.tagName.DIV')
で取得した文字列定数で goog.dom.createDOM(goog.dom.tagName.DIV)
する作法です。
ですので、てっきり Closure Library には、アプリケーションが使用する tagName
の patch だけを組み込む仕組みがあるのかと思ったのですが、詳しく見ていくとそうでは無いようです。
今回の手口を、tagName
、属性(属性名と列挙型の値)、スタイル(プロパティ名と列挙型の値)、イベントなどで実施すれば、createElement
, setAttr
, setStyle
, on
, fire
等のメソッド内からクロスブラウザ用のコードの多くを外に出し、使用するアプリケーションにだけ組み込むことが出来ます。
ということは、アプリケーションの無用な肥大化を気にせずに(適正には大きくなります)、クロスブラウザ対応のためのコードをモリモリとライブラリに追加していくことができます。
加えて、今回のように各 patch の実行時の有効化から、browser-compat-data 等を元にコンパイル(ビルド)時に死活できれば、いよいよ、ブラウザエンジンとバージョン別コンパイル(ビルド)が視野に入ります。