HTML5-Techniken zur Optimierung der Leistung auf Mobilgeräten

Wesley Hales
Wesley Hales

Einführung

Sich drehende Aktualisierungen, ruckelige Seitenübergänge und regelmäßige Verzögerungen bei Tipp-Ereignissen sind nur einige der Kopfschmerzen von heutigen mobilen Webumgebungen. Entwickler versuchen, so nah wie möglich an native Anzeigen zu kommen. Dabei lassen sie sich aber oft durch Hacks, Zurücksetzen und starre Frameworks entmutigen.

In diesem Artikel erfahren Sie, wie Sie eine mobile HTML5-Web-App erstellen können. Dabei geht es vor allem darum, die komplexen Komplexitäten aufzudecken, die mit modernen Frameworks für Mobilgeräte zu erkennen sind. Sie lernen einen minimalistischen Ansatz (mit HTML5-Kern-APIs) und grundlegende Grundlagen kennen, mit denen Sie Ihr eigenes Framework erstellen oder zu Ihrem aktuell verwendeten Framework beitragen können.

Hardwarebeschleunigung

Normalerweise verarbeiten GPUs detaillierte 3D-Modelle oder CAD-Diagramme. In diesem Fall möchten wir jedoch, dass unsere einfachen Zeichnungen (divs, Hintergründe, Text mit Schlagschatten, Bilder usw.) über die GPU flüssig erscheinen und animiert werden. Leider überlassen die meisten Front-End-Entwickler diesen Animationsprozess einem Drittanbieter-Framework, ohne sich um die Semantik zu kümmern. Aber sollten diese zentralen CSS3-Funktionen maskiert werden? Ich möchte Ihnen ein paar Gründe nennen, warum Ihnen diese Sache wichtig ist:

  1. Arbeitsspeicherzuweisung und Rechenaufwand: Wenn Sie die einzelnen Elemente im DOM nur zugunsten der Hardwarebeschleunigung zusammensetzen müssen, kann die nächste Person, die an Ihrem Code arbeitet, Sie jagen und Sie möglicherweise gewaltig schlagen.

  2. Stromverbrauch: Wenn die Hardware eingeschaltet wird, steigt natürlich auch der Akku. Bei der Entwicklung für mobile Apps müssen Entwickler beim Entwickeln von mobilen Web-Apps eine Vielzahl von Geräteeinschränkungen berücksichtigen. Dies wird sich noch verstärken, da Browser-Hersteller den Zugriff auf immer mehr Gerätehardware ermöglichen.

  3. Konflikte: Ich habe beim Anwenden der Hardwarebeschleunigung auf Teile der Seite, die bereits beschleunigt wurden, ein Störverhalten festgestellt. Es ist also sehr wichtig, zu wissen, ob sich die Beschleunigung überschneiden.

Damit die Nutzerinteraktion so einfach wie möglich und so reibungslos wie möglich verläuft, muss der Browser für uns funktionieren. Im Idealfall sollte die CPU des Mobilgeräts die anfängliche Animation einrichten und dann die GPU nur für die Erstellung verschiedener Ebenen während des Animationsprozesses verantwortlich machen. Genau das tun die animierten Elemente: „translate3d“, „scale3d“ und „translateZ“: Sie fügen den animierten Elementen eine eigene Ebene hinzu, sodass das Gerät alles reibungslos zusammen rendern kann. Um mehr über Accelerated Compositing und die Funktionsweise von WebKit zu erfahren, hat Ariya Hidayat viele nützliche Informationen in seinem Blog zusammengestellt.

Seitenübergänge

Sehen wir uns drei der häufigsten Ansätze zur Nutzerinteraktion bei der Entwicklung einer mobilen Web-App an: Schiebe-, Spiegel- und Rotationseffekte.

