Mobil performansı optimize etmek için HTML5 teknikleri

Wesley Hales
Wesley Hales

Giriş

Dönen yenilemeler, sayfa geçişlerinde yaşanan kesintiler ve dokunma etkinliklerinde yaşanan dönemsel gecikmeler, günümüzün mobil web ortamlarında karşılaşılan sorunlardan yalnızca birkaçıdır. Geliştiriciler mümkün olduğunca yerele yaklaşmaya çalışır ancak genellikle saldırılar, sıfırlamalar ve katı çerçeveler nedeniyle yoldan çıkarılır.

Bu makalede, mobil HTML5 web uygulaması oluşturmak için gereken minimum koşulları ele alacağız. Amacımız, günümüzün mobil çerçevelerinin gizlemeye çalıştığı gizli karmaşıklıkları ortaya çıkarmaktır. Temel HTML5 API'lerini kullanan minimalist bir yaklaşım ve kendi çerçevenizi yazmanıza veya şu anda kullandığınız çerçeveye katkıda bulunmanıza olanak tanıyacak temel temel bilgiler göreceksiniz.

Donanım hızlandırma

Normalde GPU'lar ayrıntılı 3D modelleme veya CAD diyagramlarını işler. Ancak bu durumda, basit çizimlerimizin (div'ler, arka planlar, gölgeli metinler, resimler vb.) GPU aracılığıyla düzgün görünmesini ve animasyonlarının düzgün olmasını istiyoruz. Kullanıcı arabirimi geliştiricilerinin çoğu, bu animasyon sürecini anlamlar Bu konulara dikkat etmenin neden önemli olduğuna dair birkaç nedenden bahsedeyim:

  1. Bellek tahsisi ve işlem yükü - Sırf donanım hızlandırmak için DOM'deki her öğeyi bir araya getirirseniz, kodunuz üzerinde çalışan bir sonraki kişi sizi kovalayabilir ve sizi ciddi şekilde yenebilir.

  2. Güç tüketimi: Donanım devreye girdiğinde pilin de devreye girdiği açıktır. Mobil cihazlar için geliştirme yapan geliştiriciler, mobil web uygulamalarını yazarken çok çeşitli cihaz kısıtlamalarını dikkate almak zorundadır. Tarayıcı üreticileri daha fazla cihaz donanımına erişim sağlamaya başladıkça bu durum daha da yaygınlaşacaktır.

  3. Çakışmalar — Sayfanın zaten hızlandırılmış bölümlerine donanım hızlandırması uygularken bir hatayla karşılaştım. Bu nedenle, çakışan hızlandırmanız olup olmadığını bilmek çok önemlidir.

Kullanıcı etkileşimini mümkün olduğunca sorunsuz ve yerele yakın bir hale getirmek amacıyla, tarayıcının bizim için çalışmasını sağlamalıyız. İdeal olarak, mobil cihazın CPU'sunun ilk animasyonu oluşturmasını, ardından animasyon işlemi sırasında yalnızca farklı katmanları oluşturmaktan GPU'nun sorumlu olmasını isteriz. translate3d, scale3d ve translateZ'nin yaptığı budur. Animasyonlu öğelere kendi katmanlarını vererek cihazın her şeyi birlikte sorunsuz bir şekilde oluşturmasına olanak tanır. Hızlandırılmış kompozisyon ve WebKit'in işleyiş şekli hakkında daha fazla bilgi edinmek için Ariya Hidayat'ın blogunda çok sayıda faydalı bilgi bulabilirsiniz.

Sayfa geçişleri

Mobil web uygulaması geliştirirken en yaygın kullanıcı etkileşimi yaklaşımlarından üç tanesine göz atalım: kaydırma, çevirme ve döndürme efektleri.

Bu kodun işleyişini http://slidfast.appspot.com/slide-flip-rotate.html adresinde görebilirsiniz (Not: Bu demo mobil cihazlar için tasarlanmıştır. Bu nedenle, bir emülatör başlatın, telefonunuzu veya tabletinizi kullanın ya da tarayıcı pencerenizin boyutunu yaklaşık 1024 piksel veya daha az olacak şekilde küçültün).

İlk olarak slayt, çevirme ve döndürme geçişlerini ve bunların nasıl hızlandırıldıklarını inceleyeceğiz. Her animasyonun yalnızca üç veya dört satır CSS ve JavaScript aldığına dikkat edin.

