บทนำ
การรีเฟรชที่วนไปวนมา การเปลี่ยนหน้าเว็บที่กระตุก และการหน่วงเวลาเป็นระยะในเหตุการณ์การแตะเป็นเพียงปัญหาบางส่วนในสภาพแวดล้อมเว็บบนอุปกรณ์เคลื่อนที่ในปัจจุบัน นักพัฒนาแอปพยายามทำให้แอปทำงานได้ใกล้เคียงกับแอปเนทีฟมากที่สุด แต่มักถูกขัดจังหวะด้วยการแฮ็ก การรีเซ็ต และเฟรมเวิร์กแบบตายตัว
ในบทความนี้ เราจะพูดถึงสิ่งที่จําเป็นขั้นต่ำในการสร้างเว็บแอป HTML5 สําหรับอุปกรณ์เคลื่อนที่ โดยจุดประสงค์หลักคือการเปิดเผยความซับซ้อนที่ซ่อนอยู่ซึ่งเฟรมเวิร์กบนอุปกรณ์เคลื่อนที่ในปัจจุบันพยายามปกปิด คุณจะเห็นแนวทางแบบเรียบง่าย (โดยใช้ HTML5 API หลัก) และพื้นฐานเบื้องต้นที่จะช่วยให้คุณเขียนเฟรมเวิร์กของคุณเองหรือมีส่วนร่วมในเฟรมเวิร์กที่คุณใช้อยู่ได้
การเร่งฮาร์ดแวร์
โดยปกติแล้ว GPU จะจัดการการสร้างแบบจำลอง 3 มิติโดยละเอียดหรือแผนภาพ CAD แต่ในกรณีนี้เราต้องการให้ภาพวาดพื้นฐานของเรา (div, พื้นหลัง, ข้อความที่มีเงาตกกระทบ, รูปภาพ ฯลฯ) แสดงได้อย่างราบรื่นและเคลื่อนไหวได้อย่างราบรื่นผ่าน GPU โชคไม่ดีที่นักพัฒนาฟรอนท์เอนด์ส่วนใหญ่เปลี่ยนกระบวนการของภาพเคลื่อนไหวนี้ออกไปสำหรับเฟรมเวิร์กของบุคคลที่สามโดยไม่กังวลเกี่ยวกับความหมาย แต่คุณลักษณะ CSS3 หลักเหล่านี้ควรกำบังไหม ฉันขออธิบายเหตุผล 2-3 ข้อว่าเหตุใดการดูแลเรื่องนี้จึงเป็นสิ่งสำคัญ
การจัดสรรหน่วยความจำและภาระการประมวลผล - หากคุณคอมโพสองค์ประกอบทุกรายการใน DOM เพื่อประโยชน์ในการเร่งด้วยฮาร์ดแวร์ บุคคลคนถัดไปที่ทำงานกับโค้ดของคุณอาจไล่ล่าคุณและทุบตีคุณอย่างรุนแรง
การใช้พลังงาน - เมื่อฮาร์ดแวร์ทำงาน แบตเตอรี่ก็ทำงานด้วย เมื่อพัฒนาแอปสำหรับอุปกรณ์เคลื่อนที่ นักพัฒนาแอปต้องคำนึงถึงข้อจำกัดของอุปกรณ์ที่หลากหลายขณะเขียนเว็บแอปบนอุปกรณ์เคลื่อนที่ การดำเนินการนี้จะแพร่หลายมากขึ้นเมื่อผู้ผลิตเบราว์เซอร์เริ่มเปิดใช้การเข้าถึงฮาร์ดแวร์ของอุปกรณ์มากขึ้น
ข้อขัดแย้ง - ฉันพบปัญหาการทำงานขัดข้องเมื่อใช้การเร่งฮาร์ดแวร์กับบางส่วนของหน้าเว็บที่มีการเร่งแล้ว ดังนั้น การทราบว่าคุณมีการเร่งความเร็วที่ทับซ้อนกันหรือไม่จึงสำคัญมาก
เราต้องทำให้เบราว์เซอร์ทำงานตามที่เราต้องการเพื่อให้การโต้ตอบของผู้ใช้เป็นไปอย่างราบรื่นและใกล้เคียงกับการใช้งานแบบดั้งเดิมมากที่สุด ตามหลักการแล้ว เราต้องการให้ 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) ที่ดีกัน
- เปิดเว็บเบราว์เซอร์ Google Chrome
- ในแถบ URL ให้พิมพ์ about:flags
- เลื่อนลง 2-3 รายการแล้วคลิก "เปิดใช้" สำหรับตัวนับ FPS
หากคุณดูหน้านี้ใน Chrome เวอร์ชันที่อัปเกรดแล้ว คุณจะเห็นตัวนับ FPS สีแดงที่มุมซ้ายบน
วิธีนี้ช่วยให้เราทราบว่าการเร่งฮาร์ดแวร์เปิดอยู่ นอกจากนี้ยังช่วยให้เรามีไอเดียเกี่ยวกับวิธีการทำงานของภาพเคลื่อนไหวและหากคุณเกิดการรั่วไหลใดๆ (ภาพเคลื่อนไหวที่กำลังทำงานอยู่อย่างต่อเนื่องซึ่งควรหยุด)
อีกวิธีหนึ่งในการเห็นภาพการเร่งด้วยฮาร์ดแวร์จริงคือให้เปิดหน้าเดียวกันใน Safari (โดยใช้ตัวแปรสภาพแวดล้อมที่ฉันพูดถึงข้างต้น) องค์ประกอบ DOM ที่เร่งความเร็วทุกรายการจะมีสีแดง ซึ่งจะแสดงให้เราเห็นว่ามีการ Composite ใดเป็นเลเยอร์ โปรดสังเกตว่าการนำทางสีขาวไม่ใช่สีแดง เพราะไม่ได้เร่ง
การตั้งค่าที่คล้ายกันสำหรับ Chrome มีอยู่ใน about:flags "เส้นขอบเลเยอร์การแสดงผลแบบคอมโพสิต"
อีกวิธีหนึ่งที่ยอดเยี่ยมในการดูเลเยอร์แบบคอมโพสิตคือดูการสาธิตใบไม้ร่วงของ WebKit ขณะใช้ม็อดนี้
และสุดท้าย เพื่อให้เข้าใจประสิทธิภาพฮาร์ดแวร์กราฟิกของแอปพลิเคชันของเราอย่างแท้จริง เรามาดูการใช้หน่วยความจำกันดีกว่า ในส่วนนี้ เราเห็นว่าเราส่งคำสั่งวาดขนาด 1.38 MB ไปยังบัฟเฟอร์ CoreAnimation ใน Mac OS บัฟเฟอร์หน่วยความจำของ Core Animation จะแชร์ระหว่าง OpenGL ES และ GPU เพื่อสร้างพิกเซลสุดท้ายที่คุณเห็นบนหน้าจอ
เมื่อเราปรับขนาดหรือขยายหน้าต่างเบราว์เซอร์ จะเห็นหน่วยความจำเพิ่มขึ้นด้วย
ซึ่งจะทำให้คุณทราบว่าอุปกรณ์เคลื่อนที่ใช้หน่วยความจำอย่างไรก็ต่อเมื่อคุณปรับขนาดเบราว์เซอร์เป็นขนาดที่ถูกต้องเท่านั้น หากคุณกำลังแก้ไขข้อบกพร่องหรือทดสอบสภาพแวดล้อมของ iPhone ให้ปรับขนาดเป็น 480 x 320 พิกเซล ตอนนี้เราเข้าใจวิธีการทำงานของการเร่งด้วยฮาร์ดแวร์และสิ่งที่ต้องทำเพื่อแก้ไขข้อบกพร่องแล้ว การอ่านเกี่ยวกับเรื่องนี้ก็ดี แต่การได้เห็นบัฟเฟอร์หน่วยความจำ GPU ทำงานจริง ๆ นั้นช่วยให้เห็นภาพได้ชัดเจนขึ้น
เบื้องหลัง: การดึงข้อมูลและการแคช
ตอนนี้ถึงเวลายกระดับการแคชหน้าเว็บและทรัพยากรไปอีกขั้น เราจะดึงข้อมูลล่วงหน้าและแคชหน้าเว็บด้วยการเรียก AJAX พร้อมกัน ซึ่งคล้ายกับแนวทางที่ JQuery Mobile และเฟรมเวิร์กที่เกี่ยวข้องใช้
มาดูปัญหาหลักๆ ของเว็บบนอุปกรณ์เคลื่อนที่และเหตุผลที่เราต้องดำเนินการนี้กัน
- การดึงข้อมูล: การดึงข้อมูลหน้าเว็บล่วงหน้าช่วยให้ผู้ใช้ใช้แอปแบบออฟไลน์ได้และไม่ต้องรอระหว่างการไปยังส่วนต่างๆ แน่นอนว่าเราไม่ต้องการให้แบนด์วิดท์ของอุปกรณ์ถูกจำกัดเมื่ออุปกรณ์ออนไลน์ ดังนั้นเราจึงต้องใช้ฟีเจอร์นี้อย่างจำกัด
- การแคช: ถัดไป เราต้องการแนวทางแบบพร้อมกันหรือไม่พร้อมกันเมื่อดึงข้อมูลและแคชหน้าเว็บเหล่านี้ นอกจากนี้ เรายังต้องใช้ localStorage (เนื่องจากอุปกรณ์ต่างๆ รองรับได้ดี) ซึ่งไม่ทำงานแบบแอซิงโครนัส
- AJAX และการแยกวิเคราะห์คำตอบ: การใช้ innerHTML() เพื่อแทรกคำตอบ AJAX ลงใน DOM เป็นอันตราย (และไม่น่าเชื่อถือใช่ไหม) แต่เราใช้กลไกที่เชื่อถือได้ในการแทรกคำตอบ AJAX และจัดการการเรียกใช้พร้อมกันแทน นอกจากนี้ เรายังใช้ประโยชน์จากฟีเจอร์ใหม่บางอย่างของ HTML5 ในการแยกวิเคราะห์
xhr.responseText
ด้วย
จากโค้ดในการสาธิตการเลื่อน พลิก และบิด เราจะเริ่มต้นด้วยการเพิ่มหน้ารองบางส่วนและลิงก์ไปยังหน้าเหล่านั้น จากนั้นเราจะแยกวิเคราะห์ลิงก์และสร้างทรานซิชันขณะที่เล่น
ดูการสาธิตการดึงข้อมูลและแคชที่นี่
คุณจะเห็นว่าเรากำลังใช้ประโยชน์จากมาร์กอัปเชิงความหมายที่นี่ เป็นเพียงลิงก์ไปยังหน้าอื่น หน้าย่อยจะมีโครงสร้างโหนด/คลาสเหมือนกับหน้าหลัก เราอาจพัฒนาไปอีกขั้นและใช้แอตทริบิวต์ 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') &&
//'#' 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) &&
//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 (แบบซิงโครนัส)
ไทม์ไลน์คำขอ Wi-Fi (แบบไม่พร้อมกัน)
วิธีนี้จะช่วยให้ปรับประสบการณ์ของผู้ใช้ได้บางวิธีเป็นอย่างน้อย โดยอิงตามการเชื่อมต่อที่ช้าหรือเร็ว การดำเนินการนี้ไม่ใช่วิธีแก้ปัญหาที่สมบูรณ์แบบ อีกสิ่งที่ต้องทําคือแสดงโมดัลการโหลดเมื่อมีการคลิกลิงก์ (ในการเชื่อมต่อที่ช้า) ขณะที่แอปอาจยังดึงข้อมูลหน้าของลิงก์นั้นอยู่เบื้องหลัง ประเด็นสำคัญในที่นี้คือการลดเวลาในการตอบสนอง ในขณะเดียวกันก็เป็นการใช้ประโยชน์จากความสามารถเต็มรูปแบบของการเชื่อมต่อของผู้ใช้ด้วย HTML5 รุ่นล่าสุดและดีที่สุดที่มีให้ ดูการสาธิตการตรวจหาเครือข่ายได้ที่นี่
บทสรุป
เส้นทางของแอป HTML5 บนอุปกรณ์เคลื่อนที่เพิ่งเริ่มต้นขึ้น ตอนนี้คุณได้เห็นรากฐานพื้นฐานง่ายๆ ของ “เฟรมเวิร์ก” มือถือที่สร้างขึ้นจาก HTML5 เท่านั้นและรองรับเทคโนโลยีต่างๆ เราคิดว่านักพัฒนาซอฟต์แวร์ควรใช้และจัดการกับฟีเจอร์เหล่านี้ที่ส่วนสำคัญ ไม่ใช่ใช้ Wrapper มาปกปิด