เทคนิค HTML5 สำหรับการเพิ่มประสิทธิภาพการทำงานบนมือถือ

บทนำ

การรีเฟรชที่วนไปวนมา การเปลี่ยนหน้าเว็บที่กระตุก และการหน่วงเวลาเป็นระยะในเหตุการณ์การแตะเป็นเพียงปัญหาบางส่วนในสภาพแวดล้อมเว็บบนอุปกรณ์เคลื่อนที่ในปัจจุบัน นักพัฒนาแอปพยายามทำให้แอปทำงานได้ใกล้เคียงกับแอปเนทีฟมากที่สุด แต่มักถูกขัดจังหวะด้วยการแฮ็ก การรีเซ็ต และเฟรมเวิร์กแบบตายตัว

ในบทความนี้ เราจะพูดถึงสิ่งที่จําเป็นขั้นต่ำในการสร้างเว็บแอป HTML5 สําหรับอุปกรณ์เคลื่อนที่ โดยจุดประสงค์หลักคือการเปิดเผยความซับซ้อนที่ซ่อนอยู่ซึ่งเฟรมเวิร์กบนอุปกรณ์เคลื่อนที่ในปัจจุบันพยายามปกปิด คุณจะเห็นแนวทางแบบเรียบง่าย (โดยใช้ HTML5 API หลัก) และพื้นฐานเบื้องต้นที่จะช่วยให้คุณเขียนเฟรมเวิร์กของคุณเองหรือมีส่วนร่วมในเฟรมเวิร์กที่คุณใช้อยู่ได้

การเร่งฮาร์ดแวร์

โดยปกติแล้ว GPU จะจัดการการสร้างแบบจำลอง 3 มิติโดยละเอียดหรือแผนภาพ CAD แต่ในกรณีนี้เราต้องการให้ภาพวาดพื้นฐานของเรา (div, พื้นหลัง, ข้อความที่มีเงาตกกระทบ, รูปภาพ ฯลฯ) แสดงได้อย่างราบรื่นและเคลื่อนไหวได้อย่างราบรื่นผ่าน GPU โชคไม่ดีที่นักพัฒนาฟรอนท์เอนด์ส่วนใหญ่เปลี่ยนกระบวนการของภาพเคลื่อนไหวนี้ออกไปสำหรับเฟรมเวิร์กของบุคคลที่สามโดยไม่กังวลเกี่ยวกับความหมาย แต่คุณลักษณะ CSS3 หลักเหล่านี้ควรกำบังไหม ฉันขออธิบายเหตุผล 2-3 ข้อว่าเหตุใดการดูแลเรื่องนี้จึงเป็นสิ่งสำคัญ

  1. การจัดสรรหน่วยความจำและภาระการประมวลผล - หากคุณคอมโพสองค์ประกอบทุกรายการใน DOM เพื่อประโยชน์ในการเร่งด้วยฮาร์ดแวร์ บุคคลคนถัดไปที่ทำงานกับโค้ดของคุณอาจไล่ล่าคุณและทุบตีคุณอย่างรุนแรง

  2. การใช้พลังงาน - เมื่อฮาร์ดแวร์ทำงาน แบตเตอรี่ก็ทำงานด้วย เมื่อพัฒนาแอปสำหรับอุปกรณ์เคลื่อนที่ นักพัฒนาแอปต้องคำนึงถึงข้อจำกัดของอุปกรณ์ที่หลากหลายขณะเขียนเว็บแอปบนอุปกรณ์เคลื่อนที่ การดำเนินการนี้จะแพร่หลายมากขึ้นเมื่อผู้ผลิตเบราว์เซอร์เริ่มเปิดใช้การเข้าถึงฮาร์ดแวร์ของอุปกรณ์มากขึ้น

  3. ข้อขัดแย้ง - ฉันพบปัญหาการทำงานขัดข้องเมื่อใช้การเร่งฮาร์ดแวร์กับบางส่วนของหน้าเว็บที่มีการเร่งแล้ว ดังนั้น การทราบว่าคุณมีการเร่งความเร็วที่ทับซ้อนกันหรือไม่จึงสำคัญมาก

