htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

Surface重量計算機

ありそうでなかったっぽい(もしかしたら検索力不足かも)ので作りました。
自分が調べたかったので、そのついでです。

Surface重量計算機

本体:

カバー:

合計重量は 約0g です。

FAQ

動きませんが?

あなたが使っているブラウザが古いと思います。

DOMParser と XMLSerializer

DOMParserは覚えてるけどXMLSerializerをしょっちゅう忘れちゃうのでメモ。

var parser, serializer;
var input, output;
var dom;

input = '<div id="test"><p>hogehoge</p></div>';

parser = new DOMParser();
try {
  dom = parser.parseFromString(input, "text/html").getElementById("test");
}catch(e){
  dom = parser.parseFromString(input, "application/xml").documentElement;
}

document.body.appendChild(document.adoptNode(dom));

serializer = new XMLSerializer();
output = serializer.serializeToString(document.getElementById("test"));

input === output; //==> true // IEやFxはxmlns属性が付いてfalseが返ることが稀によくある


XMLSerializerはDOM標準ではないっぽいけどinnerHTMLよりはマシでしょ…。
速度的には、innerHTMLを参照するのに比べてseliarizeToStringを使うと3〜10倍くらい*1高速化します。

*1:ブラウザによる

テキストをパースして要素に追加する処理比較

有名なのはinnerHTMLですね。
ただ、これ以外にもテキストを評価してDOMツリーに追加する方法がありますので、今回はこれをやってみました。

単純にループで同じメソッドおよびプロパティ操作をしているだけです。
試行回数は1000回ですが、これはinnerHTMLの実行時間が長くなりすぎるからです。

まず計測用の関数を定義しておきます。
console.timeが使える場合はこれを使うようにし、使えない場合(IEさん)はDate()で時間を計測します。

var benchmark = function(callback){
	if (console.time && console.timeEnd){
		console.time("benchmark");
		callback();
		console.timeEnd("benchmark");
	}
	else {
		var time = new Date();
		callback();
		console.log("benchmark: ", new Date() - time);
	}
};


比較するのは次の4選手。
ご存じinnerHTMLに加えて、
insertAdjacentHTMLメソッド、
RangeオブジェクトのcreateContextualFragmentメソッド。
そしてDOMParserオブジェクトのparseFromStringメソッドです。

1) HTMLElement #innerHTML

benchmark(function(){
	Array.apply(null, Array(1000)).forEach(function(){
		document.body.innerHTML += "<div></div>";
	});
});

2) HTMLElement #insertAdjacentHTML

benchmark(function(){
	Array.apply(null, Array(1000)).forEach(function(){
		document.body.insertAdjacentHTML("beforeend", "<div></div>");
	});
});

3) Range #createContextualFragment

benchmark(function(){
	var range = document.createRange();
	range.selectNodeContents(document.body);
	Array.apply(null, Array(1000)).forEach(function(){
		document.body.appendChild(range.createContextualFragment("<div></div>"));
	});
});

4) DOMParser #parseFromString

benchmark(function(){
	var parser = new DOMParser();
	Array.apply(null, Array(1000)).forEach(function(){
		document.body.appendChild(
			parser.parseFromString("<div></div>", "application/xml").documentElement
		);
	});
});

結果

公平を期すため、ベンチ結果はabout:blank上で実行しました。

Internet Explorer 10 Mozilla Firefox 21 Google Chrome 26
innerHTML 3162ms (*) 619ms (*) 1238ms (*)
insertAdjacentHTML 49ms 13ms (*) 11ms
createContextualFragment 67ms 18ms (*) 9ms
parseFromString 78ms 83ms (*) 22ms

(*) ただし挿入対象のDOMツリーが巨大になればなるほど遅くなる

結論

  • Chromeは全体的に速い。流石Chrome先生。
  • Firefoxは基本的にDOMツリーがでかければでかいほど計算コストもでかくなる。
    普通の重量級ページで実行した場合、Firefoxが最も低速でした。
  • IEは可もなく不可もなく。
  • 何回も処理回す場合はinnerHTMLはなるべく避けるようにしましょう。
  • コード量と速度のバランスを考えるとinsertAdjacentHTMLがいいと思います。

っつーか

正直、別に今更やらなくてもベンチマーク取ってるサイトはググればいくらでも出てきますけどね…。

はてなダイアリーでキーワードポップアップ

Greasemonkey用スクリプト「キーワードポップアップ」の配布について - はてなダイアリー日記

ずいぶん昔の記事ですが、はてなさんはこんなの出してたんですね。
これを他のブラウザでも動くようにちょちょいと改造しましたよっていうのが今回のエントリ。
詳しく調べてないのでよく知りませんが、きっと俺以外にもやった人はたくさんいると思います。

本当はこのスクリプトをこのはてダに貼り付けたかったんですが、やってみたらはてなさんから「スクリプト埋め込みは許可してねーよ」って怒られたので仕方なしにコードだけ置いとく。

