تقنيات HTML5 لتحسين أداء الجوّال

مقدمة

إنّ عمليات إعادة التحميل الدوّارة، والانتقالات المتقطّعة بين الصفحات، والتأخيرات الدورية في أحداث النقر ليست سوى بعض المشاكل التي تواجهها بيئات الويب على الأجهزة الجوّالة اليوم. يحاول المطوّرون الاقتراب قدر الإمكان من التطبيقات الأصلية، ولكن غالبًا ما تعيقهم عمليات الاختراق وعمليات إعادة الضبط وأُطر العمل الصارمة.

في هذه المقالة، سنناقش الحد الأدنى من المتطلبات اللازمة لإنشاء تطبيق ويب HTML5 للأجهزة الجوّالة. والهدف الرئيسي هو الكشف عن التعقيدات الخفية التي تحاول أُطر العمل الحالية للأجهزة الجوّالة إخفاءها. ستتعرّف على نهج بسيط (باستخدام واجهات برمجة تطبيقات HTML5 الأساسية) وأساسيات تمكّنك من كتابة إطار العمل الخاص بك أو المساهمة في الإطار الذي تستخدمه حاليًا.

تسريع الأجهزة

عادةً ما تتعامل وحدات معالجة الرسومات مع النماذج الثلاثية الأبعاد المفصّلة أو المخططات البيانية للتصميم بمساعدة الكمبيوتر (CAD)، ولكن في هذه الحالة، نريد أن تظهر رسوماتنا البدائية (عناصر div والخلفيات والنصوص ذات الظلال المسقطة والصور وما إلى ذلك) بسلاسة وأن تتحرك بسلاسة من خلال وحدة معالجة الرسومات. المؤسف في الأمر هو أن معظم مطوري الواجهة الأمامية يتركون عملية الرسوم المتحركة هذه لإطار عمل تابع لجهة خارجية دون الاهتمام بالدلالات، ولكن هل يجب إخفاء ميزات CSS3 الأساسية هذه؟ إليك بعض الأسباب التي تجعل الاهتمام بهذه الأمور مهمًا:

  1. تخصيص الذاكرة والعبء الحسابي: إذا كنت تقوم بتركيب كل عنصر في DOM فقط من أجل تسريع الأجهزة، فقد يطاردك الشخص التالي الذي يعمل على التعليمات البرمجية الخاصة بك ويضربك بشدة.

  2. استهلاك الطاقة: من الواضح أنه عندما تبدأ الأجهزة في العمل، تبدأ البطارية أيضًا. عند تطوير تطبيقات الويب للأجهزة الجوّالة، يضطر المطوّرون إلى مراعاة مجموعة واسعة من القيود التي تفرضها الأجهزة أثناء كتابة هذه التطبيقات. وسيكون هذا أكثر انتشارًا عندما يبدأ صانعو المتصفحات في تمكين الوصول إلى المزيد والمزيد من أجهزة الجهاز.

  3. التعارضات: واجهتُ سلوكًا غير مستقر عند تطبيق تسريع الأجهزة على أجزاء من الصفحة تم تسريعها من قبل. لذلك، من المهم جدًا معرفة ما إذا كان لديك تسارع متداخل.

لجعل تفاعل المستخدم سلسًا وقريبًا من التطبيق الأصلي قدر الإمكان، يجب أن نجعل المتصفح يعمل لصالحنا. من الناحية المثالية، نريد أن تقوم وحدة المعالجة المركزية للجهاز المحمول بإعداد الحركة الأولية، ثم تكون وحدة معالجة الرسومات مسؤولة فقط عن تركيب طبقات مختلفة أثناء عملية الحركة. هذا ما تفعله translate3d وscale3d وtranslateZ - فهي تمنح العناصر المتحركة طبقتها الخاصة، مما يسمح للجهاز بعرض كل شيء معًا بسلاسة. لمزيد من المعلومات حول ميزة "التركيب المسرَّع" وطريقة عمل WebKit، يقدّم "أريا هدايت" الكثير من المعلومات المفيدة في مدونته.

الانتقالات بين الصفحات

لنلقِ نظرة على ثلاثة من أكثر أساليب تفاعل المستخدم شيوعًا عند تطوير تطبيق ويب للأجهزة الجوّالة: تأثيرات التمرير والقلب والتدوير.

يمكنك الاطّلاع على هذا الرمز أثناء العمل هنا http://slidfast.appspot.com/slide-flip-rotate.html (ملاحظة: تم إنشاء هذا العرض التوضيحي لجهاز جوّال، لذا يمكنك تشغيل محاكي أو استخدام هاتفك أو جهازك اللوحي أو تقليل حجم نافذة المتصفّح إلى 1024 بكسل تقريبًا أو أقل).

