Mash up Caravan 技術セッション
JavaScriptによるHTML生成
実践ノウハウ
- 株式会社ECナビ 須藤洋一
- ブログ(*「ふっかつのじゅもんがちがいます」)
- http://blog.ajiyoshi.org
- ソーシャルブックマーク
- http://buzzurl.jp/user/y-sudo
自己紹介
- ソーシャルブックマークBuzzurl(http://buzzurl.jp)のプロダクトマネージャ
- 企画設計から開発テスト運用までなんでもやってます
前提知識
- JavaScriptを読み書きできること
- prototype.jsを多少触ったことがあること
- HTML/HTTPに関する基礎知識
取り扱わない話題
- JavaScriptの書き方
- 個別のライブラリの詳細な使い方
基本のおさらい
JavaScriptでHTMLを生成する(1)
- Ajax/JSONPで何かするときはほぼ必須
- Web上であまり情報が見つからない
- どんな場合でも使える方法はおそらくない
- LivedoorReaderのようなフルAjaxアプリケーション
- フォームや読み込みを一部Ajax化
- クライアントサイドマッシュアップ
- Greasemonkey
JavaScriptでHTMLを生成する(2)
大きく2つの方法
- DOM操作APIを使う
- innerHTMLを使う
(1) DOM操作APIを使う
例:Buzzurlへのリンクを追加する
var url = "http://buzzurl.jp"; var a = document.createElement("a"); a.href = url; a.appendChild(document.createTextNode("Buzzurl(バザール)")); document.getElementById("target_1").appendChild(a);
ここに追加されます
Issue:
DOM APIはかったるい
- どんなHTMLになるか分かりにくい
- 複雑なHTMLを作ろうと思うと辛い
- 見栄えとロジックを分離しづらい
Solution:
- 既存のライブラリを使う
- jQuery
- Mochikit.DOM
- XMLBuilder
- 自作のライブラリを作る
- innerHTMLを使う
(2) innerHTMLを使う
サンプル
var html = "現在時刻:" + (new Date()).toString(); document.getElementById("target_2").innerHTML = html;
ここに表示されます
Issue:
innerHTMLもかったるい
- 文字列連結でHTMLを作るのは面倒
- 変数を適切にエスケープしないとXSSの原因になりやすい(後述)
Solution:
テンプレートエンジンを使う
- 既存のライブラリ
- prototype.js
- ajax pages
- TrimPath
- ...etc. etc.
- 自前で作る
テンプレートエンジンを使う
例:prototype.jsのTemplate
<textarea id="templ_1">
<a href="#{home}"><img src="#{photo}"></a>
</textarea>
var t = new Template($F("templ_1")); var data = { home:"http://buzzurl.jp/user/y-sudo", photo:"http://file.blog.ajiyoshi.org/photo.jpg" }; $("target_3").innerHTML = t.evaluate(data);
実践セキュリティ
実践セキュリティ
安全でないデータ
<textarea id="templ_1">
<a href="#{home}"><img src="#{photo}"></a>
</textarea>
var t = new Template($F("templ_1")); var unsafe = { home:"http://buzzurl.jp/user/y-sudo", photo:'http://file.blog.ajiyoshi.org/photo.jpg" onload="alert(\'xss\')' }; $("target_4").innerHTML = t.evaluate(unsafe);
実践セキュリティ
表示する直前にHTMLエスケープ
<textarea id="templ_0">
<img src="http://file.blog.ajiyoshi.org/photo.jpg" onload="alert('XSS')">
</textarea>
function escape_html(str){
str = str.replace(/&/g, "&");
str = str.replace(/"/g, """);
str = str.replace(/'/g, "'");
str = str.replace(/</g, "<");
str = str.replace(/>/g, ">");
return str;
}
var unsafe = $F("templ_0");
$("target_esc").innerHTML = escape_html(unsafe);
実践セキュリティ
HTMLエスケープ
<textarea id="templ_1">
<a href="#{home}"><img src="#{photo}"></a>
</textarea>
var t = new Template($F("templ_1")); var unsafe = { home:"http://buzzurl.jp/user/y-sudo", photo:'http://cdn.buzzurl.jp/user/y-sudo/photo" onload="alert(\'xss\')' }; var escaped = { home:escape_html(unsafe.home), photo:escape_html(unsafe.photo) }; $("target_5").innerHTML = t.evaluate(escaped);
実践セキュリティ
注意:prototype.jsは " や ' をエスケープしない
<textarea id="templ_1">
<a href="#{home}"><img src="#{photo}"></a>
</textarea>
var t = new Template($F("templ_1")); var unsafe = { home:"http://buzzurl.jp/user/y-sudo", photo:'http://file.blog.ajiyoshi.org/photo.jpg" onload="alert(\'xss\')' }; var escaped = { home:unsafe.home.escapeHTML(), photo:unsafe.photo.escapeHTML() }; $("target_6").innerHTML = t.evaluate(escaped);
実践セキュリティ
まとめ
- 表示の直前でエスケープするのが鉄則
- HTMLエスケープされた状態で受け取ることもありうる
- アプリケーション側はエスケープ済みと期待すべきではない
- 混在環境ではアンエスケープしてからエスケープする
実践パフォーマンス
実践パフォーマンス
次のことをすると とても 重い
- 巨大なDOMツリーを作成
- onclick等のイベントハンドラを含むとさらに重い(特にIE)
- imgはさらにさらに重い
実践パフォーマンス
単純な<a>タグ
function now(){ return (new Date()).getTime() } var num = Math.floor($F('num_7')); var html = (new Array(num)).map( function(){ return $F('templ_2') } ).join(""); $("target_7").innerHTML = ""; setTimeout(function(){ var start = now(); var p = document.createElement("p"); p.innerHTML = html; $("target_7").appendChild(p); var end = now(); var li = document.createElement("li"); li.appendChild(document.createTextNode((end-start)+"msec to "+(num*100)+" elements")); $("score_7").insertBefore(li, $("score_7").firstChild); }, 100);
実践パフォーマンス
イベントハンドラ付きの<a>
function now(){ return (new Date()).getTime() } var num = Math.floor($F('num_8')); var html = (new Array(num)).map( function(){ return $F('templ_3') } ).join(""); $("target_8").innerHTML = ""; setTimeout(function(){ var start = now(); var p = document.createElement("p"); p.innerHTML = html; $("target_8").appendChild(p); var end = now(); var li = document.createElement("li"); li.appendChild(document.createTextNode((end-start)+"msec to "+(num*100)+" elements")); $("score_8").insertBefore(li, $("score_8").firstChild); }, 100);
実践パフォーマンス
イベントハンドラ付きの<img>
function now(){ return (new Date()).getTime() } var num = Math.floor($F('num_9')); var html = (new Array(num)).map( function(){ return $F('templ_4') } ).join(""); $("target_9").innerHTML = ""; setTimeout(function(){ var start = now(); var p = document.createElement("p"); p.innerHTML = html; $("target_9").appendChild(p); var end = now(); var li = document.createElement("li"); li.appendChild(document.createTextNode((end-start)+"msec to "+(num*100)+" elements")); $("score_9").insertBefore(li, $("score_9").firstChild); }, 100);
DOM操作は重い
まとめ
- なるべく巨大なDOMツリーの作成/更新をしない
- 必要最小限の更新にとどめる
- 文字列の構築と描画の間に制御を返す
おしまい
- どうみても終わりです。
- 本当にご清聴ありがとうございました。



