บทนำ
การรีเฟรชที่หมุน การเปลี่ยนหน้าเว็บที่ไม่ราบรื่น และความล่าช้าเป็นระยะๆ ในเหตุการณ์การแตะเป็นเพียงปัญหาเล็กๆ น้อยๆ ในสภาพแวดล้อมเว็บในอุปกรณ์เคลื่อนที่ในปัจจุบัน นักพัฒนาแอปพยายามทำให้แอปของตนเองทำงานได้ใกล้เคียงกับแอปเนทีฟมากที่สุด แต่ก็มักจะถูกขัดขวางด้วยการแฮ็ก การรีเซ็ต และเฟรมเวิร์กที่เข้มงวด
ในบทความนี้ เราจะพูดถึงสิ่งจำเป็นขั้นต่ำในการสร้างเว็บแอป HTML5 สำหรับอุปกรณ์เคลื่อนที่ ประเด็นหลักคือการเปิดเผยความซับซ้อนที่ซ่อนอยู่ซึ่งเฟรมเวิร์กสำหรับอุปกรณ์เคลื่อนที่ในปัจจุบันพยายามซ่อนไว้ คุณจะเห็นแนวทางที่เรียบง่าย (ใช้ API หลักของ HTML5) และพื้นฐานเบื้องต้นที่จะช่วยให้คุณเขียนเฟรมเวิร์กของคุณเองหรือมีส่วนร่วมในเฟรมเวิร์กที่คุณใช้อยู่ในปัจจุบัน
การเร่งฮาร์ดแวร์
โดยปกติแล้ว GPU จะจัดการการสร้างโมเดล 3 มิติแบบละเอียดหรือไดอะแกรม CAD แต่ในกรณีนี้ เราต้องการให้ภาพวาดดั้งเดิม (div, พื้นหลัง, ข้อความที่มีเงาตกกระทบ, รูปภาพ ฯลฯ) ปรากฏอย่างราบรื่นและเคลื่อนไหวอย่างราบรื่นผ่าน GPU สิ่งที่น่าเสียดายคือ นักพัฒนาฟรอนต์เอนด์ส่วนใหญ่จะส่งต่อกระบวนการภาพเคลื่อนไหวนี้ไปยังเฟรมเวิร์กของบุคคลที่สามโดยไม่สนใจความหมาย แต่ควรซ่อนฟีเจอร์หลักของ CSS3 เหล่านี้ไว้หรือไม่ มาดูเหตุผลที่ว่าทำไมการใส่ใจเรื่องนี้จึงสำคัญกัน
การจัดสรรหน่วยความจำและภาระการคำนวณ - หากคุณพยายามคอมโพสิตทุกองค์ประกอบใน DOM เพียงเพื่อการเร่งด้วยฮาร์ดแวร์ ผู้ที่ทำงานกับโค้ดของคุณในอนาคตอาจตามหาคุณและทำร้ายคุณอย่างรุนแรง
การใช้พลังงาน - แน่นอนว่าเมื่อฮาร์ดแวร์เริ่มทำงาน แบตเตอรี่ก็จะเริ่มทำงานด้วย เมื่อพัฒนาแอปสำหรับอุปกรณ์เคลื่อนที่ นักพัฒนาแอปจะต้องพิจารณาข้อจำกัดของอุปกรณ์ที่หลากหลายขณะเขียนเว็บแอปสำหรับอุปกรณ์เคลื่อนที่ ซึ่งจะมีความสำคัญมากยิ่งขึ้นเมื่อผู้ผลิตเบราว์เซอร์เริ่มเปิดใช้การเข้าถึงฮาร์ดแวร์ของอุปกรณ์มากขึ้นเรื่อยๆ
ความขัดแย้ง - ฉันพบพฤติกรรมที่ผิดปกติเมื่อใช้การเร่งฮาร์ดแวร์กับบางส่วนของหน้าที่ได้รับการเร่งแล้ว ดังนั้นการทราบว่าคุณมีการเร่งความเร็วที่ทับซ้อนกันหรือไม่จึงสำคัญมาก
เราต้องทำให้เบราว์เซอร์ทำงานเพื่อเราเพื่อให้การโต้ตอบของผู้ใช้เป็นไปอย่างราบรื่นและใกล้เคียงกับแอปเนทีฟมากที่สุด เราต้องการให้ CPU ของอุปกรณ์เคลื่อนที่ตั้งค่าภาพเคลื่อนไหวเริ่มต้น จากนั้นให้ GPU มีหน้าที่รับผิดชอบเฉพาะการคอมโพสเลเยอร์ต่างๆ ในระหว่างกระบวนการภาพเคลื่อนไหว ซึ่งเป็นสิ่งที่ translate3d, scale3d และ translateZ ทำ นั่นคือการให้เลเยอร์ของตัวเองแก่องค์ประกอบที่มีภาพเคลื่อนไหว ซึ่งช่วยให้อุปกรณ์แสดงผลทุกอย่างร่วมกันได้อย่างราบรื่น หากต้องการดูข้อมูลเพิ่มเติมเกี่ยวกับการคอมโพสิตที่เร่งความเร็วและวิธีที่ WebKit ทำงาน Ariya Hidayat มีข้อมูลดีๆ มากมายในบล็อกของเขา
การเปลี่ยนหน้า
มาดูแนวทางการโต้ตอบกับผู้ใช้ที่พบบ่อยที่สุด 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 ที่จัดการการตรวจหาและการวางแนวอุปกรณ์เคลื่อนที่กัน เราสามารถกำหนดเป้าหมายอุปกรณ์และความละเอียดทุกประเภทได้ (ดูความละเอียดของ Media Query) ฉันใช้ตัวอย่างง่ายๆ เพียงไม่กี่ตัวอย่างในการสาธิตนี้เพื่อครอบคลุมมุมมองแนวตั้งและแนวนอนส่วนใหญ่บนอุปกรณ์เคลื่อนที่ นอกจากนี้ยังเป็นประโยชน์สำหรับการใช้การเร่งฮาร์ดแวร์ต่ออุปกรณ์ด้วย ตัวอย่างเช่น เนื่องจาก 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;
}
เนื่องจากเราใช้การเปลี่ยนผ่านแบบ Ease-Out ของ 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 ที่เร่งความเร็วแล้วทุกรายการจะมีสีแดงจางๆ ซึ่งจะแสดงให้เราเห็นว่าเลเยอร์ใดบ้างที่กำลังคอมโพสิต สังเกตว่าการนำทางสีขาวไม่เป็นสีแดงเนื่องจากไม่ได้เร่งความเร็ว
นอกจากนี้ ยังมีการตั้งค่าที่คล้ายกันสำหรับ 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 ซึ่งจะปิดใช้สคริปต์และมีฟีเจอร์ด้านความปลอดภัยมากมาย...
จากข้อกำหนด เมื่อระบุแอตทริบิวต์ 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 "หยุดทำงานแบบสุ่ม" แต่คงน่าสนใจมากที่จะได้เห็นแพลตฟอร์มทั้งหมดที่ได้รับผลกระทบ นอกจากนี้ เรายังอยากรู้ว่าแนวทางใดมีประสิทธิภาพมากกว่ากันด้วย เราได้ยินคำกล่าวอ้างจากทั้ง 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);
ใน EventListeners ด้านบน เราต้องบอกโค้ดว่ามีการเรียกใช้จากเหตุการณ์หรือคำขอหน้าเว็บจริงหรือการรีเฟรช สาเหตุหลักคือระบบจะไม่ทริกเกอร์เหตุการณ์ 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 ได้หลายอย่าง แต่ที่ฉันทำที่นี่คือบอกให้ดึงข้อมูลทรัพยากรแบบอะซิงโครนัส (true) หรือซิงโครนัส (false) สำหรับการเชื่อมต่อที่กำหนด
ไทม์ไลน์คำขอที่ Edge (แบบซิงโครนัส)
ไทม์ไลน์คำขอผ่าน Wi-Fi (แบบอะซิงโครนัส)
ซึ่งช่วยให้ปรับประสบการณ์ของผู้ใช้ได้อย่างน้อย 1 วิธีตามการเชื่อมต่อที่ช้าหรือเร็ว แต่ไม่ได้หมายความว่าจะเป็นโซลูชันที่ครอบคลุมทุกอย่าง อีกสิ่งที่ควรทำคือแสดงโมดอลการโหลดเมื่อมีการคลิกลิงก์ (ในการเชื่อมต่อที่ช้า) ขณะที่แอปอาจยังดึงข้อมูลหน้าของลิงก์นั้นในเบื้องหลัง ประเด็นสำคัญในที่นี้คือการลดเวลาในการตอบสนองขณะที่ใช้ความสามารถทั้งหมดของการเชื่อมต่อของผู้ใช้ด้วย HTML5 เวอร์ชันล่าสุดและดีที่สุด ดูเดโมการตรวจหาเครือข่ายได้ที่นี่
บทสรุป
การเดินทางบนเส้นทางของแอป HTML5 บนอุปกรณ์เคลื่อนที่เพิ่งเริ่มต้นขึ้น ตอนนี้คุณจะเห็นรากฐานที่เรียบง่ายและพื้นฐานของ "เฟรมเวิร์ก" สำหรับอุปกรณ์เคลื่อนที่ที่สร้างขึ้นจาก HTML5 และเทคโนโลยีที่รองรับเท่านั้น ผมคิดว่านักพัฒนาแอปควรทำงานและจัดการกับฟีเจอร์เหล่านี้ที่แกนหลัก ไม่ใช่ผ่านการห่อหุ้ม