簡介
在現今的行動網路環境中,旋轉重新整理、網頁轉換不順暢,以及輕觸事件的週期性延遲,只是其中幾項令人頭痛的問題。開發人員盡可能想接近原生,但經常會因駭客入侵、重設和僵硬的架構而脫軌。
本文將討論建立行動 HTML5 網頁應用程式的最低需求。重點是揭露現今行動架構試圖隱藏的複雜性。您會看到簡約的做法 (使用核心 HTML5 API) 和基本原理,有助於編寫自己的架構,或為目前使用的架構貢獻心力。
硬體加速
一般來說,GPU 會處理詳細的 3D 模型或 CAD 圖表,但在這個案例中,我們希望透過 GPU 讓原始繪圖 (div、背景、帶有陰影的文字、圖片等) 呈現平滑效果,並順暢地製作動畫。遺憾的是,大多數前端開發人員都會將這項動畫程序交給第三方架構,而不會考慮語意,但這些核心 CSS3 功能是否應該遮蓋?以下是重視這些事項的幾個重要原因:
記憶體配置和運算負擔 - 如果您為了硬體加速而組合 DOM 中的每個元素,下一個處理您程式碼的人可能會追上您,並狠狠地揍您一頓。
耗電量:顯然,硬體啟動時,電池也會開始耗電。開發行動版網頁應用程式時,開發人員必須考量各種裝置限制。隨著瀏覽器製造商開始允許存取越來越多裝置硬體,這種情況將更加普遍。
衝突:如果對已加速的網頁部分套用硬體加速,就會發生故障。因此,瞭解是否出現加速重疊非常重要。
為了讓使用者互動順暢,並盡可能接近原生體驗,我們必須讓瀏覽器為我們工作。理想情況下,我們希望行動裝置 CPU 設定初始動畫,然後讓 GPU 僅負責在動畫程序期間合成不同圖層。translate3d、scale3d 和 translateZ 的作用就是為動畫元素提供專屬圖層,讓裝置能順暢地一起算繪所有內容。如要進一步瞭解加速合成和 WebKit 的運作方式,請參閱 Ariya Hidayat 網誌上的這篇文章。
頁面轉場效果
開發行動網路應用程式時,最常見的使用者互動方式有三種:滑動、翻轉和旋轉效果。
如要查看實際運作的程式碼,請前往 http://slidfast.appspot.com/slide-flip-rotate.html (注意:這個範例是為行動裝置建構,因此請啟動模擬器、使用手機或平板電腦,或將瀏覽器視窗縮小至約 1024 像素以下)。
首先,我們會剖析滑動、翻轉和旋轉轉場效果,以及這些效果的加速方式。請注意,每個動畫只需要三到四行的 CSS 和 JavaScript。
滑動
滑動頁面轉場效果是三種轉場方式中最常見的一種,可模擬行動應用程式的原生體驗。系統會叫用投影片轉場效果,將新的內容區域帶入檢視區塊。
如要使用滑動效果,請先宣告標記:
<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。 我們可以處理每部裝置和每個解析度 (請參閱媒體查詢解析度)。我在這個示範中只用了幾個簡單的例子,涵蓋行動裝置上大部分的直向和橫向檢視畫面。這項功能也適用於為每個裝置套用硬體加速功能。舉例來說,由於 WebKit 的電腦版會加速所有轉換的元素 (無論是 2D 或 3D),因此建立媒體查詢並排除該層級的加速功能是合理的做法。 請注意,在 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 版本缺少 3D CSS 轉換功能,因此無法正確轉譯旋轉轉場效果。很遺憾,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);
}
偵錯硬體加速
現在我們已介紹過基本轉場效果,接著來看看轉場效果的運作和合成機制。
如要進行這場神奇的偵錯工作階段,請啟動幾個瀏覽器和您選擇的 IDE。首先,從指令列啟動 Safari,以便使用部分偵錯環境變數。我使用的是 Mac,因此指令可能會因作業系統而異。 開啟終端機並輸入下列內容:
- $> export CA_COLOR_OPAQUE=1
- $> export CA_LOG_MEMORY_USAGE=1
- $> /Applications/Safari.app/Contents/MacOS/Safari
這會啟動 Safari,並提供幾個偵錯輔助工具。CA_COLOR_OPAQUE 會顯示實際合成或加速的元素。CA_LOG_MEMORY_USAGE 會顯示我們將繪圖作業傳送至備份儲存空間時的記憶體用量。這項資訊會顯示行動裝置的負擔程度,並可能提供提示,說明 GPU 使用率如何耗盡目標裝置的電池電量。
現在啟動 Chrome,看看每秒影格數 (FPS) 資訊:
- 開啟 Google Chrome 網路瀏覽器。
- 在網址列中輸入 about:flags。
- 向下捲動幾項,然後按一下「FPS 計數器」的「啟用」。
如果您在強化版 Chrome 中查看這個頁面,左上角會顯示紅色 FPS 計數器。
這表示硬體加速功能已開啟。這也有助於我們瞭解動畫的執行方式,以及您是否有任何洩漏 (應停止的連續執行動畫)。
如要實際查看硬體加速功能,請在 Safari 中開啟相同頁面 (使用上述環境變數)。所有加速 DOM 元素都會帶有紅色色調。這會顯示各層合成的確切內容。 請注意,白色導覽並未變成紅色,因為這項導覽並未加速。
您也可以在 about:flags 的「Composited render layer borders」中,找到 Chrome 的類似設定。
如要查看合成層,另一個好方法是套用這個模組,然後查看 WebKit 落葉示範。
最後,為了真正瞭解應用程式的圖形硬體效能,讓我們看看記憶體的使用情況。 從這裡可以看到,我們將 1.38 MB 的繪圖指令推送至 Mac OS 上的 CoreAnimation 緩衝區。OpenGL ES 和 GPU 會共用 Core Animation 記憶體緩衝區,以建立您在螢幕上看到的最終像素。
當我們只是調整瀏覽器視窗大小或將視窗放到最大時,也會看到記憶體擴充。
只有將瀏覽器調整為正確尺寸,才能瞭解行動裝置的記憶體用量。如果您是針對 iPhone 環境進行偵錯或測試,請將大小調整為 480 像素 x 320 像素。 我們現在已確切瞭解硬體加速的運作方式,以及偵錯所需的條件。閱讀相關資訊是一回事,但實際看到 GPU 記憶體緩衝區的運作情形,確實能幫助您瞭解情況。
幕後花絮:擷取和快取
現在,我們將進一步瞭解網頁和資源快取。與 JQuery Mobile 和類似架構採用的方法類似,我們將使用並行 AJAX 呼叫預先擷取及快取網頁。
讓我們來解決幾個核心行動網站問題,並說明我們為何需要這麼做:
- 擷取:預先擷取網頁可讓使用者離線使用應用程式,且瀏覽動作之間不必等待。當然,我們不希望裝置連線時頻寬受到限制,因此請謹慎使用這項功能。
- 快取:接下來,我們希望在擷取及快取這些網頁時,採用並行或非同步方法。我們也需要使用 localStorage (因為裝置普遍支援),但很遺憾的是,這並非非同步作業。
- AJAX 和剖析回應:使用 innerHTML() 將 AJAX 回應插入 DOM 很危險 (而且不可靠?)。我們改用可靠機制插入 AJAX 回應,並處理並行呼叫。我們也會利用 HTML5 的一些新功能剖析
xhr.responseText。
以「滑動、翻轉和旋轉」示範中的程式碼為基礎,我們首先會新增一些次要頁面,並連結至這些頁面。然後系統會剖析連結,並即時建立轉場效果。
如您所見,我們在這裡運用了語意標記。只是連結到其他頁面。子頁面會採用與父項相同的節點/類別結構。我們可以進一步使用「page」節點的 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」物件,確保後續處理程序能以非同步方式正確執行。如要進一步瞭解如何在 AJAX 呼叫中使用 localStorage,請參閱「Working Off the Grid with HTML5 Offline」。在這個範例中,您會看到在每個要求中快取的基本用法,以及伺服器傳回成功 (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。下一節將說明在應用程式快取範圍外擷取及快取這些網頁/標記的完整原因。
隨著 HTML5 的 iframe 元素最近的進展,我們現在可以簡單有效地剖析從 AJAX 呼叫傳回的 responseText。有許多 3000 行的 JavaScript 剖析器和正規運算式,可移除指令碼標記等。但何不讓瀏覽器發揮所長?在本範例中,我們將 responseText 寫入暫時隱藏的 iframe。我們使用 HTML5「沙箱」屬性,可停用指令碼並提供多項安全功能…
根據規格: 指定沙箱屬性後,系統會對 iframe 代管的任何內容套用一組額外限制。這個值必須是不重複的無序權杖集,以空格分隔,且不區分大小寫。允許的值包括 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 隨機停止運作」進行任何大量測試,但很想瞭解這會影響哪些平台。此外,哪種做法的成效較好也很有趣,我聽說雙方都有相關說法。
偵測、處理及分析網路類型
現在我們有能力緩衝 (或預測快取) 網頁應用程式,因此必須提供適當的連線偵測功能,讓應用程式更智慧。因此,行動應用程式開發作業對線上/離線模式和連線速度極為敏感。輸入網路資訊 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 (同步) 要求時間軸
WIFI (非同步) 要求時間軸
這樣一來,至少可以根據連線速度調整使用者體驗。 這絕非萬靈丹,另一個待辦事項是,在點選連結時 (連線速度緩慢時) 顯示載入模式,因為應用程式可能仍在背景擷取該連結的網頁。 重點在於減少延遲,同時充分運用使用者連線的功能,並提供最新最棒的 HTML5 體驗。 按這裡查看網路偵測示範。
結論
行動 HTML5 應用程式的發展才剛起步,現在您可以看到行動「架構」的簡單基本原理,完全是以 HTML5 和支援技術為基礎。我認為開發人員應處理這些核心功能,而非透過包裝函式遮蓋。