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

Làm mới xoay, chuyển đổi trang rời rạc và độ trễ định kỳ trong các sự kiện nhấn chỉ là một vài vấn đề trong môi trường web di động ngày nay. Các nhà phát triển đang cố gắng tiếp cận gần nhất có thể với nền tảng gốc, nhưng thường bị chệch hướng do các thủ thuật, hoạt động đặt lại và khung hình 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à để khám phá những điểm phức tạp ẩn mà các khung di động ngày nay đang 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à những kiến thức cơ bản giúp bạn có thể viết khung riêng hoặc đóng góp cho khung mà bạn hiệ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ẽ cơ bản (div, nền, văn bản có bóng đổ, hình ảnh, v.v.) xuất hiện mượt mà và chuyển động mượt mà thông qua GPU. Điều đáng tiếc là hầu hết 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. Tuy nhiên, liệu những tính năng cốt lõi này của CSS3 có bị che giấu không? Tôi sẽ đưa ra một vài lý do cho thấy tầm quan trọng của việc quan tâm đến những điều này:

  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ẽ tìm đến và đánh bạ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 cân nhắc nhiều hạn chế của thiết bị trong khi viết ứng dụng web di động. Điều này sẽ càng trở nên 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 của thiết bị.

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

Để hoạt động tương tác của người dùng diễn ra suôn sẻ và gần với ứng dụng gốc nhất có thể, chúng ta phải khiến trình duyệt hoạt động cho mình. Lý tưởng nhất là chúng ta muốn CPU của thiết bị di động thiết lập ảnh động ban đầu, sau đó GPU chỉ chịu trách nhiệm tổng 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 – chúng cung cấp cho các phần tử được tạo ảnh động lớp riêng, nhờ đó cho phép thiết bị kết xuất mọi thứ cùng nhau một cách mượt mà. Để tìm hiểu thêm về tính năng kết hợp tăng tốc và cách WebKit hoạt động, Ariya Hidayat có rất nhiều thông tin hữu ích trên blog của mình.

Hiệu ứng chuyển trang

Hãy xem xét 3 phương pháp tương tác phổ biến nhất của người dùng khi phát triển một ứng dụng web 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 một 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 các hiệu ứng này được tăng tốc. Lưu ý cách mỗi ảnh động chỉ chiếm 3 hoặc 4 dòng CSS và JavaScript.

Trượt

Là cách chuyển đổi phổ biến nhất trong số 3 cách chuyển đổi, hiệu ứng chuyển đổi trang trượt mô phỏng cảm giác tự nhiên của các ứng dụng di động. Hiệu ứng chuyển trang được gọi để đưa một vùng nội dung mới vào khung hiển thị.

Đố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 dàn trang sang trái hoặc phải. Về cơ bản, bạn có thể di chuyển theo bất kỳ hướng nào, nhưng hướng này là phổ biến nhất.

Giờ đây, chúng ta có ảnh động và tính năng tăng tốc phần cứng 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 "viên đạn bạc".

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 để hoán đổi các lớp. Không có khung hình 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 sẽ trở thành stage-center và buộc trang trượt vào khung hiển thị trung tâm. Chúng ta hoàn toàn phụ thuộc vào CSS3 để thực hiện những công việc nặng nhọc.

.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 thiết bị di động và hướ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 để trình bày 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 chế độ 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 tăng tốc tất cả các phần tử được biến đổi (bất kể là 2D hay 3D), nên bạn cần tạo một truy vấn nội dung nghe nhìn và loại trừ việc tăng tốc ở cấp độ đó. Xin 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 trang được hiểu là thao tác vuốt trang ra ngoà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 hiệu ứng này trong thực tế tại http://slidfast.appspot.com/slide-flip-rotate.html.

Khi xử lý các sự kiện và hiệu ứng chuyển đổi khi chạm, điều đầu tiên bạn cần làm là nắm bắt 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 cho hiệu ứng 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 ra hướng mà người dùng đang lật và đặt một ngưỡng để 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 tôi đ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 nhanh chóng vuốt màn hình để chuyển trang.

Để định vị trang và làm cho ảnh động trông tự nhiên trong khi ngón tay chạm vào màn hình, chúng tôi sử dụng hiệu ứ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ử dùng cubic-bezier để mang lại cảm giác tự nhiên nhất cho các hiệu ứng chuyển đổi, nhưng ease-out đã giải quyết được vấn đề.

Cuối cùng, để quá trình điều hướng diễn ra, 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 xét ảnh động xoay đang được dùng trong bản minh hoạ này. Bất cứ lúc nào, bạn cũng có thể xoay trang đang xem 180 độ để xem mặt sau bằng cách nhấn vào lựa chọn "Liên hệ" trong trình đơn. Quá trình này chỉ cần vài dòng CSS và một số JavaScript để chỉ định một lớp chuyển đổi onclick. LƯU Ý: Hiệu ứng chuyển đổi xoay không hiển thị đúng cách 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 lại khiến trang "lộn nhào" bằng cách xoay thay vì lật. Bạn nên hạn chế sử dụng hiệu ứng chuyển đổi này cho đến khi tính năng hỗ trợ được cải thiện.

