JavaScript: Browserübergreifende Entwicklung

JavaScript und die Browser-Wirklichkeit

Wer schon etwas Erfahrung in der Webentwicklung gesammelt hat, kennt das Problem: Ein Großteil der Arbeit muss dafür investiert werden, Unterschiede und Fehler der verbreiteten Browser zu berücksichtigen. Besonders im Bereich CSS muss man mit Browserweichen arbeiten, um alle großen Browser zu einer halbwegs einheitlichen Darstellung zu bringen.

Wie sieht es im Bereich JavaScript aus? Zunächst einmal ähnlich schlecht. Manche Browser setzen die DOM-Standards nicht vollständig um und für zahlreiche Aufgaben existieren nur proprietäre Technike. Somit sind oftmals mehrgleisige Scripte nötig, die dieselbe Aufgabe je nach Browser auf eine unterschiedliche Weise lösen.

Dennoch gibt es keinen Grund, vor dem Browser-Durcheinander zu kapitulieren. Während es in CSS nur ungenaue Browserweichen gibt, ist es in JavaScript meistens möglich, gezielt die Existenz der Objekte abzufragen. Existiert ein Objekt und hat gegebenenfalls einen bestimmten Typ und Wert, kann damit gearbeitet werden. Andernfalls können Alternativlösungen greifen. Verwendet man solche »Fähigkeiten-Weichen« anstelle von Browserweichen, die bloß über den Browsernamen Rückschlüsse ziehen, sind zuverlässige und zukunftsfähige Scripte möglich.

Zudem wurden mittlerweile die wichtigsten Unterschiede, Eigenheiten und Fehler der verbreiteten Browser in mühsamer Kleinarbeit dokumentiert. Dabei fielen fertige Script-Schnipsel und bewährte Verfahren ab, mit denen sich Standardaufgaben browserübergreifend lösen lassen. Schließlich ist in puncto JavaScript Bewegung in den Browsermarkt gekommen: Nach und nach werden Fehler bei der Umsetzung von Standards behoben, undokumentierte Techniken werden standardisiert und in die Browser eingebaut.

Dieser Ausblick soll optimistisch stimmen und dazu anregen, die Herausforderung der Browser-Wirklichkeit anzunehmen. Es ist jedoch auch ein hartes Stück Arbeit, sich in die browserübergreifende JavaScript-Entwicklung hineinzudenken.

Abwärtskompatibilität und Zukunftsfähigkeit

Was bedeutet eigentlich »browserübergreifende Entwicklung«? Üblicherweise wird darunter verstanden: Ein Script soll in allen Browsern den gewünschten Zweck erfüllen und dasselbe leisten. Von dieser einseitigen Vorstellung sollten Sie sich verabschieden, denn in vielen Fällen ist sie schlicht nicht umsetzbar!

Das realistische Ziel sollte vielmehr lautet, die vergangenen, gegenwärtigen und zukünftigen Browser gemäß ihrer jeweiligen Fähigkeiten zu bedienen. Dies bedeutet nicht, dass ihr Script auf allen Browsern und Browserversionen exakt denselben Effekt haben muss. Es ist nicht immer sinnvoll, technisch veralteten oder wenig verbreiteten Browser mit unverhältnismäßigem Aufwand dasselbe Ergebnis zu liefern.

Dennoch ist es durch geschickte Programmierung meistens möglich, alle relevanten Browsern eine funktionsfähige Seite zu präsentieren - auch wenn diese z.B. in älteren Browsern nicht ganz so ansehnlich und komfortabel bedienbar ist wie in weiter entwickelten Browsern.

Abwärtskompatibilität bedeutet, dass Ihr Script auf den Fall vorbereitet ist, dass gewisse JavaScript-Techniken nicht zur Verfügung steht oder von Seiten des Browsers fehlerhaft umgesetzt sind. Zukunftsfähigkeit bedeutet, dass Sie durchaus neue und noch nicht breit unterstützte Techniken verwenden können. Voraussetzung ist jeweils, dass sie keine Techniken stillschweigend voraussetzen, sondern immer prüfen, ob die benötigten Objekte existieren und die Teilanweisungen ihres Programmes die erwarteten Ergebnisse liefern.

Fallunterscheidungen und Vereinheitlichungen

Das Grundelement der browserübergreifenden Programmierung ist der mehrgleisiger Ablauf. Scripte nutzen immer wieder eine solche Struktur:

Wenn die nötige Technik zur Verfügung steht,
Dann:
  Löse das Problem auf die eine Weise