أولاً، سنحلّل تأثيرات الانتقال "انزلاق" و"قلب" و"تدوير" وكيفية تسريعها. لاحظ كيف أن كل حركة تستغرق ثلاثة أو أربعة أسطر فقط من 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';
}

حاولتُ استخدام منحنى بيزييه التكعيبي لإضفاء أفضل شعور أصلي على الانتقالات، ولكنّ تأثير التلاشي التدريجي كان كافيًا.

أخيرًا، لإجراء عملية التنقّل، علينا استدعاء طرق 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 مقدار الذاكرة التي نستخدمها عند إرسال عمليات الرسم إلى مخزن النسخ الاحتياطي. يخبرك هذا بالضبط عن مقدار الضغط الذي تضعه على الجهاز المحمول، وربما يعطي تلميحات حول كيفية استنزاف استخدام وحدة معالجة الرسومات لبطارية جهاز الاختبار.

لنفتح الآن Chrome لنطّلع على بعض المعلومات المفيدة حول عدد اللقطات في الثانية (FPS):

  1. افتح متصفّح الويب Google Chrome.
  2. في شريط عنوان URL، اكتب about:flags.
  3. انتقِل للأسفل بضعة عناصر وانقر على "تفعيل" (Enable) لعداد اللقطات في الثانية (FPS Counter).

إذا عرضت هذه الصفحة في إصدار Chrome المحسَّن، سيظهر لك عدّاد اللقطات في الثانية باللون الأحمر في أعلى يمين الصفحة.

Chrome FPS

هذه هي الطريقة التي نعرف بها أنّ ميزة "تسريع الأجهزة" مفعَّلة. ويعطينا أيضًا فكرة عن كيفية تشغيل الصور المتحركة وما إذا كان هناك أي تسرُّب (صور متحركة تعمل بشكل مستمر ويجب إيقافها).

هناك طريقة أخرى لتصوّر تسريع الأجهزة، وهي فتح الصفحة نفسها في Safari (باستخدام متغيّرات البيئة المذكورة أعلاه). كل عنصر DOM مسرَّع له لون أحمر. يوضّح لنا ذلك ما يتم تركيبه بالضبط حسب الطبقة. لاحظ أنّ التنقّل الأبيض ليس أحمر لأنّه لم يتم تسريعه.

جهة اتصال مركّبة

يتوفّر أيضًا إعداد مشابه لمتصفّح Chrome في about:flags "حدود طبقة العرض المركّبة".

هناك طريقة أخرى رائعة للاطّلاع على الطبقات المركّبة، وهي عرض العرض التوضيحي لأوراق الشجر المتساقطة في WebKit أثناء تطبيق هذا التعديل.

أوراق مركّبة

أخيرًا، لفهم أداء أجهزة الرسومات في تطبيقنا بشكل حقيقي، دعونا نلقي نظرة على كيفية استهلاك الذاكرة. هنا نرى أنّنا نرسل 1.38 ميغابايت من تعليمات الرسم إلى مخازن CoreAnimation المؤقتة على نظام التشغيل Mac. تتم مشاركة مخازن الذاكرة المؤقتة لـ Core Animation بين OpenGL ES ووحدة معالجة الرسومات لإنشاء وحدات البكسل النهائية التي تراها على الشاشة.

Coreanimation 1

عندما نغيّر حجم نافذة المتصفّح أو نكبّرها، نلاحظ أنّ الذاكرة تتوسّع أيضًا.

Coreanimation 2

يمنحك هذا فكرة عن كيفية استهلاك الذاكرة على جهازك الجوّال فقط إذا قمت بتغيير حجم المتصفح إلى الأبعاد الصحيحة. إذا كنت تقوم بتصحيح الأخطاء أو الاختبار لبيئات iPhone، فقم بتغيير الحجم إلى 480 بكسل × 320 بكسل. أصبحنا نفهم الآن بدقة آلية عمل ميزة "تسريع الأجهزة" وما يتطلّبه تصحيح الأخطاء. إنّ قراءة المعلومات حول هذا الموضوع أمر، ولكن رؤية مخازن ذاكرة وحدة معالجة الرسومات تعمل بشكل مرئي يوضّح الأمور حقًا.

وراء الكواليس: الجلب والتخزين المؤقت

حان الوقت الآن للانتقال إلى المستوى التالي في تخزين الصفحات والموارد مؤقتًا. على غرار النهج الذي تستخدمه JQuery Mobile وأُطر العمل المشابهة، سنقوم بالجلب المسبق لصفحاتنا وتخزينها مؤقتًا من خلال طلبات AJAX المتزامنة.