Đá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 đã nắm được các hiệu ứng chuyển cảnh cơ bản, hãy cùng xem cơ chế hoạt động và cách kết hợp các hiệu ứng này.

Để 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, nên các lệnh có thể khác nhau tuỳ theo hệ điều hành của bạn. Mở Terminal rồi nhập nội dung sau:

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

Thao tác này sẽ khởi động Safari cùng với một số trình gỡ lỗi trợ giúp. CA_COLOR_OPAQUE cho 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 kho lưu trữ dự phòng. Thông tin này cho bạn biết chính xác mức độ tải mà bạn đang đặt lên thiết bị di động, đồng thời có thể gợi ý về cách mức sử dụng GPU có thể làm 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 về số khung hình trên 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 được tăng tốc, bạn sẽ thấy bộ đếm FPS màu đỏ ở góc trên cùng bên trái.

FPS của Chrome

Đây là cách chúng tôi biết chế độ tăng tốc phần cứng đã được bật. Điều này cũng giúp chúng ta biết được cách chạy của ảnh động và liệu bạn có gặp phải bất kỳ rò rỉ nào hay không (ảnh động chạy liên tục nhưng đáng lẽ phải dừng).

Một cách khác để thực sự hình dung khả 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 đỏ nhạt. Đ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.

Mục liên hệ tổng hợp

Một chế độ cài đặt tương tự cho Chrome cũng có trong "Composited render layer borders" (Đường viền lớp kết xuất kết hợp) của about:flags.

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

omposited Leaves

Cuối cùng, để thực sự hiểu được hiệu suất phần cứng đồ hoạ của ứng dụng, hãy xem xét mức tiêu thụ bộ nhớ. Ở đây, chúng ta thấy rằng chúng ta đang đẩy 1,38 MB hướng dẫn vẽ vào các 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 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

Điều này giúp bạn biết được mức bộ nhớ đang được sử dụng trên thiết bị di động của mình, nhưng chỉ khi bạn đổi kích thước trình duyệt thành kích thước phù hợp. 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 480 x 320 pixel. 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 thiết để gỡ lỗi. Đọc về vấn đề này là một chuyện, nhưng thực sự thấy các vùng đệ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

Giờ là lúc chúng ta nâng cấp tính năng lưu vào bộ nhớ đệm của trang và tài nguyên. Giống 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 vào bộ nhớ đệm các trang bằng các lệnh gọi AJAX đồng thời.

Hãy giải quyết một số vấn đề cốt lõi về trang 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: Việc 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 giúp người dùng không phải 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 nghẽn băng thông của thiết bị khi thiết bị 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 vào bộ nhớ đệm các trang này. Chúng tôi cũng cần sử dụng localStorage (vì được các thiết bị hỗ trợ tốt), nhưng rất tiếc là localStorage 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 tôi 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ã từ Bản minh hoạ về hiệu ứng trượt, lật và xoay, chúng ta sẽ 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 ta sẽ phân tích cú pháp các đường liên kết và tạo hiệu ứng chuyển đổi ngay lập tức.

Trang chủ iPhone

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

