依存関係を考慮した部品の読み込み Feature List

思えば、JavaScript を学び始めて、最初に立ちふさがった壁が「IEと他のブラウザで動きが違う」「include すら無い」でした。
これって、プログラミング初学者に誤解を与えるには十分な威力です。

それゆえ「文字を点滅、広告を次々に表示、画面を揺らす」などのチープな使われ方に甘んじた暗黒の時代もありました。

しかし、それは、使う側のスキルとセンスの無さを、「JavaScript とブラウザ側の問題だ!!」と、問題をすげ替えていたに過ぎません。

クレーマーはまず自分にクレームを出すべきなんです。

前フリおしまい

uupaa.js では、不足しているソースコード(機能)を動的にロードする機能(uu.module)を提供しています。
今日は「uu.module は廃棄予定ですよ」日記です。

uu.module の 何が問題だったか?

uu.module は お手製の include 機能としては上手く動いているのですが、依存関係までは解決しないので、使う側が include 順を正確に指示する必要がありました。

つまり、機能A が 機能B と 機能C に依存し、機能C が 機能D を必要としている場合に uu.module はスマートな解決方法を提示していないのです。
「とにかく include 機能を提供する」というポリシーで設計された uu.module は、ライブラリ大規模化に伴いやっかいな問題を引き起こしかねない弱点でした。

依存関係をどうやって管理するか

依存関係を自前で解決するインテリジェンスな機能(※)を積むわけにはいかないので、何らかの構造を持たせ、事前に依存関係を定義する必要があります。
※ 読み込み直後にJavaScriptをテキストとしてパースし、未知のトークン(関数/メソッド/キーワード)を発見したら、あるルールに基づき…うんぬん

今回は、このようなデータ構造を定義しました。

uu.featureList = {
  A: "B,C,D",
  B: "D",
  D: "E"
};

これは、

  • 機能A は、機能B,C,D に依存しているため、それらを自動的にロードし利用可能になった後にロードされるべし
  • 機能B は、機能D に依存しているため、D を自動的にロードし利用可能になった後にロードされるべし
  • 機能D は、機能E に依存しているため、E を自動的にロードし利用可能になった後にロードされるべし

という依存関係を表しています。

昨今の日本事情に照らし合わせて説明すると、

車A を使う(作る)ためには、町工場B,C,D,Eが生きてることが必要なんだけど、
天下のトヨタさんは、日頃から生かさず殺さずで容赦ないから、この不況で下請けが軒並み機能しなくなっちゃって、
重要な部品が調達できないと車は生産できねーし、北米で車売れねーし、輸出しようにもはんぱねぇ円高で、
「わが国のトヨタが世界一」って意味無く報道しておべんちゃらするメディアは広告費の超削減で、そろそろ終了ですね。
広告費に依存しきっているWebサイトは 収益構造の改革を断行する余裕も無く 死んでいくのでしょうね。

ってことです。

一番上の行以外は全く関係ない話になってるし、なんか書いててイライラする構造ですね。
# 儲かってたときの利益はどこに消えたんだよ。と。

すまんすまん。話を戻そう

uu.featureList = {
  A: "B,C,D",
  B: "D",
  D: "E"
};

このリスト構造は、依存関係を表すリストにも見えるため、dependencyList のようなネーミングが直球なのですが、より抽象化した使われ方を想定しているので、 featureList としています(理由は後で)。

依存関係の設定は、機能が増えたときに一度だけやれば良いので、次はユーザからの使われ方を考えてみます。

ユーザの視点で

<script type="text/javascript" src="uupaa-detect.js"></script>
<script type="text/javascript" src="uupaa-feature.js"></script>
<script>
function boot() {
  uu.feature("A,X", function(ok, miss) {
    window.status = (ok ? "ok" : "ng") + ": " + miss;
  });
}
window.onload = boot;
</script>