Sie können diesen Code hier in Aktion sehen: http://slidfast.appspot.com/slide-flip-rotate.html (Hinweis: Diese Demo wurde für ein Mobilgerät entwickelt. Starten Sie daher einen Emulator, verwenden Sie Ihr Telefon oder Tablet oder verringern Sie die Größe des Browserfensters auf ca. 1024 Pixel.)

Zunächst sehen wir uns die Übergänge beim Schieben, Umblättern und Drehen sowie deren Beschleunigung an. Beachten Sie, dass jede Animation nur drei oder vier Zeilen CSS- und JavaScript-Code benötigt.

Schiebefenster

Der häufigste der drei Übergangsansätze ist das Verschieben von Seitenübergängen, das der nativen Bedienung mobiler Apps nachahmt. Der Folienübergang wird aufgerufen, um einen neuen Inhaltsbereich in den Darstellungsbereich zu bringen.

Für den Folieneffekt 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 das Konzept, bei dem Seiten nach links oder rechts angeordnet werden. Es kann sich auch um eine beliebige Richtung handeln, aber diese trifft am häufigsten zu.

Mit nur wenigen CSS-Zeilen stehen jetzt Animation und Hardwarebeschleunigung zur Verfügung. Die eigentliche Animation findet statt, wenn die Klassen in den page-div-Elementen ausgetauscht werden.

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

Der translate3d(0,0,0) ist als Patentlösung bekannt.

Wenn der Nutzer auf ein Navigationselement klickt, führen wir den folgenden JavaScript-Code aus, um die Klassen auszutauschen. Es werden keine Frameworks von Drittanbietern verwendet. Es handelt sich um 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. Dadurch wird die Seite in den mittleren Darstellungsbereich verschoben. Für den Großteil der Arbeit sind wir voll und ganz auf CSS3 angewiesen.

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

.stage-right {
  left: 480px;
}

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

Als Nächstes werfen wir einen Blick auf den CSS-Code, der für die Erkennung und Ausrichtung von Mobilgeräten zuständig ist. Wir könnten jedes Gerät und jede Auflösung berücksichtigen (siehe Auflösung von Medienabfragen). In dieser Demo habe ich nur einige einfache Beispiele verwendet, um die meisten Hoch- und Querformatansichten auf Mobilgeräten darzustellen. Dies ist auch nützlich, um die Hardwarebeschleunigung pro Gerät anzuwenden. Da beispielsweise die Desktop-Version 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. Beachte, dass Tricks zur Hardwarebeschleunigung unter Android Froyo 2.2 und höher keine Geschwindigkeitsverbesserung zur Folge haben. Alle Zusammensetzungen werden in der Software vorgenommen.

