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

ווסלי היילס
ווסלי היילס

מבוא

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

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

האצת חומרה

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

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

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

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

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

מעברי דפים

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

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

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

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

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

  • $> ייצוא CA_COLOR_OPAQUE=1
  • $> ייצוא 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. בסרגל של כתובות ה-URL, מקלידים about:flags.
  3. גוללים למטה כמה פריטים ולוחצים על 'הפעלה' למונה FPS.

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

FPS ב-Chrome

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

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

איש קשר מורכב

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

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

עלים מושלגים

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

ליבה 1

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

ליבה 2

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

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

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

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

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

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

דף הבית של iPhone

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

כפי שאפשר לראות, אנחנו ממנפים כאן סימון סמנטי. רק קישור לדף אחר. דף הצאצא בנוי לפי אותו מבנה של צומת/מחלקה כמו ההורה שלו. נוכל לקחת את זה צעד אחד קדימה ולהשתמש במאפיין data-* לצומתי 'דף' וכו'. זהו דף הפרטים (צאצא) שממוקם בקובץ 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. יש הסבר מתקדם יותר לגבי השימוש ב-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 מוסתר זמני. אנחנו משתמשים במאפיין "Sandbox" של 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 "הפסיק לעבוד באופן אקראי", אבל יהיה מאוד מעניין לראות על כל הפלטפורמות שיש לכך השפעה. יהיה גם מעניין לראות איזו גישה מניבה ביצועים טובים יותר... גם אני שמעתי טענות משני הצדדים.

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

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

יש אינספור שינויים שאנחנו יכולים לבצע בתהליך getAndCache, אבל כל מה שעשיתי כאן היה לשלוף את המשאבים האסינכרוניים (true) או הסינכרוניים (false) עבור חיבור נתון.

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

סנכרון Edge

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

אסינכרוניזציה של Wi-Fi

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

סיכום

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