本当にごく一部しか触ってなくて、コードの99%くらいははてなさんのオリジナルのままです。
やったことと言えば、

  • MozOpacityとかいうクソベンダプリフィクス付きプロパティをopacityに直した
  • op = parseFloat(popup.style.opacity);みたいなコードをop = parseFloat(getComputedStyle(popup).opacity);に直した
  • setTimeout(loadKeyword, 600, rssurl, aTag.innerHTML);setTimeout(function(){ loadKeyword(rssurl, aTag.innerHTML) }, 600);に直した
  • 一部のイベントハンドラをイベントリスナに直した
  • NodeListオブジェクトに対してArray.prototype.slice.callした

くらいです。

IEの場合、9以降でないと動かないと思います。
ただし addEventListener for IE8 のようなスクリプトを実行するか、

var addEvent = function(target, evt, func){
  if (window.addEventListener) {
    return target.addEventListener(evt, func, false);
  }
  else if (window.attachEvent) {
    return target.attachEvent("on" + evt, func);
  }
  else {
    target["on" + evt] = func;
  }
};

こんな感じのスクリプトを実行して、イベント関連の記述をちょこっといじれば古いIEでも動くかもしれません。
CSSのopacityプロパティは古いIEでは解釈できないのでfilter:progid:DXImageTransform〜みたいなクソめんどくさいIE独自プロパティを追加してあげる必要もあります。


このスクリプトをはてダ上で実行して、キーワード上にマウスカーソルを持って行くとキーワードの説明がポップアップします。

こんな感じです。結構便利です。

ブログパーツみたいに貼り付けられたらもっと便利だったんですけどねー。


追記:試してないけど、古いIEならCSSのbehaviorプロパティでスクリプト埋め込めるかもしんない。

マルチメディア要素へのリンクのクリックイベントに<audio>や<video>なウィンドウをポップアップする機能をオーバーライドするブックマークレット

たぶんこれで完成形。

比較的新しいAPIをフル活用しているので、動かなかったらそんなブラウザを使っている自分が悪いと思ってください。
一応IE10とChrome26で動作を確認しています。
Firefox20ではちょっと確認した限り動作しませんでした。まぁFirefoxはクズってことで


(function(){
  var checked = [];        // 同じURLに複数回リクエストを送るのを防ぐためのリストを格納
  var timeout = 30 * 1000; // タイムアウトになるまでの時間(単位:ms)
  
  var allowList = ["audio", "video"];

  if (!HTMLElement.prototype.addPopup) { // addPopupメソッドを要素に追加

    Object.defineProperty(HTMLElement.prototype, "addPopup", {
      value: function(url){
        var self = this;

        var xhr = new XMLHttpRequest(); // HEADリクエストを送る
        xhr.open("HEAD", url, true);
        xhr.onreadystatechange = function(){
          if (xhr.readyState === 4) {
            console.log(xhr.status + " : " + url);

            if (xhr.status !== 200) return;

            var header = xhr.getResponseHeader("Content-Type");
            var type   = header.split("/")[0];

            if (allowList.some(function(v){ return v === type })) { // いずれかにヒットすれば true
              console.log(header + " - OK");
              addPopupItem(self, type);
            }
          }
        };
        xhr.send(null);

        window.setTimeout(function(){ // 一定時間でタイムアウト
          xhr.abort();
        }, timeout);
      }
    });
  }
  else {
    return false;
  }

  var elems = document.querySelectorAll("a[href], area[href]");
  elems = Array.prototype.slice.call(elems); // リンカブル要素を全て抽出

  for (var i in elems) {
    var url = elems[i].href;

    if (url.indexOf("http") === 0) {

      url = url.split("#")[0]; // URLに # を含む場合は除去

      if (checked.every(function(li){ return url !== li })) { // 全てとアンマッチなら true
        checked.push(url);
        elems[i].addPopup(url);
      }
    }
  }

  function addPopupItem(target, mediaType) { // ポップアップを生成
    target.onclick = function(evt){
      if (evt.ctrlKey && evt.altKey) {
        return true; // Ctrl+Alt を押しながらクリックすれば普段のクリックと同じ効果
      }
      else {
        openPopup();
        evt.preventDefault();
      }
    };

    function openPopup() {
      var popup = window.open("", "media-popup", "titlebar=no,menubar=no,toolbar=no,location=no,status=no,scrollbars=no");
      var doc   = popup.document;
      var rect;
      var element;

      var diff = {
        width:  popup.outerWidth  - doc.documentElement.clientWidth,
        height: popup.outerHeight - doc.documentElement.clientHeight
      };

      if (!rect) {
        element = doc.createElement(mediaType);
        element.setAttribute("controls", "controls");
        doc.body.appendChild(element);
        rect = element.getBoundingClientRect();
        doc.body.removeChild(element);
      }
      
      doc.body.addEventListener("DOMSubtreeModified", function(){ // 要素数が変化したらウィンドウサイズを変える
        var len = this.childNodes.length;
        popup.resizeTo( rect.width + diff.width, rect.height * len + diff.height );
      }, false);

      var style = doc.createElement("style");
      doc.querySelector("head").appendChild(style);
      var s = style.sheet, i = 0;
      [
        "body {"
        + "margin: 0;"
        + "padding: 0;"
        + "}",
        "audio {"
        + "display: block;"
        + "margin: auto;"
        + "}"
      ].forEach(function(value){
        s.insertRule(value, i++);
      });

      element = doc.createElement(mediaType);
      element.setAttribute("src", target.href);
      element.setAttribute("autoplay", "autoplay");
      element.setAttribute("controls", "controls");
      element.addEventListener("ended", function(){ // 終端まで移動したら自らを削除する
        this.parentElement.removeChild(this);
        close();
      }, false);

      /*
        末尾に挿入する
        appendChildでないのはIEがDOMSubtreeModifiedに反応しない(らしい)ため
        http://msdn.microsoft.com/en-us/library/gg558014(v=vs.85).aspx
      */
      doc.body.insertBefore(element, null);

      function close() { // ポップアップ内のメディアがゼロのとき、子ウィンドウを閉じる
        doc.querySelectorAll( allowList.join(",") ).length === 0
        ? popup.close() : 0;
      }
    }
  }

})();