Như bạn có thể thấy, chúng tôi đang tận dụng mã đánh dấu ngữ nghĩa ở đây. Chỉ là một đường liên kết đến một trang khác. Trang con có cấu trúc nút/lớp giống 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 "page", v.v. Và đây là trang chi tiết (con) nằm trong một tệp html riêng biệt (/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ẽ không đưa bất kỳ trợ giúp hoặc hoạt động tối ưu hoá nào vào mã. Tất cả những gì chúng ta đang làm ở đây là lặp lại một mảng 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() đang được gọi khi tải trang. Chúng ta sẽ làm lại 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 cần gọi.

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ý hậu kỳ không đồng bộ diễn ra đúng cách thông qua việc sử dụng đối tượng "AJAX". Có một lời giải thích nâng cao hơn về việc sử dụng localStorage trong lệnh gọi AJAX trong Làm việc khi không có mạng bằng HTML5 Offline. 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 được lưu vào bộ nhớ đệm khi máy chủ trả về bất kỳ phản hồi nào khác ngoài 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 là vì localStorage sử dụng UTF-16 để mã hoá ký tự, nên mỗi byte đơn lẻ được lưu trữ dưới dạng 2 byte, khiến hạn mức lưu trữ 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 vào bộ nhớ đệm các trang/đánh dấu này 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 phần tử iframe bằng 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 3.000 dòng giúp xoá thẻ tập lệnh, v.v. Nhưng tại sao không để trình duyệt làm những gì nó làm 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 tôi đang sử dụng thuộc tính "hộp cát" HTML5 để vô hiệu hoá các tập lệnh và cung cấp nhiều tính năng bảo mật...

Theo quy cách: Thuộc tính hộp cát, khi được chỉ định, sẽ cho phép một số hạn chế bổ sung đối với mọi nội dung do iframe lưu trữ. Giá trị của thuộc tính này phải là một tập hợp không có thứ tự gồm các mã thông báo duy nhất được phân tách bằng dấu cách và không phân biệt chữ hoa chữ thường theo bảng mã 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ị vô hiệu hoá, các đường liên kết sẽ không nhắm đến các bối cảnh duyệt web khác và các trình bổ trợ sẽ bị vô hiệu hoá.

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 rõ ràng. Một lỗi sẽ xảy ra 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 lại là iframe? Tại sao không chỉ sử dụng innerHTML? Mặc dù innerHTML hiện là một phần của quy cách HTML5, nhưng việc chèn phản hồi từ một máy chủ (dù là máy chủ độc hại hay không) vào một khu vực chưa được kiểm tra là một hành vi nguy hiểm. Trong quá trình viết bài này, tôi không thấy bất kỳ ai sử dụng thứ gì khác ngoài innerHTML. Tôi biết JQuery sử dụng nó ở cốt lõi với một dự phòng nối chỉ khi có 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ử chuyên sâu nào liên quan đến việc innerHTML "ngừng hoạt động ngẫu nhiên", nhưng sẽ rất thú vị khi xem tất cả các nền tảng mà vấn đề này ảnh hưởng. Cũng sẽ rất thú vị khi xem cách tiếp cận nào hiệu quả hơn... Tôi cũng đã nghe thấy những tuyên bố từ cả hai phía về vấn đề này.

Phát hiện, xử lý và lập hồ sơ loại mạng

Giờ đây, khi có khả năng lưu vào bộ nhớ đệm (hoặc bộ nhớ đệm dự đoán) ứng dụng web, 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 để ứng dụng thông minh hơn. Đây là nơi quá trình phát triển ứng dụng 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 The Network Information API. Mỗi khi tôi giới thiệu tính năng này trong một bản trình bày, sẽ có người trong khán giả giơ tay và hỏi "Tôi sẽ dùng tính năng đó để làm gì?". Vì vậy, đây là một cách có thể để thiết lập một ứng dụng web di động cực kỳ thông minh.

Trước tiên, hãy xem xét một tình huống nhàm chán nhưng hợp lý... Khi bạn tương tác với Web trên thiết bị di động trong lúc đi tàu cao tốc, mạng có thể bị ngắt ở 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ể dùng được ở một số khu vực đô thị, nhưng các khu vực vùng sâu vùng xa có thể chỉ 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 và ngoại tuyến hay không.
  • Phát hiện thời điểm chuyển từ chế độ ngoại tuyến sang trực tuyến và ngược lại.
  • Phát hiện các 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 sẽ phát hiện các sự kiện và tình huống 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 hay một yêu cầu hoặc hoạt động làm mới trang thực tế hay không. Lý do chính là vì sự kiện onload của phần 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 có một quy trình kiểm tra đơn giản cho 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 phức tạp 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 cho các kết nối không liên tục.

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

Điều này cũng áp dụng cho 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. Đoạn mã dưới đây sẽ tìm ra tất cả các đường liên kết bên ngoài của chúng ta và vô hiệu hoá chúng, khiến người dùng bị mắc kẹt 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, bây giờ chúng ta sẽ chuyển sang phần quan trọng. Giờ đây, ứng dụng của chúng ta đã biết trạng thái kết nối hiện tại, chúng ta cũng có thể kiểm tra loại kết nối khi ứng dụng đang trực tuyến 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 bình luận cho từng 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);
  }
}

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

Tiến trình yêu cầu (đồng bộ) của Edge

Edge Sync

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

WIFI Async

Điều này cho phép điều chỉnh trải nghiệm người dùng theo một số phương pháp dựa trên tốc độ kết nối chậm hoặc nhanh. Đây hoàn toàn không phải là giải pháp tối ưu. Một việc khác cần làm là hiển thị một phương thức tải khi người dùng nhấp vào một đường liên kết (khi 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 những gì mới nhất và tốt nhất mà HTML5 mang lại. 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 phát triển các ứng dụng HTML5 dành cho thiết bị di động chỉ mới bắt đầu. Giờ đây, bạn có thể thấy những nền tảng rất đơn giản và cơ bản của một "khung" di động chỉ được xây dựng dựa trên HTML5 và các công nghệ hỗ trợ. Tôi nghĩ rằng các nhà phát triển cần phải làm việc và giải quyết những tính năng này ở cốt lõi chứ không phải được che giấu bằng một trình bao bọc.