昨日、「プロミス処理のラッパーをつくる」という記事を書いた動機付けが、今日の記事。
tl;dr
結論から言うと、JavaScript 側から Modernizr 挿入をコントロールして、更に Modernizr.load
機能で必要な互換処理スクリプトも挿入するフローを実装したかった。
scripts =
# 一番最初に挿入される `<script>` の配列
modernizr: [ '/js/vendor/modernizr.custom.js' ]
# `Modernizr.load` で必要に応じて挿入される `<script>`
polyfills: [
test: (M) -> M.json
nope: '//cdnjs.cloudflare.com/ajax/libs/json3/3.3.2/json3.min.js'
,
test: (M) -> M.ie8compat
yep: '//cdnjs.cloudflare.com/ajax/libs/aight/1.2.2/aight.min.js'
,
test: -> !global.console
yep: '//cdnjs.cloudflare.com/ajax/libs/loglevel/1.4.0/loglevel.min.js'
]
# Modernizr の挿入
scriptsInjected scripts.modernizr
.then ->
# `Modernizr.load` の実行
modernized scripts.polyfills
.then ->
# メイン処理
この為に 2 つ関数を作成した。
scriptsInjected
内部の loaded
関数、modernized
内部の completed
関数を Promise 化するのに、promisify
関数を使っている。
※ ちゃんと動作してなかったので、修正しました。
// Underscore.js、jQuery と Modernizr が必要。
/**
* scriptInjected
*/
var scriptsInjected = function (urls) {
var selector = 'script', fjs = document.getElementsByTagName(selector)[0];
var loaded = promisify(function (resolve, reject, url) {
var js = document.createElement(selector); js.src = url;
if (js.addEventListener) {
js.addEventListener('load', resolve, false);
js.addEventListener('error', reject, false);
fjs.parentNode.insertBefore(js, fjs);
} else {
// MSIE8 以下が `onload` 拾えないので。
$.getScript(url).then(resolve, reject);
}
});
return all(_.map(urls, loaded));
};
/**
* modernized
*/
var modernized = function (tests) {
var M = window.Modernizr, len = tests.length;
var completed = promisify(function (resolve, reject) {
var loads = _.map(tests, function (test, idx) {
test.test = test.test(M);
if (idx === len - 1) test.complete = resolve;
return test;
});
M.load(loads);
});
return completed();
};
ちなみに上記関数で使用している promisified
と all
関数は説明していないが、promisify
を用いて値を Promise にして返す関数と、複数の Promise 同期する $.when
のショートハンド。
// Underscore.js が必要。
/**
* promisified
*/
var promisified = promisify(function (resolve, reject, val) {
if (_.isObject(val) && val instanceof Error) return reject(val);
resolve(val);
});
/**
* all
* これは単に `$.when` のラッパー
*/
var all = function () {
var args = _.toArray(arguments), promises = _.isArray(args[0]) ? args[0] : args;
return $.when.apply(undefined, promises);
};
関心の分離
一連の実装動機は、Underscore.js と jQuery だけで、ES5 に満たない環境でも動作するスクリプトを書くというところから来た。
gulp.js でモジュール化して、細かい処理を沢山書いていて、Promise 系のモジュールと、クライアントサイド JS のモジュールを分けていた。上記の scriptsInjected
と modernized
関数も、クライアントサイド JS のモジュールとして書いていたが、機能的に Promise が必要になった。
上記関数内部で、直接 Deferred オブジェクトを生成する処理を書いた方が単純なんだけど、すごく冗長で、後で Promise 回りを修正するかも知れないと思うと、Promise モジュールとして一箇所に集約したかった。
後、Modernizr はタスクランナーでモジュール化する時、単純に require
だと動作しなかったり、この手の手法と相性が悪くて、自分なりの落としどころを見つけたかったんだけど、この手法はしっくりきた。これで JS の処理フローは全部 JS に定義できる。
すっきり。