เราต้องทำให้เบราว์เซอร์ทำงานตามที่เราต้องการเพื่อให้การโต้ตอบของผู้ใช้เป็นไปอย่างราบรื่นและใกล้เคียงกับการใช้งานแบบดั้งเดิมมากที่สุด ตามหลักการแล้ว เราต้องการให้ CPU ของอุปกรณ์เคลื่อนที่สร้างภาพเคลื่อนไหวเริ่มต้น จากนั้นให้ GPU รับผิดชอบเฉพาะการคอมโพสเลเยอร์ต่างๆ ในระหว่างกระบวนการภาพเคลื่อนไหว นี่คือสิ่งที่ translate3d,scale3d และ translateZ ทำโดยให้องค์ประกอบภาพเคลื่อนไหวมีเลเยอร์ของตัวเอง ทำให้อุปกรณ์แสดงผลทุกอย่างร่วมกันได้อย่างราบรื่น Ariya Hidayat มีข้อมูลดีๆ มากมายในบล็อกของเขาเพื่อดูข้อมูลเพิ่มเติมเกี่ยวกับการประสานแบบเร่งและวิธีการทำงานของ WebKit

ทรานซิชันหน้าเว็บ

มาดูแนวทางการโต้ตอบกับผู้ใช้ 3 วิธีที่พบบ่อยที่สุดเมื่อพัฒนาเว็บแอปบนอุปกรณ์เคลื่อนที่ ได้แก่ เอฟเฟกต์การเลื่อน การพลิก และการหมุน

คุณดูการทำงานของโค้ดนี้ได้ที่ http://slidfast.appspot.com/slide-flip-rotate.html (หมายเหตุ: การสาธิตนี้สร้างขึ้นสำหรับอุปกรณ์เคลื่อนที่ ดังนั้นให้เปิดโปรแกรมจำลอง ใช้โทรศัพท์หรือแท็บเล็ต หรือลดขนาดหน้าต่างเบราว์เซอร์ให้เหลือประมาณ 1024 พิกเซลหรือน้อยกว่านั้น)

ก่อนอื่น เราจะวิเคราะห์ทรานซิชันการเลื่อน การพลิก และการหมุน รวมถึงวิธีเร่งความเร็ว สังเกตว่าแต่ละภาพเคลื่อนไหวใช้ CSS และ JavaScript เพียง 3-4 บรรทัดเท่านั้น

การเลื่อน

การเปลี่ยนหน้าแบบเลื่อนเป็นแนวทางการเปลี่ยนที่พบบ่อยที่สุดจาก 3 แนวทาง ซึ่งเลียนแบบความรู้สึกแบบดั้งเดิมของแอปพลิเคชันบนอุปกรณ์เคลื่อนที่ ระบบจะเรียกใช้ทรานซิชันของภาพนิ่งเพื่อนำพื้นที่เนื้อหาใหม่มาไว้ในพอร์ตโฟกัส

สำหรับเอฟเฟกต์ภาพสไลด์ ขั้นแรกเราต้องประกาศมาร์กอัปดังนี้

<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 ที่จัดการการตรวจจับและการวางแนวอุปกรณ์เคลื่อนที่กัน เรารองรับทุกอุปกรณ์และทุกความละเอียด (ดูความละเอียดของคําค้นหาสื่อ) เราใช้เพียงตัวอย่างง่ายๆ 2-3 รายการในการแสดงตัวอย่างนี้เพื่อครอบคลุมมุมมองแนวตั้งและแนวนอนส่วนใหญ่ในอุปกรณ์เคลื่อนที่ ซึ่งการตั้งค่านี้ยังมีประโยชน์สำหรับการใช้การเร่งฮาร์ดแวร์ต่ออุปกรณ์ด้วย ตัวอย่างเช่น เนื่องจาก WebKit เวอร์ชันเดสก์ท็อปจะเร่งองค์ประกอบที่เปลี่ยนรูปแบบทั้งหมด (ไม่ว่าจะเป็น 2 มิติหรือ 3 มิติ) จึงควรสร้าง Media Query และยกเว้นการเร่งที่ระดับนั้น โปรดทราบว่าเทคนิคการเร่งฮาร์ดแวร์ไม่ได้ช่วยเพิ่มความเร็วใน 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 แบบค่อยๆ เปลี่ยนสำหรับการพลิกหน้า 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 3 มิติ แต่น่าเสียดายที่ 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);
}