Sürgülü

Üç geçiş yaklaşımından en yaygın olanı olan kaydırmalı sayfa geçişleri, mobil uygulamaların doğal hissini taklit eder. Slayt geçişi, görüntüleme alanına yeni bir içerik alanı getirmek için çağrılır.

Slayt efekti için önce işaretlememizi açıklıyoruz:

<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>

Sayfaları sola veya sağa hazırlama kavramına dikkat edin. Bu, herhangi bir yönde olabilir ancak en yaygın yönü budur.

Artık yalnızca birkaç CSS satırı kullanarak animasyon ve donanım hızlandırması elde edebiliriz. Gerçek animasyon, sayfa div öğelerindeki sınıfları değiştirdiğimizde gerçekleşir.

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

translate3d(0,0,0), "sihirli değnek" yaklaşımı olarak bilinir.

Kullanıcı bir gezinme öğesini tıkladığında sınıfları değiştirmek için aşağıdaki JavaScript'i yürütürüz. Üçüncü taraf çerçeveleri kullanılmaz, bu tamamen JavaScript'tir. ;)

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 veya stage-right, stage-center haline gelir ve sayfayı ortadaki görünüm bağlantı noktasına kaymaya zorlar. Ağır işleri CSS3'e bırakıyoruz.

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

.stage-right {
  left: 480px;
}

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

Ardından, mobil cihaz algılama ve yönünü yöneten CSS'ye göz atalım. Her cihazı ve çözünürlüğü ele alabiliriz (medya sorgusu çözünürlüğü bölümüne bakın). Bu demoda, mobil cihazlardaki çoğu dikey ve yatay görünümü kapsayacak şekilde yalnızca birkaç basit örnek kullandım. Bu, cihaz başına donanım hızlandırma uygulamak için de kullanışlıdır. Örneğin, WebKit'in masaüstü sürümü tüm dönüştürülmüş öğeleri hızlandırdığından (2D veya 3D olmasına bakılmaksızın) bir medya sorgusu oluşturup hızlandırmayı bu düzeyde hariç tutmak mantıklıdır. Donanım hızlandırma hilelerinin Android Froyo 2.2 ve sonraki sürümlerde herhangi bir hız iyileştirmesi sağlamadığını unutmayın. Tüm oluşturma işlemi yazılımın içinde yapılır.

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

Çevirme

Mobil cihazlarda sayfayı kaydırarak çevirme işlemine sayfayı çevirme denir. Burada, iOS ve Android (WebKit tabanlı) cihazlarda bu etkinliği işlemek için bazı basit JavaScript kodunu kullanırız.

Bu özelliğin nasıl çalıştığını http://slidfast.appspot.com/slide-flip-rotate.html adresinden görebilirsiniz.

Dokunma etkinlikleri ve geçişlerle çalışırken ilk yapmanız gereken, öğenin mevcut konumunu öğrenmek olmalıdır. WebKitCSSMatrix hakkında daha fazla bilgi için bu dokümana bakın.

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

Sayfa çevirme için bir CSS3 yumuşatma geçişi kullandığımızdan, normal element.offsetLeft çalışmaz.

Ardından, kullanıcının hangi yönde sayfayı çevirdiğini anlamak ve bir etkinliğin (sayfa gezinme) gerçekleşmesi için bir eşik belirlemek istiyoruz.

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;
  }
}

swipeTime değerini de milisaniye cinsinden ölçtüğümüzü göreceksiniz. Bu, kullanıcı bir sayfayı çevirmek için ekranı hızlı bir şekilde kaydırdığında gezinme etkinliğinin tetiklenmesini sağlar.

Sayfayı konumlandırmak ve bir parmak ekrana dokunurken animasyonları yerel görünmesini sağlamak için her etkinlik tetiklendikten sonra CSS3 geçişlerini kullanırız.

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';
}

Geçişlere en iyi doğal hissi vermek için cubic-bezier ile oynamaya çalıştım ancak ease-out işe yaradı.

Son olarak, gezinmeyi gerçekleştirmek için son demoda kullandığımız, daha önce tanımladığımız slideTo() yöntemlerimizi çağırmamız gerekir.

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

Döndürme

