Fragen zu HTML5 & Co beantwortet 12 - Formularvalidierung, Media-Attribute, H1-Headlines, Main-Element

Veröffentlicht am 28. November 2013

Ein letztes Mal im Jahr 2013 darf ich für euch Spezifikations-Exegese betreiben und Fragen zu HTML5 beantworten. Falls euch noch mehr brennende Fragen quälen (gerne auch CSS und JavaScript), dann schreibt mir eine E-Mail oder gebt die Frage per Twitter ab.

Bedingtes Required-Feld mit HTML5?

Kennt HTML5 für Formulare auch ein abhängiges required, dass z.B. ein Feld ist nur dann ein Pflichtfeld ist, wenn eine bestimmte Checkbox aktiviert wurde? Oder bleibt einem hier nur JavaScript?

Ohne JavaScript geht das nicht – „wenn A, dann B“ ist schon Programmierung und kein Markup mehr. Aber man kann das JS so gestalten, dass es sich in die Validierungsmechanik von HTML5 einklinkt. In konkreten Fall wäre es einfach damit getan, das required-Attribut auszutauschen, je nachdem ob die Checkbox angehakt ist oder nicht. Wichtig ist nur, dass das in dem Moment passiert, in dem sich die Checkbox ändert und nicht erst beim Absenden des Formulars.

Für die komplexeren Fälle ist es aber noch gut zu wissen, dass dank HTML5 Formularfelder eine JS-Methode namens setCustomValidity() haben, die es erlauben, eine Fehlermeldung so einzubinden, dass sie genau so funktioniert, wie die Standard-Meldungen. Eine kleine Demo gibt es hier.

media-Attribut für link-Elemente

Kann man bei HTML5 das media-Attribut für Stylesheets im link-Element weglassen? Bei www.peterkroener.de und bei HTML5 Boilerplate wird nämlich kein media-Attribut angegeben. Gibt es Nachteile, wenn man das media-Attribut nicht benutzt?

Die HTML5-Spezifikationen bezeichnen die Angabe des media-Attributs für Stylesheet-Links als prescriptive, legt aber auch fest, was passiert wenn das Attribut fehlt: in dem Falle gilt der Media Query all (Link). Insofern gibt es keine überraschenden Nachteile wenn das Attribut fehlt und durch ein Auslassen des Attributes wird das HTML wird auch nicht ungültig.

Ich könnte sogar ein Argument für das Auslassen des media-Attributs finden. Browser halten sich durchaus an das Attribut, d.h. sie wenden nur jene Styles an, auf die der Media Query im Attribut zutrifft. Sie laden aber immer alle in link-Elementen referenzierten Stylesheets herunter und schauen erst dann, ob sie die auch gebrauchen können. Überflüssige Downloads sind doof – besser ist es meist, alle Stylesheets in eine Datei zu packen und dort mit Media Queries zu arbeiten. Lässt man dann dann media-Attribut bei den link-Elementen aus, kommen weniger informierte Zeitgenossen gar nicht erst auf die Idee, der Browser würde die referenzierte Datei nicht herunterladen. Ansonsten ist es aber wirklich egal, ob dieses Attribut da ist oder nicht.

Ist <main> einsatzbereit?

Wie ist heute der Stand bezüglich des <main>-Elements? Kann man es bereits ohne Kompromisse einsetzen?

Grundsätzlich einsetzbar ist das neue Element (Details) schon und mit dem html5shiv funktioniert es auch in allen älteren Browsern. Noch sind sich W3C und WHATWG-Spezifikationen aber nicht in allen Details einig (Stand Ende 2013). Laut W3C darf es nur ein <main> pro Seite geben; es markiert den Hauptinhalt der gesamten Seite. Die WHATWG findet hingegen, dass man auch mehrere <main> haben können sollte, womit jeweils der Hauptinhalt des Umgebenden Container-Elements (z.b. <section>) ausgezeichnet wird.

Dass das neue Element kommt, scheint außer Frage zu sein, es geht nur noch um das wie. Und da der Use Case „Hauptinhalt der gesamten Seite“ in jedem Fall durch die Spezifikationen abgedeckt wird, hätte ich zumindest diesbezüglich gar keine Bedenken. Aber wie immer gilt: in Bezug auf HTML5-Markup immer schön locker bleiben und nicht überstürzt vorhandene, funktionierende Webseiten umbauen nur weil irgendwer etwas neues erfunden hat.

