HTML5-Techniken zur Optimierung der Leistung auf Mobilgeräten

Wesley Hales
Wesley Hales

Einführung

Sich drehende Aktualisierungen, abgehackte Seitenübergänge und regelmäßige Verzögerungen bei Tippereignissen sind nur einige der Probleme in den heutigen mobilen Webumgebungen. Entwickler versuchen, so nah wie möglich an native Apps heranzukommen, werden aber oft durch Hacks, Zurücksetzungen und starre Frameworks ausgebremst.

In diesem Artikel werden die Mindestanforderungen erläutert, die zum Erstellen einer mobilen HTML5-Web-App erforderlich sind. Der Hauptpunkt besteht darin, die verborgenen Komplexitäten aufzudecken, die die modernen mobilen Frameworks heute zu überwinden versuchen. Sie lernen einen minimalistischen Ansatz (mit den wichtigsten HTML5-APIs) und grundlegende Grundlagen kennen, mit denen Sie Ihr eigenes Framework erstellen oder zu dem Framework beitragen können, das Sie derzeit verwenden.

Hardwarebeschleunigung

Normalerweise verarbeiten GPUs detaillierte 3D-Modelle oder CAD-Diagramme. In diesem Fall sollen unsere primitiven Zeichnungen (Divs, Hintergründe, Text mit Schlagschatten, Bilder usw.) über die GPU flüssig dargestellt und animiert werden. Leider geben die meisten Frontend-Entwickler diesen Animationsvorgang an ein Drittanbieter-Framework weiter, ohne sich um die Semantik zu kümmern. Sollten diese grundlegenden CSS3-Funktionen aber ausgeblendet werden? Ich möchte Ihnen ein paar Gründe nennen, warum Ihnen diese Dinge wichtig sind:

  1. Speicherzuweisung und Rechenlast: Wenn Sie jedes Element im DOM nur zur Hardwarebeschleunigung zusammenstellen, wird die nächste Person, die an Ihrem Code arbeitet, Sie wahrscheinlich jagen und verprügeln.

  2. Stromverbrauch: Wenn die Hardware aktiviert wird, wird auch der Akku belastet. Bei der Entwicklung für Mobilgeräte müssen Entwickler die Vielzahl von Geräteeinschränkungen berücksichtigen, wenn sie mobile Webanwendungen erstellen. Dies wird noch häufiger vorkommen, da Browserhersteller den Zugriff auf immer mehr Gerätehardware ermöglichen.

  3. Konflikte: Wenn ich die Hardwarebeschleunigung auf Teile der Seite anwende, die bereits beschleunigt wurden, treten Fehler auf. Daher ist es sehr wichtig, zu wissen, ob es zu einer überlappenden Beschleunigung kommt.

Damit die Nutzerinteraktion reibungslos und so nah wie möglich an der nativen Anwendung abläuft, müssen wir den Browser so gestalten, dass er für uns funktioniert. Idealerweise soll die CPU des Mobilgeräts die Erstanimation einrichten und die GPU dann nur für das Zusammensetzen verschiedener Ebenen während des Animationsvorgangs verantwortlich sein. Genau das tun „translate3d“, „scale3d“ und „translateZ“ – sie verleihen den animierten Elementen eine eigene Ebene, damit das Gerät alles zusammen reibungslos rendern kann. Weitere Informationen zu beschleunigtem Compositing und zur Funktionsweise von WebKit finden Sie in Ariya Hidayats Blog.

Seitenübergänge

Sehen wir uns drei der häufigsten Ansätze für die Nutzerinteraktion bei der Entwicklung einer mobilen Web-App an: Ziehen, Umklappen und Drehen.

Sie können diesen Code hier in Aktion sehen: http://slidfast.appspot.com/slide-flip-rotate.html. Hinweis: Diese Demo ist für ein Mobilgerät konzipiert. Starten Sie also einen Emulator, verwenden Sie Ihr Smartphone oder Tablet oder verkleinern Sie das Browserfenster auf ca. 1.024 Pixel.

Zuerst sehen wir uns die Übergänge durch Ziehen, Spiegeln und Drehen an und wie sie beschleunigt werden. Beachten Sie, dass für jede Animation nur drei oder vier Zeilen CSS und JavaScript erforderlich sind.

Fließend

