טכניקות HTML5 לאופטימיזציה של ביצועים בנייד

Wesley Hales
Wesley Hales

מבוא

עדכונים מסתובבים, מעברים קטועים בין דפים ועיכובים תקופתיים באירועי הקשה הם רק חלק מהבעיות בסביבות האינטרנט לנייד של היום. מפתחים מנסים להתקרב ככל האפשר לקוד מקורי, אבל לעיתים קרובות הם נתקלים בהאקים, באיפוס ובמסגרות נוקשות.

במאמר הזה נסביר על הנדרש ליצירת אפליקציית אינטרנט לנייד ב-HTML5. המטרה העיקרית היא לחשוף את המורכבות החבויה שסביבות העבודה לנייד של היום מנסים להסתיר. נציג גישה מינימליסטית (באמצעות ממשקי API ליבה של HTML5) ויסודות בסיסיים שיעזרו לכם לכתוב מסגרת משלכם או לתרום למסגרת שבה אתם משתמשים כרגע.

שיפור מהירות באמצעות חומרה

בדרך כלל, מעבדי GPU מטפלים במודלים תלת-ממדיים מפורטים או בתרשים CAD, אבל במקרה הזה אנחנו רוצים שהציורים הפרימיטיביים שלנו (divs, רקעים, טקסט עם צלליות, תמונות וכו') יופיעו בצורה חלקה ושהאנימציה שלהם תהיה חלקה באמצעות ה-GPU. הבעיה היא שרוב מפתחי הקצה הקדמי מעבירים את תהליך האנימציה הזה למסגרת של צד שלישי בלי להתייחס לסמנטיקה, אבל האם צריך להסתיר את התכונות העיקריות האלה של CSS3? הנה כמה סיבות לכך שחשוב להקפיד על הנושאים האלה:

  1. הקצאת זיכרון ועומס חישוב – אם תיצרו קומפוזיציה של כל רכיב ב-DOM רק כדי לקבל האצה בחומרה, האדם הבא שיעבוד על הקוד שלכם עשוי לרדוף אחריכם ולהכות אתכם קשות.

  2. צריכת החשמל — כמובן, כשהחומרה נכנסת, גם הסוללה. במהלך הפיתוח לנייד, המפתחים נאלצים להביא בחשבון את המגוון הרחב של מגבלות המכשירים בזמן כתיבת אפליקציות אינטרנט לנייד. המצב הזה יתרחב עוד יותר כאשר יצרני הדפדפנים יתחילו לאפשר גישה לחומרה של יותר ויותר מכשירים.

  3. התנגשויות – נתקלתי בהתנהגות לא תקינה כשהחרגתי חלקים מהדף שכבר הותאמו להאצת חומרה. לכן חשוב מאוד לדעת אם יש לכם תאוצה חופפת.

כדי שהאינטראקציה של המשתמש תהיה חלקה וקרובה ככל האפשר לתוכן המקורי, אנחנו חייבים לגרום לדפדפן לעבוד עבורנו. באופן אידיאלי, אנחנו רוצים ש-CPU של המכשיר הנייד יקבע את האנימציה הראשונית, ואז ה-GPU יהיה אחראי רק על שילוב של שכבות שונות במהלך תהליך האנימציה. זה מה ש-translate3d,‏ scale3d ו-translateZ עושים – הם מקצים לשכבה משלהם לאלמנטים האנימציה, וכך מאפשרים למכשיר ליצור רינדור של הכל יחד בצורה חלקה. מידע נוסף על עיבוד משולב מואץ ועל אופן הפעולה של WebKit זמין בבלוג של Ariya Hidayat.

מעברים בין דפים

נבחן שלוש מהגישות הנפוצות ביותר לאינטראקציה עם משתמשים כשמפתחים אפליקציה לנייד: אפקטים של החלקה, היפוך וסיבוב.

אפשר לראות את הקוד הזה בפעולה כאן: http://slidfast.appspot.com/slide-flip-rotate.html (הערה: הדגמה זו מיועדת למכשירים ניידים, לכן יש להפעיל אמולטור, להשתמש בטלפון או בטאבלט, או להקטין את חלון הדפדפן ל-1024 פיקסלים או פחות).

קודם ננתח את המעברים של החלקה, היפוך וסיבוב, ואת האופן שבו הם מואצים. שימו לב שכל אנימציה מורכבת רק משלושה או ארבעה שורות של CSS ו-JavaScript.

הזזה

השיטה הנפוצה ביותר מבין שלוש גישות המעבר היא הזזה של מעבר בין דפים, שמחקה את התחושה המקורית של אפליקציות לנייד. שינוי השקף מופעל כדי להוסיף אזור תוכן חדש ליציאה של התצוגה.

כדי ליצור את אפקט ההזזה, קודם כול מגדירים את ה-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>

שימו לב למושג של העברת דפים ל-staging שמאלה או ימינה. זה יכול להיות בכל כיוון, אבל זה המצב הכי נפוץ.

עכשיו יש לנו אנימציה עם האצת חומרה באמצעות כמה שורות CSS בלבד. האנימציה בפועל מתרחשת כשאנחנו מחליפים בין הכיתות של רכיבי ה-div בדף.

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

translate3d(0,0,0) נקרא 'גישת הכדור הכסף'.

כשהמשתמש לוחץ על רכיב ניווט, אנחנו מריצים את הקוד הבא ב-JavaScript כדי להחליף את הכיתות. לא נעשה שימוש במסגרות של צד שלישי, זו פשוט 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 או stage-right הופכים ל-stage-center ומאלצים את הדף להחליק לחלון התצוגה במרכז. אנחנו תלויים לחלוטין ב-CSS3 כדי לבצע את העבודה הקשה.

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

.stage-right {
  left: 480px;
}

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

עכשיו נבחן את ה-CSS שמטפל בזיהוי ובכיוון של המכשיר הנייד. אנחנו יכולים לטפל בכל מכשיר ובכל רזולוציה (ראו פתרון של שאילתה לגבי מדיה). השתמשתי בכמה דוגמאות פשוטות בהדגמה הזו כדי לכסות את רוב התצוגות לאורך ולרוחב במכשירים ניידים. האפשרות הזו שימושית גם לצורך שיפור המהירות באמצעות חומרה בכל מכשיר. לדוגמה, מאחר שגרסת שולחן העבודה של WebKit מאיצה את כל הרכיבים שעברו טרנספורמציה (ללא קשר אם הם דו-ממדיים או תלת-ממדיים), מומלץ ליצור שאילתה של מדיה ולהחריג את ההאצה ברמה הזו. חשוב לדעת שטריקים של האצת חומרה לא משפרים את המהירות ב-Android Froyo מגרסה 2.2 ואילך. כל הרכבת התמונות מתבצעת בתוכנה.

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

היפוך

במכשירים ניידים, היפוך הוא למעשה החלקה של הדף. כאן אנחנו משתמשים ב-JavaScript פשוט כדי לטפל באירוע הזה במכשירי iOS ו-Android (מבוססי WebKit).

אפשר לראות את התכונה בפעולה בכתובת http://slidfast.appspot.com/slide-flip-rotate.html.

כשעובדים עם אירועי מגע ומעברים, הדבר הראשון שצריך לעשות הוא להבין את המיקום הנוכחי של הרכיב. מידע נוסף על WebKitCSSMatrix זמין במסמך הזה.

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

מכיוון שאנחנו משתמשים במעבר CSS3 עם יציאה הדרגתית (ease-out) כדי להציג את הדף הבא, הקוד הרגיל element.offsetLeft לא יפעל.

בשלב הבא אנחנו רוצים להבין באיזה כיוון המשתמש עובר כדי להגדיר סף שאירוע (ניווט בדפים) יתרחש.

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 באלפיות השנייה. כך אירוע הניווט יופעל אם המשתמש מחליק במהירות על המסך כדי לעבור לדף אחר.

כדי למקם את הדף ולגרום לאנימציות להיראות מותאמות כשאצבע נוגעת במסך, אנחנו משתמשים במעברים ב-CSS3 לאחר כל הפעלה של אירוע.

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

ניסיתי להתנסות עם cubic-bezier כדי לתת למעברים תחושה מקורית יותר, אבל ease-out עבד מצוין.

לסיום, כדי שהניווט יתבצע, אנחנו צריכים להפעיל את השיטות slideTo() שהגדרנו קודם לכן והשתמשנו בהן בדמו האחרונה.

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

סיבוב

עכשיו נבחן את אנימציית הסיבוב שנעשה בה שימוש בדמו הזה. אתם תמיד יכולים לסובב את הדף שמוצג כרגע ב-180 מעלות כדי לחשוף את הצד השני. לשם כך, מקישים על אפשרות התפריט 'יצירת קשר'. שוב, כדי להקצות את סיווג המעבר onclick, צריך רק כמה שורות של CSS וקצת JavaScript. הערה: המעבר מסיבוב לא מוצג בצורה נכונה ברוב הגרסאות של Android כי אין לו יכולות טרנספורמציה של CSS תלת-ממדי. לצערי, במקום להתעלם מההיפוך, Android גורמת לדף 'להסתובב' על צירו במקום להתהפך. מומלץ להשתמש במעבר הזה במשורה עד שהתמיכה תשתפר.

הרכיב המטא (הקונספט הבסיסי של 'חזית' ו'גב'):

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

ניפוי באגים בשיפור המהירות באמצעות חומרה

אחרי שסיכמנו על המעברים הבסיסיים, נבחן את המנגנונים שלהם ואת האופן שבו הם מורכבים.

כדי שנוכל להתחיל את סשן ניפוי הבאגים הקסום הזה, נפעיל כמה דפדפנים וה-IDE שבחרתם. קודם צריך להפעיל את Safari משורת הפקודה כדי להשתמש במשתני סביבה מסוימים לניפוי באגים. אני משתמש ב-Mac, כך שהפקודות עשויות להיות שונות בהתאם למערכת ההפעלה שלך. פותחים את Terminal ומקלידים את הפקודה הבאה:

  • $> ייצוא CA_COLOR_OPAQUE=1
  • $> export CA_LOG_MEMORY_USAGE=1
  • $> /Applications/Safari.app/Contents/MacOS/Safari

הפעולה הזו מפעילה את Safari באמצעות כמה כלים שעוזרים לניפוי באגים. בעזרת CA_COLOR_OPAQUE אפשר לראות אילו רכיבים עוברים למעשה שילוב או האצה. בעמודה CA_LOG_MEMORY_USAGE מוצג נפח הזיכרון שאנחנו משתמשים בו כשאנחנו שולחים את פעולות הציור שלנו למאגר התמיכה. כך תוכלו לדעת בדיוק כמה עומס אתם מטילים על המכשיר הנייד, ואולי לקבל רמזים לגבי האופן שבו השימוש ב-GPU עלול לרוקן את הסוללה של מכשיר היעד.

עכשיו נפעיל את Chrome כדי שנוכל לראות כמה נתונים טובים לגבי מספר הפריימים לשנייה (FPS):

  1. פותחים את דפדפן האינטרנט Google Chrome.
  2. בסרגל הכתובות, מקלידים about:flags.
  3. גוללים למטה כמה פריטים ולוחצים על 'הפעלה' למונה FPS.

אם פותחים את הדף הזה בגרסה המשופרת של Chrome, מונה ה-FPS האדום יופיע בפינה הימנית העליונה.

Chrome FPS

כך אנחנו יודעים שהאפשרות לשיפור המהירות באמצעות חומרה מופעלת. הוא גם נותן מושג לגבי האופן שבו האנימציה פועלת ואם יש דליפות (אנימציות רצופות שצריכות להיפסק).

דרך נוספת להמחיש את האצת החומרה היא לפתוח את אותו דף ב-Safari (עם משתני הסביבה שציינתי למעלה). לכל אלמנט DOM מואץ יש גוון אדום. הפעולה הזו מראה לנו בדיוק את מה שמרכיבים לפי שכבה. שימו לב שהניווט הלבן לא אדום מפני שהוא לא מואץ.

איש קשר מורכב

הגדרה דומה ל-Chrome זמינה גם ב-about:flags בקטע 'גבולות של שכבות רינדור מורכבות'.

דרך מצוינת נוספת לראות את השכבות המשולבות היא לצפות בדמו של WebKit של עלים נופלים בזמן שהמודיפיקציה הזו מופעלת.

עלים מורכבים

ולבסוף, כדי להבין באמת את ביצועי החומרה של הגרפיקה באפליקציה שלנו, נבחן את אופן השימוש בזיכרון. כאן אנחנו רואים שאנחנו מעבירים 1.38MB של הוראות ציור למאגרי CoreAnimation ב-Mac OS. מאגרי הזיכרון של Core Animation משותפים בין OpenGL ES לבין ה-GPU כדי ליצור את הפיקסלים הסופיים שרואים במסך.

Coreanimation 1

כשמשנים את הגודל של חלון הדפדפן או מגדילים אותו למסך מלא, גם הזיכרון מתרחב.

Coreanimation 2

כך תוכלו לקבל מושג לגבי האופן שבו הזיכרון נעשה במכשיר הנייד שלכם רק אם תשנו את גודל הדפדפן למידות הנכונות. אם ביצעתם ניפוי באגים או בדיקה בסביבות iPhone, צריך לשנות את הגודל ל-480px על 320px. עכשיו אנחנו מבינים בדיוק איך פועל שיפור המהירות באמצעות חומרה ומה נדרש כדי לנפות באגים. זה דבר אחד לקרוא על זה, אבל לראות בפועל את מאגרי הזיכרון של ה-GPU פועלים באופן חזותי מאפשר להבין את הדברים בצורה טובה יותר.

מאחורי הקלעים: אחזור ושמירה במטמון

עכשיו הגיע הזמן לשפר את שמירת הדפים והמשאבים במטמון. בדומה לגישה של JQuery Mobile ותבניות דומות, אנחנו נבצע אחזור מראש של הדפים שלנו ונשמור אותם במטמון באמצעות קריאות AJAX בו-זמניות.

נתייחס לכמה בעיות מרכזיות באינטרנט לנייד ולסיבות לכך:

  • אחזור: אחזור מראש של הדפים שלנו מאפשר למשתמשים להשתמש באפליקציה במצב אופליין, וגם מאפשר להם לא להמתין בין פעולות הניווט. כמובן שאנחנו לא רוצים לכווץ את רוחב הפס של המכשיר כשהוא מחובר לאינטרנט, ולכן עלינו להשתמש בתכונה הזו באופן מדוד.
  • אחסון במטמון: בשלב הבא, אנחנו רוצים להשתמש בגישה בו-זמנית או אסינכררונית בזמן אחזור הדפים האלה ושמירתם במטמון. אנחנו צריכים גם להשתמש ב-localStorage (כי יש לו תמיכה רחבה במכשירים), ולצערנו הוא לא אסינכררוני.
  • AJAX וניתוח התגובה: שימוש ב-innerHTML() כדי להוסיף את תגובת AJAX אל ה-DOM הוא מסוכן (ולא אמין?). במקום זאת, אנחנו משתמשים במנגנון מהימן להוספת תגובות AJAX ולטיפול בקריאות בו-זמניות. אנחנו גם משתמשים בכמה תכונות חדשות של HTML5 כדי לנתח את ה-xhr.responseText.

על סמך הקוד מהדוגמה להצגת שקפים, היפוך וסיבוב, נתחיל בהוספת כמה דפים משניים ויצירת קישורים אליהם. לאחר מכן ננתח את הקישורים ונוצר מעברים בזמן אמת.

דף הבית של iPhone

כאן אפשר לצפות בהדגמה של אחזור והטמעה במטמון.

כפי שאפשר לראות, אנחנו משתמשים כאן בסימון סמנטי. רק קישור לדף אחר. הדף הצאצא פועל לפי אותו מבנה צומת/כיתה כמו הדף ההורה. אפשר לקחת את זה צעד אחד קדימה ולהשתמש במאפיין data-* בשביל צמתים של "page" וכו'. וכאן מופיע דף הפרטים (צאצא) שממוקם בקובץ HTML נפרד (/demo2/home-detail.html) שייטען, יישמר במטמון ויוגדר למעבר לאחר טעינת אפליקציה.

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

עכשיו נסתכל על ה-JavaScript. כדי לפשט את הקוד, לא אכלול בו פונקציות עזר או אופטימיזציות. כל מה שאנחנו עושים כאן הוא לעבור בלולאה על מערך מסוים של צמתים ב-DOM כדי לחפש קישורים לאחזור ולאחסון במטמון. הערה: בדמו הזה, הקריאה ל-method‏ fetchAndCache() מתבצעת בזמן טעינת הדף. אנחנו עובדים על זה שוב בקטע הבא כשאנחנו מזהים את החיבור לרשת וקובעים מתי צריך לקרוא לו.

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'. הסבר מתקדם יותר על השימוש ב-localStorage בקריאה של AJAX זמין במאמר עבודה מחוץ לרשת עם HTML5 אופליין. בדוגמה הזו מוצג השימוש הבסיסי בשמירת נתונים במטמון בכל בקשה, והצגת האובייקטים שנשמרו במטמון כשהשרת מחזיר תגובה שאינה תגובה מוצלחת (200).

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

לצערנו, מכיוון ש-localStorage משתמש ב-UTF-16 לקידוד תווים, כל בייט מאוחסן כ-2 בייטים, כך שמגבלת האחסון שלנו ירדה מ-5MB ל-2.6MB סה"כ. הסיבה המלאה לאחזור ולשמירה במטמון של הדפים או הרכיבים האלה מחוץ להיקף של מטמון האפליקציה תופיע בקטע הבא.

בעקבות החידושים האחרונים באלמנט iframe בשילוב עם HTML5, יש לנו עכשיו דרך פשוטה ויעילה לנתח את responseText שחוזרים מקריאת AJAX. יש הרבה מנתח JavaScript וביטויים רגולריים באורך 3,000 שורות שמסירים תגי סקריפט וכו'. אבל למה לא לתת לדפדפן לעשות את מה שהוא עושה הכי טוב? בדוגמה הזו, נכתוב את responseText ב-iframe זמני מוסתר. אנחנו משתמשים במאפיין 'ארגז חול' של HTML5, שמשבית סקריפטים ומציע תכונות אבטחה רבות…

מהמפרט: מאפיין Sandbox, אם הוא מצוין, מפעיל קבוצה של הגבלות נוספות על כל תוכן שמתארח ב-iframe. הערך שלו חייב להיות קבוצה לא מסודרת של אסימונים ייחודיים מופרדים ברווחים, שהם ASCII ללא תלות רישיות. הערכים המותרים הם allow-forms,‏ allow-same-origin,‏ allow-scripts ו-allow-top-navigation. כשהמאפיין מוגדר, התוכן מטופל כתוכן ממקור ייחודי, טפסים וסקריפטים מושבתים, קישורים לא יכולים לטרגט הקשרי גלישה אחרים ויישומי הפלאגין מושבתים.

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 מסרב בצורה נכונה להעביר צומת באופן משתמע ממסמך אחד למסמך אחר. נוצרת שגיאה אם צומת הצאצא החדש נוצר במסמך אחר. כאן אנחנו משתמשים ב-adoptNode והכול בסדר.

אז למה להשתמש ב-iframe? למה לא להשתמש רק ב-innerHTML? אמנם innerHTML הוא עכשיו חלק מהמפרט של HTML5, אבל זהו שימוש מסוכן להוסיף את התשובה מהשרת (רעה או טובה) לאזור לא מאומת. במהלך כתיבת המאמר הזה, לא הצלחתי למצוא אף אחד שמשתמש בכל דבר מלבד innerHTML. ברור לי ש-JQuery משתמשת בו בליבה, עם תוספת חלופית במקרה חריג. גם ב-JQuery Mobile נעשה בו שימוש. עם זאת, לא ביצעתי בדיקות מעמיקות לגבי innerHTML 'מפסיק לפעול באופן אקראי', אבל יהיה מעניין מאוד לראות את כל הפלטפורמות שמושפעות מכך. מעניין גם לראות איזו גישה מניבה ביצועים טובים יותר… גם בנושא הזה שמעתי טענות משני הצדדים.

זיהוי, טיפול ויצירת פרופילים של סוג הרשת

עכשיו, כשיש לנו אפשרות לאגור באחסון זמין (או במטמון חזוי) את אפליקציית האינטרנט שלנו, אנחנו צריכים לספק את התכונות המתאימות לזיהוי חיבורים, שיעזרו לנו לשפר את הבינה של האפליקציה. כאן פיתוח אפליקציות לנייד הופך להיות רגיש מאוד למצבים אופליין/אונליין ולמהירות החיבור. מזינים את Network Information API. בכל פעם שאני מציג את התכונה הזו במצגת, מישהו בקהל מרים את היד ושואל "למה צריך את זה?". אז הנה דרך אפשרית להגדיר אפליקציית אינטרנט חכמה במיוחד לנייד.

קודם כל, תרחיש משעמם של שיקול דעת ברור… כשמשתמשים באינטרנט בנייד ברכבת מהירה, יכול להיות שהרשת תיעלם ברגעים שונים, ויכול להיות שמיקומים גיאוגרפיים שונים יתמכו במהירויות העברה שונות (למשל, יכול להיות ש-HSPA או 3G יהיו זמינים באזורים עירוניים מסוימים, אבל באזורים מרוחקים יכול להיות שתהיה תמיכה בטכנולוגיות 2G איטיות בהרבה). הקוד הבא מטפל ברוב התרחישים של חיבורים.

הקוד הבא מספק:

  • גישה אופליין דרך applicationCache.
  • זיהוי אם הדף נוסף לסימניות והוא במצב אופליין.
  • זיהוי מעבר ממצב אופליין למצב אונליין ולהפך.
  • זיהוי חיבורים איטיים ואחזור תוכן על סמך סוג הרשת.

שוב, כל התכונות האלה דורשות מעט מאוד קוד. קודם כול אנחנו מזהים את האירועים ואת התרחישים של הטעינה:

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

ב-EventListeners שלמעלה, אנחנו צריכים להודיע לקוד שלנו אם הוא נקרא מאירוע או מבקשה או רענון של דף בפועל. הסיבה העיקרית לכך היא שאירוע הגוף onload לא יופעל במעבר בין המצבים אונליין ואוף-ליין.

בשלב הבא אנחנו מבצעים בדיקה פשוטה של אירועים מסוג ononline או onload. הקוד הזה מאפס קישורים מושבתים כשעוברים ממצב אופליין למצב אונליין. אבל, אם האפליקציה הזו מתוחכמת יותר, אפשר להוסיף לוגיקה שתחדש את אחזור התוכן או לטפל בחוויית המשתמש בחיבורים מקוטעים.

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

אותו עיקרון חל על processOffline(). כאן צריך לבצע שינויים באפליקציה כדי להעביר אותה למצב אופליין ולנסות לשחזר עסקאות שהתרחשו מאחורי הקלעים. הקוד הבא מחפש את כל הקישורים החיצוניים שלנו ומשבית אותם – כך המשתמשים נלכדים באפליקציה שלנו במצב אופליין לנצח, אה-האה-האה!

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

    }
  }
}

