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

第3回クロスブラウザの傾向と対策

こんにちは、太田です。第1回は各ブラウザの特徴をまとめ、第2回は環境作りについて解説したので、ようやく今回からクロスブラウザの本題に入っていきます。まずはクロスブラウザ対応のパターンを整理し、そのパターンごとの対策をまとめます。

クロスブラウザとはなにか

そもそも、クロスブラウザ対応とはどういうことでしょうか?ここで、この連載におけるクロスブラウザの定義を決めておきます。

サポート対象のブラウザで設計通りの表示・動作をして、たとえ未知のブラウザであっても、そのブラウザがWeb標準に沿っているなら最低限の表示・動作をすること

サポート対象のブラウザについては第1回で解説した通りですが、メインターゲットはIE(6/7/8⁠⁠、Firefox(3.6⁠⁠、Chrome(4.1⁠⁠、Safari(4.0.5⁠⁠、Opera(10.51)とします。カッコ内はフルサポートするバージョンで、それ以外は最低限の確認のみを行います。

設計通りの表示・動作というのは、文字通りではありますが、あえて「同じ見た目」とは表現していません。例えば、WindowsとMacではフォントなどのベースとなる環境が異なるため、まったく同じ見た目を再現することは難しいといえます。フォントの違いは文字をすべて画像にしてしまうことで統一できますが、そもそも(特にこの連載では)ブラウザ間で見た目をまったく同じにしなければいけない理由がありません(むしろ、ブラウザごとに個性を出しても良いくらいです⁠⁠。

そして、未知のブラウザへの対応はこの連載におけるキーポイントだと考えています。未知のブラウザとは、サポート外としたブラウザや、IE 9やFirefox 4などの未リリースブラウザ、今後登場するかもしれないまったく新しいブラウザなどを含みます。それらのブラウザがHTML(4.01、5)やCSS(2.1、3)にDOM(level 2、3⁠⁠、ECMA-262などのWeb標準といわれる仕様に準拠しているのであれば、正常に表示・動作できるようにすることが目標です。確認した限りで動いていればOKということにはせず、将来を見据えることを課題とします。100%の正解の無いテーマですので、実際には当てが外れてしまうこともあるかもしれませんが、その点はご了承ください。

では、実際にクロスブラウザ対応にどういったパターンがあるのか見ていきます。

パターン1:標準準拠

前述の通りWeb標準関連だけでも多数の仕様が存在します。上であげたもの以外にもRFCのHTTPやCookie関連の仕様にも留意する必要があります。

Firefox、Chrome、Safari、Operaなどのブラウザに限定すれば、標準仕様に準拠するだけでクロスブラウザ対応ができてしまうことも珍しくありません。さらに前述のとおり、Web標準に準拠することで未知のブラウザにも対応できることが期待できます。よって、この連載では標準仕様への準拠を重要なテーマとしています。

しかし、現実には実装の前に仕様を正しく理解するという過程が必要となるので、その点をクリアすることが難しいことも多々あります。また仕様に対する解釈がブラウザごとに微妙に異なることがあったり、実装後に仕様のほうが変更されて齟齬が生まれていたり、そもそも仕様とは異なる実装が普及していたりすることもないわけではありません。そのため、なるべく仕様を優先しつつ、実装にあわせて調整するというプロセスが必要となります。

以下はDOM Level 2 Eventsに準拠したサンプルコードです。これはFirefox、Chrome、Safari、Operaはもちろん、DOM Eventsをサポート(一部のみ)するIE 9のPreview版でも動作します。

DOM Level 2でのイベント処理のサンプル
window.addEventListener('load',function(){
  var form = document.getElementById('userform');
  form.addEventListener('submit',function(evt){
    if (!check()) {
      evt.preventDefault();
    }
  },false);
  function check(){
    // 
  }
},false);

パターン2:未実装・独自実装

主にIEで多いのがこのパターンです。当然ですがIE以外でもバージョンが古いほど未実装に当てはまることが多くなります。具体例を挙げると、IE 6は透過PNGの透過部分を正しく表示できませんが、AlphaImageLoaderを使うことで正常に表示させることができます。ほかにも要素を画面内に固定表示したい場合で、IE 6はposition:fixedをサポートしていないので、同様の動作をJavaScriptで実装する必要があります。またIEやSafari 3.0.4はDOMContentLoaded(ページの読み込みの完了を待たずに、DOMが構築された段階でJavaScriptの処理を開始させることができるイベント)をサポートしていないので、代わりの処理が必要となります(IE 9ではDOMContentLoadedをサポート予定です⁠⁠。このようにブラウザの機能不足を主にJavaScriptなどでサポートする必要があります。

第1回でも紹介したイベント処理用のaddEvent関数は標準準拠と独自実装の組み合わせです。DOM Level 2に準拠したブラウザではaddEventListenerを使用し、IE 6~8ではIE独自実装のattachEventを使用します。

クロスブラウザ対応のイベント処理関数
var addEvent;
if(document.addEventListener) {
  addEvent = function(node,type,handler){
    node.addEventListener(type,handler,false);
  };
} else if (document.attachEvent) {
  addEvent = function(node,type,handler){
    node.attachEvent('on' + type, function(evt){
      handler.call(node, evt);
    });
  };
}

パターン3:バグ

こちらは一見サポートされているようで実はバグが潜んでいるという、遭遇することは多くないですがもっとも厄介なパターンです。経験的には、古いブラウザや、シェアが少なくて比較的検証されていないブラウザで遭遇しやすい傾向があるように思います。

IEやFirefoxなどはこれまで多くのバグが発見され、同時に対策が公開されているので、バグを正しく把握できれば解決したも同然であることも少なくありません。一方でOpera・Chrome・Safariのバグは検索しても日本語圏での情報が見つからないといったことも珍しくありません。そういった場合、自力でなんとかするしかないかもしれません。

なお、バグはそのブラウザの仕様とみなされていることもあるため、パターン2と区別がつかないこともあります。

JavaScriptではなくCSS関連ですが、IE 6にはfloat指定した要素にmarginを指定すると指定した値の2倍の値が適用されるという比較的有名なバグがあります。float指定と一緒にdisplay:inlineを指定するとこのバグを回避することができます。このとき、IE 6以外ではinlineの指定は余計ですが、float指定された要素はdisplayの指定に関わらず、block要素として扱われるのでinline指定は無視されるので問題ありません。

inline指定によるmarginバグの回避
.floatbox{
width:50px;
margin:20px;
float:left;
display:inline;
}
inline指定なし
left
left
right
right

inline指定あり

left
left
right
right
図1 IE 6での表示
図1 IE 6での表示

パターン4:レガシーコード

ブラウザの実装の中には、標準仕様にはなっていないが各ブラウザがそれなりに足並みを揃えて実装したため、ある程度は共通した実装になっていることがあります。それらの多くはDOM Level 0と呼ばれており、W3CがDOMの仕様を定める前に実装された部分です。現在ではHTML5や関連APIで再定義され、標準化されつつあります。しかし、基本的に互換性のために定義された仕様であり、既存のブラウザ間で微妙な誤差があったり、より高機能なAPIが存在したり、といったことがあるので、より適切な実装を選択するべきです。

レガシーコードによるフォーム操作
<form id="userform" name="userform">
    <input type="text" name="name">
</form>
<script type="text/javascript">
window.onload = function(){
  // name属性で要素にアクセス
  var form = document.userform;//(1)
  //var form = document.getElementById('userform');
  // on + イベント名でイベント処理(2)
  form.onsubmit = function(evt){
    if (!check()) {
      return false;
    }
  };
  function check(){
    // form要素の中にあるnameを参照
    var user = form.name.value;//(3)
    // 省略
  }
};
</script>

(1)ように、formやinputなどの要素に加えて、a要素、img要素などの要素はname属性を指定することができ、そのnameを使って document[name] とすることでその要素を参照することができます。ただし、name属性の値は重複が許されているので、同じnameを持つ要素が複数あった場合は要素自身ではなく要素配列を取得します。つまり、document[name](もしくはform[name])で要素にアクセスする場合は要素自身を取得するか、要素配列を取得するかの2通りがあるため、(1)のようなコードはバグを生みやすくなってします。

そこで、確実に特定の要素を取得できるgetElementByIdを使用することを推奨します(JavaScriptから扱いたい要素にはid属性を指定するようにしましょう⁠⁠。また、同じnameをもつ複数の要素を取得する場合にはdocument.getElementsByNameやgetElementsByTagNameを使用するのがよいでしょう。getElementsByNameはdocumentに固定ですが、getElementsByTagNameはある要素を基点にその子孫にあたる要素だけを取得できるので、取得したい要素に応じて使い分ける必要があります。

(2)のような on+イベント名 によるイベント処理はひとつの要素にひとつのイベントしか設定できないため、うっかり先に定義されていたイベントを消してしまうといったことが起こり得ますし、どちらにしても元のイベントを残しつつ処理を追加するという実装が必要となるため、保守が必要なケースでは推奨できません。すでに何度か登場しているaddEvent関数のような実装をするべきでしょう。もちろん、ちょっとしたコードでは手軽なon+イベントを使用しても問題ありません。なお、on+イベント名の形はHTML4.01、HTML5で定義されています。

また、(3)は「form要素のname属性」と、⁠form要素内のname属性の値がnameであるinput要素」のどちらを参照しているのか見た目で判断できません。実際には、(3)のnameはinput要素であり、form要素のname属性は隠れてしまっています。form要素のname属性を取得したい場合、form.getAttribute('name')とすることでname属性の値を取得できますが、IE 6ではgetAttributeを使ってもinput要素を取得してしまうため、name属性を取得できなくなってしまいます。

パターン5:先行実装

HTML5やCSS 3などの勧告には至っていない仕様を各ブラウザは先行して実装しており、それらを利用することができます。新しい仕様は有用なものばかりですので、積極的に使いたいところです。ただし、非対応ブラウザでどうするのか、勧告までに仕様が変わる可能性があるといったことに注意する必要があります。こちらは少々高度な内容になりがちなので、連載の後半で扱うことになると思います。

パターン6:最適化

最後に、パフォーマンスについても取り上げます。特にIE 6などはブラウザ自体のパフォーマンスが相対的に低く、IE 6が載っているマシンもそれなりに古いものであることを想定する必要があり、そういった環境でもなるべく快適に動作するように最適化を行います。こちらは適宜簡単に言及しつつ、連載の後半で集中的に扱う予定です。

まとめ

今回はクロスブラウザ対応のパターンを整理しました。いくつか具体例をあげましたが、これらは代表的なものを挙げたに過ぎません。実際に実装する上ではこれらの問題がいくつか絡み合ってきます。なお、第1回の入力値チェックはひとつの解答例となっていますので、今回のパターンを踏まえてコードを読んでみると前回と違った発見があるかもしれませんので、是非復習をしてみてください。次回からはIE対策を中心にクロスブラウザ実装の実例を見ていきたいと思います。

おすすめ記事

記事・ニュース一覧