Der häufigste der drei Übergänge ahmt das native Erscheinungsbild mobiler Apps nach. Der Folienübergang wird aufgerufen, um einen neuen Inhaltsbereich in den Ansichtsbereich zu bringen.

Für den Schiebeeffekt deklarieren wir zuerst unser Markup:

<div id="home-page" class="page">
  <h1>Home Page</h1>
</div>

<div id="products-page" class="page stage-right">
  <h1>Products Page</h1>
</div>

<div id="about-page" class="page stage-left">
  <h1>About Page</h1>
</div>

Beachten Sie, dass wir die Seiten links oder rechts platzieren. Es kann in jede Richtung gehen, aber diese ist am häufigsten.

Wir haben jetzt Animationen und Hardwarebeschleunigung mit nur wenigen Zeilen CSS. Die eigentliche Animation erfolgt, wenn wir die Klassen in den div-Elementen auf der Seite austauschen.

.page {
  position: absolute;
  width: 100%;
  height: 100%;
  /*activate the GPU for compositing each page */
  -webkit-transform: translate3d(0, 0, 0);
}

translate3d(0,0,0) gilt als Alleinstellungsmerkmal.

Wenn der Nutzer auf ein Navigationselement klickt, wird das folgende JavaScript ausgeführt, um die Klassen zu tauschen. Es werden keine Frameworks von Drittanbietern verwendet, sondern nur JavaScript. ;)

function getElement(id) {
  return document.getElementById(id);
}

function slideTo(id) {
  //1.) the page we are bringing into focus dictates how
  // the current page will exit. So let's see what classes
  // our incoming page is using. We know it will have stage[right|left|etc...]
  var classes = getElement(id).className.split(' ');

  //2.) decide if the incoming page is assigned to right or left
  // (-1 if no match)
  var stageType = classes.indexOf('stage-left');

  //3.) on initial page load focusPage is null, so we need
  // to set the default page which we're currently seeing.
  if (FOCUS_PAGE == null) {
    // use home page
    FOCUS_PAGE = getElement('home-page');
  }

  //4.) decide how this focused page should exit.
  if (stageType > 0) {
    FOCUS_PAGE.className = 'page transition stage-right';
  } else {
    FOCUS_PAGE.className = 'page transition stage-left';
  }

  //5. refresh/set the global variable
  FOCUS_PAGE = getElement(id);

  //6. Bring in the new page.
  FOCUS_PAGE.className = 'page transition stage-center';
}

stage-left oder stage-right wird zu stage-center und die Seite wird in den mittleren Ansichtsbereich verschoben. Wir sind vollständig auf CSS3 angewiesen, um die schwere Arbeit zu erledigen.

.stage-left {
  left: -480px;
}

.stage-right {
  left: 480px;
}

.stage-center {
  top: 0;
  left: 0;
}

Als Nächstes werfen wir einen Blick auf das CSS, das die Erkennung und Ausrichtung von Mobilgeräten übernimmt. Wir könnten jedes Gerät und jede Auflösung ansprechen (siehe Auflösung von Medienabfragen). In dieser Demo habe ich nur einige einfache Beispiele verwendet, um die meisten Hoch- und Querformate auf Mobilgeräten abzudecken. Das ist auch nützlich, um die Hardwarebeschleunigung pro Gerät anzuwenden. Da beispielsweise die Desktopversion von WebKit alle transformierten Elemente beschleunigt (unabhängig davon, ob es sich um 2D oder 3D handelt), ist es sinnvoll, eine Medienabfrage zu erstellen und die Beschleunigung auf dieser Ebene auszuschließen. Hinweis: Hardwarebeschleunigungstricks bieten unter Android Froyo 2.2 und höher keine Geschwindigkeitsverbesserung. Die gesamte Zusammensetzung erfolgt innerhalb der Software.

/* iOS/android phone landscape screen width*/
@media screen and (max-device-width: 480px) and (orientation:landscape) {
  .stage-left {
    left: -480px;
  }

  .stage-right {
    left: 480px;
  }

  .page {
    width: 480px;
  }
}

Spiegeln

Auf Mobilgeräten wird das Umblättern auch als Wischen der Seite bezeichnet. Hier verwenden wir einfaches JavaScript, um dieses Ereignis auf iOS- und Android-Geräten (WebKit-basiert) zu verarbeiten.