Mehr als eine H1 pro Seite?

Kann ich mehr als eine <h1> in ein HTML5-Dokument schreiben ohne mit <article> o.Ä. zu separieren? Einfach so?

Es spricht aus rein semantische Sicht nichts dagegen. Wenn es mehrere Überschrifen gleicher Ebene innerhalb eines Abschnitts gibt, dann sind das eben Überschriften mit gleichem Rang (Specs). Und nirgends steht geschrieben, dass das nicht nicht auch für <h1> gilt. Bevor man aber wirklich mehrere <h1>-Elemente in der Seite verteilt, sollte man die SEO-Abteilung konsultieren, denn Suchmaschinen-Nerds haben oft sehr spezielle Meinungen zu Überschrift-Elementen allgemein und zu <h1> im Besonderen.

Weitere Fragen?

Eure Fragen zu HTML5, JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach eine E-Mail schreiben oder Twitter bemühen und ein bisschen Geduld mit der Antwort haben. Ansonsten kann man mich natürlich auch mieten.

Hardware-Review: Samsung 900X3E K06 ATIV Book 9 (in Tateinheit mit Ubuntu)

Veröffentlicht am 23. Oktober 2013

Mein (gar nicht mal so) alter Laptop ist einigen Jahren des Dauereinsatzes physisch ganz schön am Ende. Nachdem das Ding im wirklich Wochentakt von einer Ecke des Landes an die andere geschleppt wurde ist so manches Scharnier und so mancher Anschluss ausgeleiert, eine Akkuzelle ist tot und der Sugru-Anteil an der Gesamtmasse steigt so langsam auf bedenkliche Werte. Außerdem reicht die noch heile Hardware kaum aus, um die abgefahreneren HTML5-Demos wie z.B. Epic Citadel ruckelfrei abzuspielen. Zeit also für ein neues Gerät! Gesucht war ein Ultrabook mit ordentlich Power, das linuxfreundlich und so klein und leicht wie möglich sein sollte. Geworden ist es das vollkommen bescheuert benannte Samsung 900X3E K06 ATIV Book 9 (Amazon ), ein Nachfolgemodell des bisher von mir eingesetzen (und ebenso bescheuert benannten) Samsung NP900X3A A01. Auf Twitter wurde mehrfach ein Review verlangt, das ich hiermit nach den ersten Testeinsätzen in den letzten Woche liefere.

Eigentlich macht das ATIV Book 9 alles, was ich brauche. Es hat auch für die verrückteste WebGL-Demo genug Power (i7-3537U, Intel HD 4000, 4 GB RAM) und ist dabei klein (313,8 x 218,5 x 12,9 mm) und wiegt nur 1,16 kg. Das ist noch mal ein kleine Stück kleiner und leichter als das Vorgängermodell, das von den Abmessungen her eine 1:1-Entsprechung des MacBook Air war.

Das Samsung 900X3E K06 ATIV Book 9a ist sehr klein und leicht

Das Gehäuse ist komplett aus Metall, macht einen soliden Eindruck und sieht für einen PC auch gar nicht mal so schlimm aus. Ubuntu 13.04 ließ sich widerstandslos an die Stelle des vorinstallierten Windows 8 setzen und alles wichtige funktioniert aus dem Stand, ähnlich wie bei den Vorgängermodellen. Außer Betrieb ist bei mir im Moment die Regelung der Tastaturbeleuchtung und hin und wieder berichten nach dem Standby einzelne Indikatoren Unfug – z.B. behauptet angeschaltetes Bluetooth dass es aus ist und die Batterie zeigt nicht immer an, ob sie gerade be- oder entladen wird. Ich nehme an, dass dieser Kleinkram mit ein bisschen Frickelei zu reparieren ist oder sich mit der neuen Ubuntu-Version von selbst behebt.