لنتناول بعض المشاكل الأساسية في الويب على الأجهزة الجوّالة وأسباب حاجتنا إلى معالجتها:

  • الجلب: يتيح الجلب المُسبق لصفحاتنا للمستخدمين استخدام التطبيق بلا إنترنت، كما يتيح عدم الانتظار بين إجراءات التنقّل. بالطبع، لا نريد خنق النطاق الترددي للجهاز عندما يتصل الجهاز بالإنترنت، لذلك نحتاج إلى استخدام هذه الميزة باعتدال.
  • التخزين المؤقت: بعد ذلك، نريد اتّباع نهج متزامن أو غير متزامن عند جلب هذه الصفحات وتخزينها مؤقتًا. نحتاج أيضًا إلى استخدام localStorage (لأنّه متوافق مع الأجهزة)، ولكنّه للأسف ليس غير متزامن.
  • ‫AJAX وتحليل الردّ: إنّ استخدام innerHTML() لإدراج ردّ AJAX في نموذج DOM هو أمر خطير (وغير موثوق به؟). بدلاً من ذلك، نستخدم آلية موثوقة لإدراج استجابة AJAX ومعالجة المكالمات المتزامنة. نستفيد أيضًا من بعض الميزات الجديدة في HTML5 لتحليل xhr.responseText.

استنادًا إلى الرمز البرمجي من العرض التوضيحي Slide, Flip, and Rotate، نبدأ بإضافة بعض الصفحات الثانوية والربط بها. بعد ذلك، سنحلّل الروابط وننشئ انتقالات بشكل فوري.

شاشة 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، يتم تخزين كل بايت واحد على أنّه بايتان، ما يؤدي إلى خفض الحدّ الأقصى لمساحة التخزين من 5 ميغابايت إلى 2.6 ميغابايت في الإجمالي. يتم الكشف عن السبب الكامل لجلب هذه الصفحات/الترميز وتخزينها مؤقتًا خارج نطاق ذاكرة التخزين المؤقت للتطبيق في القسم التالي.

بفضل التطورات الأخيرة في عنصر iframe باستخدام HTML5، أصبح لدينا الآن طريقة بسيطة وفعّالة لتحليل responseText الذي نحصل عليه من طلب AJAX. هناك الكثير من المحللات اللغوية لـ JavaScript التي تتألف من 3000 سطر والتعبيرات العادية التي تزيل العلامات البرمجية وما إلى ذلك. ولكن لماذا لا ندع المتصفّح يقوم بما يتقنه؟ في هذا المثال، سنكتب 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 أو شبكة الجيل الثالث في بعض المناطق الحضرية، ولكن قد تتيح المناطق النائية تقنيات شبكة الجيل الثاني الأبطأ بكثير). يعالج الرمز التالي معظم سيناريوهات الاتصال.

يوفّر الرمز التالي:

  • الوصول بلا إنترنت من خلال "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، ولكن كل ما فعلته هنا هو إخبارها بجلب الموارد بشكل غير متزامن (صحيح) أو متزامن (خطأ) لاتصال معين.

المخطط الزمني للطلبات على Edge (المتزامنة)

Edge Sync

المخطط الزمني لطلبات Wi-Fi (غير المتزامنة)

WIFI Async

يتيح ذلك على الأقل طريقة ما لتعديل تجربة المستخدم استنادًا إلى الاتصالات البطيئة أو السريعة. هذا ليس بأي حال من الأحوال حلاً شاملاً. هناك مهمة أخرى تتمثل في عرض نافذة منبثقة للتحميل عند النقر على رابط (على الاتصالات البطيئة) بينما قد لا يزال التطبيق يجلب صفحة هذا الرابط في الخلفية. الهدف الرئيسي هنا هو تقليل وقت الاستجابة مع الاستفادة من الإمكانات الكاملة لاتصال المستخدم بأحدث وأفضل ما يقدّمه HTML5. يمكنك الاطّلاع على العرض التوضيحي لميزة "رصد الشبكة" هنا.

الخاتمة

إنّ رحلة تطوير تطبيقات HTML5 للأجهزة الجوّالة لا تزال في بدايتها. الآن ترى الأسس البسيطة والأساسية لـ "إطار عمل" للهاتف المحمول تم إنشاؤه فقط حول HTML5 والتقنيات الداعمة له. أعتقد أنّه من المهم أن يعمل المطوّرون على هذه الميزات وأن يعالجوها في جوهرها، وليس من خلال تغليفها.