Surface重量計算機
ありそうでなかったっぽい(もしかしたら検索力不足かも)ので作りました。
自分が調べたかったので、そのついでです。
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ツリーが巨大になればなるほど遅くなる
結論
っつーか
正直、別に今更やらなくてもベンチマーク取ってるサイトはググればいくらでも出てきますけどね…。
はてなダイアリーでキーワードポップアップ
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.outerWidth
とwindow.outerHeight
で取れるし
ウィンドウの中身はdocument.documentElement.clientWidth
とdocument.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では言及されていませんが、非推奨なのはどうやら同期的に動作するから、らしいです。
つまりイベントで発火した処理が全て終わるまで次の処理に移れない、ということみたいですね。