Wie bei den Vorgängermodellen fällt das Ultrabook-typische, spartanische Anschluss-Angebot aus: neben 2× USB, 1× Micro-HDMI, einer kombinierten 3,5 mm-Audiobuchse und einem Kartenslot gibt es noch einen Anschluss für einen (mitgelieferten) proprietären RJ45-Adapter. Das ist nicht ungewöhnlich, aber auf der linken Seite sind die Anschlüsse so dicht aneinander gepackt, dass man dort außer dünnsten USB-Steckern nicht viel anschließen kann, wenn man nebenbei auch Strom und HDMI-Output haben möchte. Das folgende Bild zeigt die linke Seite bei voller Belegung  der nun wirklich nicht besonders dicke nicht angeschlossene Speicherstick würde sich nur mit sanfter Gewalt anstelle des USB-Kabels anschließen lassen.

Sehr beengte Platzverhältnisse bei den Anschlüssen des Ultrabooks

Als Star der Show dürfte das Display durchgehen. Es ist matt (wichtig!) und sportet mit 1.920 x 1.080 eine stattliche Auflösung. Was sich mit dieser Auflösung anstellen lässt, ist eine Software-Frage. Moderne Betriebssysteme wie Windows 8.1 sind wohl in der Lage, die Auflösung in Bildschärfe umzusetzen statt einfach alles auf dem 13,3"-Bildschirm kleiner erscheinen zu lassen. Ob man das braucht, ist wohl eine Geschmacksfrage und ob Ubuntu das kann, habe ich nicht erforscht. Mir gefällt die normale hohe Auflösung ausgesprochen gut.

Scharfes Display mit hoher Auflösung

Wo wurde gespart und geschlampt? Die eingebauten Lautsprecher sind natürlich nichts tolles, aber auch nicht so schlimm wie schon in anderen Laptops gehört. Die Webcam reißt ebenfalls keine Bäume aus. Und man darf natürlich nicht vergessen, dass das Gerät richtig schön teuer ist. Samsungs UVP liegt bei 1.999 € und obwohl ich mit etwas unter 1.700 € davongekommen bin, ist das nicht gerade billig. Da es an der Video-Ausgang-Front nur Micro-HDMI gibt, darf man ggf. noch zusätzlich für z.B. VGA-Adapter nicht unerhebliche Summen löhnen (ich nutze den Vorgänger dieses Teils). Fazit?

Pro:

  • Klein, leicht, flott
  • Mattes, sehr hoch auflösendes Display
  • Ubuntu 13.04 läuft aus dem Stand so gut wie perfekt

Contra:

  • Sehr eng aneinanderliegende Anschlüsse
  • Teuer
  • Ggf. müssen nicht ganz billige Micro-HDMI-Adapter gekauft werden

Für meinen Use Case (Dauermobil-Einsatz mit Ubuntu) scheint das Gerät wie gemalt zu sein. Den größten Haken würde ich eindeutig an der Kostenfront ausmachen. Man bekommt wenig Gerät (d.h. wenig Größe und Gewicht) mit angemessen viel Power für sehr viel Geld. Wer willens und in der Lage ist, etwas mehr zu schleppen oder auf das Monster-Display zu verzichten, wird sicher auch mit weniger Finanzaufwand glücklich.

Unsync: synchrone JavaScript-Funktionen im Browser einfach asynchron machen

Veröffentlicht am 16. Oktober 2013

In letzter Zeit hab ich hin und wieder Dinge gebastelt, die schwergewichtige Rechenaufgaben in Browsern durchführen. Dank HTML5 gibt es hierfür ja mehr als genug Anlässe, wobei mein konkretes Anliegen die Analyse von Bilddaten aus Webcam-Feeds war. Das Grundproblem bei so etwas ist, dass lang laufende Scripts (z.B. für Gesichtserkennung) den Browser komplett blockieren können. In meinem Fall ruckelte meist nur das Video, aber der Extremfall dieses Problems ist das allseits bekannte Ein-Script-auf-dieser-Seite-läuft-zu-lange-Popup. All das will man nicht haben und eigentlich hat HTML5 auch hierzu eine Lösung parat. Das Dumme an der Lösung ist, dass ihre Benutzung recht viel Aufwand bedeutet, so dass ich etwas drumherum konstruiert habe, was die Angelegenheit in vielen Fällen auf einen JavaScript-Einzeiler reduziert: Unsync.

