提高行動成效的 HTML5 技巧

Wesley Hales
Wesley Hales

簡介

在現今的行動網路環境中,旋轉重新整理、網頁轉換不順暢,以及輕觸事件的週期性延遲,只是其中幾項令人頭痛的問題。開發人員盡可能想接近原生,但經常會因駭客入侵、重設和僵硬的架構而脫軌。

本文將討論建立行動 HTML5 網頁應用程式的最低需求。重點是揭露現今行動架構試圖隱藏的複雜性。您會看到簡約的做法 (使用核心 HTML5 API) 和基本原理,有助於編寫自己的架構,或為目前使用的架構貢獻心力。

硬體加速

一般來說,GPU 會處理詳細的 3D 模型或 CAD 圖表,但在這個案例中,我們希望透過 GPU 讓原始繪圖 (div、背景、帶有陰影的文字、圖片等) 呈現平滑效果,並順暢地製作動畫。遺憾的是,大多數前端開發人員都會將這項動畫程序交給第三方架構,而不會考慮語意,但這些核心 CSS3 功能是否應該遮蓋?以下是重視這些事項的幾個重要原因:

  1. 記憶體配置和運算負擔 - 如果您為了硬體加速而組合 DOM 中的每個元素,下一個處理您程式碼的人可能會追上您,並狠狠地揍您一頓。

  2. 耗電量:顯然,硬體啟動時,電池也會開始耗電。開發行動版網頁應用程式時,開發人員必須考量各種裝置限制。隨著瀏覽器製造商開始允許存取越來越多裝置硬體,這種情況將更加普遍。

  3. 衝突:如果對已加速的網頁部分套用硬體加速,就會發生故障。因此,瞭解是否出現加速重疊非常重要。

為了讓使用者互動順暢,並盡可能接近原生體驗,我們必須讓瀏覽器為我們工作。理想情況下,我們希望行動裝置 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-leftstage-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) 資訊:

  1. 開啟 Google Chrome 網路瀏覽器。
  2. 在網址列中輸入 about:flags
  3. 向下捲動幾項,然後按一下「FPS 計數器」的「啟用」。

如果您在強化版 Chrome 中查看這個頁面,左上角會顯示紅色 FPS 計數器。

Chrome FPS

這表示硬體加速功能已開啟。這也有助於我們瞭解動畫的執行方式,以及您是否有任何洩漏 (應停止的連續執行動畫)。

如要實際查看硬體加速功能,請在 Safari 中開啟相同頁面 (使用上述環境變數)。所有加速 DOM 元素都會帶有紅色色調。這會顯示各層合成的確切內容。 請注意,白色導覽並未變成紅色,因為這項導覽並未加速。

複合聯絡人

您也可以在 about:flags 的「Composited render layer borders」中,找到 Chrome 的類似設定。

如要查看合成層,另一個好方法是套用這個模組,然後查看 WebKit 落葉示範

omposited Leaves

最後,為了真正瞭解應用程式的圖形硬體效能,讓我們看看記憶體的使用情況。 從這裡可以看到,我們將 1.38 MB 的繪圖指令推送至 Mac OS 上的 CoreAnimation 緩衝區。OpenGL ES 和 GPU 會共用 Core Animation 記憶體緩衝區,以建立您在螢幕上看到的最終像素。

Coreanimation 1

當我們只是調整瀏覽器視窗大小或將視窗放到最大時,也會看到記憶體擴充。

Coreanimation 2

只有將瀏覽器調整為正確尺寸,才能瞭解行動裝置的記憶體用量。如果您是針對 iPhone 環境進行偵錯或測試,請將大小調整為 480 像素 x 320 像素。 我們現在已確切瞭解硬體加速的運作方式,以及偵錯所需的條件。閱讀相關資訊是一回事,但實際看到 GPU 記憶體緩衝區的運作情形,確實能幫助您瞭解情況。

幕後花絮:擷取和快取

現在,我們將進一步瞭解網頁和資源快取。與 JQuery Mobile 和類似架構採用的方法類似,我們將使用並行 AJAX 呼叫預先擷取及快取網頁。

讓我們來解決幾個核心行動網站問題,並說明我們為何需要這麼做:

  • 擷取:預先擷取網頁可讓使用者離線使用應用程式,且瀏覽動作之間不必等待。當然,我們不希望裝置連線時頻寬受到限制,因此請謹慎使用這項功能。
  • 快取:接下來,我們希望在擷取及快取這些網頁時,採用並行或非同步方法。我們也需要使用 localStorage (因為裝置普遍支援),但很遺憾的是,這並非非同步作業。
  • AJAX 和剖析回應:使用 innerHTML() 將 AJAX 回應插入 DOM 很危險 (而且不可靠?)。我們改用可靠機制插入 AJAX 回應,並處理並行呼叫。我們也會利用 HTML5 的一些新功能剖析 xhr.responseText

以「滑動、翻轉和旋轉」示範中的程式碼為基礎,我們首先會新增一些次要頁面,並連結至這些頁面。然後系統會剖析連結,並即時建立轉場效果。

iPhone 主畫面

在此查看「擷取及快取」示範。

如您所見,我們在這裡運用了語意標記。只是連結到其他頁面。子頁面會採用與父項相同的節點/類別結構。我們可以進一步使用「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') &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();
      }
    }
  }
};

我們使用「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 事件。

接著,我們簡單檢查 ononlineonload 事件。這個程式碼會在從離線切換為線上時重設已停用的連結,但如果這個應用程式更複雜,您可能會插入邏輯,以便繼續擷取內容或處理間歇性連線的 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 (同步) 要求時間軸

Edge Sync

WIFI (非同步) 要求時間軸

WIFI Async

這樣一來,至少可以根據連線速度調整使用者體驗。 這絕非萬靈丹,另一個待辦事項是,在點選連結時 (連線速度緩慢時) 顯示載入模式,因為應用程式可能仍在背景擷取該連結的網頁。 重點在於減少延遲,同時充分運用使用者連線的功能,並提供最新最棒的 HTML5 體驗。 按這裡查看網路偵測示範

結論

行動 HTML5 應用程式的發展才剛起步,現在您可以看到行動「架構」的簡單基本原理,完全是以 HTML5 和支援技術為基礎。我認為開發人員應處理這些核心功能,而非透過包裝函式遮蓋。