Andernfalls:
    Wenn die Alternativtechnik zur Verfügung steht,
    Dann:
      Löse die Aufgabe auf eine andere Weise

Diese Fallunterscheidungen werden in JavaScript üblicherweise mit bedingten Anweisungen umgesetzt: if (Bedingung) { Anweisungen } else { Anweisungen }

Entscheidend ist, genau die Unterschiede zu kennen und punktuell solche Abzweigungen einzubauen. Nicht das ganze Script sollte eine solche Struktur haben, es sollte sich dieser verzweigten Abläufe nur dort bedienen, wo es nötig ist. Auf diese Weise sparen Sie sich doppelte Arbeit.

Wo Unterschiede auftreten, sollten Sie mit solchen Abfragen möglichst für Vereinheitlichung sorgen. Darauffolgende Anweisungen müssen sich dann um die Unterschiede keine Gedanken mehr machen. Bis zur nächsten punktuellen Fallunterscheidung folgt wieder Code, der von allen Browsern verstanden wird.

Nehmen wir ein Beispiel aus dem : Eine Handler-Funktion bekommt das in manchen Browsern als Parameter übergeben. Im Internet Explorer ist es jedoch nur über window.event zugänglich. Über das Event-Objekt hat man Zugriff auf das , bei dem das Ereignis passiert ist, welches die Handler-Funktion gerade verarbeitet. Auch in dem Punkt unterscheiden sich die Browser: Das Element ist entweder in der Eigenschaft target (W3C-konforme Browser) oder srcElement (Internet Explorer) gespeichert. Nun könnte man folgendermaßen vorgehen:

function handlerFunktion (eventObjekt) {
  if (eventObjekt) {
    alert("Element, an dem das Ereignis passierte: " + eventObjekt.target.nodeName);
  } else if (window.event) {
    alert("Element, an dem das Ereignis passierte: " + window.event.srcElement.nodeName);
  }
}

Allerdings ist dieser Code stellenweise redundant und setzt stillschweigend voraus, dass die Übergabe des Event-Objektes als Parameter und target sowie window.event und srcElement notwendigerweise zusammengehören - das ist zwar bei diesem Beispiel kein Problem, aber in anderen Fällen kann eine solche Annahme Probleme bringen.

Anstatt direkt browserspezifischen Code zu schreiben, nutzen wir punktuelle Vereinheitlichung, die gleiche Voraussetzungen für das Script schafft. Nachdem die Browserunterschiede eingeebnet wurden, können wir die Aufgabe, die wir eigentlich lösen wollen, viel einfacher umsetzen. Vor allem wird der Code übersichtlicher und besser strukturiert: Die Bereiche, die sich nur den Browserproblemen widmen, sind getrennt von denen, die die eigentliche Aufgabe lösen.

function handlerFunktion (eventObjekt) {

  // Vereinheitliche den Zugriff auf das Event-Objekt:
  if (!eventObjekt) {
    eventObjekt = window.event;
  }
  // Das Objekt ist jetzt browserübergreifend in der Variable eventObj gespeichert.

  // Vereinheitliche den Zugriff auf das Ziel-Element:
  var target;
  if (eventObjekt.target) {
    target = eventObjekt.target;
  } else if (eventObjekt.srcElement) {
    target = eventObjekt.srcElement;
  }
  // Das Objekt ist nun browserübergreifend in der Variable target gespeichert.

  // Nach den Vereinheitlichungen folgt die eigentliche Umsetzung.
  // Stelle das Gewünschte mit dem Ziel-Element an:
  alert("Element, an dem das Ereignis passierte: " + target.nodeName);

}

Diese Umsetzung mag zunächst länger und umständlicher scheinen. Das liegt jedoch bloß an der ausführlichen Schreibweise. Eine mögliche Kurzschreibweise könnte so aussehen:

function handlerFunktion (e) {
  e = e || window.event;
  var target = e.target || e.srcElement;
  alert("Element, an dem das Ereignis passierte: " + target.nodeName);
}

Mit dem ||-Operator wird hier die Fallunterscheidung umgesetzt. Was kryptisch aussehen mag, hat bloß folgenden Effekt: Wenn der Parameter e gesetzt ist, nehme diesen als Event-Objekt, andernfalls versuche es mit window.event.

Dasselbe Schema wird in der nächsten Zeile angewendet: Wenn die Eigenschaft target des Event-Objektes gesetzt ist, so speichere diese in der Variable target, andernfalls verwende die Eigenschaft srcElement.

