Kỹ thuật HTML5 để tối ưu hóa hiệu suất trên thiết bị di động

Wesley Hales
Wesley Hales

Giới thiệu

Hoạt động làm mới hình ảnh xoay tròn, chuyển đổi trang bị giật và độ trễ định kỳ trong các sự kiện nhấn chỉ là một số vấn đề đau đầu trong môi trường web dành cho thiết bị di động hiện nay. Các nhà phát triển đang cố gắng làm cho ứng dụng của họ gần với ứng dụng gốc nhất có thể, nhưng thường bị gián đoạn bởi các bản hack, lượt đặt lại và khung cứng nhắc.

Trong bài viết này, chúng ta sẽ thảo luận về những điều tối thiểu cần thiết để tạo một ứng dụng web HTML5 dành cho thiết bị di động. Điểm chính là để vén bức màn phức tạp ẩn giấu mà các khung di động hiện nay cố gắng che giấu. Bạn sẽ thấy một phương pháp tối giản (sử dụng các API HTML5 cốt lõi) và các nguyên tắc cơ bản giúp bạn tự viết khung hoặc đóng góp cho khung mà bạn đang sử dụng.

Tăng tốc phần cứng

Thông thường, GPU xử lý mô hình 3D chi tiết hoặc sơ đồ CAD, nhưng trong trường hợp này, chúng ta muốn các bản vẽ gốc (div, nền, văn bản có bóng đổ, hình ảnh, v.v.) xuất hiện mượt mà và tạo ảnh động mượt mà thông qua GPU. Điều đáng tiếc là hầu hết các nhà phát triển giao diện người dùng đều chuyển quy trình tạo ảnh động này sang một khung bên thứ ba mà không quan tâm đến ngữ nghĩa, nhưng liệu các tính năng CSS3 cốt lõi này có nên bị ẩn không? Tôi sẽ cho bạn một vài lý do tại sao việc quan tâm đến vấn đề này lại quan trọng:

  1. Phân bổ bộ nhớ và gánh nặng tính toán – Nếu bạn kết hợp mọi phần tử trong DOM chỉ để tăng tốc phần cứng, thì người tiếp theo làm việc trên mã của bạn có thể sẽ đuổi theo và đánh bạn một trận.

  2. Mức tiêu thụ điện năng – Rõ ràng là khi phần cứng hoạt động, pin cũng sẽ hoạt động. Khi phát triển cho thiết bị di động, nhà phát triển buộc phải xem xét nhiều quy tắc ràng buộc của thiết bị trong khi viết ứng dụng web dành cho thiết bị di động. Việc này thậm chí sẽ phổ biến hơn khi các nhà sản xuất trình duyệt bắt đầu cho phép truy cập vào ngày càng nhiều phần cứng thiết bị hơn.

  3. Xung đột – Tôi gặp phải hành vi giật khi áp dụng tính năng tăng tốc phần cứng cho các phần của trang đã được tăng tốc. Vì vậy, việc biết liệu bạn có gia tốc chồng chéo hay không là rất quan trọng.

Để tương tác với người dùng một cách suôn sẻ và gần giống với giao diện gốc nhất có thể, chúng ta phải làm cho trình duyệt hoạt động hiệu quả. Lý tưởng nhất là CPU của thiết bị di động sẽ thiết lập ảnh động ban đầu, sau đó GPU chỉ chịu trách nhiệm kết hợp các lớp khác nhau trong quá trình tạo ảnh động. Đây là những gì translate3d, scale3d và translateZ làm — các hàm này cung cấp cho các phần tử ảnh động một lớp riêng, nhờ đó cho phép thiết bị kết xuất mọi thứ một cách liền mạch. Để tìm hiểu thêm về tính năng kết hợp tăng tốc và cách hoạt động của WebKit, Ariya Hidayat có rất nhiều thông tin hữu ích trên blog của anh ấy.

Hiệu ứng chuyển đổi trang

Hãy cùng xem xét 3 phương pháp tương tác với người dùng phổ biến nhất khi phát triển ứng dụng web dành cho thiết bị di động: hiệu ứng trượt, lật và xoay.