Ardından, bu demoda kullanılan döndürme animasyonuna göz atalım. İstediğiniz zaman, "Kişi" menü seçeneğine dokunarak görüntülediğiniz sayfayı 180 derece döndürerek ters tarafını görüntüleyebilirsiniz. Geçiş sınıfı onclick atamak için yine yalnızca birkaç satır CSS ve biraz JavaScript yeterlidir. NOT: Döndürme geçişi, 3D CSS dönüştürme özelliklerinden yoksun olduğu için Android'in çoğu sürümünde doğru şekilde oluşturulmaz. Maalesef Android, çevirmeyi göz ardı etmek yerine, çevirmek yerine döndürerek sayfayı "sepet tekerleği"nden uzaklaştırıyor. Destek iyileşene kadar bu geçişi ölçülü bir şekilde kullanmanızı öneririz.

İşaretleme (ön ve arka tarafın temel kavramı):

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

JavaScript:

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;
  }
}

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

Donanım hızlandırmada hata ayıklama

Temel geçişleri ele aldığımıza göre, geçişlerin işleyişine ve birleştirilmesine bakalım.

Bu büyülü hata ayıklama oturumunu gerçekleştirmek için birkaç tarayıcı ve tercih ettiğiniz IDE'yi açalım. Bazı hata ayıklama ortam değişkenlerinden yararlanmak için önce Safari'yi komut satırından başlatın. Mac kullanıyorum. Bu nedenle komutlar işletim sisteminize göre farklılık gösterebilir. Terminal'i açıp şunları yazın:

  • $> dışa aktar CA_COLOR_OPAQUE=1
  • $> export CA_LOG_MEMORY_USAGE=1
  • $> /Applications/Safari.app/Contents/MacOS/Safari

Bu işlem, Safari'yi birkaç hata ayıklama yardımcısıyla başlatır. CA_COLOR_OPAQUE, hangi öğelerin gerçekten birleştirildiğini veya hızlandırıldığını gösterir. CA_LOG_MEMORY_USAGE, çizim işlemlerimizi destek deposuna gönderirken ne kadar bellek kullandığımızı gösterir. Bu sayede mobil cihaza ne kadar yük bindirdiğinizi tam olarak anlayabilir ve GPU kullanımınızın hedef cihazın pilini nasıl tükettiğine dair ipuçları edinebilirsiniz.

Şimdi Chrome'u açıp saniyedeki kare sayısı (FPS) bilgilerini inceleyelim:

  1. Google Chrome web tarayıcısını açın.
  2. URL çubuğuna about:flags yazın.
  3. Birkaç öğe aşağı ilerleyip FPS Sayacı için "Etkinleştir"i tıklayın.

Bu sayfayı Chrome'un geliştirilmiş sürümünde görüntülerseniz sol üst köşede kırmızı FPS sayacını görürsünüz.

Chrome FPS

Donanım hızlandırmanın açık olduğunu bu şekilde anlarız. Ayrıca, animasyonun nasıl çalıştığı ve herhangi bir sızıntı (durdurulması gereken, sürekli çalışan animasyonlar) olup olmadığı konusunda da bize bir fikir verir.

Donanım hızlandırmasını görselleştirmenin bir başka yolu da aynı sayfayı Safari'de (yukarıda bahsettiğim ortam değişkenleriyle) açmaktır. Hızlandırılmış her DOM öğesi kırmızı renktedir. Bu sayede, katman bazında tam olarak nelerin birleştirildiğini görebiliriz. Hızlandırılmadığı için beyaz gezinme düğmesinin kırmızı olmadığını fark edin.

Birleştirilmiş Kişi

Chrome için benzer bir ayar, about:flags "Kompozit oluşturma katmanı kenarlıkları" bölümünde de mevcuttur.

Birleştirilmiş katmanları görmenin en iyi yollarından biri, bu mod uygulanırken WebKit düşen yapraklar demosunu izlemektir.

omposed Leaves

Son olarak, uygulamamızın grafik donanım performansını gerçekten anlamak için belleğin nasıl tüketildiğini inceleyelim. Burada, 1,38 MB boyutundaki çizim talimatlarını Mac OS'taki CoreAnimation arabelleklerine aktardığımızı görüyoruz. Ekranda gördüğünüz nihai pikselleri oluşturmak için Core Animation bellek arabellekleri OpenGL ES ile GPU arasında paylaşılır.