Web Workers werden gerne als „Threads für JavaScript“ beworben. Dieses Label ist auch nicht falsch, aber ob Web Workers ihren Job durch Threads oder mit Mitteln schwarzer Magie ausführen, ist eigentlich egal. Der interessante Effekt von Workers ist, dass sie synchronen Code asynchronisieren und die Blockade des Browsers verhindern. Dieses JS-Snippet bringt (Stand Mitte 2013) selbst die neueste Chrome-Beta ins Schwitzen:

for(var i = 0; i < 50000000; i++){
  Math.sqrt(Math.random() * 1000000000);
}
window.alert('Fertig!');

Bevor die Fertig-Meldung kommt, müssen wir mehrere Sekunden einen blockierten Browser ertragen. Web Workers erlauben es, die Blockade zu lösen. Dazu muss man den zeitkritischen Code in eine Extra-JS-Datei verfrachten und diese Extra-Datei durch einen Worker laden lassen:

// Haupt-JS-File
var myWorker = new Worker('worker.js');
myWorker.postMessage(null); // Startschuss geben
myWorker.onmessage = function(){ // Fertig-Meldung empfangen
  window.alert('Fertig!')
};

// worker.js
this.onmessage = function(){ // Startschuss empfangen
  for(var i = 0; i < 5000000000; i++){
    Math.sqrt(Math.random() * 1000000000);
  }
  this.postMessage(null); // Fertig-Meldung abschicken
};

Diese Lösung finde ich reichlich unzumutbar. Warum sollte man denn bitte den Code für die Rechenaufgabe in eine Extra-Datei verfrachten wollen? Code modularisiert man bitteschön nach rein inhaltlichen Gesichtspunkten festgelegten Grenzen, die Performance darf da nichts zu melden haben. Das ganze Gewurschtel mit Events und postMessage() könnte auch bequemer sein bzw. im Idealfall gar nicht stattfinden. Es wäre doch viel schöner wenn man einfach aus einer synchronen JS-Funktion mittels eines Einzeilers eine asnchrone Variante bauen könnte, in etwa so:

var asyncFn = macheAsynchron(synchroneFn);
asyncFn(arg1, arg2, function(result){
  console.log(result);
});

Meine kleine Library namens Unsync macht genau das. Und obwohl ein ganzer Haufen HTML5-Technik zum Einsatz kommt, ist die Funktionsweise eigentlich ganz simpel und die Browserunterstützung ist auch nicht vollends katastrophal.

Wie Unsync funktioniert

Einen neuen Web Worker startet man, indem man eine URL zu einem Script in den Worker-Constructor steckt:

var myWorker = new Worker('pfad/zu/worker.js');

Es steht aber nirgends geschrieben, dass ein Script für einen Worker tatsächlich in einer eigenen Datei existieren muss … die Spezifikationen sehen nur vor, dass die Constructorfunktion eine resource identified by url übergeben bekommen soll. Und Dateiressourcen nebst URL kann man in HTML5 einfach aus heißer Luft erzeugen, File API sei Dank. Der Blob-Constructor baut uns eine ausreichend dateiartige Ressource …

var myBlob = new Blob(['Hallo Welt!'], {
  type: 'text/plain'
});

… und die Funktion window.URL.createObjectURL() erzeugt eine URL auf diese Ressource, obwohl sie eigentlich nur ein JavaScript-Objekt in unserer JS-Sandbox ist:

var myBlobUrl = window.URL.createObjectURL(myBlob);
location.href = myBlobUrl;

Hiermit schickt uns der Browser an eine URL, die in etwa wie blob:http%3A//localhost/8f0a8135-ebef-43eb-b660-9ef69ce8cf3e aussieht und eine Plaintext-Datei mit dem Inhalt „Hallo Welt“ zu referenzieren scheint. Dass das Ganze nur innerhalb unserer JavaScript-Welt existiert, ist dabei für den Browser nicht von Belang – eine mit einer URL referenzierte Ressource ist eine mit einer URL referenzierte Ressource, egal woher. Und das klappt natürlich auch mit JavaScript:

