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

Wesley Hales
Wesley Hales

מבוא

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

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

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

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

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

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

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

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

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

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

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

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

הזזה

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

כדי ליצור את אפקט השקף, קודם צריך להצהיר על התגיות:

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

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

עכשיו אפשר להוסיף אנימציה והאצת חומרה בכמה שורות קוד 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 מעלות כדי לחשוף את הצד השני, על ידי הקשה על אפשרות התפריט 'יצירת קשר'. שוב, צריך רק כמה שורות של CSS וקצת JavaScript כדי להקצות מחלקת מעבר onclick. הערה: מעבר הסיבוב לא מוצג בצורה נכונה ברוב הגרסאות של 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 ומקלידים את הפקודה הבאה:

  • ‪$> export 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

כדי לקבל מושג לגבי השימוש בזיכרון במכשיר הנייד, צריך לשנות את הגודל של חלון הדפדפן למידות הנכונות. אם ניסיתם לאתר באגים או לבצע בדיקות בסביבות אייפון, תשנו את הגודל ל-‎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 כדי לחלץ קישורים להבאה ולשמירה במטמון. הערה – בהדגמה הזו, הקריאה לשיטה 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 כדי להבטיח עיבוד נכון של נתונים שמתקבלים באופן אסינכרוני. במאמר Working Off the Grid with HTML5 Offline מוסבר בצורה מפורטת יותר איך להשתמש ב-localStorage בקריאת AJAX. בדוגמה הזו אפשר לראות את השימוש הבסיסי בשמירה במטמון בכל בקשה, ואת האפשרות לספק את האובייקטים שנשמרו במטמון כשהשרת מחזיר תגובה שהיא לא תגובה מוצלחת (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 בייטים, ולכן מגבלת האחסון שלנו היא 2.6MB בסך הכול במקום 5MB. הסיבה המלאה לאחזור ולשמירה במטמון של הדפים או התגיות האלה מחוץ להיקף של מטמון האפליקציה מפורטת בקטע הבא.

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

מהמפרט: מאפיין ארגז החול, כשמציינים אותו, מאפשר להגדיר קבוצה של הגבלות נוספות על כל תוכן שמארח ה-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 משתמשת ב-appendChild כליבת הפעולה שלה, עם חזרה ל-append רק במקרה של חריגה. גם JQuery Mobile משתמשת בו. עם זאת, לא ביצעתי בדיקות מקיפות לגבי innerHTML “stops working randomly”, אבל יהיה מעניין מאוד לראות את כל הפלטפורמות שמושפעות מכך. יהיה מעניין לראות איזו גישה מניבה ביצועים טובים יותר… שמעתי טענות משני הצדדים גם לגבי זה.

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

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

סנכרון Edge

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

WIFI Async

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

סיכום

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