Todo: Erklärung des ||-Operators in den Sprachgrundlagen verlinken

Das Beispiel lässt außen vor, dass Browser denkbar sind, die weder die eine noch die andere Vorgehensweise unterstützen. Oder die Handler-Funktion wird aus Versehen außerhalb einer Ereignis-Verarbeitung aufgerufen. Es ist nicht immer nötig, diese denkbaren, aber unrealistischen Fälle abzudecken. Sie können es dennoch tun, indem Sie abfragen, ob die Variablen e und target nach der Vereinheitlichung korrekt gesetzt sind. Wenn dies nicht gegeben ist, kann die Funktionsausführung z.B. durch die Anweisung return; abgebrochen werden.

e = e || window.event;
if (!e) {
  // Kein Zugriff auf das Event-Objekt möglich, breche Funktion ab,
  // um einen Scriptabbruch zu umgehen.
  return;
}

Beachten Sie, dass diese Schreibweise zum Überprüfen der Variable e nur Sinn ergibt, wenn es sich um einen Funktionsparameter handelt, der entweder den Wert undefined hat (falls kein Parameter übergeben wurde) oder eben das Event-Objekt enhält. Beim Umgang mit anderen Objekttypen sähe die Abfrage anders aus.

Fähigkeitenerkennung statt Browsererkennung

Lange Zeit bedienten sich browserübergreifende JavaScripte einer sogenannten Browsererkennung. Anstatt in Erfahrung zu bringen, mit welchen konkreten Unterschieden das Script konfrontiert ist, fragte man kurzerhand den Browsernamen ab. Die Struktur einer solchen Browserweiche sah etwa so aus:

Wenn der Browser den Namen »Internet Explorer« hat,
Dann:
  Löse die Aufgabe auf die die IE-typische Weise
Andernfalls:
  Löse die Aufgabe auf die Netscape-typische Weise

Umgesetzt wurden solche Abfrage mit dem JavaScript-Objekt window.navigator, das verschiedene Informationen über den Browser liefert, der das JavaScript ausführt.

In dem obigen Beispiel werden allein die Browser Internet Explorer und Netscape Navigator berücksichtigt, die sich Ende der 1990er Jahre gegenüberstanden und den Browsermarkt beherrschten. Diese Vorgehensweise ging so lange gut, wie nur diese beiden Browser verbreitet waren und sich deren Versionen gleich verhielten. Diese Situation war jedoch höchstens für eine kurze Zeit gegeben - danach funktionierten solche Scripte nicht mehr zuverlässig.

Alternativ zur Abfrage des Browsernamens wurden zentrale Objekte zur Browsererkennung verwendet:

Wenn das Objekt document.all existiert,
Dann:
  Nimm an, es ist ein Internet Explorer und löse die Aufgabe
  auf die die IE-typische Weise
Andernfalls:
  Wenn das Objekt document.layers existiert,
  Dann:
    Nimm an, es ist ein Netscape Navigator und löse die Aufgabe
    auf die Netscape-typische Weise

Solche Objektabfragen sind nicht völlig abwegig, denn die abgefragten Objekte document.all und document.layers wurden bei der Umsetzung meistens auch verwendet. Wenn jedoch von der Existenz eines Objektes wird stillschweigend auf die Existenz vieler anderer Browserfähigkeiten geschlossen wird, handelt es sich um eine versteckte Browserabfrage.

Eine Browserweiche geht davon aus, dass der Browser eine Reihe von Techniken unterstützt, nur weil er einen bestimmten Namen trägt oder ein zentrales Objekt existiert. Zum einen können damit immer nur die derzeit bekannten Browser in ihren aktuellen Versionen berücksichtigt werden.

Zum anderen halten sich viele Browser an herstellerunabhängige Standards, die sie mehr oder weniger korrekt umsetzen. Aber auch zahlreiche proprietäre, das heißt ursprünglich browserspezifische Erfindungen sind nicht mehr auf einen Browser begrenzt. Andere Hersteller haben sie ebenfalls übernommen. »Browserabfragen« sind schon aus diesem Grund nicht zuverlässig und zukunftsfähig.

Beispielsweise wird das längst veraltete Objekt document.all immer noch zur Erkennung des Internet Explorers verwendet. Das ist unklug, denn oftmals gibt es andere Browser, die die verwendeten Fähigkeiten beherrschen, jedoch nicht document.all.

Browserweichen können daher prinzipiell nicht alle Fälle angemessen berücksichtigen. Sie sollten sie möglichst vermeiden und stattdessen abfragen, ob der jeweilige Browser die Fähigkeiten und Teiltechniken unterstützt, die sie tatsächlich in ihrem Script verwenden.