var workerBlob = new Blob(['this.onmessage = function(){\
  for(var i = 0; i   5000000000; i++){\
    Math.sqrt(Math.random() * 1000000000);\
  }\
  this.postMessage(null);\
};'], {
  type: 'text/javascript'
});
var workerUrl = window.URL.createObjectURL(workerBlob);
var myWorker = new Worker(workerUrl);
myWorker.postMessage(null);
myWorker.onmessage = function(){
  window.alert('Fertig!')
};

… aber Code als String? Das geht ja nun mal gar nicht, schon gar nicht wenn es das performancekritische Herz unserer App ist. Lieber würde man eine Funktion in den Worker schieben. Das ist aber nicht ohne weiteres möglich – der für die Datenübermittlung in den Worker verwendete structured clone algorithm kann das nicht. Aber es gibt zum Glück Function.prototype.toString().

Funktionen haben, anders als die meisten anderen JavaScript-Objekte, eine sehr nützliche Variante der toString()-Methode. Diese spuckt tatsächlich eine brauchbare String-Repräsentation der betroffenen Funktion aus:

(function foo(){ return 42; }).toString()
// > "function foo(){ return 42; }"

Warum also nicht eine normale Funktion schreiben, sie stringifizieren und das ganze als Blob bzw. Objekt-URL in einen Web Worker schieben? Anfang (function berechnung(){) und Ende (}) der Funktion stehen hierbei freilich ein wenig im Weg, denn eigentlich wollen wir bloß den Inhalt der Funktion haben, nicht die gesamte Deklaration. Zum Glück können wir die Deklaration bequem in einen sofort ausgeführten Funktionsausdruck (IIFE) verwandeln. Hierfür müssen wir bloß ein paar Klammern vorn und hinten an den Funktionscode anbringen, was leicht ist, da der Blob-Constructor ohnehin als erstes Argument ein Array von Strings erwartet. Wir nehmen also die String-Repräsentation unserer Rechnen-Funktion, hängen vorn eine und hinten drei Klammern an und stecken das ganze via Blob und Blob-URL in den Worker:

// Quellfunktion
function berechnung(){
  this.onmessage = function(){
    for(var i = 0; i < 5000000000; i++){
      Math.sqrt(Math.random() * 1000000000);
    }
    this.postMessage(null);
  }
}

// Quellfunktion als String
var code = berechnung.toString();

// Blob aus dem String plus Klammern für die IIFE
var workerBlob = new Blob(['(', code, ')()'], {
  type: 'text/javascript'
});
var workerUrl = window.URL.createObjectURL(workerBlob);

// Worker erzeugen und benutzen
var myWorker = new Worker(workerUrl);
myWorker.postMessage(null);
myWorker.onmessage = function(){
  window.alert('Fertig!');
};

Die Quellfunktion berechnung() unterliegt einigen Beschränkungen. Es wird nicht die Funktion selbst, sondern nur ein diese Funktion abbildender String in den Web Worker geschoben, was bedeutet, dass die Funktion berechnung() selbst keine Seiteneffekte/Nebenwirkungen haben kann. Sie kann zum Beispiel nicht das DOM anfassen und kann keine Variablen aus übergeordneten Scopes verwenden. Außerdem muss sie zum jetzigen Zeitpunkt noch immer für Web Workers geschrieben werden, d.h. postMessage() zur Kommunikation über die Worker-Grenzen hinweg benutzen. Und das ist doof, denn der der Autor der Quellfunktion sollte nicht wissen müssen, wie ein Worker bzw. dessen Messaging-System funktioniert. Außerdem gibt es bisher keine Möglichkeit, Daten in den Worker hinein oder aus dem Worker herauszubekommen und wenn man den Worker nach getaner Arbeit schließen und seine Blob-URL deaktivieren könnte, wäre das ebenfalls nicht verkehrt. Und ohnehin wäre das ganze als Library-Funktion außerhalb des eigentlichen Moduls viel besser aufgeboben.