uu.feature( feat, fn ); は、

  • feat で指定された機能をロードする
    • ロードするファイル名は、feat.js
  • ロード成功で fn(true, "") をコールバックする
  • ロード失敗で fn(false, 読み込めなかった機能の一覧) をコールバックする
  • feat には "feat" または "feat,feat2, ..." のようにカンマ区切りの文字列を指定する
  • 一定時間レスポンスが無い場合は、ロード失敗とする。
    • ロード失敗で、ロード中のスクリプトを全てアンロードする
    • 完了している分についてはアンロードしない
    • 失敗したファイル(機能)のリストを "feat" または "feat,feat2, ..." の形で fn の第二引数に渡してくる。
    • タイムアウト時間は変更可能(uu.feature.timeout)で、デフォルトは 10秒

uu.module との違い

  • ロードするファイル名の変更
    • uu.module は "uu.module.モジュール名.js"
    • uu.feature は "機能名.js"
  • uu.module にあった非同期ロード(パラレルロード) と uu.module.loadSync による同期ロード(シリアライズロード) は無くなる。
    • uu.feature はパラレルでロードし、依存関係がある場合だけは自動的にシリアライズしつつロードする。
  • 機能(JavaScriptファイル)の読み込み先を指定する引数を削る。
    • デフォルトの読み込み先は、uupaa-detect.js または uupaa.js が設置されているディレクト
    • uu.feature.loadFrom(path) でロード先を指定可能。
  • ロードに失敗したファイルを自動的にリロードする機能を削る。
    • ロードに失敗したファイルを別のサーバから読み込む処理はユーザが記述する(uu.module は半自動でやってた)
  uu.feature("A,X", function(ok, miss) {
    if (!ok) {
      uu.feature.loadFrom("http://www.example.com/js"); // 読み込み先変更
      uu.feature(miss, function(ok, miss) {
        if (!ok) {
          alert("fail");
        }
      });
    }
  });
  • 設計ポリシーの違い
    • uu.module は、やりたいこと(大きな一塊の機能)を実現するために、何が必要かをユーザが知っていて、ユーザは必要な部品を全て順序正しく指示する。依存している場合は順番を守らないとエラー。C の include がまさにこれ。
    • uu.feature は、やりたいことを実現するために、何が必要かをシステム(機能の作者)が知っていて、ユーザはやりたいことを指示するだけ。

最大の違いは設計ポリシーです。
半年前に設計したときには見えていなかったものが、やっと見えてきただけなんですけどね。

コードに落としてみた。