Objektabfragen

Objekte und Methoden abfragen

Die einzelnen Fähigkeiten eines Browsers drücken sich meist darin aus, dass bestimmte vordefinierte Objekte, Eigenschaften bzw. Methoden existieren. In manchen Fällen müssen Sie zusätzlich prüfen, ob die Eigenschaft auch einen bestimmten Typ oder Wert hat.

Wenn wir eine bedingte Anweisung mit if (...) {...} notieren, so wird die Bedingung (der Ausdruck zwischen den runden Klammern) letztlich in einen Boolean-Wert, also true oder false umgewandelt. Allgemeine Objekte vom Typ Object ergeben bei der Umwandlung in dem Typ Boolean den Wert true.

Das bedeutet, Sie können einfach if (objekt.unterobjekt) { ... } notieren, um die Existenz des Unterobjektes abzufragen. Für Funktionsobjekte gilt dasselbe, also notieren wir if (objekt.methode) { ... }. Die Schreibweise objekt.unterobjekt bzw. objekt.methode ist dabei entscheidend. Nur in besonderen Fällen ist dies durch if (objekt) bzw. if (methode) ersetzbar.

Das folgende Beispiel veranschaulicht die Existenzabfrage von Objekten und Methoden. Die Funktion bringt den aktuell markierten Text im Dokument in Erfahrung und gibt diesen in einem Meldungsfenster aus.

function selektierterText () {
  var text = "";

  if (window.getSelection) {
    text = window.getSelection();
  } else if (document.getSelection) {
    text = document.getSelection();
  } else if (document.selection && document.selection.createRange) {
    text = document.selection.createRange().text;
  } else {
    return;
  }

  alert(text);
}

Es existieren für diese Aufgabenstellung drei Lösungsweisen, die in unterschiedlichen Browsern zum Ziel führen. Das Beispiel demonstriert daher eine dreiteilige Fallunterscheidung mit verschachtelten if-else-Anweisungen.

Je nachdem, welche Objekte bzw. Methoden existieren, werden diese verwenden. Kennt der JavaScript-Interpreter keines dieser Objekte, wird die Funktion vorzeitig beendet. Die verschachtelten Fallunterscheidung in verständlicher Sprache:

Existiert die Methode window.getSelection?
Falls ja:
  benutze diese Methode.
Falls nein:
  Existiert die Methode document.getSelection?
  Falls ja:
    Benutze diese Methode.
  Falls nein:
    Existiert das Objekt document.selection und hat es eine Methode createRange?
      Falls ja:
        Benutze diese Methode.
      Falls nein:
        Brich ab.

Wie Sie sehen, kommt dieses Beispiel ganz ohne Browserabfragen aus und ist doch für verschiedene Browser ausgelegt.

Andere Typen abfragen

Wenn Sie die Existenz von Objekten anderer Typen prüfen wollen, müssen Sie gegebenenfalls anders vorgehen.

Zahlen (Typ Number) und Zeichenketten (Typ String) können Sie im Prinzip zwar auch mit if (objekt.numberEigenschaft) bzw. if (objekt.stringEigenschaft) abfragen. Die Bedingung in den Klammern wird aber wie gesagt in ein Boolean-Wert umgewandelt. Leere Zeichenketten ("") und gewisse Zahlenwerte (z.B. 0) ergeben bei dieser Umwandlung false. Diese Umwandlungsregeln können Verwirrung stiften: Manchmal ist 0 ein gültiger Wert und Sie können damit arbeiten, in anderen Fällen weist er darauf hin, dass der Browser die von Ihnen benötigte Fähigkeit nicht hat und Sie die abgefragte Eigenschaft nicht verwenden können.

ToDo: Link auf "Was ist alles true/false?"

Wenn Sie mit solchen Typen umgehen, sollten Sie daher auf den Operator typeof ausweichen. Dieser gibt den Typ einer Eigenschaft als String zurück. In den beschriebenen Fällen wäre das "string" bzw. "number"). Ein Beispiel ist die Abfrage der Existenz von window.innerHeight:

if (typeof window.innerHeight == "number") {
  alert("Der Anzeigebereich des Browserfensters ist " +
    window.innerHeight + " Pixel breit!");
}

Da manche Browser, insbesondere der Internet Explorer beim Einsatz typeof den Typen nicht immer korrekt wiedergeben, hat sich eingebürgert, bei Existenzabfragen bloß zu prüfen, ob typeof nicht "undefined" liefert:

