初心に帰れるうちが花

今後の uupaa.js の形を決めました。

これまで

  • uupaa.js ファイルに必要と思われる機能をギチギチに詰め込んでる。
  • コードの可読性が低い + version間でdiff取れない(弄りすぎ)。
  • uu.module() でモジュールを読み込むと色々機能が増えるみたいだけど、実用性はほとんどない。
なんとかスパイラル

☆ 新機能を追加する ☆
 → 依存関係が発生したり、依存関係が複雑化
  → 依存関係を解消するために本体(uupaa.js)に必要なコードを詰め込む
   → コード量増加に伴い、デバッグ効率が落ちる(モッサリ Firebug)
    → 本体(uupaa.js)をさらに圧縮する
     → ★ たいへんなことに ★
      → ちゃぶ台がえし ← いまここ

これから

  • 1ファイルに詰め込むやり方じゃ、だめだぁ〜。
  • uupaa.js はビルドして作り出すファイルになる。
    • 最小構成でビルドすると 500〜600行ぐらいに。
      • 核 は 100行ぐらい。
  • 不足している機能は、uu.feat() でオンデマンドロードすると使えるようになる。
    • 依存関係の管理は、uu.feat() がやってくれる。
  • コードのリライトは必要最小限に
    • 部品を大幅に書き換えたり、互換性が失われる場合は、ファイル名を変更する
  • ビルダーを提供する
    • make ファイル相当のものか、サイト上で必要な機能を選択しビルドする機能を提供する
      • 多数のファイルをネットワーク越しに読み込みたくない人は、必要な機能を詰め込んで 1ファイル化 してしまえるようにする。

0.7 以降で解決する(解決したい)問題

  1. 依存関係を気にしたくない
  2. 詰め込みたくない
    • 依存関係を気にする必要が無くなると、詰め込む必要も無くなる
    • 1ファイルが100〜500stepぐらいの大きさになるので、Firebug もサクサク動いてくれる
    • 誰にでも読めるような平易なコードをそのままリリースできる。

具体的にはどんな感じになるのさ?

手元のやつはこんな感じ

