これでできる! クロスブラウザJavaScript入門

第8回 実践DOMスクリプティング#1:HTMLとテキストの操作

この記事を読むのに必要な時間:およそ 5 分

こんにちは,太田です。前々回前回で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を実行できます。ただし,2010年6月時点でこのasync属性をサポートしているのはFirefox 3.6のみです。ただ,サポートしていないからといって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要素の挿入方法です。

scriptタグの挿入

(function(){
  var script = document.createElement('script');
  var head = document.getElementsByTagName('head')[0];
  script.src = 'xxx.js';
  head.appendChild(script);
})();

ほとんどの場合は上記のコードで問題ありませんが,IE6でbase要素がある場合にscript要素を削除するとエラーが発生するという問題があります。そのエラーを回避するのが次のコードです。

scriptタグの挿入#2

(function(){
  var script = document.createElement('script');
  var head = document.getElementsByTagName('head')[0];
  script.src = 'xxx.js';
  head.insertBefore(script, head.firstChild);
})();

このように,appendChildの代わりにinsertBeforeを使うだけです。このバグに付いてはjQueryのバグトラッカー #2709に詳細が書かれています。発生条件は十分に特殊(scriptを削除する理由がない)なので,appendChildでも十分でしょう。

ところで,document.getElementsByTagName('head')[0]という部分に,head要素がなかったらエラーになるのではないかと思われるかもしれません。しかし,実際に次のようなhead要素のないhtmlを実行してみるとしっかりhead要素が取得できて,省略したhead要素がブラウザ(のレンダリングエンジン)によって補完されていることを確認できます。

head要素の補完

<script>
var head = document.getElementsByTagName('head')[0];
var x = head.innerHTML;
</script>
<title>head test</title>
<body>
<script>
document.body.appendChild(document.createTextNode(x));
// <script> var head = … </script>
document.body.appendChild(document.createTextNode(document.documentElement.innerHTML));
// <head><script> … </head><body> … </body>
</script>
</body>

HTML 4.01ではHTML要素の直下に含めることができるのはhead要素かbody要素のみと定義されています。これに従ってブラウザはHTML要素の直下にはhead要素を補完してその中にscript要素などのhead要素の子要素にできる要素を入れていきます。head要素の子要素にできない要素を見つけるとそこでhead要素を閉じ,bodyタグを開始します。このようにHTMLの仕様を把握することでJavaScriptを書く際の疑問点が解消されることは多々あります。

ただ,document.getElementsByTagName('head')[0] || document.documentElement と書いておくことも決して無駄ではありません。HTMLの仕様に従わないブラウザがあってもおかしくはありませんし(もちろんバグとして認識するのが普通ですが,修正されないバグもあります),書くことで問題が出るわけでないので,保険は多いほうがよいという考えもあります。唯一の正解はなく,好みの問題とも言えます。

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/

コメント

コメントの記入