Die ersten beiden Hürden sind schnell genommen; nur der String, in den die Quellfunktion eingepackt wird, muss ein bisschen angepasst werden. Die Kommunikation zwischen Web Worker und Außenwelt wird über Events abgewickelt. Worker und Außenwelt verwenden die postMessage()-Methode, um auf der jeweiligen Gegenseite ein message-Event zu triggern. Das an postMessage() übergebene Argument wird als Nachricht übertragen, was mit so gut wie jedem JS-Datentyp funktioniert (Funktionen ausgenommen). Das wegzuabstrahieren ist recht simpel: wir ersetzen einfach die Klammern, die aus dem Funktionstemplate eine IIFE machen durch Code, der die Funktion in ein message-Event einpackt, die Arguments an das Funktionstemplate weiterleitet und das Ergebnis der Funktion mittels postMessage() zurücksendet. Aus alt …

var workerBlob = new Blob(['(', code, ')()'], {
  type: 'text/javascript'
});

 … wird neu:

var workerBlob = new Blob([
  'this.onmessage = function(evt){',
  '  var result = (', code, ').apply(null, evt.data);',
  '  this.postMessage(result);',
  '};'
], {
  type: 'text/javascript'
});

Jetzt kann als Quellfunktion wirklich jede Funktion herhalten, die ohne Seiteneffekte bzw. Nebenwirkungen und globale Variablen auskommt und der Autor der Funktion braucht nichts mehr über Web Workers zu wissen – dass es keine Seiteneffekte geben darf, sind die einzige verbleibende Beschränkung. Unsere Funktion berechnung() darf nun wieder aussehen wie eine ganz normale Funktion, ist schön zu lesen und durch die Verwendung im Web Worker aus nicht mehr blockierend:

// Quellfunktion
function berechnung(){
  for(var i = 0; i < 500000000; i++){
    Math.sqrt(Math.random() * 1000000000);
  }
}

// Quellfunktion als String
var code = berechnung.toString();

// Der Rest geht von selbst :)

Nun müsste nur noch der übrige Web-Worker-spezifische Code aus unserem Modul verschwinden. Außerdem gibt es noch zwei Details, die beachtet werden wollen und die sich nicht komplett abstrahieren lassen. Mit createObjectURL() erzeugte URLs sind potenziell immerwährende Referenzen auf das betroffene Objekt und daher ein schönes Speicherleck, was auch für die Web Workers selbst gilt – wenn wir die Hintergrundprozesse nicht terminieren, tut es niemand und sie verbrauchen bis ans Ende aller Tage Speicherplatz und Rechenpower. Die Unsyc-Library versucht all das so bequem wie möglich zu gestalten.

Unsync benutzen

Unsync besteht aus eigentlich nur einer einzigen Funktion namens unsync(), die aus einer synchronen Funktion ohne Seiteneffekte/Nebenwirkungen eine äquivalente asynchrone Funktion erzeugt:

var asyncFn = unsync(fn);
asyncFn(callback);

Wenn die originale Funktion n Arguments erwartet, erwartet die asynchrone Funktion n + 1 – alle normalen Arguments plus einem Callback am Ende. Dem Callback wird als erstes Argument das von der Quellfunktion errechnete Ergebnis übergeben:

// Quellfunktion; rechnet rum und gibt ein Benchmark zurück
function crunchNumbers(x){
  var startTime = new Date();
  for(var i = 0; i < x; i++){
    Math.sqrt(Math.random() * 1000000000);
  }
  var totalTime = new Date() - startTime;
  return totalTime;
}

// Asynchrone Version der obrigen Funktion erzeugen
var crunchAsync = unsync(crunchNumbers);

// Async-Function mit Arguments und Callback ausrufen
crunchAsync(5000000000, function(time){
  window.alert('Fertig nach ' + time);
});

So muss man fast gar nichts mehr über Web Workers wissen – fast. Das einzige, was sich nicht komplett automatsieren lässt, ist das Abschalten der Worker-Hintergrundprozesse, wenn diese nicht mehr gebraucht werden. Hierfür gibt es in Unsync zwei Möglichkeiten. Zum einem kann eine Einweg-Funktion erzeugt werden, die ihren Hintergrundprozess nach der erstmaligen Verwendung selbsttätig terminiert. Diese Funktion kann dann nur exakt einmal verwendet werden und reagiert auf neuerliche Aufruf-Versuche mit Exceptions. Um diesen Selbstzerstörungs-Mechanismus zu aktivieren einfach unsync() als zweites Argument true mitgeben:

var unsyncedEinweg = unsync(fn, true);
unsyncedEinweg(function(){      // Einmal klappt...
  unsyncedEinweg(function(){}); // Exception!
});

Alternativ können mit unsync() erzeugte Funktionen über ihre terminate()-Methode manuell ihren Worker ausgeknipst bekommen. Die Eigenschaft isTerminated verrät, ob der Worker einer Funktion terminiert wurde:

var unsynced = unsync(fn, true);
unsynced(); // Berechnung startet

// Wenn wir nach einer Minute nicht fertig sind und terminiert
// haben, ist es eh zu spät...
setTimeout(function(){
  if(!unsynced.isTerminated){
    unsynced.terminate();
    window.alert('Timeout!');
  }
}, 60 * 1000);

Das wäre eigentlich alles, was es zur Benutzung Unsync zu wissen gibt, wären da nicht die Browser bzw. die zwei ausgesuchten Prachtexemplare ihrer Zunft.

Wo ist der Haken?

Alle halbwegs aktuellen Versionen aller relevanten Browser unterstützen die nötigen APIs … bis auf den Android-Browser, der keine Web Workers kennt. Die IE 10 und 11 unterstützen zwar alle APIs, wissen aber anscheinend nicht, dass Blob-URLs dem gleichen Origin wie die sie erzeugende Webseite zuzuordnen sind. Für den IE gelten alle Blob-URLs gefährlicher Schadcode von außerhalb, der nicht ausgeführt werden darf. Das macht die komplette Objekt-URL-API im IE ziemlich nutzlos und verhindert natürlich auch, dass Unsync dort funktioniert. Es gibt mehrere Bug Reports zu dem Thema (das ist meiner, falls ihr ein paar me too-Kommentare loswerden wollt), aber Microsoft macht bisher nicht den Eindruck, als wäre ein Fix für den finalen IE 11 angedacht. Da dürfen wir wohl auf den IE 12 warten. Bis dahin gilt:

Browser Chrome Firefox Safari Opera iOS Android IE
Unterstützung

Bis dahin könnt ihr Unsync eigentlich in der Pfeife rauchen und dürft weiterhin mit Extra-Dateien für Web Workers jonglieren. Aber immerhin besteht zumindest für eure Enkel Hoffnung auf bequeme asynchrone Funktionen im IE 18 … und das ist doch auch schon mal was, oder?

Internet Explorer, Blob URLs, Web Workers, die Same Origin Policy und der IE-Bugtracker

Veröffentlicht am 9. Oktober 2013

Ich bin nicht der einzige, der sich in den letzten Wochen auf Twitter kritisch über billige IE-Witze geäußert hat. Schließlich scheint Microsoft ja mittlerweile begriffen zu haben, wie man einen Browser baut und die Internet Explorer 10 und 11 haben so viel HTML5 und CSS3 an Bord, dass man auf den ersten Blick vergleichsweise wenig zu meckern findet, gerade wenn man sie mit den diversen Mobile-Browsern vergleicht. Android-Standardbrowser und iOS-Safari sind was die fehlenden Features, zahllosen Bugs und Extravaganzen angeht nicht in einer Liga mit modernen Internet Explorern … jedenfalls quantitativ betrachtet. Was die „Qualität“ der einzelnen Probleme angeht fühlt man sich punktuell auch bei den IE mit zweistelliger Versionsnummer wie früher, zu glorreichen IE6-Zeiten …

Die Same-Origin-Policy (SOP) ist ein Sicherheitskonzept in Browsern, das den Zugriff auf Objekte wie z.B. Scripts nur dann erlaubt, wenn sie dem gleichen Speicherort entspringen. Als Ursprung bzw. Origin gilt die Kombination als Host, Protokoll und Port einer Web-Adresse; Ressourcen von http://foo.com kann http://www.bar.org nicht ohne weiteres verwenden – bei einem entsprechenden Versuch hagelt es Sicherheitsfehler (Details bei Wikipedia). In modernen Browsern lässt sich die SOP punktuell außer Kraft setzen um z.B. API-Verwendung über Domaingrenzen hinweg zu ermöglichen. Und manchmal sollten Browser die SOP auf eigene Faust ignorieren. Und hier fangen die IE-Probleme an.