In Aktion ansehen: http://slidfast.appspot.com/slide-flip-rotate.html

Wenn Sie mit Touch-Ereignissen und Übergängen arbeiten, sollten Sie zuerst die aktuelle Position des Elements ermitteln. Weitere Informationen zu WebKitCSSMatrix finden Sie in diesem Dokument.

function pageMove(event) {
  // get position after transform
  var curTransform = new WebKitCSSMatrix(window.getComputedStyle(page).webkitTransform);
  var pagePosition = curTransform.m41;
}

Da wir für das Umblättern eine CSS3-Übergangsanimation verwenden, funktioniert die übliche element.offsetLeft nicht.

Als Nächstes möchten wir herausfinden, in welche Richtung die Nutzenden spiegeln, und einen Schwellenwert für ein Ereignis (Seitennavigation) festlegen, das stattfinden soll.

if (pagePosition >= 0) {
 //moving current page to the right
 //so means we're flipping backwards
   if ((pagePosition > pageFlipThreshold) || (swipeTime < swipeThreshold)) {
     //user wants to go backward
     slideDirection = 'right';
   } else {
     slideDirection = null;
   }
} else {
  //current page is sliding to the left
  if ((swipeTime < swipeThreshold) || (pagePosition < pageFlipThreshold)) {
    //user wants to go forward
    slideDirection = 'left';
  } else {
    slideDirection = null;
  }
}

Außerdem wird die swipeTime in Millisekunden gemessen. So kann das Navigationsereignis ausgelöst werden, wenn der Nutzer schnell über den Bildschirm wischt, um eine Seite zu wechseln.

Um die Seite zu positionieren und die Animationen nativ aussehen zu lassen, während ein Finger den Bildschirm berührt, verwenden wir nach jedem Auslösen eines Ereignisses CSS3-Übergänge.

function positionPage(end) {
  page.style.webkitTransform = 'translate3d('+ currentPos + 'px, 0, 0)';
  if (end) {
    page.style.WebkitTransition = 'all .4s ease-out';
    //page.style.WebkitTransition = 'all .4s cubic-bezier(0,.58,.58,1)'
  } else {
    page.style.WebkitTransition = 'all .2s ease-out';
  }
  page.style.WebkitUserSelect = 'none';
}

Ich habe versucht, mit kubischen Bézierkurbeln zu experimentieren, um den Übergängen ein optimales natives Gefühl zu verleihen, aber das ease-out war das Richtige.

Damit die Navigation funktioniert, müssen wir schließlich die zuvor definierten slideTo()-Methoden aufrufen, die wir in der letzten Demo verwendet haben.

track.ontouchend = function(event) {
  pageMove(event);
  if (slideDirection == 'left') {
    slideTo('products-page');
  } else if (slideDirection == 'right') {
    slideTo('home-page');
  }
}

Drehen

Als Nächstes sehen wir uns die Rotationsanimation an, die in dieser Demo verwendet wird. Sie können die Seite, die Sie gerade sehen, jederzeit um 180 Grad drehen, um die Rückseite zu sehen. Tippen Sie dazu auf die Menüoption „Kontakt“. Auch hier sind nur wenige Zeilen CSS und etwas JavaScript erforderlich, um eine Übergangsklasse onclick zuzuweisen. HINWEIS: Die Drehungsübergang wird auf den meisten Android-Versionen nicht richtig gerendert, da keine 3D-CSS-Transformationsfunktionen vorhanden sind. Anstatt die Drehung zu ignorieren, dreht Android die Seite leider weg, indem es sie dreht statt dreht. Wir empfehlen, diese Umstellung sparsam zu verwenden, bis der Support besser wird.

Das Markup (Grundlegendes Konzept von Vorder- und Rückseite):

<div id="front" class="normal">
...
</div>
<div id="back" class="flipped">
    <div id="contact-page" class="page">
        <h1>Contact Page</h1>
    </div>
</div>

Der JavaScript-Code:

function flip(id) {
  // get a handle on the flippable region
  var front = getElement('front');
  var back = getElement('back');

  // again, just a simple way to see what the state is
  var classes = front.className.split(' ');
  var flipped = classes.indexOf('flipped');

  if (flipped >= 0) {
    // already flipped, so return to original
    front.className = 'normal';
    back.className = 'flipped';
    FLIPPED = false;
  } else {
    // do the flip
    front.className = 'flipped';
    back.className = 'normal';
    FLIPPED = true;
  }
}