Bạn có thể xem mã này hoạt động tại đây http://slidfast.appspot.com/slide-flip-rotate.html (Lưu ý: Bản minh hoạ này được tạo cho thiết bị di động, vì vậy, hãy khởi động trình mô phỏng, sử dụng điện thoại hoặc máy tính bảng hoặc giảm kích thước cửa sổ trình duyệt xuống khoảng 1024px trở xuống).

Trước tiên, chúng ta sẽ phân tích các hiệu ứng chuyển đổi trượt, lật và xoay cũng như cách tăng tốc các hiệu ứng này. Lưu ý rằng mỗi ảnh động chỉ cần 3 hoặc 4 dòng CSS và JavaScript.

Trượt

Đây là phương pháp chuyển đổi phổ biến nhất trong số 3 phương pháp, chuyển đổi trang trượt mô phỏng cảm giác gốc của ứng dụng di động. Hiệu ứng chuyển đổi trượt được gọi để đưa một khu vực nội dung mới vào cổng xem.

Đối với hiệu ứng trượt, trước tiên, chúng ta khai báo mã đánh dấu:

<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>

Hãy lưu ý cách chúng ta có khái niệm về việc tạo bản sao trang ở bên trái hoặc bên phải. Về cơ bản, hướng này có thể là bất kỳ hướng nào, nhưng hướng này là phổ biến nhất.

Chúng ta hiện đã có tính năng tăng tốc phần cứng và hoạt ảnh chỉ với một vài dòng CSS. Ảnh động thực tế xảy ra khi chúng ta hoán đổi các lớp trên các phần tử div của trang.

.page {
  position: absolute;
  width: 100%;
  height: 100%;
  /*activate the GPU for compositing each page */
  -webkit-transform: translate3d(0, 0, 0);
}

translate3d(0,0,0) được gọi là phương pháp "biện pháp hiệu quả".

Khi người dùng nhấp vào một phần tử điều hướng, chúng ta sẽ thực thi JavaScript sau đây để hoán đổi các lớp. Không có khung của bên thứ ba nào được sử dụng, đây là JavaScript thuần tuý! ;)

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 hoặc stage-right trở thành stage-center và buộc trang trượt vào khung nhìn trung tâm. Chúng ta hoàn toàn phụ thuộc vào CSS3 để thực hiện các thao tác nặng.

.stage-left {
  left: -480px;
}

.stage-right {
  left: 480px;
}

.stage-center {
  top: 0;
  left: 0;
}

Tiếp theo, hãy xem CSS xử lý việc phát hiện và hướng thiết bị di động. Chúng ta có thể giải quyết mọi thiết bị và mọi độ phân giải (xem độ phân giải truy vấn nội dung nghe nhìn). Tôi chỉ sử dụng một vài ví dụ đơn giản trong bản minh hoạ này để đề cập đến hầu hết các chế độ xem dọc và ngang trên thiết bị di động. Điều này cũng hữu ích khi áp dụng tính năng tăng tốc phần cứng cho mỗi thiết bị. Ví dụ: vì phiên bản WebKit dành cho máy tính để bàn tăng tốc tất cả các phần tử đã chuyển đổi (bất kể đó là 2D hay 3D), nên bạn nên tạo truy vấn nội dung nghe nhìn và loại trừ tính năng tăng tốc ở cấp đó. Lưu ý rằng các thủ thuật tăng tốc phần cứng không giúp cải thiện tốc độ trong Android Froyo 2.2 trở lên. Mọi thành phần đều được thực hiện trong phần mềm.