// uupaa.js version 0.7.0α
if (!window.uu) {
// === Global namespace pollution ==========================
var uu, // core and class namespace
    UU; // const and config namespace

// === Core ================================================
window.uu = uu = function() {
  return uu._impl.apply(this, arguments); // adapter
};

window.UU = UU = {
  VERSION: [0, 7, 0],
  CONFIG: {
    AUTO_RUN: "boot",       // uu.ready(window.boot) function
    FEAT_URL1: "src",       // primary url
    FEAT_URL2: "",          // secondary url
    FEAT_TIMEOUT: 20000,    // timeout(unit: ms) - 20sec
    QUERY_CACHE: 1,         // 1: enable cache
    DEBUG: 0                // 1: debug mode
  }
};

// uu.mix - mixin
uu.mix = function(base, flavor, aroma, override /* = true */) {
  var i, ride = (override === void 0) || override;

  for (i in flavor) {
    if (ride || !(i in base)) {
      base[i] = flavor[i];
    }
  }
  return aroma ? uu.mix(base, aroma, null, ride) : base;
};

uu.mix(uu, {
  // uu.uuid - unique-id generator
  uuid: function() {
    return ++uu._uuid;
  },
  _uuid: 0,

  // uu.id - id query
  id: function(id) { // tiny
    return document.getElementById(id);
  },

  // uu.tag - tag query
  tag: function(tag, context /* = document */) { // tiny
    var ary = (context || document).getElementsByTagName(tag),
        rv = [], ri = -1, v, i = 0, iz = ary.length;

    for (; i < iz; ++i) {
      v = ary[i];
      v.nodeType === 1 && (rv[++ri] = v);
    }
    return rv;
  },

  // uu.className - className query
  className: function(expr, context /* = document */, really /* = false */) {
    return uu.className.query(expr, context, really); // adapter
  },

  // uu.css - css query
  css: function(expr, context /* = document */, really /* = false */) {
    return uu.css.query(expr, context, really); // adapter
  },

  // uu.feat - load feature
  feat: function(feat, fn, url1 /* = "" */, url2 /* = "" */) {
    uu.feat._impl(feat, fn, url1 || UU.CONFIG.FEAT_URL1,
                            url2 || UU.CONFIG.FEAT_URL2); // adapter
  },

  // uu.base - ref base directory
  base: function() {
    if (!uu._base) {
      uu._base = location.protocol + "//"
               + location.pathname.replace(/\\/g, "/");

      uu.tag("script").forEach(function(v) {
        if (/uupaa.*\.js$/.test(v.src)) {
          var elm = document.createElement("div"), ary;
          elm.innerHTML = '<a href="' + v.src + '" />';
          ary = elm.firstChild.href.split("/"); // "http://example.com/dir/uupaa.js"
          ary.pop();                            // drop file name "uupaa.js"
          uu._base = ary.join("/");             // "http://example.com/dir"
        }
      });
    }
    return uu._base;
  },
  _base: "",

  // uu.head - ref <head> tag
  head: document.getElementsByTagName("head")[0]
});

uu.feat.list = {
  // feat   : "depend, ..."
  core      : "xbrowser,feat,ready",
  query     : "core,oop,type,ua,aid,node,attr,viewport,css,classname,query.quick",
  "query+"  : "query",
  j         : "core,xbrowser+,event,query,query+"
};

// === Cross Browser =======================================
// depend: none

// --- Array.prototype Cross Browser ---
uu.mix(Array.prototype, {
  // Array.prototype.lastIndexOf
  lastIndexOf: function(needle, fromIndex /* = this.length */) {
    var iz = this.length, i = fromIndex;
    i = (i < 0) ? i + iz : iz - 1;

    for (; i > -1; --i) {
      if (i in this && this[i] === needle) {
        return i;
      }
    }
    return -1;
  },

  // Array.prototype.indexOf
  indexOf: function(needle, fromIndex /* = 0 */) {
    var iz = this.length, i = fromIndex || 0;
    i = (i < 0) ? i + iz : i;

    for (; i < iz; ++i) {
      if (i in this && this[i] === needle) {
        return i;
      }
    }
    return -1;
  },

  // Array.prototype.forEach
  forEach: function(fn, bindThis /* = undefined */) {
    var i = 0, iz = this.length;

    for (; i < iz; ++i) {
      if (i in this) {
        fn.call(bindThis, this[i], i, this);
      }
    }
  },

  // Array.prototype.filter
  filter: function(fn, bindThis /* = undefined */) {
    var rv = [], ri = -1, v, i = 0, iz = this.length;

    for (; i < iz; ++i) {
      if (i in this) {
        v = this[i];
        if (fn.call(bindThis, v, i, this)) {
          rv[++ri] = v;
        }
      }
    }
    return rv;
  },

  // Array.prototype.every
  every: function(fn, bindThis /* = undefined */) {
    var i = 0, iz = this.length;

    for (; i < iz; ++i) {
      if (i in this) {
        if (!fn.call(bindThis, this[i], i, this)) {
          return false;
        }
      }
    }
    return true;
  },

  // Array.prototype.some
  some: function(fn, bindThis /* = undefined */) {
    var i = 0, iz = this.length;

    for (; i < iz; ++i) {
      if (i in this) {
        if (fn.call(bindThis, this[i], i, this)) {
          return true;
        }
      }
    }
    return false;
  },

  // Array.prototype.map
  map: function(fn, bindThis /* = undefined */) {
    var rv = Array(this.length), i = 0, iz = this.length;

    for (; i < iz; ++i) {
      if (i in this) {
        rv[i] = fn.call(bindThis, this[i], i, this);
      }
    }
    return rv;
  }
}, 0, 0);

// --- String.prototype Cross Browser ---
uu.mix(String.prototype, {
  // String.prototype.trim - from Firefox3.1
  trim: function() {
    return this.replace(/^\s+|\s+$/g, "");
  }
}, 0, 0);

// === Feature =============================================
// depend: xbrowser

uu.mix(uu.feat, {
  // uu.feat._impl
  _impl: function(feat, fn, url1, url2) {
    function DIE(miss) {
      throw TypeError("uu.feat: " + miss);
    }
    uu.feat.from(url1).load(feat, function(ok, miss) {
      if (ok) {
        fn(); // loaded
      } else {
        !url2 && DIE(miss);
        uu.feat.from(url2).load(miss, function(ok, miss) {
          !ok && DIE(miss);
          fn(); // safeguard
        });
      }
    });
  },

  // uu.feat.from - set feature(script file) base path
  from: function(url) {
    uu.feat._from = url.replace(/\/+$/, ""); // trim tail "/"
    return uu.feat;
  },
  _from: uu.base(),

  // uu.feat.load - load feature
  load: function(feat, fn /* = undefined */) {
    var feats = [], jobid = uu.uuid(),
        tm = UU.CONFIG.FEAT_TIMEOUT;

    // collect dependency list
    feat.split(",").forEach(function(v) {
      uu.feat._collect(v.trim(), feats);
    });

    if (feats.length) {
      uu.feat._job[jobid] = {
        fn:      fn,
        feats:   feats,
        timeout: +new Date + tm
      };
      uu.feat._runner(jobid);
      setTimeout(uu.feat._watchdog, tm);
    } else {
      fn(true, ""); // already loaded
    }
  },

  // uu.feat.already
  already: function(feat) { // feat = "feat" or "feat, feat, ..."
    return feat.split(",").every(function(v) {
      return v.trim() in uu.feat;
    });
  },

  // uu.feat.mix - mix feature-list(override)
  mix: function(featureList) {
    uu.mix(uu.feat.list, featureList);
    return uu.feat;
  },

  // job database
  _job: { /* jobid: { fn, feats, timeout } */ },

  // status keeper
  _run: { core: 2, xbrowser: 2, feat: 2, ready: 2 }, // 1: loading, 2: loaded

  _collect: function(feat, rv) {
    var run = uu.feat._run[feat] || (uu.feat._run[feat] = 0);

    if (!run) { // loading(1) or loaded(2) -> skip
      (rv.indexOf(feat) < 0) && rv.push(feat); // pushed -> skip
      if (feat in uu.feat.list) {
        uu.feat.list[feat].split(",").forEach(function(v) {
          uu.feat._collect(v.trim(), rv); // recursive call
        });
      }
    }
  },

  _runner: function(jobid) {
    uu.feat._job[jobid].feats.forEach(function(v) {

      if (uu.feat._run[v] || uu.feat._isDepend(v)) { // skip or lazy
        return;
      }

      // build script element
      // <script id="{feat}.js" type="text/javascript" charset="utf-8">
      var scr = document.createElement("script");
      scr.id      = v + ".js";
      scr.type    = "text/javascript";
      scr.charset = "utf-8";

      if (document.uniqueID) { // IE
        scr.onreadystatechange = function() {
          if (/loaded|complete/.test(this.readyState)) {
            (v in uu.feat) ? uu.feat._done(jobid, v)
                           : uu.feat._kill(jobid);
          }
        };
      } else {
        scr.onload = function() {
          (v in uu.feat) ? uu.feat._done(jobid, v)
                         : uu.feat._kill(jobid);
        };
        scr.onerror = function() {
          uu.feat._kill(jobid);
        };
      }
      scr.setAttribute("src", uu.feat._from + "/" + v + ".js");
      uu.head.appendChild(scr);
      uu.feat._run[v] = 1;
    });
  },

  _isDepend: function(feat) {
    if (feat in uu.feat.list) {
      return uu.feat.list[feat].split(",").some(function(v) {
        return uu.feat._run[v.trim()] !== 2; // depend
      });
    }
    return 0; // not depend
  },

  _done: function(jobid, feat) {
    uu.feat._run[feat] = 2; // loaded

    var job = uu.feat._job[jobid], fn = job.fn;

    if (job) {
      job.feats.splice(job.feats.indexOf(feat), 1); // delete
      if (job.feats.length) {
        uu.feat._runner(jobid); // next
      } else {
        delete uu.feat._job[jobid]; // delete job
        fn && fn(true, ""); // load complete
      }
    } else {
      throw Error("lost job");
    }
  },

  _kill: function(jobid) {
    var job = uu.feat._job[jobid], fn = job.fn, feats;

    // remove <script id="{feat}.js"> element
    job.feats.forEach(function(v, i) {
      var e = document.getElementById(v + ".js");
      e && e.parentNode.removeChild(e);
    });

    feats = job.feats.join(",");
    delete uu.feat._job[jobid];
    fn && fn(false, feats); // fail
  },

  _watchdog: function() {
    var run = 0, time = +new Date, jobid;

    for (jobid in uu.feat._job) {
      if (time > uu.feat._job[jobid].timeout) {
        uu.feat._kill(jobid);
      } else {
        ++run;
      }
    }
    run && setTimeout(uu.feat._watchdog, UU.CONFIG.FEAT_TIMEOUT);
  }
});

// === Ready ===============================================
// depend: none

uu.mix(uu, {
  // uu.ready - ready event handler
  ready: function(fn) {
    (function LOOP() {
      uu.domReady ? fn() : setTimeout(LOOP, 0);
    })();
  },

  // uu.domReady - DomReady state(1: ready, 0: not ready)
  domReady: 0,

  // uu.unready - unready event handler
  unready: function(fn) {
    document.uniqueID ? attachEvent("onunload", fn) // IE
                      : addEventListener("unload", fn, false);
  }
});

(function(_doc, navi, fn) {
  if (_doc.uniqueID || (/WebKit/.test(navi) && _doc.readyState)) {
    (function LOOP() {
      var ok = 0;
      try {
        if (_doc.uniqueID) { // IE
          // http://javascript.nwbox.com/IEContentLoaded/
          ok = _doc.documentElement.doScroll("up") || 1;
        } else {
          ok = /loaded|complete/.test(_doc.readyState);
        }
      } catch(err) {}
      ok ? fn() : setTimeout(LOOP, 0);
    })();
  } else if (/Gecko\/|Opera/.test(navi)) {
    _doc.addEventListener("DOMContentLoaded", fn, false);
  } else {
    // for legacy browser
    addEventListener("load", fn, false);
  }
})(document, navigator.userAgent, function() { ++uu.domReady; });

// --- boot loader ---
uu.ready(function() {
  var fn = window[UU.CONFIG.AUTO_RUN];
  fn && fn();
});

} // if (window.uu)

こんな感じに使います。

<html><head><title>uupaa.js 0.7</title>
<script src="uupaa-0.7.js"></script>
</head>
<body>
<script>
function boot() {
  uu.feat("jQuery,canvas,widget", function() {
    // alert("A Happy new year 2009");
  });
}
</script>
</body></html>
  • window.boot 関数が定義されていると、DomReady(DOMContentLoaded)のタイミングで自動的に呼び出してくれると楽かもね。
  • uu.feat でjQueryとか、jQueryプラグインなんかをロードできるかもね。
  • 短いキーワード("canvas", "widget")を指定するだけでリッチな機能が使えればハッピーだな。

うかうかしてたら


開発がコミュニティーベースではない故に、実装が安定しない

  • みごとです。あたってます。