Das Preisvergleichsportal:

/*----------------------------flip transition */
#back,
#front {
  position: absolute;
  width: 100%;
  height: 100%;
  -webkit-backface-visibility: hidden;
  -webkit-transition-duration: .5s;
  -webkit-transform-style: preserve-3d;
}

.normal {
  -webkit-transform: rotateY(0deg);
}

.flipped {
  -webkit-user-select: element;
  -webkit-transform: rotateY(180deg);
}

Hardwarebeschleunigung beheben

Nachdem wir uns mit den grundlegenden Übergängen vertraut gemacht haben, sehen wir uns an, wie sie funktionieren und zusammengesetzt werden.

Damit diese magische Debugging-Sitzung stattfinden kann, starten wir einige Browser und Ihre bevorzugte IDE. Starten Sie Safari zuerst über die Befehlszeile, um einige Umgebungsvariablen für die Fehlerbehebung zu verwenden. Ich verwende einen Mac, die Befehle können sich je nach Betriebssystem unterscheiden. Öffnen Sie das Terminal und geben Sie Folgendes ein:

  • $> export CA_COLOR_OPAQUE=1
  • $> export CA_LOG_MEMORY_USAGE=1
  • $> /Applications/Safari.app/Contents/MacOS/Safari

Dadurch wird Safari mit einigen Debugging-Hilfsmitteln gestartet. CA_COLOR_OPAQUE gibt an, welche Elemente tatsächlich zusammengesetzt oder beschleunigt werden. CA_LOG_MEMORY_USAGE gibt an, wie viel Speicher beim Senden der Zeichenvorgänge an den Sicherungsspeicher verwendet wird. Daran erkennst du genau, wie stark das Mobilgerät belastet wird, und gibt möglicherweise Hinweise darauf, wie sich die GPU-Nutzung auf den Akku des Zielgeräts auswirken könnte.

Öffnen wir jetzt Chrome, damit wir uns die Informationen zu den Bildern pro Sekunde (FPS) ansehen können:

  1. Öffnen Sie den Google Chrome-Webbrowser.
  2. Geben Sie in der URL-Leiste about:flags ein.
  3. Scrollen Sie ein paar Elemente nach unten und klicken Sie bei „FPS-Zähler“ auf „Aktivieren“.

Wenn du diese Seite in der optimierten Version von Chrome aufrufst, wird oben links der rote FPS-Zähler angezeigt.

fps für Chrome

So wissen wir, dass die Hardwarebeschleunigung aktiviert ist. Außerdem erhalten wir so einen Eindruck davon, wie die Animation abläuft und ob es Lecks gibt (fortlaufend laufende Animationen, die beendet werden sollten).

Eine weitere Möglichkeit, die Hardwarebeschleunigung zu visualisieren, besteht darin, dieselbe Seite in Safari mit den oben genannten Umgebungsvariablen zu öffnen. Jedes beschleunigte DOM-Element hat einen roten Farbton. So sehen wir genau, was auf Ebenen zusammengesetzt wird. Die weiße Navigation ist nicht rot, da sie nicht beschleunigt wird.

Zusammengesetzter Kontakt

Eine ähnliche Einstellung für Chrome ist auch unter about:flags unter „Composited render layer borders“ verfügbar.

Eine weitere gute Möglichkeit, die zusammengesetzten Ebenen zu sehen, ist die WebKit-Demo zu Herbstblättern, während dieser Mod angewendet wird.

gemischte Blätter

Und schließlich sehen wir uns an, wie der Arbeitsspeicher genutzt wird, um die Leistung der Grafikhardware unserer Anwendung wirklich zu verstehen. Hier sehen wir, dass wir 1,38 MB Zeichnungsanweisungen an die CoreAnimation-Puffer unter Mac OS senden. Die Core Animation-Speicherpuffer werden von OpenGL ES und der GPU gemeinsam genutzt, um die endgültigen Pixel zu erzeugen, die auf dem Bildschirm zu sehen sind.

Coreanimation 1

Wenn wir die Größe des Browserfensters einfach ändern oder maximieren, sehen wir, dass sich auch der Arbeitsspeicher erhöht.

CoreAnimation 2