Die File API von HTML5 erlaubt es, aus dem nichts Dateiressourcen zu erzeugen. Einfach mit new Blob(content, type) ein neues Blob-Objekt anlegen und fertig! Für sich genommen ist ein solches Objekt aber noch ziemlich nutzlos, denn ohne eine URL kann der Browser es außerhalb von JavaScript nicht verwenden. Mit Objekt-URLs lassen sich aber URLs auf beliebige JS-Objekte erzeugen, die dann benutzt werden können wie jede andere URL auch. Der folgende Schnipsel (hier auf jsFiddle) öffnet ein neues Fenster mit einer Textdatei, die es eigentlich gar nicht gibt; sie ist ein reines JavaScript-Konstrukt:

var textFile = new Blob(['Hallo Welt!'], {
  type: 'text/plain'
});
var url = window.URL.createObjectURL(textFile);
window.open(url);

Auffällig an der URL des Popups ist, dass sie einen anderen Origin als die erzeugende Seite zu haben scheint; logisch, denn sie ist ja auch nicht der Seite selbst, sondern einem Script entsprungen:

blob:http%3A//fiddle.jshell.net/753debc6-73cf-4349-a69f-c6c1e477286b

Natürlich wissen alle Browser, dass sie eine solche URL behandeln sollten, als hätte sie den gleichen Origin wie die Seite, die die URL erzeugt hat … es sei denn der Browser ist IE 10 oder 11 und versucht etwas mit Web Workers zu machen. Erzeugt man einen JavaScript-Blob und steckt diesen in den Worker-Constructor hinein, so werfen IE 10 und 11 einen SecurityError. Dieser ist eigentlich für den Fall reserviert, dass die die Constructor-Script-URL von einem fremden Origin kommt – und nicht mal Microsofts eigene Dokumentation behauptet, dass das hier der Fall sein könnte. dieses kleine Testscript produziert in allen modernen Browsern außer den IE 10 und 11 (und natürlich dem Android-Browser, der keine Web Workers kennt) ein Alert, im IE 10 und 11 eine Security-Exception.

Interessant ist, dass Web Workers die einzige API zu sein scheint, bei der dem IE dieses SOP-Fehlurteil unterläuft. Ajax-Requests auf Blob-URLs stellen jedenfalls kein Problem dar. Ob da wohl jede API ihre eigene SOP-Implementierung hat? Man weiß es nicht, aber das ist auch egal. Man soll ja nicht nur rumjammern, sondern sein Schicksal selbst in die Hand nehmen – also auf zum IE-Bugtracker!

Dort stellt man fest, dass dieses Problem schon mehrfach gemeldet, aber auf verschiedenste Weisen von Microsoft und der Gesellschaft zu Förderung von Textbausteinen abgebügelt wurde (1, 2, 3). Aus irgendwelchen Gründen hat mein Bug Report zu dem Thema (wer mehr als eine SOP-Implementierung hat, kann auch mehrere identische Bugreports aushalten) eine Antwort aus Textbausteinen bekommen, die nicht komplett ablehnend sind. Dass es explizit verboten ist, Dateien mit der Erweiterung .html an Bugreports für einen Webbrowser anzuhängen lässt mich zwar ein bisschen an der Ersthaftigkeit der ganzen Bugtracker-Operation zweifeln. Und ein Fix vor dem IE 12 ist natürlich ausgeschlossen. Aber immerhin!

Was ich jetzt gern von euch hätte, wäre dass ihr euch jetzt alle einen Microsoft-Account klickt und mit aller Macht den „betrifft mich ebenfalls“-Link in meinem Bug Report malträtiert. Ich würde erstens gern diesen Bug gefixt sehen, zweitens würde mich stark interessieren, ob wirklich irgendwer bei MS den Bugtracker zur Kenntnis nimmt. Dann wäre es nämlich wirklich mal an der Zeit, mit den billigen IE-Witzen aufzuhören.