אוקיי, עכשיו נעבור לדברים הטובים. עכשיו, כשהאפליקציה יודעת באיזה מצב חיבור היא נמצאת, אנחנו יכולים גם לבדוק את סוג החיבור כשהיא אונליין ולבצע התאמות בהתאם. רשמתי את זמני האחזור וההורדות הרגילים של ספקים בצפון אמריקה בתגובות לכל חיבור.

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 שלנו, אבל כל מה שעשיתי כאן היה להורות לו לאחזר את המשאבים באופן אסינכרוני (true) או באופן סינכרוני (false) בחיבור נתון.

ציר הזמן של בקשה (סינכרונית) בקצה

Edge Sync

ציר זמן של בקשה ל-Wi-Fi (אסינכרוני)

WIFI Async

כך אפשר לפחות לבצע התאמה מסוימת של חוויית המשתמש על סמך חיבורים איטיים או מהירים. זהו בהחלט לא פתרון מושלם. משימה נוספת: הקפצת חלון טעינה כאשר לוחצים על קישור (בחיבורים איטיים) בזמן שהאפליקציה עדיין מאחזרת את דף הקישור ברקע. הנקודה החשובה כאן היא לצמצם את זמני האחזור תוך ניצול מלוא היכולות של החיבור של המשתמש באמצעות התכונות החדשות והמתקדמות ביותר של HTML5. כאן אפשר לצפות בהדגמה של זיהוי הרשת.

סיכום

המסע בעולם האפליקציות לנייד מבוססות-HTML5 רק מתחיל. עכשיו אתם רואים את היסודות הפשוטים והבסיסיים של 'מסגרת' לנייד שנבנתה אך ורק סביב HTML5 והטכנולוגיות התומכות שלו. לדעתי, חשוב למפתחים לעבוד עם התכונות האלה ולטפל בהן ברמה הבסיסית, ולא להסתיר אותן בתוך מעטפת.