5.5分HTML操作の基本document.writeと同期読み込みscriptの非同期読み込みinnerHTMLとDOMHTML中の文字列を置換する方法まとめこんにちは、太田です。前々回、前回でDOMの基礎を簡単に解説しました。今回からは、DOMを使った実用的なスクリプトを解説していきます。今回はHTMLの操作、テキストの操作にフォーカスを当てていくつかのサンプルコードを解説していきます。HTML操作の基本JavaScriptによってHTMLを書き出したり、一部を書き換えたり、削除したりといった方法は実は様々な方法が用意されています。目的に合わせて適切な方法を選ばないと非効率だったり、最悪クロスサイトスクリプティングなどの問題を抱えてしまう危険もあります。document.writeと同期読み込みJavaScriptでHTMLを書き出すというと、最初に学ぶのはこのdocument.writeかもしれません。いわゆるprint文のようにシンプルなAPIなので、入門書の最初のサンプルなどで扱われることも多いようです。document.writeは扱いに注意が必要なメソッドです。ウェブページはブラウザに読み込まれ始めたときにdocumentがopenな状態になり、すなわちHTMLを解析し、画像・CSS・JavaScriptなどの外部ファイルを取得し、取得したそれぞれもやはり解析して最終的にレンダリングを行っていくという一連の処理が行われます。それらの処理が終わったところでdocumentはclose状態になります。このopenな状態なときにwriteを実行すると現在のdocumentにHTMLを書き出すことができます。読み込みが完了してcloseな状態でwriteを実行すると暗黙的にopen状態になり、その際にそれまでのdocumentはすべてリセットされて白紙になってしまいます。このときは読み込みの完了という自動的にcloseする機会が存在しないので、closeを明示的に呼ばない限りopen状態(読み込み中)のままになってしまいます。document.writeを使うとタイミングによって一度表示したページをリセットしてしまうことがあるのかというと、そういったことは起こりません。ロード後にdocument.writeを呼び出せばそれが起こるわけですが、その場合はその現象が必ず発生するのですぐに気が付くことができます。問題は、外部JavaScriptなどの読み込みと実行に極端に時間がかかった場合ですが、その場合ブラウザはHTMLの解析を止めてJavaScriptの読み込みを待ってから実行し、JavaScriptの実行完了後にHTMLの解析を再開します。従って、JavaScriptの読み込みが遅延しても、後からdocument.writeが実行されてしまうということは起こりません。逆に言えば、JavaScriptの読み込みと実行中はHTMLの解析が止まってしまうのでユーザーを待たせる重大な原因となるということです。実は、まさにdocument.writeがあるために、ブラウザはJavaScriptを同期的に実行しなければいけないのです。外部JavaScriptについてソースの読み込みと実行を非同期で行いつつHTMLの解析を先に進めて行くという「HTMLとJavaScriptの並列処理」ができれば表示を高速化することができます。それが現状できないのはdocumet.writeがあるためなのです。scriptの非同期読み込みここでやや脱線しますが、JavaScriptを非同期読み込みする方法を紹介します。方法としてはいくつかありますが、今回はウェブ標準な方法と実用的な方法の2つに絞ります。非同期に読み込むということは実行順が不定になるので、うっかりライブラリなどを非同期にしてしまうと、ライブラリを利用しようとしたらまだ読み込みが完了していなかったといったことが起こりえます。非同期化は慎重に行う必要があります。HTML5ではこのJavaScriptの読み込みについての解決策が用意されています。document.writeが使われていないという条件付きのもと、scriptタグにasync属性を設定することでまさにHTMLの解析と並行してJavaScriptを実行できます。サポートしていないからといってasync属性を設定することで何か問題の起こることはないので、将来多くのブラウザでサポートされることを期待して今から設定しておくのもよいでしょう。あるブラウザがasync属性をサポートした時に動くのか、つまり突然非同期になっても問題がないとはっきりしているなら、という前提がある場合に限ります。ちなみに、async属性と似たdefer属性というものもあります。こちらは非同期ではなく遅延です。scriptの実行をDOMContentLoaded相当のタイミングに設定します。DOMの構築が完了しているので確実にDOMを操作することができます。asyncはなるべく早く実行したい場合、deferはDOM構築後の処理と、役割が分かれています。asyncで実行してDOMContentLoadedをセットするという方法はDOMContentLoadedイベントが発生する前にイベントをセットできる保証がないので避けたほうがよいでしょう。1つの実用的な方法として、document.createElementでscriptタグを作り、src属性に外部スクリプトのURLを設定してappendChildなどで挿入するという方法があります。DOMのメソッドで挿入された要素は既に構築されている要素に追加されるので、HTMLを解析していく処理とは切り離されます。外部スクリプトの読み込みも非同期に行われることになり、HTMLの解析をブロックしません。これはクロスブラウザで動作します。念のため注意しなければいけないのがscript要素の挿入方法です。head.appendChild(script);})();ほとんどの場合は上記のコードで問題ありませんが、IE6でbase要素がある場合にscript要素を削除するとエラーが発生するという問題があります。そのエラーを回避するのが次のコードです。head.firstChild);})();このように、appendChildの代わりにinsertBeforeを使うだけです。#2709に詳細が書かれています。発生条件は十分に特殊(scriptを削除する理由がない)なので、appendChildでも十分でしょう。ところで、document.getElementsByTagName('head')[0]という部分に、head要素がなかったらエラーになるのではないかと思われるかもしれません。実際に次のようなhead要素のないhtmlを実行してみるとしっかりhead要素が取得できて、省略したhead要素がブラウザ(のレンダリングエンジン)によって補完されていることを確認できます。4.01ではHTML要素の直下に含めることができるのはhead要素かbody要素のみと定義されています。これに従ってブラウザはHTML要素の直下にはhead要素を補完してその中にscript要素などのhead要素の子要素にできる要素を入れていきます。head要素の子要素にできない要素を見つけるとそこでhead要素を閉じ、bodyタグを開始します。このようにHTMLの仕様を把握することでJavaScriptを書く際の疑問点が解消されることは多々あります。書いておくことも決して無駄ではありません。HTMLの仕様に従わないブラウザがあってもおかしくはありませんし(もちろんバグとして認識するのが普通ですが、修正されないバグもあります⁠)⁠、書くことで問題が出るわけでないので、保険は多いほうがよいという考えもあります。唯一の正解はなく、好みの問題とも言えます。innerHTMLとDOMdocument.writeの次はやはりinnerHTMLです。こちらも手軽なAPIで、なんといってもクロスブラウザで動作するので頻繁に使用されています。前回の最後でも触れましたが、innerHTMLはイベントを消してしまうことがありますし、その削除されたイベントが原因でメモリリークを起こすこともあります(メモリリークについては、やはり連載の中盤以降で取り上げたいと思います⁠)⁠。パフォーマンスについては使い方を間違えなければ比較的優秀ですが、最善というほどではありません。そのため、innerHTMLを使う際はほかによい方法がないか検討してみるべきでしょう。1つ典型的なinnerHTMLの誤用例を見てみます。'</ul>';このコードは、<ul></ul><li>1</li><li>2</li><li>3</li>のようなHTMLを作ってしまいます。liがulの中に含まれていません。これはinnerHTMLへの代入のたびにHTMLとして解釈されていることを示しています。HTMLのパースとレンダリングは重い処理なので、パフォーマンスの点でも問題があります。そこで次のように文字列を組み立ててから1度で代入するのが定石です。html_str;ただし、長い文字列を+=で連結するのはIE(特にIE6)で重くなることがあります。そこで、配列を使う方法もよく使われます。html_strs.join('');なお、このケースではさらに次のように最適化することもできます。'</ul>';配列の長さが先に決まっているならその長さで配列を初期化しておき、さらにpushではなくカウント用の変数を添字にして代入を行うことで関数呼び出しを減らすことができます。大きな文字列を作る場合にはこういった最適化が大きく影響することもありますが、実際には1msにも満たない差でしかないことのほうが多いでしょう。最適化を行うべきかどうかは悩ましいところですが、1つの目安として「廉価なネットブック上のIE6で遅さを感じるほどでなければ、そもそも最適化の必要はないと判断する」など、何か基準を決めて判断するとよいでしょう。最適化する場合も、最適化の結果を見ながら可読性やメンテナンスを考えて、落しどころを決めることになります。HTML中の文字列を置換する方法ユーザーが検索したキーワードなどをハイライトさせたり、何かの条件にマッチした文字列をマークアップし直したい、置換したいといったケースはよくあることだと思います。いったHTML中の文字列を置換するとき、最も手軽で乱暴な方法はdocument.body.innerHTMLの全書き換えです。document.body.innerHTML.replace(/foo/g,'bar');この方法は多くの問題を抱えているので、オススメできません。実際にどういった方法があるのかというと、これが少々厄介です。テキストノードを取得する必要がありますが、クロスブラウザなコードとしては単純に全ノードを再帰的に探索するしかありません。コードとして次のようになります。複雑ではありませんが、HTMLが大きくなるほど処理が重くなってしまいます。}};walker(document.body);テキストノードに対する処理、ここでは特定のキーワードにマッチしたとき、その文字列をspanタグで囲ってハイライトするようにしてみます。}}実例をヒット数:処理時間:splitTextは指定した位置でテキストノードを分割するメソッドです。1で定義されており、IEでも使えるためクロスブラウザで動きます。少々複雑な処理に見えるかもしれませんが、順を追ってみればテキストノードを2回分割して間のノードをspanタグに詰め込んでいるだけの処理です。テキストノードの探索はなんとかしたいですね。いくつか方法はありますが、まずIE以外のブラウザで有効なのがXPathです。};xpath(document.body);XPathの場合、XPath自体に検索条件として絞り込みをすることが可能なため、一発で指定のキーワードを含むテキストノードを取得することができます。そのため、非常に高速に動作します。XPathが万能というわけではなく、今回のようないくつかの条件で絞込したいというケースにマッチしたという面はあります。IEも何とか高速化したいところです。IEではネイティブに実装されたXPathは使えません。実はIEには少しユニークな方法があります。IEはTextRangeというテキストノードを操作するためAPIを持っています。このTextRangeのfindTextメソッドでテキストを検索し、さらにexecCommandで文字色や背景色を変えたり、といったことが可能です。}}IEではこれだけで検索からハイライトまで対応できてしまいます。パフォーマンスも十分に高速です。マッチする文字列が少ない時に最低限の探索で済むのが特徴です。実例(最適化)をヒット数:処理時間:まとめ今回はDOM操作の実例に触れつつ、最適化にも踏み込んで解説しました。次回も引き続きDOM周りの実用的なコードを解説していきたいと思います。Ltd.ページ内容の全部あるいは一部を無断で利用することを禁止します⁠。個別にライセンスが設定されている記事等はそのライセンスに従います。