Coreanimation 1

Tarayıcı penceresinin boyutunu değiştirdiğimizde veya pencereyi büyüttüğümüzde bellek kullanımının da arttığını görüyoruz.

Coreanimation 2

Bu sayede, mobil cihazınızda belleğin nasıl kullanıldığı hakkında fikir edinebilirsiniz. Bunun için tarayıcıyı doğru boyutlara göre yeniden boyutlandırmanız gerekir. iPhone ortamları için hata ayıklama veya test yapıyorsanız resmi 480x320 piksel olarak yeniden boyutlandırın. Artık donanım hızlandırmanın tam olarak nasıl çalıştığını ve hata ayıklama için ne gerektiğini anlıyoruz. Bu konuda bilgi edinmek önemli bir şey. Ancak GPU bellek arabelleklerinin görsel olarak çalıştığını görmek bazı şeyleri bakış açıyor.

Sahne Arkası: Getirme ve Önbelleğe Alma

Şimdi sayfa ve kaynak önbelleğe alma işlemini bir üst seviyeye taşıma zamanı. JQuery Mobile ve benzer çerçevelerin kullandığı yaklaşıma benzer şekilde, eşzamanlı AJAX çağrılarıyla sayfalarımızı önceden getireceğiz ve önbelleğe alacağız.

Mobil web'deki temel sorunlardan birkaçını ve bu sorunları ele almamız gereken nedenleri inceleyelim:

  • Getirme: Sayfalarımızı önceden getirmek, kullanıcıların uygulamayı çevrimdışına almalarına olanak tanır ve ayrıca gezinme işlemleri arasında beklememe olanaklarını sağlar. Elbette, cihaz internete bağlandığında cihazın bant genişliğini aşmak istemiyoruz. Bu nedenle, bu özelliği dikkatli bir şekilde kullanmamız gerekiyor.
  • Önbelleğe alma: Ardından, bu sayfaları alırken ve önbelleğe alırken eşzamanlı veya eşzamansız bir yaklaşım istiyoruz. Ayrıca, cihazlar arasında iyi desteklendiği için maalesef eşzamanlı olmayan localStorage'ı da kullanmamız gerekiyor.
  • AJAX ve yanıtın ayrıştırılması: AJAX yanıtını DOM'a eklemek için innerHTML() kullanmak tehlikelidir (ve güvenilir değildir). Bunun yerine, AJAX yanıtı ekleme ve eşzamanlı çağrıları işleme için güvenilir bir mekanizma kullanıyoruz. xhr.responseText öğesini ayrıştırmak için HTML5'in bazı yeni özelliklerinden de yararlanırız.

Slayt, Çevir ve Döndür demosundaki kodu kullanarak işe birkaç ikincil sayfa ekleyip bunların bağlantılarını vererek başlıyoruz. Ardından bağlantıları ayrıştırır ve anında geçişler oluştururuz.

iPhone Ana Sayfası

Getir ve Önbelleğe Al demosunu buradan inceleyebilirsiniz.

Gördüğünüz gibi burada anlamsal işaretlemeden yararlanıyoruz. Başka bir sayfanın bağlantısı. Alt sayfa, üst sayfayla aynı düğüm/sınıf yapısını izler. Bunu bir adım daha ileri taşıyarak "sayfa" düğümleri vb. için data-* özelliğini kullanabiliriz. Ayrıca, ayrı bir html dosyasında (/demo2/home-detail.html) yer alan ayrıntı sayfasını (alt) da yükleme, önbelleğe alma ve uygulama yükleme sırasında geçiş için ayarlanacak şekilde kullanabiliriz.

<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>

Şimdi JavaScript'e göz atalım. Basitlik açısından, yardımcıları veya optimizasyonları koddan çıkarıyorum. Burada tek yaptığımız, getirilecek ve önbelleğe alınacak bağlantıları bulmak için belirli bir DOM düğümü dizisinde döngü oluşturmaktır. Not: Bu demoda, sayfa yüklendiğinde fetchAndCache() yöntemi çağrılır. Bir sonraki bölümde ağ bağlantısını tespit ettiğimizde ve ne zaman çağrılacağını belirlediğimizde bağlantı tekrar hazırlayacağız.

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();
      }
    }
  }
};