if (typeof window.innerHeight != "undefined") {
  alert("Der Anzeigebereich des Browserfensters ist " +
    window.innerHeight + " Pixel breit!");
}

ToDo: siehe den weiterführenden Artikel Objektabfragen und Fallunterscheidungen in JavaScript

Browsererkennung in Sonderfällen

Eine Fähigkeitenerkennung mit Abfragen der verwendeten Objekte ist in den meisten Fällen möglich und vorzuziehen. Wenn Sie diese Vorgehensweise gewohnt sind, werden Sie bei neuen Aufgaben schnell einen Weg finden, wie sie die Browserunterschiede in Ihrem Script zuverlässig prüfen und entsprechende Fallunterscheidungen einbauen können. Es sei allerdings nicht verschwiegen, dass sich manche Browsereigenheiten und -fehler nicht so einfach erkennen lassen. Diese Fälle sind glücklicherweise äußerst selten.

Ein Beispiel: Sie prüfen, ob eine Methode existiert und rufen Sie auf, falls sie existiert. Wenn ein Browser die Methode nun kennt, aber falsch implementiert hat, sodass sie nicht das beabsichtigte tut, können Sie dies nicht immer erkennen. Wenn auch der Rückgabewert in Ordnung scheint, können Sie den Browserfehler schwer feststellen.

In diesen Ausnahmefällen bleibt oft nichts anderes als eine Browserabfrage übrig. Wenn Sie eine solche einbauen, müssen Sie darauf achten, dass das Script nicht unter unerwarteten Umständen fehlerhaft arbeitet. Beispielweise kann der Browserfehler, den Sie umgehen wollen, schon in der nächsten Version behoben sein. Dann kann es sein, dass Ihr Script abbricht. Eine Regel lautet daher, nur bekannte Browserversionen anzusprechen. Doch auch diese Vorgehensweise hat ihren Nachteil: Erscheint eine neue Version, die den Fehler immer noch beinhaltet, müssen Sie das Script zeitnah aktualisieren und die Erkennung anpassen. Wie Sie sehen, handelt Sie sich mit Browserabfragen schnell Probleme ein.

Eine allgemeine Browsererkennung ist über das Objekt window.navigator und dessen Eigenschaften möglich. Ein Fertigscript, das Ihnen Browsername und -version zur Verfügung stellt, finden Sie unter Browser detect [en]. Seien Sie sich jedoch darüber im klaren, dass diese navigator-Eigenschaften nicht verfügbar oder manipuliert sein können - eine Erkennung über navigator ist nicht hundertprozentig zuverlässig!

Für spezifische Browser gibt es zuverlässigere Erkennungsmethoden:

Opera bietet das Objekt window.opera an, welches auch nur im Opera existiert. Dieses können Sie einfach mit if (window.opera) {...} abfragen.

Wenn Sie den Internet Explorer in einem Script erkennen wollen, so können Sie alternativ zu Conditional Compilation (engl. bedingte Ausführung) greifen. Dabei handelt es sich um eine Microsoft-eigene Zusatzsprache, die Sie in den JavaScript-Code einfügen können. Im Grunde handelt es sich um JavaScript-Kommentare nach dem Schema /*@cc_on ... @*/. Während die anderen Browser sie ignorieren, erkennt der Internet Explorer @cc_on eine Anweisung. In diesem Fall: Schalte Conditional Compilation (CC) an. Das bedeutet, dass der Internet Explorer den Text zwischen @cc_on und @ als JavaScript-Code behandelt und ausführt, obwohl diese sich in einem Kommentar befindet. Lange Rede, kurzer Sinn: Auf diese Weise können Sie JavaScript-Code notieren, der garantiert nur vom Internet Explorer ausgeführt wird - denn vor allen anderen Browser ist er in einem Kommentar versteckt. Ein Beispiel:

if (/*@cc_on ! @*/ false) {
  window.alert("Dies wird nur im Internet Explorer ausgeführt!");
}

Alle Browser außer dem Internet Explorer führen den Code if (false) {...} aus, nur im Internet Explorer wird mittels speziellem Kommentar ein Negationsoperator (das Ausrufezeichen) eingefügt. Der IE führt also if (!false) {...} und damit die Anweisungen im if-Block aus.

...

JavaScript-Unterstützung verbreiteter Browser

Einleitung

... Engines statt Browser!

Mozilla Firefox (Gecko- und SpiderMonkey-Engine)

Internet Explorer

Opera

Safari (Webkit-Engine)