解説書くのすげーめんどくさいんですが一応解説

前回はウィンドウのサイズを中身の大きさに合わせて変更する術が思いつかなかったので適当にサイズ決めてましたが、
ウィンドウサイズはwindow.outerWidthwindow.outerHeightで取れるし
ウィンドウの中身はdocument.documentElement.clientWidthdocument.documentElement.clientHeightで取れることに
今更気づいてその辺をルーチンに組み込みました。
気づいてみれば呆気ないものですね…。


で、ウィンドウサイズを変更するタイミングですが、当然中身のサイズが変わったときです。
中身のサイズが変わるということはDOMツリーに変化があるということ。

DOMツリーが変わると発火する便利なイベントがあります。
それがMutationEventsと呼ばれるものです。
実はこのMutationEvents、利用を推奨されていません
可能ならばDOM4のMutationObserverを使って欲しいと、MDNに書かれています。*1
しかしながら、MutationObserverはIEでは実装されていません。
機能的には同じなので今回はMutationEventsを利用しました。

var diff = {
  width:  popup.outerWidth  - doc.documentElement.clientWidth,
  height: popup.outerHeight - doc.documentElement.clientHeight
};

if (!rect) {
  element = doc.createElement(mediaType);
  element.setAttribute("controls", "controls");
  doc.body.appendChild(element);
  rect = element.getBoundingClientRect();
  doc.body.removeChild(element);
}

doc.body.addEventListener("DOMSubtreeModified", function(){ // 要素数が変化したらウィンドウサイズを変える
  var len = this.childNodes.length;
  popup.resizeTo( rect.width + diff.width, rect.height * len + diff.height );
}, false);

この部分が今回のキモです。

一旦要素の大きさを測るために空のメディア要素を作ってすぐ削除しています。
そしてこれを元にして、DOMツリーが変化する度にウィンドウの大きさを変えていくのです。

これを見れば勘のいい方はすぐ気づくかと思いますが、
最初に作成される要素が<audio><video>かで大きさの基準が決まってしまいます。
つまり<audio><video>が混在する場合、ウィンドウの大きさがえらいことになってしまいます。
なのでこれは運用で回避してください。めんどくさいので仕様バグとします。


イベントはDOMSubtreeModifiedで拾っています。
読んで字のごとく、ツリーに変化があったときに発火します。
これ以外にもMutationEventsには、属性に変化があった時を検知するDOMAttrModifiedや、ツリーへの挿入時のみ検知するDOMNodeInsertedなど便利なイベントがたくさんあります。
しかし繰り返しますがMutationEventsは非推奨です

非推奨と言えば、(使うのは)やめろやめろと言われていたにもかかわらずHTML5で正式に採用が決定した<iframe>とか、
標準技術じゃないよーと言われているにもかかわらずかなり普及しているinnerHTMLとかいろいろありますねー。

まぁMutationEventsに関してはあまり普及してない感じしますし、
IE11がMutationObserverをどうするのかは知りませんが、採用するのであれば取って代わられるのかもしれません。
もしかしたらそのまま残るのかもしれません。
別にどっちでもいいです。

*1:https://developer.mozilla.org/en-US/docs/DOM/Mutation_events
URLでは言及されていませんが、非推奨なのはどうやら同期的に動作するから、らしいです。
つまりイベントで発火した処理が全て終わるまで次の処理に移れない、ということみたいですね。