"AJAX" nesnesi kullanılarak doğru eşzamansız son işleme işlemi sağlanır. HTML5 ile Çevrimdışı Çalışma başlıklı makalede, localStorage'ın bir AJAX çağrısında kullanılmasıyla ilgili daha gelişmiş bir açıklama yer almaktadır. Bu örnekte, her istek için önbelleğe alma işleminin temel kullanımını ve sunucu başarılı (200) bir yanıt dışında bir yanıt döndürdüğünde önbelleğe alınmış nesnelerin sağlanmasını görüyorsunuz.

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;
      }
    }
  }
}

Maalesef localStorage, karakter kodlaması için UTF-16 kullandığından her bayt 2 bayt olarak depolanır. Bu da depolama alanımızın sınırını 5 MB'tan toplam 2,6 MB'ya çıkarır. Bu sayfaların/işaretlemelerin uygulama önbelleği kapsamı dışında getirilmesi ve önbelleğe alınmasının nedeni sonraki bölümde açıklanmaktadır.

HTML5'teki iframe öğesinde yapılan son geliştirmelerle birlikte, AJAX çağrımızdan döndürülen responseText öğesini ayrıştırmanın basit ve etkili bir yoluna sahip olduk. 3.000 satırdan oluşan çok sayıda JavaScript ayrıştırıcı ve komut dosyası etiketlerini vb. kaldıran normal ifade vardır. Ancak tarayıcının en iyi yaptığı işi yapmasına neden izin vermiyorsunuz? Bu örnekte, responseText öğesini geçici bir gizli iframe'e yazacağız. Komut dosyalarını devre dışı bırakan ve birçok güvenlik özelliği sunan HTML5 "korumalı alan" özelliğini kullanıyoruz…

Spesifikasyondan: Belirtildiğinde korumalı alan özelliği, iframe tarafından barındırılan tüm içerikler için ek kısıtlamalar sağlar. Değeri, ASCII büyük/küçük harfe duyarlı olmayan, boşlukla ayrılmış benzersiz jetonlardan oluşan bir sırasız küme olmalıdır. İzin verilen değerler allow-forms, allow-same-origin, allow-scripts ve allow-top-navigation'dır. Özellik ayarlandığında içerik benzersiz bir kaynaktan geliyormuş gibi değerlendirilir, formlar ve komut dosyaları devre dışı bırakılır, bağlantıların diğer tarama bağlamlarını hedeflemesi engellenir ve eklentiler devre dışı bırakılır.

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, bir düğümü dolaylı yoldan bir dokümandan diğerine taşımayı doğru şekilde reddediyor. Yeni alt öğe farklı bir dokümanda oluşturulduysa hata oluşur. Burada adoptNode kullanıyoruz ve her şey yolunda.

Peki neden iframe? Neden sadece innerHTML'yi kullanmıyorsunuz? innerHTML artık HTML5 spesifikasyonunun bir parçası olsa da bir sunucudan gelen yanıtı (kötü veya iyi) kontrol edilmeyen bir alana eklemek tehlikeli bir uygulamadır. Bu makale yazılırken innerHTML dışında bir şey kullanan hiç kimse bulunamadı. JQuery'nin onu özünde sadece istisna olarak bir yedek ekleyerek kullandığını biliyorum. JQuery Mobile da bunu kullanır. Ancak innerHTML "rastgele çalışmayı durduruyor" ile ilgili yoğun bir test yapmadım ancak bunun etkilediği tüm platformları görmek çok ilginç olur. Hangi yaklaşımın daha etkili olduğunu görmek de ilginç olabilir... Bu konuda her iki taraftan da iddialar duydum.

Ağ türünü algılama, işleme ve profil oluşturma

Web uygulamamızı arabelleğe alma (veya tahmini önbelleğe alma) özelliğine sahip olduğumuza göre, uygulamamızı daha akıllı hale getiren uygun bağlantı algılama özelliklerini sunmamız gerekiyor. Mobil uygulama geliştirme, bu noktada online/offline modlara ve bağlantı hızına karşı son derece hassas hale gelir. Network Information API'yi girin. Bu özelliği bir sunumda her gösterdiğimde, katılımcılardan biri söz isteyerek “Bunu ne için kullanırım?” diye soruyor. İşte son derece akıllı bir mobil web uygulaması oluşturmanın olası bir yolu.

