JavaScript: Browserübergreifende Entwicklung

JavaScript und die Browser-Wirklichkeit

Vor einigen Jahren bestand ein großer Teil der Webentwicklung daraus, Unterschiede und Fehler der Browser zu berücksichtigen. Besonders im Bereich CSS gab es lange unterschiedliche Darstellungen. Nach und nach haben die großen Browser ihre Fehler korrigiert und es ist einfacher geworden, ein CSS-Layout zu erstellen.

Wie sieht es im Bereich JavaScript aus? Die großen Browser setzen die DOM-Standards weitgehend um. Nach und nach wurden undokumentierte Techniken standardisiert und Fehler bei der Umsetzung dieser Standards behoben.

Dennoch entstehen ständig neue Techniken, die noch nicht breit unterstützt werden. Für einige Aufgaben sind daher mehrgleisige Scripte nötig, die dieselbe Aufgabe je nach Browserfähigkeiten auf eine unterschiedliche Weise lösen.

Glücklicherweise ist es in JavaScript möglich, gezielt die Existenz von Objekten abzufragen. Existiert ein Objekt und hat gegebenenfalls einen bestimmten Typ und Wert, kann damit gearbeitet werden. Andernfalls greifen Alternativlösungen.

Eine solche Fähigkeiten-Weiche macht zuverlässige und zukunftsfähige Scripte möglich. Sie ist besser als eine Browserweiche, die lediglich über den Browsernamen Rückschlüsse zieht.

Mittlerweile wurden die wichtigsten Unterschiede und Fehler der verbreiteten Browser dokumentiert. Dabei entstanden fertige Scripte und bewährte Verfahren, mit denen sich Standardaufgaben browserübergreifend lösen lassen.

Dieser Ausblick soll optimistisch stimmen und dazu anregen, die Herausforderung der Browser-Wirklichkeit anzunehmen. Es ist jedoch harte 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.

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

Durch geschickte Programmierung ist es meistens möglich, allen relevanten Browsern eine funktionsfähige Seite zu präsentieren. In älteren Browsern ist die Seite dann weniger ansehnlich und komfortabel bedienbar wie neueren 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 in beiden Fällen, dass sie keine Techniken stillschweigend voraussetzen, sondern immer prüfen, ob die benötigten Objekte existieren und die Anweisungen ihres Programmes die erwarteten Ergebnisse liefern.

Fallunterscheidungen und Vereinheitlichungen

Die Grundstruktur der browserübergreifenden Programmierung ist der mehrgleisiger Ablauf. Scripte nutzen sie immer wieder:

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 verzweigten Abläufe nur dort nutzen, 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. Die nachfolgenden Anweisungen müssen sich dann um die Unterschiede keine Gedanken mehr machen. Bis zur nächsten Fallunterscheidung folgt Code, der von allen Browsern verstanden wird.

Nehmen wir ein Beispiel aus dem Event-Handling. Wie wir im Kapitel Arbeiten mit dem Event-Objekt gelernt, bekommt eine Handler-Funktion das Event-Objekt als Parameter übergeben. Im Internet Explorer vor Version 9 ist es stattdessen über window.event zugänglich. Über das Event-Objekt hat man Zugriff auf das Zielelement. Auch in dem Punkt unterscheiden sich die Browser: Das Element ist entweder in der Eigenschaft target (DOM-konforme Browser) oder srcElement (Internet Explorer < 9) gespeichert. Nun könnte man folgendermaßen vorgehen:

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

Allerdings ist dieser Code redundant. 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 eigentliche Aufgabe einfach umsetzen. Der Code wird übersichtlicher: Die Bereiche, die sich den Browser-Unterschieden widmen, sind getrennt von denen, die die eigentliche Aufgabe lösen.

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

  // Vereinheitliche den Zugriff auf das Zielelement :
  var target;
  if (event.target) {
    target = event.target;
  } else if (event.srcElement) {
    target = event.srcElement;
  }
  // Das Zielelement ist nun browserübergreifend in der Variable 'target' gespeichert.

  // Nach der Vereinheitlichung folgt die eigentliche Umsetzung.
  window.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 haben wir bereits kennengelernt:

function handlerFunktion(event) {
  // Vereinheitlichung
  event = event || window.event;
  var target = event.target || event.srcElement;
  // Eigentliche Umsetzung
  window.alert('Element, an dem das Ereignis passierte: ' + target.nodeName);
}

Fähigkeitenerkennung statt Browsererkennung

Lange Zeit bedienten sich browserübergreifende JavaScripte einer sogenannten Browsererkennung. Anstatt die konkreten Unterschiede in Erfahrung zu bringen, 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

Diese Browserweichen nutzen das JavaScript-Objekt window.navigator, das verschiedene Informationen über den Browser liefert, der das JavaScript ausführt.

Das obige Beispiel berücksichtigt allein die beiden Browser Internet Explorer und Netscape Navigator. Diese standen sich Ende der 1990er-Jahre gegenüber und beherrschten den Browsermarkt. Diese Vorgehensweise ging so lange gut, wie nur diese beiden Browser verbreitet waren und sich deren Versionen gleich verhielten. Diese Situation war nur 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 waren nicht abwegig, denn die abgefragten Objekte document.all und document.layers wurden bei der Umsetzung meistens auch verwendet. Wenn jedoch von der Existenz eines Objekts 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 Browser an herstellerunabhängige Standards. Aber auch ursprünglich browserspezifische Erfindungen sind nicht mehr auf einen Browser begrenzt. Andere Hersteller haben sie ebenfalls übernommen.

Browserweichen sind daher nicht zuverlässig und zukunftsfähig. Browserweichen können prinzipiell nicht alle Fälle angemessen berücksichtigen. Sie sollten sie möglichst vermeiden und stattdessen abfragen, ob der jeweilige Browser die Techniken 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 umgewandelt, also true oder false. Objekte ergeben bei der Umwandlung in dem Typ Boolean den Wert true.

Das bedeutet, Sie können if (objekt.eigenschaft) { … } notieren, um die Existenz eines Untero abzufragen. Für Funktionsobjekte gilt dasselbe, also notieren wir if (objekt.methode) { … }.

Die Schreibweise objekt.eigenschaft bzw. objekt.methode ist dabei entscheidend. Wenn die Eigenschaft oder Methode nämlich nicht existiert, ergibt der Ausdruck schlicht undefined. Die Bedingung ist damit nicht erfüllt.

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.selection && document.selection.createRange) {
    text = document.selection.createRange().text;
  } else {
    return;
  }

  window.alert(text);
}

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

Es werden die Objekte verwendet, die zur Verfügung stehen. Kennt der JavaScript-Interpreter keines dieser Objekte, wird die Funktion vorzeitig beendet. Die Fallunterscheidung in verständlicher Sprache:

Existiert die Methode window.getSelection?
Falls ja:
  Rufe diese Methode auf.
Falls nein:
  Existiert das Objekt document.selection und hat es eine Methode createRange?
    Falls ja:
      Rufe diese Methode auf und greife auf die Eigenschaft text zu.
    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 Nicht-Objekten prüfen wollen, müssen Sie anders vorgehen.

Zahlen (Typ Number) und Zeichenketten (Typ String) können Sie zwar auch mit if (objekt.numberEigenschaft) bzw. if (objekt.stringEigenschaft) abfragen. Die Bedingung in den Klammern wird aber wie gesagt in einen Boolean-Wert umgewandelt. Leere Zeichenketten ('') und manche 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 benötigte Fähigkeit nicht hat und Sie die abgefragte Eigenschaft nicht verwenden können.

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, welche die Höhe des Anzeigebereichs des Browsers als Zahl enthält:

if (typeof window.innerHeight === 'number') {
  window.alert('Der Anzeigebereich des Browsers ist ' +
    window.innerHeight + ' Pixel hoch!');
}

Manche Browser, insbesondere alte Internet-Explorer-Versionen, geben den den Typ nicht immer korrekt wieder. Daher hat es sich eingebürgert, bei Existenzabfragen bloß zu prüfen, ob typeof nicht 'undefined' liefert:

if (typeof window.innerHeight !== 'undefined') {
  window.alert('Der Anzeigebereich des Browsers ist ' +
    window.innerHeight + ' Pixel hoch!');
}