Ajax リクエストの順番を直感的に記述可能な uu.ajax.queue を追加


uu.ajax.queue は順番や待ち合わせなどの規則を指定した Ajax リクエストを可能にします。

uu.ajax.queue(cmd, [url, ...], [option, ...], [fn, ...], lastfn, ngfn)

  • cmd には "0+1+2" や "a>b+c>d" などのような規則を文字列で記述します
    • a+b なら a と b を同時に読み込みます。
      • ブラウザ毎に、同時にリクエスト可能な数に制限があるので、同時にならないこともあります。
    • a>b なら a をロード後に b をロードします。
    • a や b の部分には一文字の英数字(0-9,a-z,A-Z)を指定できます。
    • ここで指定した ID は lastfn や ngfn の第一引数(hash.id)に設定され、リクエストの追跡に利用できます。
  • option オプションを Hash で指定します。省略可能で、デフォルトは {} です。
    • option.data - Mix(= null): request data
    • option.header - Array(= []): [(key, value), ...]
    • option.timeout - Number(= 10): unit sec
    • option.xhr - Object(= void 0): overrideMimeType
    • option.method - String(= "GET" or "POST"):
  • fn 各リクエスト成功時のコールバック関数を指定します。省略可能です。
    • fn は fn({ code, rv, url, guid, type, id }) の形でコールバックします。
      • code - statusCode (200 や 404)
      • rv - responseText や responseXML など(JSONP なら json オブジェクト)
      • url - リクエストURL
      • guid - GUID(システム全体でユニークな番号)
      • type - Content-Type: "text/css" や "text/html" など
      • id - cmd で指定した ID
  • lastfn 全リクエスト成功時のコールバック関数を指定します。省略可能です。
    • lastfn は lastfn([{ code, ...}, ...]) の形でコールバックします。 { code, ...} の内容は fn と同じです
  • ngfn いずれかのリクエスト失敗時のコールバック関数を指定します。省略可能です。
    • ngfn は ngfn({ code, ...) の形でコールバックします。 { code, ...} の内容は fn と同じです
    • 複数のリクエストが失敗しても最初のエラーしか捕捉しません。

実装

// uu.ajax.queue - request queue
// uu.ajax.queue("a+b>c", [url, ...], [option, ...], [fn], lastfn, ngfn)
function uuajaxqueue(cmd,    // @param String: "0>1", "0+1", "0+1>2>3"
                     urlary, // @param Array: [url, ...]
                     optary, // @param Array: [option, ...]
                     fnary,  // @param Array: [fn, ...]
                     lastfn, // @param Function(= void 0): lastfn([{...}, ... ])
                     ngfn) { // @param Function(= void 0): ngfn({...})
  _uuajaxqueue(cmd, urlary, optary, fnary, lastfn || uuvain, ngfn || uuvain, []);
}

// inner -
function _uuajaxqueue(cmd, urlary, optary, fnary, lastfn, ngfn, rv) {
  function _nextq(r) {
    _uuajaxqueue(cmd, urlary, optary, fnary, lastfn, ngfn, rv.concat(r)); // recursive call
  }
  if (!cmd) {
    lastfn(rv); // finish
    return;
  }
  var ary = cmd.split(""), v, url = [], opt = [], fn = [];

  cmd = "";
  while ( (v = ary.shift()) ) { // v = "0"
    url.push(urlary.shift())
    opt.push(uumix(optary.shift() || {}, { id: v })); // set option.id
    fn.push(fnary.shift() || uuvain);
    if (ary.shift() === ">") { // v = "+" or ">"
      cmd = ary.join(""); // rebuild command, "0+1>2>3" -> "2>3"
      break;
    }
  }
  url.length && _uuajaxparallel(url, opt, fn, _nextq, ngfn);
}

// inner - ajax parallel load
function _uuajaxparallel(urlary, optary, fnary, lastfn, ngfn) {
  function _nextp(hash) {
    var idx = uuaryindexof(atom, hash.guid); // = Array.indexOf

    rv[idx] = hash;
    ++n >= iz && !run++ && lastfn(rv); // fn([{...}, ..])
  }
  function _error(hash) {
    !run++ && ngfn(hash);
  }
  var rv = [], atom = [], i = 0, iz = urlary.length, n = 0, run = 0;

  for (; i < iz; ++i) {
    atom.push(uu.ajax(urlary[i], optary[i], fnary[i], _error, _nextp));
  }
}

デモ

http://pigs.sourceforge.jp/blog/20091221/uupaa.ajax.queue.htm

<!doctype html><html><head><meta charset="UTF-8" /><title></title>
<script src="uupaa.js"></script>
<script> 
function ok(ary) {
  uu.puff(ary);
}
 
function ng(hash) {
  uu.puff(hash);
}
 
// 1+2>3の順にアクセスする
function queue1() {
  uu.ajax.queue("1+2>3",
      ["1.php",
       "2.php",
       "3.php"],
      [],
      [function() { window.status += "1"; },
       function() { window.status += "2"; },
       function() { window.status += "3"; }],
      ok,
      ng);
}
// 3>1>2の順にアクセスする
function queue2() {
  uu.ajax.queue("3>1>2",
      ["3.php",
       "1.php",
       "2.php"],
      [],
      [function() { window.status += "3"; },
       function() { window.status += "1"; },
       function() { window.status += "2"; }],
      ok,
      ng);
}
// 1+2+3の順にアクセスする
function queue3() {
  uu.ajax.queue("1+2+3",
      ["1.php",
       "2.php",
       "3.php"],
      [],
      [function() { window.status += "1"; },
       function() { window.status += "2"; },
       function() { window.status += "3"; }],
      ok,
      ng);
}
// 1+2+3>0の順にアクセスする
function queue4() {
  uu.ajax.queue("1+2+3>0",
      ["1.php",
       "2.php",
       "3.php",
       "0.php"],
      [],
      [function() { window.status += "1"; },
       function() { window.status += "2"; },
       function() { window.status += "3"; },
       function() { window.status += "0"; }],
      ok,
      ng);
}
// 0+1+0>2の順にアクセスする
function queue5() {
  uu.ajax.queue("0+1+0>2",
      ["0.php",
       "1.php",
       "0.php",
       "2.php"],
      [],
      [function() { window.status += "0"; },
       function() { window.status += "1"; },
       function() { window.status += "0"; },
       function() { window.status += "2"; }],
      ok,
      ng);
}
// 0+1+0>2の順にアクセスする
// HEAD 
function queue6() {
  uu.ajax.queue("0+1+0>2",
      ["0.php",
       "1.php",
       "0.php",
       "2.php"],
      [
        { method: "HEAD" },
        { method: "GET"  },
        { method: "POST" },
        { method: "HEAD" },
      ],
      [function() { window.status += "0"; },
       function() { window.status += "1"; },
       function() { window.status += "0"; },
       function() { window.status += "2"; }],
      ok,
      ng);
}
// 0+1+9>2の順にアクセスする( 9.php が存在しないためエラーになる)
function queue7() {
  uu.ajax.queue("0+1+9>2",
      ["0.php",
       "1.php",
       "9.php",
       "2.php"],
      [
        { method: "HEAD" },
        { method: "GET"  },
        { method: "GET"  },
        { method: "HEAD" },
      ],
      [function() { window.status += "0"; },
       function() { window.status += "1"; },
       function() { window.status += "9"; },
       function() { window.status += "2"; }],
      ok,
      ng);
} 
</script></head><body>
<input type="button" value="queue(1+2>3)" onclick="queue1()" />
<input type="button" value="queue(3>1>2)" onclick="queue2()" />
<input type="button" value="queue(1+2+3)" onclick="queue3()" />
<input type="button" value="queue(1+2+3>0)" onclick="queue4()" />
<input type="button" value="queue(0+1+0>2)" onclick="queue5()" />
<input type="button" value="queue(0+1+0>2, HEAD)" onclick="queue6()" />
<input type="button" value="queue(0+1+9>2, ERR)" onclick="queue7()" />
</body></html>

複数のストリームで待ち合わせをしたい場合は

1 → 2 → 3 ┐
            ├→ A → B
a → b → c ┘

ストリーム(1 > 2 > 3)と(a > b > c)を読み込み後に、(A > B)を読み込みたい場合は以下のようにします。

var resultArray = [], run = 0;
function job() {
  uu.ajax.queue("1>2>3", [...], [], [], wait, ng);
  uu.ajax.queue("a>b>c", [...], [], [], wait, ng);
}
function wait(ary) {
  resultArray = [].concat(resultArray, ary);
  if (++run === 2) {
    uu.ajax.queue("A>B", [...], [], [], ok, ng);
  }
}
function ok(ary) {
  resultArray = [].concat(resultArray, ary); // リクエスト結果を結合
}
function ng(hash) {
  uu.puff(hash); // エラー
}

なにやらスマートさが大幅ダウンです。
このようなコードが必要になったらちょっと設計を見直すべきかもしれません。


問題がなければ、この変更は次回のリリースから適用されます。

反省会

  • コードを読めば分かるように、"+" を認識していないため、(1-2>3) も(1+2>3) も同じ結果になります。
    • 仕様をしっかり明示できていれば、これぐらいの手抜き(エラーチェック分のコード削り)はむしろ有りだと思うんだ。