簡介
在現今的行動網頁環境中,使用者會遇到許多令人頭痛的問題,例如重新整理時畫面會轉圈、網頁轉換不流暢,以及輕觸事件會定期延遲。開發人員會盡可能使用原生 API,但經常會因為駭客攻擊、重設和僵硬的架構而中斷。
在本文中,我們將討論建立行動 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';
}
我曾試著使用立方貝茲,為轉場效果最佳的原住民感覺,但緩解技巧卻提供了極限。
最後,為了讓導覽功能生效,我們必須呼叫先前在上一個示範中使用的已定義 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
- $> 匯出 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 中,您也可以找到類似的 Chrome 設定:「Composited render layer borders」。
另一種查看合成圖層的絕佳方式,就是在套用此模組時查看 WebKit 落葉示範。
最後,為了真正瞭解應用程式的圖形硬體效能,我們來看看記憶體的用量情形。我們可以看到,我們將 1.38 MB 的繪圖指令推送至 Mac OS 上的 CoreAnimation 緩衝區。Core Animation 記憶體緩衝區會在 OpenGL ES 和 GPU 之間共用,以便建立您在螢幕上看到的最終像素。
如果只是調整瀏覽器視窗的大小或將其最大化,記憶體也會跟著增加。
這樣一來,您就能瞭解行動裝置上記憶體的用量,前提是您將瀏覽器的大小調整為正確的尺寸。如果您是針對 iPhone 環境進行偵錯或測試,請將大小調整為 480 x 320 像素。我們現已瞭解硬體加速的運作方式,以及偵錯所需的項目。閱讀相關資訊是一回事,實際看到 GPU 記憶體緩衝區運作情形則是另一回事,這能讓您更清楚瞭解相關情況。
幕後:擷取及快取
現在,讓我們的網頁和資源快取功能更上一層樓。就像 JQuery Mobile 和類似架構使用的做法一樣,我們會透過並行的 AJAX 呼叫,預先擷取及快取網頁。
以下說明幾個行動網站的主要問題,以及我們需要進行這項調整的原因:
- 擷取:預先擷取網頁可讓使用者離線使用應用程式,並在瀏覽動作之間不必等待。當然,我們不希望裝置上線時,裝置的頻寬會受到限制,因此我們需要謹慎使用這項功能。
- 快取:接下來,在擷取與快取這些網頁時,我們希望以並行或非同步的方式執行。我們也需要使用 localStorage (因為各裝置都支援),但很遺憾,它不是非同步的。
- MPEG 並剖析回應:使用 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 離線功能離線作業」一文。在本例中,您將瞭解快取在每項要求上的基本用途,以及在伺服器傳回任何回應 (除了成功回應 (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「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 「隨機停止運作」進行任何嚴重測試,但如果能看到所有受影響的平台,將會非常有趣。看看哪種做法成效較佳也很有趣...我聽說雙方都對此有說法。
網路類型偵測、處理和分析
既然我們可以緩衝 (或預測快取) 我們的網路應用程式,就必須提供適當的連線偵測功能,讓應用程式變得更聰明。這就是行動應用程式開發作業對線上/離線模式和連線速度極為敏感的情況。輸入「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 中,我們必須通知程式碼,它是來自事件,還是實際網頁要求或重新整理。主要原因是,在線上和離線模式之間切換時,系統不會觸發 body 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);
}
}
我們可以對擷取 AndCache 程序進行許多調整,但剛才提到的都是,我們指出擷取某個連線的資源是以非同步 (true) 或同步 (false) 的方式擷取。
Edge (同步) 要求時間軸
WIFI (非同步) 要求時間軸
這可讓您至少以某些方式,根據連線速度快慢調整使用者體驗。這絕非萬能解決方案。另一個待辦事項是,在連線速度較慢的情況下,點選連結時,應用程式仍可能會在背景擷取該連結的網頁,此時應顯示載入模式。這麼做的重點在於縮短延遲時間,同時發揮使用者的完整功能,提供最新、最優質的 HTML5 功能。 按這裡查看網路偵測示範。
結論
行動 HTML5 應用程式之旅才剛開始。您現在已瞭解,只要以 HTML5 和相關支援技術為基礎,就能建立非常簡單且基本行動「架構」。我認為開發人員應以核心方式處理這些功能,並非透過包裝函式。