Sie erhalten nur dann einen Eindruck davon, wie viel Arbeitsspeicher auf Ihrem Mobilgerät belegt ist, wenn Sie die Größe des Browsers auf die richtigen Abmessungen anpassen. Wenn Sie die App für iPhone-Umgebungen debuggen oder testen, ändern Sie die Größe in 480 x 320 Pixel. Wir wissen jetzt genau, wie die Hardwarebeschleunigung funktioniert und was für die Fehlerbehebung erforderlich ist. Es ist eine Sache, darüber zu lesen, aber wenn Sie die GPU-Arbeitsspeicher-Buffer tatsächlich in Aktion sehen, bekommen Sie ein besseres Gefühl dafür.

Hinter den Kulissen: Abrufen und Caching

Jetzt ist es an der Zeit, das Seiten- und Ressourcen-Caching auf die nächste Stufe zu heben. Ähnlich wie beim Ansatz, den JQuery Mobile und ähnliche Frameworks verwenden, rufen wir unsere Seiten mit gleichzeitigen AJAX-Aufrufen im Voraus ab und speichern sie im Cache.

Sehen wir uns einige der wichtigsten Probleme im mobilen Web und die Gründe dafür an:

  • Abrufen: Durch das Vorab-Abrufen unserer Seiten können Nutzer die App offline verwenden und müssen nicht zwischen Navigationsaktionen warten. Natürlich möchten wir die Bandbreite des Geräts nicht überlasten, wenn es online geht. Daher müssen wir diese Funktion sparsam einsetzen.
  • Caching: Als Nächstes benötigen wir einen parallelen oder asynchronen Ansatz beim Abrufen und Caching dieser Seiten. Wir müssen auch localStorage verwenden, da es von den Geräten gut unterstützt wird. Leider ist es nicht asynchron.
  • AJAX und Parsen der Antwort: Das Einfügen der AJAX-Antwort in das DOM mit innerHTML() ist gefährlich (und unzuverlässig?). Stattdessen verwenden wir einen zuverlässigen Mechanismus für das Einfügen von AJAX-Antworten und die Verarbeitung paralleler Aufrufe. Außerdem nutzen wir einige neue Funktionen von HTML5, um die xhr.responseText zu parsen.

Aufbauend auf dem Code aus der Demo zum Verschieben, Umdrehen und Drehen fügen wir zunächst einige sekundäre Seiten hinzu und verlinken sie. Wir analysieren dann die Links und erstellen Übergänge im Handumdrehen.

iPhone-Startseite

Demo zum Abrufen und Cache ansehen

Wie Sie sehen, nutzen wir hier semantisches Markup. Nur ein Link zu einer anderen Seite. Die untergeordnete Seite folgt derselben Knoten-/Klassenstruktur wie die übergeordnete Seite. Wir könnten noch einen Schritt weitergehen und das Attribut „data-*“ für „Seite“-Knoten usw. verwenden. Im Folgenden sehen Sie die Detailseite (untergeordnet), die sich in einer separaten HTML-Datei (/demo2/home-detail.html) befindet, die beim Laden der App geladen, im Cache gespeichert und für die Umstellung eingerichtet wird.

<div id="home-page" class="page">
  <h1>Home Page</h1>
  <a href="demo2/home-detail.html" class="fetch">Find out more about the home page!</a>
</div>

Sehen wir uns nun das JavaScript an. Der Einfachheit halber lasse ich alle Hilfsfunktionen oder Optimierungen in den Code weg. Wir durchlaufen hier lediglich eine Schleife durch ein festgelegtes Array von DOM-Knoten, um Links zu finden, die abgerufen und im Cache gespeichert werden sollen. Hinweis: In dieser Demo wird diese Methode fetchAndCache() beim Laden der Seite aufgerufen. Wir überarbeiten sie im nächsten Abschnitt, wenn wir die Netzwerkverbindung erkennen und festlegen, wann sie aufgerufen werden soll.

