簡介
在現今的行動網路環境中,點擊事件旋轉、頁面是否間斷斷續續,以及輕觸事件定期延遲等等,只是其中幾個例子而已。開發人員嘗試盡可能以原生方式近乎原生,但經常受到駭客入侵、重設和嚴謹架構的限制。
本文將探討建立行動 HTML5 網頁應用程式所需的最低需求。重點是排除在現今行動架構中不得隱藏的複雜層面。您將看到一個極簡風的方法 (使用核心 HTML5 API) 和基本基本知識,可協助您撰寫自己的架構,或參與目前使用中的架構。
硬體加速
一般來說,GPU 會處理詳細的 3D 模型或 CAD 圖表,但在這個案例中,我們希望使用原始繪圖 (例如 Div、背景、含有投射陰影的文字、圖片等) 流暢呈現透過 GPU 呈現的流暢效果。可惜的是,大部分前端開發人員都會將動畫程序擴及第三方架構,而不考慮其語意,但這些核心 CSS3 功能是否應該遮蓋?以下將說明為何這點很重要:
記憶體配置和運算負擔:如果您只是為了硬體加速而組合 DOM 中的每個元素,另一位執行程式碼的人員可能會讓您受傷並擊敗您。
耗電量:顯而易見,硬體啟動後會消耗電量。開發行動裝置專用的應用程式時,開發人員必須在編寫行動網頁應用程式時,將各種裝置限制納入考量。當瀏覽器製造商開始開放使用者存取更多裝置硬體及更多硬體時,這個需求會更加普遍。
衝突 — 將硬體加速功能套用至已加速的網頁部分時,我遇到異常行為。因此瞭解是否有重疊的加速功能非常重要。
為了讓使用者互動體驗更順暢、與原生廣告相近,我們必須讓瀏覽器正常運作。在理想情況下,我們希望行動裝置 CPU 能設定初始動畫,然後讓 GPU 在動畫過程中只組合不同圖層。而轉譯 3d、scale3d 和 translationZ 都是透過獨立的圖層,為動畫元素提供專屬圖層,藉此讓裝置流暢地呈現所有內容。如要進一步瞭解加速合成和 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 會加速所有轉換元素 (無論是 2-D 或 3-D),因此建立媒體查詢並從該層級排除加速,才很合理。請注意,在 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';
}
我試著與立方針做嘗試,為轉場效果營造出最佳的原住民氛圍,但緩解過程確實能發揮效果。
最後,為進行導覽,我們必須呼叫在上一個示範中使用的先前定義的 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,所以指令可能會因作業系統而異。 開啟終端機並輸入以下內容:
- $> 匯出 CA_COLOR_OPAQUE=1
- $> 匯出 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 查看這個頁面,畫面左上角會顯示紅色的每秒影格數計數器。
這就是我們知道硬體加速功能開啟的原因。此外,這也讓我們瞭解動畫的執行方式,以及是否有任何外洩情形 (連續執行中的動畫應該停止)。
實際以視覺化方式呈現硬體加速的另一種方法,是在 Safari 中開啟相同網頁 (使用我剛才提到的環境變數)。每個加速 DOM 元素都有紅色色調。我們得以確切顯示出各圖層合成的內容。請注意,白色導覽不會加速,因此不會是紅色。
Chrome 的 about:flags 部分也提供 Chrome 的類似設定「複合轉譯圖層邊框」。
另一個查看合成圖層的好方法,就是在套用這個模組時查看 WebKit 落葉示範。
最後,為了真正瞭解應用程式的圖形硬體效能,讓我們來看看記憶體的使用情形。這裡可以看到,在 Mac OS 上,我們將 1.38 MB 的繪圖操作說明推送到 CoreAnimation 緩衝區。核心動畫記憶體緩衝區會在 OpenGL ES 和 GPU 之間共用,藉此建立畫面上顯示的最終像素。
當我們調整瀏覽器視窗大小或把瀏覽器視窗放到最大時,記憶體也會隨之展開。
因此,只有在您將瀏覽器尺寸調整為正確尺寸時,您才能夠瞭解行動裝置的記憶體用量。如果您之前是針對 iPhone 環境進行偵錯或測試,請將大小調整為 480 x 320 像素。我們現在明確瞭解硬體加速的運作方式,以及偵錯作業要採取什麼動作。值得一看,這是值得閱讀的,但實際上,要看到 GPU 記憶體緩衝區的實際運作情形,確實有助於呈現各種內容。
幕後花絮:擷取與快取
讓我們進一步提升網頁和資源快取功能。如同 JQuery Mobile 與類似架構使用的方法,我們會使用並行 AJAX 呼叫來預先擷取及快取網頁。
以下將說明幾項行動網路核心問題,並說明原因:
- 擷取:預先擷取網頁可讓使用者離線執行應用程式,也不必等待瀏覽動作之間出現。當然,我們不希望在裝置連上網路時浪費裝置的頻寬,因此需要謹慎使用這項功能。
- 快取:接下來,我們希望在擷取及快取這些網頁時,採用並行或非同步的做法。幸好,由於 localStorage 有許多裝置廣泛支援,因此也需要使用 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」物件,確保進行適當的非同步後續處理作業。如要進一步瞭解如何在 AJAX 呼叫中使用 localStorage,請參閱「使用 HTML5 離線版」的 AJAX 呼叫。在這個範例中,您會看到每項要求的基本快取用法,以及在伺服器傳回成功 (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 個位元組,使儲存空間上限從 5MB 提高至總計 2.6MB。要在應用程式快取範圍之外擷取和快取這些網頁/標記的完整原因,將於下節說明。
我們最近對 HTML5 的 iframe 元素進行了改進,現在有一個簡單又有效的方法,可用來剖析 AJAX 呼叫傳回的 responseText
。有許多 3000 行 JavaScript 剖析器和規則運算式會移除指令碼標記等。但為何不讓瀏覽器發揮最高效益?在這個範例中,我們會將 responseText
寫入臨時隱藏 iframe。我們使用的 HTML5「sandbox」屬性會停用指令碼,並提供許多安全性功能...
根據規格:如果已指定 sandbox 屬性,系統會針對 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 規格,但將回應從伺服器 (邪惡與壞) 插入至未勾選區域,仍是危險的做法。撰寫本文時,除了內部 HTML 以外,我並沒有找到任何人。我知道 JQuery 是以其核心為基礎,但僅有例外狀況的附加備用選項。而且 JQuery Mobile 也有用。不過,我還沒對內部 HTML「隨機停止運作」進行了大量測試,但能瞭解所有平台都受到影響,實在非常有趣。值得一提的是,哪種做法成效較佳...我也聽說兩方都說的話。
網路類型偵測、處理及剖析
我們現在有緩衝 (或預測快取) 網頁應用程式的能力,因此必須提供適當的連線偵測功能,讓我們的應用程式變得更聰明。行動應用程式開發對線上/離線模式和連線速度來說非常敏感。輸入 The Network Information API,每次我在簡報中展示這項功能時,聽眾會舉手並詢問「我能用在什麼地方?」,所以我們可以在這裡設定極具智慧的行動網頁應用程式。
這裡優先處理無聊的常見情境... 在高速火車上透過行動裝置與網路互動時,網路可能非常順利,因此不同地理位置可能支援不同的傳輸速度 (例如HSPA 或 3G 可能適用於部分都會區,但偏遠地區可能支援較慢的 2G 技術。以下程式碼可解決大多數的連線情境。
下列程式碼提供:
- 透過「
applicationCache
」離線存取 - 偵測是否已加入書籤或離線。
- 偵測從離線切換至線上狀態,反之亦然。
- 偵測連線速度緩慢,並根據網路類型擷取內容。
同樣地,所有功能只需極少的程式碼。首先,我們會偵測事件和載入情境:
window.addEventListener('load', function(e) {
if (navigator.onLine) {
// new page load
processOnline();
} else {
// the app is probably already cached and (maybe) bookmarked...
processOffline();
}
}, false);
window.addEventListener("offline", function(e) {
// we just lost our connection and entered offline mode, disable eternal link
processOffline(e.type);
}, false);
window.addEventListener("online", function(e) {
// just came back online, enable links
processOnline(e.type);
}, false);
在上方的 EventListener 中,我們必須告知程式碼是經由事件或實際網頁要求或重新整理來呼叫。主要原因是在線上和離線模式之間切換時,不會觸發內文 onload
事件。
接下來,我們會對 ononline
或 onload
事件進行簡單的檢查。從離線切換至線上狀態時,這個程式碼會重設已停用的連結,但如果這個應用程式較複雜,您可以插入邏輯,以便繼續擷取內容或處理使用者體驗,即使連線不間斷。
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);
}
}
我們可以對 getAndCache 程序進行許多調整,但我剛才做的都是以非同步 (true) 或同步 (false) 的方式擷取特定連線的資源。
邊緣 (同步) 要求時程
WIFI (非同步) 要求時程
這可讓系統根據連線速度緩慢或快速連線,對使用者體驗做出至少一部分的使用者體驗調整。換句話說,這並不是萬無一失的解決方案。另一個做法是在點選連結 (連線緩慢時) 時擲回載入視窗,同時應用程式可能仍在背景擷取該連結頁面。 重點在於減少延遲,同時充分利用最新、最棒的 HTML5 功能,為使用者帶來的完整連結。 如要查看網路偵測示範,請按這裡。
結語
行動 HTML5 應用程式的發展之道,現在,您可以看到完全以基本方式建構的 HTML5 行動「架構」及支援技術。我認為開發人員應該盡其核心來運用和處理這些功能,而不是以包裝函式遭到遮蓋。