การแก้ไขข้อบกพร่องการเร่งฮาร์ดแวร์

เมื่อพูดถึงทรานซิชันพื้นฐานแล้ว เรามาลองดูกลไกการทํางานและวิธีคอมโพสิทกัน

มาเปิดเบราว์เซอร์ 2-3 ตัวและ IDE ที่คุณเลือกเพื่อเริ่มเซสชันการแก้ไขข้อบกพร่องอันน่าอัศจรรย์นี้กัน ก่อนอื่น ให้เริ่ม Safari จากบรรทัดคำสั่งเพื่อใช้ประโยชน์จากตัวแปรสภาพแวดล้อมการแก้ไขข้อบกพร่องบางอย่าง เราใช้ Mac ดังนั้นคำสั่งอาจแตกต่างกันไปตามระบบปฏิบัติการของคุณ เปิดเทอร์มินัลแล้วพิมพ์คำสั่งต่อไปนี้

  • $> export CA_COLOR_OPAQUE=1
  • $> export CA_LOG_MEMORY_USAGE=1
  • $> /Applications/Safari.app/Contents/MacOS/Safari

ซึ่งจะเปิด Safari พร้อมตัวช่วยแก้ไขข้อบกพร่อง 2 ตัว CA_COLOR_OPAQUE แสดงองค์ประกอบที่คอมโพสหรือเร่งความเร็วจริง CA_LOG_MEMORY_USAGE แสดงปริมาณหน่วยความจําที่เราใช้เมื่อส่งการดำเนินการวาดไปยังพื้นที่เก็บข้อมูลสำรอง ข้อมูลนี้บอกได้ว่าคุณกำลังใช้อุปกรณ์เคลื่อนที่มากเพียงใด และอาจบอกเป็นนัยถึงวิธีที่การใช้งาน GPU อาจจะทำให้แบตเตอรี่ของอุปกรณ์เป้าหมายหมดเร็ว

มาเปิด Chrome เพื่อดูข้อมูลเฟรมต่อวินาที (FPS) ที่ดีกัน

  1. เปิดเว็บเบราว์เซอร์ Google Chrome
  2. ในแถบ URL ให้พิมพ์ about:flags
  3. เลื่อนลง 2-3 รายการแล้วคลิก "เปิดใช้" สำหรับตัวนับ FPS

หากคุณดูหน้านี้ใน Chrome เวอร์ชันที่อัปเกรดแล้ว คุณจะเห็นตัวนับ FPS สีแดงที่มุมซ้ายบน

FPS ของ Chrome

วิธีนี้ช่วยให้เราทราบว่าการเร่งฮาร์ดแวร์เปิดอยู่ นอกจากนี้ยังช่วยให้เรามีไอเดียเกี่ยวกับวิธีการทำงานของภาพเคลื่อนไหวและหากคุณเกิดการรั่วไหลใดๆ (ภาพเคลื่อนไหวที่กำลังทำงานอยู่อย่างต่อเนื่องซึ่งควรหยุด)

อีกวิธีหนึ่งในการเห็นภาพการเร่งด้วยฮาร์ดแวร์จริงคือให้เปิดหน้าเดียวกันใน Safari (โดยใช้ตัวแปรสภาพแวดล้อมที่ฉันพูดถึงข้างต้น) องค์ประกอบ DOM ที่เร่งความเร็วทุกรายการจะมีสีแดง ซึ่งจะแสดงให้เราเห็นว่ามีการ Composite ใดเป็นเลเยอร์ โปรดสังเกตว่าการนำทางสีขาวไม่ใช่สีแดง เพราะไม่ได้เร่ง

รายชื่อติดต่อแบบคอมโพสิต

การตั้งค่าที่คล้ายกันสำหรับ Chrome มีอยู่ใน about:flags "เส้นขอบเลเยอร์การแสดงผลแบบคอมโพสิต"

อีกวิธีหนึ่งที่ยอดเยี่ยมในการดูเลเยอร์แบบคอมโพสิตคือดูการสาธิตใบไม้ร่วงของ WebKit ขณะใช้ม็อดนี้

ใบไม้