/* 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;
  }
}

Lật ảnh

Trên thiết bị di động, thao tác lật được gọi là vuốt trang đi. Ở đây, chúng ta sử dụng một số JavaScript đơn giản để xử lý sự kiện này trên các thiết bị iOS và Android (dựa trên WebKit).

Xem ví dụ thực tế http://slidfast.appspot.com/slide-flip-rotate.html.

Khi xử lý các sự kiện nhấn và hiệu ứng chuyển đổi, điều đầu tiên bạn cần là nắm được vị trí hiện tại của phần tử. Hãy xem tài liệu này để biết thêm thông tin về WebKitCSSMatrix.

function pageMove(event) {
  // get position after transform
  var curTransform = new WebKitCSSMatrix(window.getComputedStyle(page).webkitTransform);
  var pagePosition = curTransform.m41;
}

Vì chúng ta đang sử dụng hiệu ứng chuyển đổi CSS3 để lật trang, nên element.offsetLeft thông thường sẽ không hoạt động.

Tiếp theo, chúng ta muốn tìm hiểu xem người dùng đang lật theo hướng nào và đặt ngưỡng cho một sự kiện (điều hướng trang) diễn ra.

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;
  }
}

Bạn cũng sẽ nhận thấy rằng chúng ta cũng đang đo lường swipeTime theo mili giây. Điều này cho phép sự kiện điều hướng kích hoạt nếu người dùng vuốt nhanh màn hình để chuyển trang.

Để định vị trang và làm cho ảnh động trông gốc trong khi ngón tay đang chạm vào màn hình, chúng ta sử dụng chuyển đổi CSS3 sau mỗi lần kích hoạt sự kiện.

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';
}

Tôi đã thử chơi với cubic-bezier để mang lại cảm giác gốc tốt nhất cho các hiệu ứng chuyển đổi, nhưng ease-out đã làm được điều đó.

Cuối cùng, để thực hiện thao tác điều hướng, chúng ta phải gọi các phương thức slideTo() đã xác định trước đó mà chúng ta đã sử dụng trong bản minh hoạ gần đây nhất.

track.ontouchend = function(event) {
  pageMove(event);
  if (slideDirection == 'left') {
    slideTo('products-page');
  } else if (slideDirection == 'right') {
    slideTo('home-page');
  }
}

Xoay

Tiếp theo, hãy xem ảnh động xoay được sử dụng trong bản minh hoạ này. Bạn có thể xoay trang bạn đang xem 180 độ bất cứ lúc nào để hiển thị mặt sau bằng cách nhấn vào tuỳ chọn trình đơn "Liên hệ". Xin nhắc lại, thao tác này chỉ cần vài dòng CSS và một số JavaScript để gán lớp chuyển đổi onclick. LƯU Ý: Hiệu ứng chuyển đổi xoay không được hiển thị chính xác trên hầu hết các phiên bản Android vì thiếu khả năng biến đổi CSS 3D. Rất tiếc, thay vì bỏ qua thao tác lật, Android sẽ làm cho trang "xoay vòng" bằng cách xoay thay vì lật. Bạn nên hạn chế sử dụng quá trình chuyển đổi này cho đến khi khả năng hỗ trợ được cải thiện.

Mã đánh dấu (khái niệm cơ bản về mặt trước và mặt sau):

<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);
}

Gỡ lỗi tính năng tăng tốc phần cứng

Giờ đây, khi chúng ta đã tìm hiểu xong các hiệu ứng chuyển đổi cơ bản, hãy xem xét cơ chế hoạt động và cách kết hợp các hiệu ứng chuyển đổi đó.

Để thực hiện phiên gỡ lỗi kỳ diệu này, hãy khởi động một vài trình duyệt và IDE mà bạn chọn. Trước tiên, hãy khởi động Safari từ dòng lệnh để sử dụng một số biến môi trường gỡ lỗi. Tôi đang dùng máy Mac, vì vậy, các lệnh có thể khác nhau tuỳ theo hệ điều hành của bạn. Mở Terminal (Dòng lệnh) rồi nhập nội dung sau:

  • $> export CA_COLOR_OPAQUE=1
  • $> export CA_LOG_MEMORY_USAGE=1
  • $> /Ứng dụng/Safari.app/Contents/MacOS/Safari

Thao tác này sẽ khởi động Safari bằng một số trình trợ giúp gỡ lỗi. CA_COLOR_OPAQUE cho chúng ta biết những phần tử nào thực sự được kết hợp hoặc tăng tốc. CA_LOG_MEMORY_USAGE cho chúng ta biết mức sử dụng bộ nhớ khi gửi các thao tác vẽ đến bộ nhớ đệm. Thông tin này cho bạn biết chính xác mức độ tải mà bạn đang đặt trên thiết bị di động và có thể đưa ra gợi ý về mức sử dụng GPU có thể làm tiêu hao pin của thiết bị mục tiêu.

Bây giờ, hãy khởi động Chrome để xem một số thông tin hữu ích về tốc độ khung hình/giây (FPS):

  1. Mở trình duyệt web Google Chrome.
  2. Trong thanh URL, hãy nhập about:flags.
  3. Di chuyển xuống một vài mục rồi nhấp vào "Bật" cho Bộ đếm FPS.

Nếu xem trang này trong phiên bản Chrome đã nâng cao, bạn sẽ thấy bộ đếm FPS màu đỏ ở góc trên cùng bên trái.

Khung hình/giây trên Chrome

Đây là cách chúng ta biết được chế độ tăng tốc phần cứng được bật. Trình phân tích tài nguyên cũng cho chúng ta biết cách ảnh động chạy và liệu bạn có bị rò rỉ hay không (ảnh động chạy liên tục cần phải dừng).

Một cách khác để thực sự hình dung tính năng tăng tốc phần cứng là nếu bạn mở cùng một trang trong Safari (với các biến môi trường mà tôi đã đề cập ở trên). Mỗi phần tử DOM được tăng tốc đều có màu đỏ. Điều này cho chúng ta biết chính xác những gì đang được kết hợp theo lớp. Lưu ý rằng thanh điều hướng màu trắng không có màu đỏ vì không được tăng tốc.

Người liên hệ tổng hợp

Chế độ cài đặt tương tự cho Chrome cũng có trong about:flags "Composited render layer borders" (Biên giới lớp kết xuất kết hợp).

Một cách hay khác để xem các lớp kết hợp là xem minh hoạ lá rơi của WebKit trong khi áp dụng bản mod này.

Lá tổng hợp

Cuối cùng, để thực sự hiểu rõ hiệu suất phần cứng đồ hoạ trong ứng dụng của chúng ta, hãy cùng xem cách bộ nhớ đang được sử dụng. Ở đây, chúng ta thấy rằng chúng ta đang đẩy 1,38 MB hướng dẫn vẽ vào vùng đệm CoreAnimation trên Mac OS. Các vùng đệm bộ nhớ Core Animation được chia sẻ giữa OpenGL ES và GPU để tạo ra các pixel cuối cùng mà bạn nhìn thấy trên màn hình.

Coreanimation 1

Khi chỉ cần thay đổi kích thước hoặc phóng to cửa sổ trình duyệt, chúng ta cũng thấy bộ nhớ mở rộng.

Coreanimation 2

Chỉ khi bạn đổi kích thước trình duyệt thành kích thước chính xác thì bạn mới biết được mức sử dụng bộ nhớ trên thiết bị di động. Nếu bạn đang gỡ lỗi hoặc kiểm thử cho môi trường iPhone, hãy đổi kích thước thành 480px x 320px. Giờ đây, chúng ta đã hiểu rõ cách hoạt động của tính năng tăng tốc phần cứng và những gì cần làm để gỡ lỗi. Việc đọc về bộ đệm bộ nhớ GPU là một chuyện, nhưng việc thực sự thấy bộ đệm bộ nhớ GPU hoạt động một cách trực quan sẽ giúp bạn hiểu rõ hơn.

Hậu trường: Tìm nạp và lưu vào bộ nhớ đệm

Bây giờ, chúng ta sẽ nâng cấp việc lưu trang và tài nguyên vào bộ nhớ đệm lên một tầm cao mới. Tương tự như phương pháp mà JQuery Mobile và các khung tương tự sử dụng, chúng ta sẽ tìm nạp trước và lưu các trang vào bộ nhớ đệm bằng các lệnh gọi AJAX đồng thời.

Hãy cùng giải quyết một số vấn đề cốt lõi về web dành cho thiết bị di động và lý do chúng ta cần làm việc này:

  • Tìm nạp: Tính năng tìm nạp trước các trang của chúng tôi cho phép người dùng sử dụng ứng dụng khi không có mạng và cũng không cần chờ đợi giữa các thao tác điều hướng. Tất nhiên, chúng ta không muốn làm tắc nghẽn băng thông của thiết bị khi thiết bị có kết nối mạng, vì vậy, chúng ta cần sử dụng tính năng này một cách tiết kiệm.
  • Lưu vào bộ nhớ đệm: Tiếp theo, chúng ta muốn có một phương pháp đồng thời hoặc không đồng bộ khi tìm nạp và lưu các trang này vào bộ nhớ đệm. Chúng ta cũng cần sử dụng localStorage (vì nó được hỗ trợ tốt giữa các thiết bị) nhưng rất tiếc là không đồng bộ.
  • AJAX và phân tích cú pháp phản hồi: Việc sử dụng innerHTML() để chèn phản hồi AJAX vào DOM là nguy hiểm (và không đáng tin cậy?). Thay vào đó, chúng ta sử dụng một cơ chế đáng tin cậy để chèn phản hồi AJAX và xử lý các lệnh gọi đồng thời. Chúng tôi cũng tận dụng một số tính năng mới của HTML5 để phân tích cú pháp xhr.responseText.

Dựa trên mã của Bản minh hoạ về tính năng Trượt, Lật và Xoay, chúng ta bắt đầu bằng cách thêm một số trang phụ và liên kết đến các trang đó. Sau đó, chúng tôi sẽ phân tích cú pháp các đường liên kết và tạo các hiệu ứng chuyển đổi nhanh chóng.

Màn hình chính của iPhone

Xem bản minh hoạ về tính năng Tìm nạp và Lưu vào bộ nhớ đệm tại đây.

Như bạn có thể thấy, chúng ta đang tận dụng tính năng đánh dấu ngữ nghĩa ở đây. Chỉ là một đường liên kết đến một trang khác. Trang con tuân theo cùng một cấu trúc nút/lớp như trang mẹ. Chúng ta có thể tiến thêm một bước nữa và sử dụng thuộc tính data-* cho các nút "trang", v.v. Trang chi tiết (con) nằm trong một tệp html riêng (/demo2/home-detail.html) sẽ được tải, lưu vào bộ nhớ đệm và thiết lập để chuyển đổi khi tải ứng dụng.

<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>

Bây giờ, hãy xem JavaScript. Để đơn giản, tôi sẽ loại bỏ mọi trình trợ giúp hoặc tính năng tối ưu hoá khỏi mã. Tất cả những gì chúng ta đang làm ở đây là lặp lại qua một mảng các nút DOM được chỉ định để tìm ra các đường liên kết cần tìm nạp và lưu vào bộ nhớ đệm. Lưu ý – Đối với bản minh hoạ này, phương thức fetchAndCache() này đang được gọi khi tải trang. Chúng ta sẽ làm lại việc này trong phần tiếp theo khi phát hiện thấy kết nối mạng và xác định thời điểm nên gọi mạng.

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();
      }
    }
  }
};

Chúng tôi đảm bảo quá trình xử lý sau không đồng bộ phù hợp thông qua việc sử dụng đối tượng "AJAX". Có một phần giải thích nâng cao hơn về cách sử dụng localStorage trong lệnh gọi AJAX trong bài viết Làm việc ngoại tuyến bằng HTML5. Trong ví dụ này, bạn sẽ thấy cách sử dụng cơ bản của tính năng lưu vào bộ nhớ đệm trên mỗi yêu cầu và cung cấp các đối tượng đã lưu vào bộ nhớ đệm khi máy chủ trả về bất kỳ nội dung nào ngoại trừ phản hồi thành công (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;
      }
    }
  }
}

Rất tiếc, vì localStorage sử dụng UTF-16 để mã hoá ký tự, nên mỗi byte được lưu trữ dưới dạng 2 byte, khiến giới hạn bộ nhớ của chúng ta giảm từ 5 MB xuống còn tổng cộng 2,6 MB. Toàn bộ lý do để tìm nạp và lưu các trang/đánh dấu này vào bộ nhớ đệm bên ngoài phạm vi bộ nhớ đệm của ứng dụng sẽ được tiết lộ trong phần tiếp theo.

Với những tiến bộ gần đây trong thành phần iframe với HTML5, giờ đây, chúng ta có một cách đơn giản và hiệu quả để phân tích cú pháp responseText mà chúng ta nhận được từ lệnh gọi AJAX. Có rất nhiều trình phân tích cú pháp JavaScript và biểu thức chính quy gồm 3000 dòng giúp xoá các thẻ tập lệnh, v.v. Nhưng tại sao không để trình duyệt thực hiện những gì nó hoạt động tốt nhất? Trong ví dụ này, chúng ta sẽ ghi responseText vào một iframe ẩn tạm thời. Chúng ta đang sử dụng thuộc tính "hộp cát" HTML5 để tắt các tập lệnh và cung cấp nhiều tính năng bảo mật…

Theo thông số kỹ thuật: thuộc tính hộp cát, khi được chỉ định, sẽ bật một bộ hạn chế bổ sung đối với mọi nội dung do iframe lưu trữ. Giá trị của giá trị này phải là một tập mã thông báo duy nhất được phân tách bằng dấu cách không theo thứ tự và không phân biệt chữ hoa chữ thường theo ASCII. Các giá trị được phép là allow-forms, allow-same-origin, allow-scripts và allow-top-navigation. Khi thuộc tính này được đặt, nội dung sẽ được coi là đến từ một nguồn gốc duy nhất, các biểu mẫu và tập lệnh sẽ bị tắt, các đường liên kết sẽ không được nhắm đến các ngữ cảnh duyệt web khác và các trình bổ trợ sẽ bị tắt.

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 từ chối di chuyển một nút từ tài liệu này sang tài liệu khác một cách ngầm ẩn. Sẽ xảy ra lỗi nếu nút con mới được tạo trong một tài liệu khác. Vì vậy, ở đây chúng ta sử dụng adoptNode và mọi thứ đều ổn.

Vậy tại sao bạn nên sử dụng iframe? Tại sao không chỉ sử dụng innerHTML? Mặc dù innerHTML hiện là một phần của thông số HTML5, nhưng việc chèn phản hồi từ máy chủ (xấu hoặc tốt) vào vùng không được chọn là một phương pháp nguy hiểm. Trong quá trình viết bài này, tôi không tìm thấy ai sử dụng bất kỳ phương thức nào khác ngoài innerHTML. Tôi biết JQuery sử dụng phương thức này ở cốt lõi với phương thức dự phòng chỉ thêm vào trường hợp ngoại lệ. JQuery Mobile cũng sử dụng thuộc tính này. Tuy nhiên, tôi chưa thực hiện bất kỳ kiểm thử nghiêm ngặt nào liên quan đến việc innerHTML “ngẫu nhiên ngừng hoạt động”, nhưng sẽ rất thú vị khi xem tất cả các nền tảng chịu ảnh hưởng của vấn đề này. Cũng sẽ rất thú vị khi xem phương pháp nào hiệu quả hơn… Tôi cũng đã nghe cả hai bên đưa ra tuyên bố về vấn đề này.

Phát hiện, xử lý và phân tích loại mạng

Giờ đây, khi đã có khả năng lưu vào bộ đệm (hoặc bộ nhớ đệm dự đoán) cho ứng dụng web của mình, chúng ta phải cung cấp các tính năng phát hiện kết nối phù hợp để giúp ứng dụng thông minh hơn. Đây là nơi việc phát triển ứng dụng dành cho thiết bị di động trở nên cực kỳ nhạy cảm với chế độ trực tuyến/ngoại tuyến và tốc độ kết nối. Nhập Network Information API (API Thông tin mạng). Mỗi khi tôi giới thiệu tính năng này trong một bản trình bày, luôn có người trong khán giả giơ tay lên và hỏi "Tôi sẽ sử dụng tính năng đó để làm gì?". Vì vậy, sau đây là một cách có thể để thiết lập một ứng dụng web dành cho thiết bị di động cực kỳ thông minh.

Trước tiên, hãy xem xét một tình huống phổ biến và nhàm chán… Trong khi tương tác với Web từ một thiết bị di động trên tàu cao tốc, mạng có thể bị mất kết nối vào nhiều thời điểm và các khu vực địa lý khác nhau có thể hỗ trợ các tốc độ truyền khác nhau (ví dụ: HSPA hoặc 3G có thể được cung cấp ở một số khu vực đô thị nhưng những khu vực xa xôi có thể hỗ trợ công nghệ 2G chậm hơn nhiều). Mã sau đây giải quyết hầu hết các trường hợp kết nối.

Mã sau đây cung cấp:

  • Truy cập khi không có mạng thông qua applicationCache.
  • Phát hiện xem có được đánh dấu trang hay không và có ngoại tuyến hay không.
  • Phát hiện khi nào chuyển từ ngoại tuyến sang trực tuyến và ngược lại.
  • Phát hiện kết nối chậm và tìm nạp nội dung dựa trên loại mạng.

Xin nhắc lại, tất cả các tính năng này đều yêu cầu rất ít mã. Trước tiên, chúng ta phát hiện các sự kiện và trường hợp tải:

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);

Trong EventListeners ở trên, chúng ta phải cho mã biết liệu mã có đang được gọi từ một sự kiện, một yêu cầu hoặc làm mới trang thực tế hay không. Lý do chính là vì sự kiện onload nội dung sẽ không được kích hoạt khi chuyển đổi giữa chế độ trực tuyến và ngoại tuyến.

Tiếp theo, chúng ta sẽ kiểm tra đơn giản một sự kiện ononline hoặc onload. Mã này đặt lại các đường liên kết bị vô hiệu hoá khi chuyển từ chế độ ngoại tuyến sang trực tuyến. Tuy nhiên, nếu ứng dụng này tinh vi hơn, bạn có thể chèn logic để tiếp tục tìm nạp nội dung hoặc xử lý trải nghiệm người dùng đối với các kết nối bị gián đoạn.

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;
    }
  }
}

Tương tự cũng áp dụng với processOffline(). Tại đây, bạn sẽ thao tác với ứng dụng của mình ở chế độ ngoại tuyến và cố gắng khôi phục mọi giao dịch đang diễn ra ở chế độ nền. Mã dưới đây sẽ tìm ra tất cả các đường liên kết bên ngoài và vô hiệu hoá chúng – giữ chân người dùng trong ứng dụng ngoại tuyến của chúng ta MÃI MÃI muhahaha!

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);

    }
  }
}

Được rồi, giờ chúng ta sẽ chuyển sang phần thú vị. Giờ đây, ứng dụng của chúng ta đã biết trạng thái kết nối, chúng ta cũng có thể kiểm tra loại kết nối khi ứng dụng có kết nối mạng và điều chỉnh cho phù hợp. Tôi đã liệt kê tốc độ tải xuống và độ trễ điển hình của các nhà cung cấp ở Bắc Mỹ trong phần nhận xét cho mỗi kết nối.

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);
  }
}

Chúng ta có thể thực hiện rất nhiều sự điều chỉnh đối với quy trình findAndCache, nhưng tất cả những gì tôi đã làm ở đây chỉ là tìm nạp các tài nguyên không đồng bộ (true) hoặc đồng bộ (false) cho một kết nối nhất định.

Dòng thời gian yêu cầu cạnh (Đồng bộ)

Edge Sync

Tiến trình yêu cầu WIFI (Không đồng bộ)

WIFI không đồng bộ

Điều này cho phép ít nhất một số phương pháp điều chỉnh trải nghiệm người dùng dựa trên tốc độ kết nối chậm hoặc nhanh. Đây không phải là giải pháp toàn diện. Một việc khác cần làm là hiển thị phương thức tải khi người dùng nhấp vào một đường liên kết (trên kết nối chậm) trong khi ứng dụng vẫn có thể đang tìm nạp trang của đường liên kết đó ở chế độ nền. Điểm quan trọng ở đây là giảm độ trễ trong khi tận dụng toàn bộ khả năng kết nối của người dùng với HTML5 mới nhất và tốt nhất. Xem bản minh hoạ về tính năng phát hiện mạng tại đây.

Kết luận

Hành trình của ứng dụng HTML5 dành cho thiết bị di động chỉ mới bắt đầu. Giờ đây, bạn đã thấy những nền tảng rất đơn giản và cơ bản của một "khung" dành cho thiết bị di động được xây dựng hoàn toàn dựa trên HTML5 và các công nghệ hỗ trợ của nó. Tôi cho rằng nhà phát triển cần phải xử lý và giải quyết các tính năng này ở cốt lõi chứ không phải ẩn sau một trình bao bọc.