/* 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 Wegwischen der Seite bezeichnet. Hier verwenden wir einfaches JavaScript, um dieses Ereignis auf iOS- und Android-Geräten (WebKit-basiert) zu verarbeiten.

Sehen Sie es sich in Aktion an: http://slidfast.appspot.com/slide-flip-rotate.html.

Wenn Sie Touch-Ereignisse und -Übergänge bearbeiten möchten, sollten Sie als Erstes einen Griff zur aktuellen Position des Elements erstellen. Weitere Informationen zu WebKitCSSMatrix findest du in diesem Dokument.

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

Da wir beim Umblättern der Seite einen CSS3-"ease-out"-Übergang verwenden, funktioniert element.offsetLeft nicht.

Als Nächstes möchten wir herausfinden, in welche Richtung die Nutzenden blättern, und einen Grenzwert für ein Ereignis (Seitennavigation) festlegen.

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 messen wir den swipeTime in Millisekunden. So kann das Navigationsereignis ausgelöst werden, wenn der Nutzer schnell über den Bildschirm wischt, um eine Seite umzublättern.

Um die Seite zu positionieren und die Animationen nativ aussehen zu lassen, während ein Finger den Bildschirm berührt, verwenden wir CSS3-Übergänge, nachdem jedes Ereignis ausgelöst wurde.

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 der kubischen Bézierkurve zu experimentieren, um den Übergängen ein optimales natives Gefühl zu geben, aber mit „ease-out“ gelang es ihnen.

Für die Navigation 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

Sehen wir uns als Nächstes die Animation zum Drehen an, die in dieser Demo verwendet wird. Sie können die angezeigte Seite jederzeit um 180 Grad drehen, um die Rückseite anzuzeigen. Tippen Sie dazu auf die Menüoption „Kontakt“. Auch hier sind nur einige CSS- und JavaScript-Zeilen erforderlich, um die Übergangsklasse onclick zuzuweisen. HINWEIS: Der Übergang zum Drehen wird in den meisten Android-Versionen nicht korrekt gerendert, da er keine 3D-CSS-Transformationsfunktionen bietet. Anstatt das Umblättern zu ignorieren, lenkt Android die Seite leider weg, indem sie sich dreht, statt sie zu drehen. Wir empfehlen, diese Umstellung sparsam zu verwenden, bis die Unterstützung besser ist.

Markup (Grundkonzept: 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 CSS:

/*----------------------------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);
}

Fehlerbehebung bei der Hardwarebeschleunigung

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

Um diese Fehlerbehebung durchzuführen, starten wir ein paar Browser und eine IDE. Starten Sie Safari zunächst über die Befehlszeile, um einige Debugging-Umgebungsvariablen zu verwenden. Ich verwende einen Mac. Die Befehle können daher je nach Betriebssystem variieren. Öffnen Sie das Terminal und geben Sie Folgendes ein:

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

Dadurch wird Safari mit einer Reihe von Debugging-Hilfsprogrammen gestartet. CA_COLOR_OPAQUE zeigt uns, welche Elemente tatsächlich zusammengesetzt oder beschleunigt werden. CA_LOG_MEMORY_USAGE zeigt an, wie viel Speicher beim Senden unserer Zeichenvorgänge an den Sicherungsspeicher verwendet wird. So erfahren Sie genau, wie stark Sie das Mobilgerät belasten, und geben möglicherweise Hinweise darauf, wie Ihre GPU-Nutzung den Akku des Zielgeräts belasten könnte.

Lassen Sie uns Chrome starten, um einige Informationen zu guten Bildern pro Sekunde (FPS) zu erhalten:

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

Wenn Sie diese Seite in Ihrer aktuellen Chrome-Version aufrufen, sehen Sie links oben den roten FPS-Zähler.

FPS von Chrome

So wissen wir, dass die Hardwarebeschleunigung aktiviert ist. Außerdem erfahren wir, wie die Animation läuft und ob fortlaufende Animationen vorhanden sind, die angehalten werden sollten.

Eine andere Möglichkeit, die Hardwarebeschleunigung zu visualisieren, besteht darin, dieselbe Seite in Safari zu öffnen (mit den oben erwähnten Umgebungsvariablen). Jedes beschleunigte DOM-Element hat eine rote Färbung. So sehen wir genau, was aus den Ebenen zusammengesetzt wird. Beachten Sie, dass die weiße Navigation nicht rot ist, weil sie nicht beschleunigt wird.

Kombinierter Kontakt

Eine ähnliche Einstellung für Chrome ist unter about:flags „Rahmen für zusammengesetzte Renderingebenen“ verfügbar.

Eine weitere gute Möglichkeit, sich die zusammengesetzten Ebenen anzusehen, ist die WebKit-Demo für Herbstblätter, während dieser Mod angewendet wird.

Blätter

Um schließlich die Grafikhardware-Leistung unserer Anwendung wirklich zu verstehen, sehen wir uns den Speicherverbrauch genauer an. Hier sehen wir, dass unter Mac OS 1,38 MB Zeichenanweisungen in die CoreAnimation-Puffer geladen werden. Die Core Animation-Zwischenspeicher werden von OpenGL ES und der GPU gemeinsam genutzt, um die endgültigen Pixel zu erzeugen, die auf dem Bildschirm zu sehen sind.

Core Animation 1

Wenn wir die Größe des Browserfensters einfach anpassen oder es maximieren, vergrößert sich auch der Speicher.

Core Animation 2

So erhalten Sie nur dann eine Vorstellung davon, wie viel Arbeitsspeicher auf Ihrem Mobilgerät verbraucht wird, wenn Sie die Größe des Browsers entsprechend anpassen. Beim Debugging oder Testen für iPhone-Umgebungen ändern Sie die Größe auf 480 x 320 Pixel. Wir wissen jetzt genau, wie die Hardwarebeschleunigung funktioniert und wie sich Fehler beheben lassen. Es ist eine Sache, darüber zu lesen, aber wenn man die GPU-Arbeitsspeicherpuffer tatsächlich anschaut, sieht man die Dinge wirklich aus einer anderen Perspektive.

Hinter den Kulissen: Abrufen und Caching

Nun ist es an der Zeit, das Caching unserer Seiten und Ressourcen zu optimieren. Ähnlich wie bei dem Ansatz, den JQuery Mobile und ähnliche Frameworks verwenden, rufen wir unsere Seiten mit gleichzeitigen AJAX-Aufrufen vorab ab und speichern sie im Cache.

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

  • Abruf: Durch das Vorabrufen 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 behindern, wenn es online geht. Deshalb müssen wir diese Funktion sparsam einsetzen.
  • Caching: Als Nächstes wollen wir einen gleichzeitigen oder asynchronen Ansatz beim Abrufen und Caching dieser Seiten. Wir müssen auch localStorage verwenden (da es von vielen Geräten gut unterstützt wird), was leider nicht asynchron ist.
  • AJAX und Parsen der Antwort: Die Verwendung von innerHTML() zum Einfügen der AJAX-Antwort in das DOM ist gefährlich (und unzuverlässig?). Wir verwenden stattdessen einen zuverlässigen Mechanismus für das Einfügen von AJAX-Antworten und die Verarbeitung gleichzeitiger Aufrufe. Außerdem nutzen wir einige neue Funktionen von HTML5 zum Parsen von xhr.responseText.

Aufbauend auf dem Code aus der Demo zum Verschieben, Umblättern und Drehen fügen wir zunächst einige sekundäre Seiten hinzu und verlinken sie. Anschließend parsen wir die Links und erstellen spontan Übergänge.

iPhone-Startseite

Sehen Sie sich hier die Demo zum Abrufen und Cache an.

Wie Sie sehen, nutzen wir hier semantisches Markup. Nur ein Link zu einer anderen Seite. Die untergeordnete Seite hat dieselbe Knoten-/Klassenstruktur wie ihre übergeordnete Seite. Wir könnten noch einen Schritt weiter gehen und das data-*-Attribut für „page“-Knoten usw. verwenden. Hier sehen Sie die Detailseite (untergeordnet) in einer separaten HTML-Datei (/demo2/home-detail.html), die geladen, im Cache gespeichert und für die Umstellung beim Laden der App 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>

Werfen wir nun einen Blick auf das JavaScript. Der Einfachheit halber lasse ich Hilfestellungen oder Optimierungen im Code weg. Wir durchlaufen hier lediglich ein bestimmtes Array von DOM-Knoten als Schleifen, um Links zu finden, die abgerufen und im Cache gespeichert werden sollen. Hinweis: In dieser Demo wird die Methode fetchAndCache() beim Seitenaufbau aufgerufen. Im nächsten Abschnitt überarbeiten wir dies, wenn wir die Netzwerkverbindung erkennen und bestimmen, 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();
      }
    }
  }
};

Wir gewährleisten eine ordnungsgemäße asynchrone Nachbearbeitung durch die Verwendung des "AJAX"-Objekts. Eine ausführlichere Erklärung zur Verwendung von localStorage in einem AJAX-Aufruf finden Sie unter Offline mit HTML5 offline arbeiten. In diesem Beispiel sehen Sie die grundlegende Verwendung des Cachings bei jeder Anfrage und der Bereitstellung der im Cache gespeicherten Objekte, wenn der Server alles außer einer erfolgreichen 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 für die Zeichencodierung UTF-16 verwendet, wird jedes einzelne Byte leider als 2 Byte gespeichert.Dadurch erhöht sich unser Speicherlimit von 5 MB auf insgesamt 2, 6 MB. Der Grund dafür, warum diese Seiten bzw. dieses Markup außerhalb des Cache-Bereichs der Anwendung abgerufen und im Cache gespeichert werden, wird im nächsten Abschnitt erläutert.

Dank der jüngsten Fortschritte beim iFrame-Element bei HTML5 haben wir nun eine einfache und effektive Möglichkeit zum Parsen des responseText, das wir von unserem AJAX-Aufruf erhalten. Es gibt viele JavaScript-Parser mit 3.000 Zeilen und reguläre Ausdrücke, mit denen sich Skript-Tags und so weiter entfernen lassen. Aber warum sollte der Browser nicht das tun lassen, was er am besten kann? In diesem Beispiel wird responseText in einen temporären verborgenen iFrame geschrieben. Wir verwenden das HTML5-Attribut "sandbox", das Skripts deaktiviert und viele Sicherheitsfunktionen bietet...

Spezifikation der Spezifikation: Wenn das Attribut „sandbox“ angegeben wird, aktiviert es eine Reihe von zusätzlichen Einschränkungen für alle Inhalte, die vom iFrame gehostet werden. Der Wert muss ein ungeordneter Satz eindeutiger, durch Leerzeichen getrennter Tokens sein, bei denen die ASCII-Groß-/Kleinschreibung nicht berücksichtigt wird. Die zulässigen Werte sind „allow-forms“, „allow-same-origin“, „allow-scripts“ und „allow-top-navigation“. Wenn das Attribut festgelegt ist, wird der Inhalt so behandelt, als käme er von einem eindeutigen Ursprung. Außerdem werden Formulare und Skripts deaktiviert, es wird verhindert, dass Links auf andere Browserkontexte ausgerichtet sind, 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 verweigert korrekterweise das implizite Verschieben eines Knotens von einem Dokument in ein anderes. Wenn der neue untergeordnete Knoten in einem anderen Dokument erstellt wurde, wird ein Fehler ausgegeben. Hier verwenden wir adoptNode und alles ist gut.

Warum also iFrame? Warum nicht einfach nur innerHTML verwenden? Obwohl innerHTML jetzt Teil der HTML5-Spezifikation ist, ist es gefährlich, die Antwort eines Servers (schlecht oder gut) in einen ungeprüften Bereich einzufügen. Als ich diesen Artikel verfasste, konnte ich keinen Nutzer finden, der etwas anderes als innerHTML verwendet hat. Ich weiß, dass JQuery im Kern mit einem Anfüge-Fallback für Ausnahmen verwendet wird. JQuery Mobile verwendet es ebenfalls. Ich habe aber noch keine ausführlichen Tests dazu durchgeführt, ob innerHTML nach dem Zufallsprinzip nicht mehr funktioniert. Es wäre aber sehr interessant zu sehen, welche Plattformen davon betroffen sind. Es wäre auch interessant zu sehen, welcher Ansatz leistungsstärker ist... Ich habe auch von beiden Seiten Behauptungen gehört.

Erkennung, Verarbeitung und Profilerstellung des Netzwerktyps

Da wir jetzt die Möglichkeit haben, unsere Webanwendung zu puffern (oder vorausschauender Cache) zu haben, müssen wir die richtigen Funktionen zur Verbindungserkennung bereitstellen, die unsere App intelligenter machen. Hier kommt die Entwicklung mobiler Apps ins Spiel: Der Online-/Offlinemodus und die Verbindungsgeschwindigkeit werden extrem sensibel. Geben Sie The Network Information API ein. Jedes Mal, wenn ich diese Funktion in einer Präsentation vorstelle, hebt ein Publikum die Hand und fragt: „Wozu soll ich das verwenden?“. Hier ist eine Möglichkeit, eine extrem intelligente mobile Web-App einzurichten.

Zuerst ein langweiliges Szenario mit gesundem Menschenverstand... Wenn du in einem Hochgeschwindigkeitszug über ein Mobilgerät mit dem Web interagierst, kann das Netz sehr schnell verschwinden und verschiedene Regionen unterstützen möglicherweise unterschiedliche Übertragungsgeschwindigkeiten (z. B. HSPA oder 3G ist möglicherweise in einigen städtischen Gebieten verfügbar, in abgelegenen Gegenden werden jedoch möglicherweise deutlich langsamere 2G-Technologien unterstützt. Der folgende Code deckt die meisten Verbindungsszenarien ab.

Der folgende Code bietet Folgendes:

  • Offlinezugriff über applicationCache.
  • Erkennt, ob ein Lesezeichen gespeichert und offline ist.
  • Erkennt, wenn von offline zu online gewechselt wird und umgekehrt.
  • Erkennt langsame Verbindungen und ruft Inhalte basierend auf dem Netzwerktyp ab.

Auch diese Funktionen erfordern nur sehr wenig Code. 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 obigen EventListenern müssen wir unserem Code mitteilen, ob er von einem Ereignis oder einer tatsächlichen Seitenanforderung oder -aktualisierung aufgerufen wird. Der Hauptgrund ist, dass das body-onload-Ereignis beim Wechsel zwischen Online- und Offlinemodus nicht ausgelöst wird.

Als Nächstes prüfen wir das Ereignis ononline oder onload. Mit diesem Code werden deaktivierte Links zurückgesetzt, wenn von offline zu online gewechselt wird. Wäre diese App jedoch ausgefeilter, könnten Sie eine Logik einfügen, die das Abrufen von Inhalten wiederaufnimmt oder die UX für zwischenzeitliche 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. Der folgende Code durchsucht alle unsere externen Links und deaktiviert sie. So werden Nutzer für immer in unserer Offline-App gefangen!

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, weiter mit den guten Dingen. Da unsere App nun den Verbindungsstatus kennt, können wir auch die Art der Verbindung prüfen, wenn sie online ist, und die Einstellungen entsprechend anpassen. In den Kommentaren für jede 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);
  }
}

Es gibt zahlreiche Anpassungen, die wir an unserem AbrufAndCache-Prozess vornehmen könnten, aber hier habe ich nur festgelegt, dass die Ressourcen für eine bestimmte Verbindung asynchron (true) oder synchron (false) abgerufen werden sollen.

Anfragezeitstrahl für Edge (synchron)

Edge-Synchronisierung

Anfragezeitachse für WLAN (asynchron)

WLAN asynchron

Dies ermöglicht zumindest eine Methode zur Anpassung der Nutzererfahrung basierend auf langsamen oder schnellen Verbindungen. Dies ist keineswegs eine End-all-Be-all-Lösung. Eine weitere Aufgabe wäre, beim Klicken auf einen Link (bei langsamen Verbindungen) ein Lade-Modaldialogfeld anzuzeigen, während die App die Seite dieses Links noch im Hintergrund abruft. Wichtig ist hier, die Latenz zu reduzieren und gleichzeitig die Möglichkeiten der Verbindung des Nutzers mit den neuesten und besten HTML5-Funktionen voll auszuschöpfen. Demo zur Netzwerkerkennung ansehen

Fazit

Die Entwicklung mobiler HTML5-Apps hat gerade erst begonnen. Jetzt sehen Sie die sehr einfachen und grundlegenden Grundlagen eines mobilen "Frameworks", das ausschließlich auf HTML5 und den unterstützenden Technologien aufbaut. Ich denke, es ist wichtig, dass Entwickler mit diesen Funktionen im Kern arbeiten und sie angehen und nicht durch einen Wrapper verdeckt werden.