และสุดท้าย เพื่อให้เข้าใจประสิทธิภาพฮาร์ดแวร์กราฟิกของแอปพลิเคชันของเราอย่างแท้จริง เรามาดูการใช้หน่วยความจำกันดีกว่า ในส่วนนี้ เราเห็นว่าเราส่งคำสั่งวาดขนาด 1.38 MB ไปยังบัฟเฟอร์ CoreAnimation ใน Mac OS บัฟเฟอร์หน่วยความจำของ Core Animation จะแชร์ระหว่าง OpenGL ES และ GPU เพื่อสร้างพิกเซลสุดท้ายที่คุณเห็นบนหน้าจอ

Coreanimation 1

เมื่อเราปรับขนาดหรือขยายหน้าต่างเบราว์เซอร์ จะเห็นหน่วยความจำเพิ่มขึ้นด้วย

Coreanimation 2

ซึ่งจะทำให้คุณทราบว่าอุปกรณ์เคลื่อนที่ใช้หน่วยความจำอย่างไรก็ต่อเมื่อคุณปรับขนาดเบราว์เซอร์เป็นขนาดที่ถูกต้องเท่านั้น หากคุณกำลังแก้ไขข้อบกพร่องหรือทดสอบสภาพแวดล้อมของ iPhone ให้ปรับขนาดเป็น 480 x 320 พิกเซล ตอนนี้เราเข้าใจวิธีการทำงานของการเร่งด้วยฮาร์ดแวร์และสิ่งที่ต้องทำเพื่อแก้ไขข้อบกพร่องแล้ว การอ่านเกี่ยวกับเรื่องนี้ก็ดี แต่การได้เห็นบัฟเฟอร์หน่วยความจำ GPU ทำงานจริง ๆ นั้นช่วยให้เห็นภาพได้ชัดเจนขึ้น

เบื้องหลัง: การดึงข้อมูลและการแคช

ตอนนี้ถึงเวลายกระดับการแคชหน้าเว็บและทรัพยากรไปอีกขั้น เราจะดึงข้อมูลล่วงหน้าและแคชหน้าเว็บด้วยการเรียก AJAX พร้อมกัน ซึ่งคล้ายกับแนวทางที่ JQuery Mobile และเฟรมเวิร์กที่เกี่ยวข้องใช้

มาดูปัญหาหลักๆ ของเว็บบนอุปกรณ์เคลื่อนที่และเหตุผลที่เราต้องดำเนินการนี้กัน

  • การดึงข้อมูล: การดึงข้อมูลหน้าเว็บล่วงหน้าช่วยให้ผู้ใช้ใช้แอปแบบออฟไลน์ได้และไม่ต้องรอระหว่างการไปยังส่วนต่างๆ แน่นอนว่าเราไม่ต้องการให้แบนด์วิดท์ของอุปกรณ์ถูกจำกัดเมื่ออุปกรณ์ออนไลน์ ดังนั้นเราจึงต้องใช้ฟีเจอร์นี้อย่างจำกัด
  • การแคช: ถัดไป เราต้องการแนวทางแบบพร้อมกันหรือไม่พร้อมกันเมื่อดึงข้อมูลและแคชหน้าเว็บเหล่านี้ นอกจากนี้ เรายังต้องใช้ 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 ไบต์ ซึ่งทำให้ขีดจํากัดพื้นที่เก็บข้อมูลลดลงจาก 5 MB เป็น 2.6 MB รวม ดูสาเหตุทั้งหมดในการดึงข้อมูลและแคชหน้า/มาร์กอัปเหล่านี้นอกขอบเขตแคชของแอปพลิเคชันได้ในส่วนถัดไป