var fetchAndCache = function() {
  // iterate through all nodes in this DOM to find all mobile pages we care about
  var pages = document.getElementsByClassName('page');

  for (var i = 0; i < pages.length; i++) {
    // find all links
    var pageLinks = pages[i].getElementsByTagName('a');

    for (var j = 0; j < pageLinks.length; j++) {
      var link = pageLinks[j];

      if (link.hasAttribute('href') &amp;&amp;
      //'#' in the href tells us that this page is already loaded in the DOM - and
      // that it links to a mobile transition/page
         !(/[\#]/g).test(link.href) &amp;&amp;
        //check for an explicit class name setting to fetch this link
        (link.className.indexOf('fetch') >= 0))  {
         //fetch each url concurrently
         var ai = new ajax(link,function(text,url){
              //insert the new mobile page into the DOM
             insertPages(text,url);
         });
         ai.doGet();
      }
    }
  }
};

Durch die Verwendung des „AJAX“-Objekts wird eine ordnungsgemäße asynchrone Nachverarbeitung sichergestellt. Eine ausführlichere Erklärung zur Verwendung von localStorage in einem AJAX-Aufruf finden Sie unter Offline mit HTML5 arbeiten. In diesem Beispiel sehen Sie die grundlegende Verwendung des Cachings bei jeder Anfrage und die Bereitstellung der im Cache gespeicherten Objekte, wenn der Server eine andere Antwort als eine erfolgreiche Antwort (200) zurückgibt.

function processRequest () {
  if (req.readyState == 4) {
    if (req.status == 200) {
      if (supports_local_storage()) {
        localStorage[url] = req.responseText;
      }
      if (callback) callback(req.responseText,url);
    } else {
      // There is an error of some kind, use our cached copy (if available).
      if (!!localStorage[url]) {
        // We have some data cached, return that to the callback.
        callback(localStorage[url],url);
        return;
      }
    }
  }
}

Da localStorage jedoch UTF-16 für die Zeichencodierung verwendet, wird jedes einzelne Byte als 2 Byte gespeichert. Das Speicherlimit wird daher von 5 MB auf insgesamt 2,6 MB reduziert. Im nächsten Abschnitt wird erläutert, warum diese Seiten/Markups außerhalb des Anwendungscaches abgerufen und im Cache gespeichert werden.

Dank der jüngsten Fortschritte beim Iframe-Element mit HTML5 haben wir jetzt eine einfache und effektive Möglichkeit, die responseText zu parsen, die wir von unserem AJAX-Aufruf zurückbekommen. Es gibt viele JavaScript-Parser mit 3.000 Zeilen und reguläre Ausdrücke, mit denen Script-Tags entfernt werden. Aber warum sollten Sie nicht den Browser das tun lassen, was er am besten kann? In diesem Beispiel schreiben wir das responseText in einen temporären, versteckten iFrame. Wir verwenden das HTML5-Attribut „sandbox“, das Scripts deaktiviert und viele Sicherheitsfunktionen bietet…

Aus der Spezifikation: Wenn das Sandbox-Attribut angegeben ist, werden zusätzliche Einschränkungen für alle Inhalte aktiviert, die vom Iframe gehostet werden. Der Wert muss eine unsortierte Reihe eindeutiger, durch Leerzeichen getrennter ASCII-Tokens sein, bei denen die Groß- und Kleinschreibung nicht beachtet wird. Zulässige Werte sind „allow-forms“, „allow-same-origin“, „allow-scripts“ und „allow-top-navigation“. Wenn das Attribut festgelegt ist, werden die Inhalte als aus einer einzelnen Quelle stammend behandelt, Formulare und Scripts werden deaktiviert, Links können nicht auf andere Browserkontexte ausgerichtet werden und Plug-ins werden deaktiviert.

var insertPages = function(text, originalLink) {
  var frame = getFrame();
  //write the ajax response text to the frame and let
  //the browser do the work
  frame.write(text);

  //now we have a DOM to work with
  var incomingPages = frame.getElementsByClassName('page');

  var pageCount = incomingPages.length;
  for (var i = 0; i < pageCount; i++) {
    //the new page will always be at index 0 because
    //the last one just got popped off the stack with appendChild (below)
    var newPage = incomingPages[0];

    //stage the new pages to the left by default
    newPage.className = 'page stage-left';

    //find out where to insert
    var location = newPage.parentNode.id == 'back' ? 'back' : 'front';

    try {
      // mobile safari will not allow nodes to be transferred from one DOM to another so
      // we must use adoptNode()
      document.getElementById(location).appendChild(document.adoptNode(newPage));
    } catch(e) {
      // todo graceful degradation?
    }
  }
};

Safari lehnt es korrekterweise ab, einen Knoten implizit von einem Dokument in ein anderes zu verschieben. Wenn der neue untergeordnete Knoten in einem anderen Dokument erstellt wurde, wird ein Fehler ausgegeben. Hier verwenden wir adoptNode und alles ist in Ordnung.

Warum also iFrames? Warum nicht einfach innerHTML verwenden? Auch wenn innerHTML jetzt Teil der HTML5-Spezifikation ist, ist es gefährlich, die Antwort eines Servers (böswillig oder gut) in einen nicht überprüften Bereich einzufügen. Als ich diesen Artikel verfasste, konnte ich keinen finden, der etwas anderes als innerHTML verwendet hat. Ich weiß, dass JQuery es im Grunde verwendet, wobei nur bei einer Ausnahme ein Append-Fallback verwendet wird. Auch JQuery Mobile verwendet diese Funktion. Ich habe jedoch keine umfassenden Tests bezüglich innerHTML durchgeführt, wenn es „zufällig aufhört zu funktionieren“. Es wäre aber sehr interessant, alle Plattformen zu sehen, auf die sich das auswirkt. Es wäre auch interessant zu sehen, welcher Ansatz am besten funktioniert... Ich habe auch Aussagen von beiden Seiten dazu gehört.

Erkennung, Verarbeitung und Profilerstellung des Netzwerktyps

Da wir nun die Möglichkeit haben, unsere Web-App zu puffern (bzw. den Cache für Vorhersagen) zu speichern, müssen wir die entsprechenden Funktionen zur Verbindungserkennung bereitstellen, die unsere App intelligenter machen. Hier ist die Entwicklung mobiler Apps extrem empfindlich gegenüber Online-/Offlinemodi und Verbindungsgeschwindigkeit. Geben Sie The Network Information API ein. Jedes Mal, wenn ich diese Funktion in einer Präsentation zeige, hebt ein Teilnehmer die Hand und fragt: „Wofür könnte ich das verwenden?“ Hier ist also eine Möglichkeit, eine äußerst intelligente mobile Web-App einzurichten.

Zuerst ein langweiliges, aber naheliegendes Szenario: Wenn Sie in einem Hochgeschwindigkeitszug mit einem Mobilgerät im Web surfen, kann die Verbindung zu verschiedenen Zeitpunkten unterbrochen werden. Außerdem können in verschiedenen Regionen unterschiedliche Übertragungsgeschwindigkeiten unterstützt werden (z. B. HSPA oder 3G sind möglicherweise in einigen städtischen Gebieten verfügbar, in abgelegenen Gegenden werden jedoch möglicherweise viel langsamere 2G-Technologien unterstützt. Der folgende Code deckt die meisten Verbindungsszenarien ab.

Der folgende Code bietet Folgendes:

  • Offlinezugriff über applicationCache
  • Erkennt, ob eine Seite als Lesezeichen gespeichert und offline verfügbar ist.
  • Erkennt, wenn Sie von offline zu online und umgekehrt wechseln.
  • Erkennt langsame Verbindungen und ruft Inhalte basierend auf dem Netzwerktyp ab.

Auch für diese Funktionen ist nur sehr wenig Code erforderlich. Zuerst ermitteln wir unsere Ereignisse und Ladeszenarien:

window.addEventListener('load', function(e) {
 if (navigator.onLine) {
  // new page load
  processOnline();
 } else {
   // the app is probably already cached and (maybe) bookmarked...
   processOffline();
 }
}, false);

window.addEventListener("offline", function(e) {
  // we just lost our connection and entered offline mode, disable eternal link
  processOffline(e.type);
}, false);

window.addEventListener("online", function(e) {
  // just came back online, enable links
  processOnline(e.type);
}, false);

In den oben aufgeführten EventListenern muss der Code angegeben werden, ob er von einem Ereignis oder von einer tatsächlichen Seitenanforderung oder -aktualisierung aus aufgerufen wird. Das liegt hauptsächlich daran, dass das Ereignis „body“ onload nicht ausgelöst wird, wenn zwischen dem Online- und dem Offlinemodus gewechselt wird.

Als Nächstes folgt eine einfache Prüfung auf ein ononline- oder onload-Ereignis. Mit diesem Code werden deaktivierte Links zurückgesetzt, wenn von Offline zu Online gewechselt wird. Wenn diese App jedoch ausgefeilter ist, könnten Sie eine Logik einfügen, die das Abrufen von Inhalten fortgesetzt oder die UX bei unzuverlässigen Verbindungen verarbeitet.

function processOnline(eventType) {

  setupApp();
  checkAppCache();

  // reset our once disabled offline links
  if (eventType) {
    for (var i = 0; i < disabledLinks.length; i++) {
      disabledLinks[i].onclick = null;
    }
  }
}

Dasselbe gilt für processOffline(). Hier würden Sie Ihre App für den Offlinemodus konfigurieren und versuchen, alle Transaktionen wiederherzustellen, die im Hintergrund ausgeführt wurden. Mit dem folgenden Code werden alle unsere externen Links herausgefunden und deaktiviert. So werden die Nutzer in unserer Offline-App für immer gesperrt.

function processOffline() {
  setupApp();

  // disable external links until we come back - setting the bounds of app
  disabledLinks = getUnconvertedLinks(document);

  // helper for onlcick below
  var onclickHelper = function(e) {
    return function(f) {
      alert('This app is currently offline and cannot access the hotness');return false;
    }
  };

  for (var i = 0; i < disabledLinks.length; i++) {
    if (disabledLinks[i].onclick == null) {
      //alert user we're not online
      disabledLinks[i].onclick = onclickHelper(disabledLinks[i].href);

    }
  }
}

Okay, kommen wir zu den guten Neuigkeiten. Da unsere App jetzt weiß, in welchem Verbindungsstatus sie sich befindet, können wir auch die Art der Verbindung prüfen, wenn sie online ist, und sie entsprechend anpassen. In den Kommentaren zu jeder Verbindung habe ich die Download- und Latenzzeiten typischer nordamerikanischer Anbieter aufgeführt.

function setupApp(){
  // create a custom object if navigator.connection isn't available
  var connection = navigator.connection || {'type':'0'};
  if (connection.type == 2 || connection.type == 1) {
      //wifi/ethernet
      //Coffee Wifi latency: ~75ms-200ms
      //Home Wifi latency: ~25-35ms
      //Coffee Wifi DL speed: ~550kbps-650kbps
      //Home Wifi DL speed: ~1000kbps-2000kbps
      fetchAndCache(true);
  } else if (connection.type == 3) {
  //edge
      //ATT Edge latency: ~400-600ms
      //ATT Edge DL speed: ~2-10kbps
      fetchAndCache(false);
  } else if (connection.type == 2) {
      //3g
      //ATT 3G latency: ~400ms
      //Verizon 3G latency: ~150-250ms
      //ATT 3G DL speed: ~60-100kbps
      //Verizon 3G DL speed: ~20-70kbps
      fetchAndCache(false);
  } else {
  //unknown
      fetchAndCache(true);
  }
}

Wir könnten an unserem Prozess „fetchAndCache“ zahlreiche Anpassungen vornehmen, aber ich habe hier nur angegeben, dass die Ressourcen für eine bestimmte Verbindung asynchron (wahr) oder synchron (falsch) abgerufen werden sollen.

Zeitachse für Edge-Anfragen (synchron)

Edge Sync

Zeitachse für WIFI-Anfragen (asynchron)

WIFI Async

So lässt sich die Nutzererfahrung zumindest teilweise an langsame oder schnelle Verbindungen anpassen. Dies ist keine Allroundlösung. Ein weiteres To-do wäre, bei langsamen Verbindungen ein Modal-Fenster für das Laden einzublenden, während die App die Seite des Links im Hintergrund abruft. Ziel ist es, die Latenz zu verringern und gleichzeitig die volle Bandbreite der Nutzerverbindung mit den neuesten HTML5-Technologien zu nutzen. Demo zur Netzwerkerkennung ansehen

Fazit

Die Entwicklung mobiler HTML5-Apps steht erst am Anfang. Jetzt kennen Sie die einfachen und grundlegenden Grundlagen eines mobilen "Frameworks", das ausschließlich auf HTML5 und den unterstützenden Technologien basiert. Ich denke, dass es für Entwickler wichtig ist, mit diesen Funktionen zu arbeiten und diese zu bewältigen, und nicht durch einen Wrapper verdeckt zu werden.