if (!uu.feature) {

uu.featureList = {
  A: "B,C,D",
  B: "D",
  D: "E"
};

// uu.feature - load feature
uu.feature = function(names, fn /* = undefined */) {
  uu.feature._impl(names, fn);
};

// --- local scope ------------------------------------------------------
(function() {
var _win = window, _doc = document, _ua = uuClass.Detect,
    _feat = uu.feature,
    _from = _ua.base, _jobid = 0, _debug = 0,
    _head = _doc.getElementsByTagName("head")[0],
    FEATURE_LIST = "featureList";

// uu.feature.timeout - timeout (unit: ms)
_feat.timeout = 10 * 1000; // 10s

// uu.feature.loadFrom - set script base path
_feat.loadFrom = function(url) {
  _from = url;
};

// job database
_feat._job = { /* jobid: { fn: fn, ary: [name, ...], timeout } */ };

// status keeper
_feat._run = { /* feat: run */ }; // 1: loading, 2: loaded

_feat._impl = function(names, fn) {
  var ary = [], jobid = ++_jobid;
  names.split(",").forEach(function(v) {
    _feat._collect(v, ary);
  });
  if (ary.length) {
    ary.reverse();
    _feat._job[jobid] = { fn: fn, ary: ary, timeout: (new Date | 0) + _feat.timeout };
    _feat._runner(jobid);
    setTimeout(_feat._watchdog, _feat.timeout);
  } else {
    fn(true, ""); // already loaded
  }
};

_feat._collect = function(name, rv) {
  if (_feat._run[name]) { return; } // loading or loaded -> skip
  (rv.indexOf(name) < 0) && rv.push(name); // pushed -> skip
  if (name in uu[FEATURE_LIST]) {
    uu[FEATURE_LIST][name].split(",").forEach(function(v) {
      _feat._collect(v, rv); // recursive call
    });
  }
};

_feat._runner = function(jobid) {
  var job = _feat._job[jobid];
  job.ary.forEach(function(v) {
    if (_feat._run[v]) { return; } // skip
    if (_feat._isDepend(v)) { return; } // lazy

    var scr = _doc.createElement("script");
    scr.id = "uu.feature." + v + ".js"; // "uu.feature.{name}.js"
    scr.type = "text/javascript";
    scr.charset = "utf-8";
    if (_ua.ie) {
      scr.onreadystatechange = function() {
        if (!(/loaded|complete/.test(this.readyState))) { return; }
        (v in _feat) ? _feat._done(jobid, v) : _feat._destroy(jobid);
      };
    } else {
      scr.onload =  function() {
        (v in _feat) ? _feat._done(jobid, v) : _feat._destroy(jobid);
      };
      scr.onerror = function() {
        _feat._destroy(jobid);
      };
    }
    scr.setAttribute("src", _from + "/" + v + ".js");
    _head.appendChild(scr);
    _feat._run[v] = 1;
  });
};

_feat._isDepend = function(name) {
  if (!(name in uu[FEATURE_LIST])) { return false; }
  var ary = uu[FEATURE_LIST][name].split(","), i = 0, iz = ary.length;
  for (; i < iz; ++i) {
    if (_feat._run[ary[i]] !== 2) {
      return true; // depend
    }
  }
  return false; // not depend
};

_feat._done = function(jobid, name) {
  if (_debug) { _win.status += name; }

  _feat._run[name] = 2; // loaded

  var job = _feat._job[jobid], fn;
  if (job) {
    job.ary.splice(job.ary.indexOf(name), 1); // delete
    if (!job.ary.length) {
      fn = job.fn;
      delete _feat._job[jobid]; // delete job
      fn && fn(true, ""); // load complete
    } else {
      _feat._runner(jobid); // next
    }
  } else {
    throw Error("lost job");
  }
};

_feat._watchdog = function() {
  var n = 0, jobid, time = new Date | 0;
  for (jobid in _feat._job) {
    ++n;
    if (time > _feat._job[jobid].timeout) {
      _feat._destroy(jobid);
    }
  }
  if (n) {
    setTimeout(_feat._watchdog, _feat.timeout);
  }
};

_feat._destroy = function(jobid) {
  var job = _feat._job[jobid], fn, names;

  job.ary.forEach(function(v, i) {
    var e = _doc.getElementById("uu.feature." + v + ".js");
    e && e.parentNode.removeChild(e);
  });

  fn = job.fn;
  names = job.ary.join(",");
  delete _feat._job[jobid];
  fn && fn(false, names); // call
};

})(); // end (function())() scope

} // if (!uu.feature)

試してみる

uupaa-detect.js と同じディレクトリに、ファイル(A.js, B.js, C.js, D.js, E.js, X.js)を作成します。
ファイルの中身は、

uu.feature.A = {};

のようにしておきます。A の部分はファイル名にあわせて変えます。

<html><head><title>uu.feature</title>
<script type="text/javascript" src="uupaa-detect.js"></script>
<script type="text/javascript" src="uupaa-feature.js"></script>
</head>
<body>
<script>
/*
uu.featureList = {
  A: "B,C,D",
  B: "D",
  D: "E"
};
 */
function boot() {
  uu.feature("A,X", function(ok, miss) {
    window.status = (ok ? "ok" : "ng") + miss;
  });
}
window.onload = boot;
</script>
</body></html>

負荷テストなどはやって無いのでアレですが、上手く依存関係を処理してくれてるようです。