ความก้าวหน้าล่าสุดขององค์ประกอบ 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 แต่การแทรกการตอบกลับจากเซิร์ฟเวอร์ (ไม่ว่าดีหรือไม่ดี) ลงในส่วนที่ไม่ได้ตรวจสอบเป็นแนวทางปฏิบัติที่อันตราย ในระหว่างที่เขียนบทความนี้ ฉันไม่พบใครเลยที่ใช้อะไรก็ตามยกเว้น InenHTML เราทราบดีว่า JQuery ใช้การต่อท้ายเป็นหัวใจสําคัญโดยมีสำรองการต่อท้ายสำหรับข้อยกเว้นเท่านั้น และ JQuery Mobile ก็ใช้รูปแบบนี้ด้วย อย่างไรก็ตาม เรายังไม่ได้ทำการทดสอบอย่างหนักเกี่ยวกับ innerHTML ที่ "หยุดทํางานแบบสุ่ม" แต่เราอยากทราบมากว่าแพลตฟอร์มทั้งหมดที่ได้รับผลกระทบคืออะไร นอกจากนี้ เรายังอยากทราบว่าแนวทางใดมีประสิทธิภาพมากกว่ากันด้วย… เราได้ยินการกล่าวอ้างจากทั้ง 2 ฝ่ายเกี่ยวกับเรื่องนี้ด้วย

การตรวจหา การจัดการ และการสร้างโปรไฟล์ประเภทเครือข่าย

เมื่อสามารถบัฟเฟอร์ (หรือแคชตามการคาดการณ์) เว็บแอปได้แล้ว เราจึงต้องจัดเตรียมฟีเจอร์การตรวจหาการเชื่อมต่อที่เหมาะสมซึ่งทําให้แอปของเราฉลาดขึ้น โหมดออนไลน์/ออฟไลน์และความเร็วในการเชื่อมต่อเป็นจุดที่มีการพัฒนาแอปบนอุปกรณ์เคลื่อนที่อย่างมาก ป้อน 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);

ใน EventListener ด้านบน เราต้องบอกโค้ดว่ามีการเรียกใช้จากเหตุการณ์หรือคำขอหน้าเว็บหรือการรีเฟรชจริงหรือไม่ สาเหตุหลักคือเหตุการณ์ onload ของร่างกายจะไม่ทริกเกอร์เมื่อสลับระหว่างโหมดออนไลน์และโหมดออฟไลน์

ต่อไป เรามีการตรวจสอบง่ายๆ สําหรับเหตุการณ์ ononline หรือ onload โค้ดนี้จะรีเซ็ตลิงก์ที่ปิดใช้เมื่อเปลี่ยนจากออฟไลน์เป็นออนไลน์ แต่หากแอปนี้มีความซับซ้อนมากขึ้น คุณอาจแทรกตรรกะที่ดึงข้อมูลเนื้อหาต่อหรือจัดการ UX สำหรับการเชื่อมต่อที่ไม่ต่อเนื่อง

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

ไทม์ไลน์คำขอ Wi-Fi (แบบไม่พร้อมกัน)

WIFI Async

วิธีนี้จะช่วยให้ปรับประสบการณ์ของผู้ใช้ได้บางวิธีเป็นอย่างน้อย โดยอิงตามการเชื่อมต่อที่ช้าหรือเร็ว การดำเนินการนี้ไม่ใช่วิธีแก้ปัญหาที่สมบูรณ์แบบ อีกสิ่งที่ต้องทําคือแสดงโมดัลการโหลดเมื่อมีการคลิกลิงก์ (ในการเชื่อมต่อที่ช้า) ขณะที่แอปอาจยังดึงข้อมูลหน้าของลิงก์นั้นอยู่เบื้องหลัง ประเด็นสำคัญในที่นี้คือการลดเวลาในการตอบสนอง ในขณะเดียวกันก็เป็นการใช้ประโยชน์จากความสามารถเต็มรูปแบบของการเชื่อมต่อของผู้ใช้ด้วย HTML5 รุ่นล่าสุดและดีที่สุดที่มีให้ ดูการสาธิตการตรวจหาเครือข่ายได้ที่นี่

บทสรุป

เส้นทางของแอป HTML5 บนอุปกรณ์เคลื่อนที่เพิ่งเริ่มต้นขึ้น ตอนนี้คุณได้เห็นรากฐานพื้นฐานง่ายๆ ของ “เฟรมเวิร์ก” มือถือที่สร้างขึ้นจาก HTML5 เท่านั้นและรองรับเทคโนโลยีต่างๆ เราคิดว่านักพัฒนาซอฟต์แวร์ควรใช้และจัดการกับฟีเจอร์เหล่านี้ที่ส่วนสำคัญ ไม่ใช่ใช้ Wrapper มาปกปิด