Önce sıkıcı bir sağduyu senaryosu... Yüksek hızlı trende bulunan bir mobil cihazdan web ile etkileşimde bulunurken, ağ çeşitli zamanlarda çok iyi olmayabilir ve farklı coğrafyalar farklı iletim hızlarını destekleyebilir (ör. HSPA veya 3G bazı kentsel alanlarda kullanılabilir ancak uzak bölgelerde çok daha yavaş 2G teknolojileri desteklenebilir). Aşağıdaki kod, bağlantı senaryolarının çoğunu ele alır.

Aşağıdaki kod:

  • applicationCache üzerinden çevrimdışı erişim.
  • Yer işareti eklenip eklenmediğini ve çevrimdışı olup olmadığını algılar.
  • Çevrimdışıyken çevrimiçiye veya çevrimiçiyken çevrimdışıya geçişleri algılar.
  • Yavaş bağlantıları algılar ve ağ türüne bağlı olarak içeriği getirir.

Bu özelliklerin tümü için çok az kod gerekir. Öncelikle etkinliklerimizi ve yükleme senaryolarımızı algılarız:

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);

Yukarıdaki EventListener'larda, kodumuzun bir etkinlikten mi yoksa gerçek bir sayfa isteğinden mi yoksa yenilemeden mi çağrıldığını belirtmemiz gerekir. Bunun başlıca nedeni, online ve çevrimdışı modlar arasında geçiş yapılırken body onload etkinliğinin tetiklenmemesidir.

Ardından, ononline veya onload etkinliği için basit bir kontrol gerçekleştiririz. Bu kod, çevrimdışı ortamdan online moda geçerken devre dışı bırakılmış bağlantıları sıfırlar. Ancak daha gelişmiş bir uygulama olsaydı, içerik getirmeye devam edecek veya kesintili bağlantılar için kullanıcı deneyimini işleyecek bir mantık ekleyebilirsiniz.

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;
    }
  }
}

Aynı durum processOffline() için de geçerlidir. Burada, uygulamanızı çevrimdışı modda çalıştırır ve arka planda yapılan tüm işlemleri kurtarmaya çalışırsınız. Aşağıdaki kod, tüm harici bağlantılarımızı bulup devre dışı bırakır. Böylece kullanıcıları çevrimdışı uygulamamızda sonsuza kadar hapsetmiş oluruz. muhahaha!

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);

    }
  }
}

Tamam, şimdi iyi haberlere geçelim. Uygulamamız artık hangi bağlı durumda olduğunu bildiğine göre, internete bağlıyken bağlantı türünü de kontrol edip buna göre ayarlama yapabiliriz. Her bağlantının yorumlarında, Kuzey Amerika'daki tipik sağlayıcıların indirme hızlarını ve gecikmeleri listelenmiştir.

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

fetchAndCache işlemimizde yapabileceğimiz çok sayıda ayar var ancak burada tek yaptığım, belirli bir bağlantı için kaynakları eşzamansız (true) veya senkron (false) olarak getirmesini söylemekti.

Edge (Eşzamanlı) İstek Zaman Çizelgesi

Edge Sync

WIFI (Asenkron) İstek Zaman Çizelgesi

WIFI Async

Bu sayede, yavaş veya hızlı bağlantılara göre kullanıcı deneyimini ayarlama yöntemlerinden en azından bazıları kullanılabilir. Bu, her durumda geçerli olan bir çözüm değildir. Bir bağlantı tıklandığında (yavaş bağlantılarda) uygulama arka planda bağlantının sayfasını getirirken yükleme modülü göstermek de yapılacaklar listesinde yer alıyor. Buradaki en önemli nokta, kullanıcının bağlantısının tüm özelliklerinden yararlanırken HTML5'in sunduğu en yeni ve en iyi özelliklerden yararlanarak gecikmeleri azaltmaktır. Ağ algılama demosunu buradan görüntüleyin.

Sonuç

Mobil HTML5 uygulamalarıyla ilgili yolculuk daha yeni başlıyor. Artık yalnızca HTML5 ve destekleyici teknolojiler etrafında oluşturulmuş bir mobil "çerçeve"nin çok basit ve temel temellerini görüyorsunuz. Geliştiricilerin bu özellikleri bir sarmalayıcıyla gizlenmeden, temelinde ele almaları gerektiğini düşünüyorum.