編輯:中級開發
簡介: 本文是共兩部分的系列文章 “android 和 iPhone 浏覽器之戰” 的第 2 部分,主要關注為 iPhone 和 Android 開發基於浏覽器的應用程序。在第 1 部分中,我們介紹了 iPhone 和 Android 中的浏覽器的核心引擎 WebKit。在本文中,我們將更進一步,構建一個運行在 iPhone 和 android 浏覽器之上的網絡管理應用程序。該應用程序同時展示了浏覽器的本地 SQL 存儲和 AJax,後者是幫助移動浏覽器獲得豐富應用程序體驗的關鍵技術。此外,此應用程序還利用了流行的 jQuery JavaScript 庫。
簡介
開發具有豐富特性的移動 Web 應用程序在過去一直面臨著重重困難,但是這一局面正在迅速改變。網速不夠快的問題仍然沒有完全解決,但是隨著 3G 技術開始廣泛應用於移動電話,這一問題已經得到了極大地改善。同樣,用戶界面也有了很大的改進,iPhone 是這一領域的領先者,而 android 則通過創新的設計以及能夠實現最佳 Html 和 CSS 呈現的強大的 WebKit 引擎獲得了發展動力。用戶使用蹩腳的移動 UI 再也無法出色地完成工作。
和桌面浏覽器體驗一樣,當將本地數據庫存儲用於浏覽器後,移動 Web 應用程序也經歷了一次復興。另一項助力下一代移動 Web 應用程序的技術是 Ajax。AJax 描述了這樣一種實踐:使用 JavaScript 調用服務器端頁面以通過 HTTP 檢索特定數據元素,這個過程不需要檢索和重新呈現全部 Web 頁面。由於 AJax 以異步方式工作,因此移動 Web 應用程序在特性功能方面得到了巨大的改進。
在本文中,我們將主要針對 iPhone 和 android。為了方便開發,我們還將在桌面中測試 Safari 內的應用程序。由於 Safari 同樣基於 WebKit,因此它是一個理想的開發平台,它加速了開發並提供了出色的調試幫助,這要歸功於十分有用的 WebKit Inspector 應用程序。
應用程序
在開始研究代碼細節之前,讓我們先來了解一下這個應用程序的目標。我們希望通過這個網絡監控/管理應用程序完成哪些事呢?
如果您曾經管理過一個面向客戶的 Web 站點 — 內部和外部 — 那麼您很可能收到過一個表示 Web 站點停止運轉的通知。這條通知可能來自一個自動的主動監視工具,有時甚至是您不願意看到的形式,比如來自客戶的電子郵件或電話,表示 “網站停止運行。請盡快檢查並給予回復。”
當遇到此類問題時您的第一反應是什麼?您會打開浏覽器並嘗試加載主頁。也許宕機與某個位置的連接性有關。有時會發生這種情況,並且這樣做也是有收獲的,避免您花費力氣來尋找根本不存在的問題。
當我們排除了基本的客戶機連接性問題後,診斷的下一步是嘗試收集一些關鍵的細節,比如文件系統資源、可用內存、各種連接性、最新的錯誤消息,等等。執行這些操作通常需要通過 Remote Desktop 或 SSH 會話訪問服務器本身。但是,如果您碰巧不能使用桌面計算機,那該怎麼辦?
收到表示 “站點停止運轉” 的通知絕對不是件好玩的事。它經常會在不合時宜的時間發生,比如當您不在辦公室並且沒辦法使用傳統 Internet 連接時。這常常令您一籌莫展,不知該如何是好。隨著時間的推移,您了解到某個特定站點出現故障的原因無非就那麼幾個。問題可能是由於一個外部資源(數據庫或第三方支付處理程序)不可用;或者文件傳輸失敗,Web 站點的部分呈現包含了不恰當或過時的信息。不管出於哪些問題,在診斷問題的初始階段,一些關鍵的統計數據會有幫助。這些統計數據或性能指標因站點而異。而我們將在本文探討的應用程序的目標就是構建一種可以解決此類問題的工具。
本文討論的應用程序旨在幫助您在辦公室以外的環境下診斷 Web 站點問題。如果您有一台 iPhone 或 android 設備,那麼您已經獲得了一些動力。我們可以利用這些強大平台的功能來幫助管理我們的 Web 站點。
設計考慮事項
此應用程序的設計動機是盡可能地獨立於服務器數據庫 — 換言之,我們希望此應用程序可以在無需對第三方服務創建帳戶的情況下運行。我們准備這樣構建應用程序:用戶只需下載一次 Web 頁面,之後就可以在其浏覽器中本地運行它。當然,用戶可以根據需要對頁面設置書簽並執行刷新,以獲取隨後添加到應用程序中的任何新特性。然而,所有數據都被本地存儲在移動設備中的 SQL 數據庫中。
雖然將這類數據存儲到服務器等位置確實有其優勢,但是針對多名用戶管理和存儲數據的功能不屬於本文討論范圍。我們關注的主要問題是利用本地存儲和 AJax 構建一個實用的、有用的應用程序。我們最後將提出一些用於擴展應用程序的邏輯步驟。
此應用程序的代碼可以分為兩個不同的部分。我們具有運行在移動設備上的代碼,其中包括:
我們還包含運行在服務器上的代碼,並且這些代碼對於每一個希望管理的站點都是不同的。這些代碼的細節在其實現方面存在差異,但是產生的內容始終是相同的:一個 JavaScript Object Notation (JSON) 對象,其中包含了一些特定的屬性,包括屬性名,即由名稱/值對組成的數組。這個 JSON 對象中的數據描述了如何在應用程序的浏覽器中呈現它。此外,名稱/值對包含針對每個站點的關鍵操作數據,因此提供了快速了解情況並幫助支持人員正確地確定和解決問題所必需的信息。
構建應用程序
我們首先了解此應用程序的數據模型。當對數據模型有所了解後,我們將查看與數據交互的代碼,從而構建實際的應用程序。
數據模型
當通過浏覽器將數據存儲在移動設備上時,實際上數據被存儲在一個 Html 5 數據庫,或一個可通過浏覽器訪問的 SQL 數據庫存儲中。基於浏覽器的 SQL 數據的規范仍然在不斷演變,具體細節仍然在整理當中。然而,就實踐而言,我們現在已經可以將它用於 iPhone、android 和其他支持 WebKit 的環境中。這個數據庫功能實際上就是一個與底層 SQLite 實現交互的 JavaScript 接口。我們的數據庫只包含一個表:tbl_resources。
該表模擬了應用程序在運行時使用的數據 — 實際上就是一個 JSON 對象。每個對象包含以下內容:
清單 1 是一個表示某個站點的示例 JSON 對象。
清單 1. 表示某個站點的 JSON 對象
[ { name : 'msi-wireless.com', homeurl : 'http://msiservices.com', pingurl : 'http://ibm.msi-wireless.com/mobile2/netmon.PHP', status : 'OK', summary : 'Everything is fine...', items : [ {name : 'DiskSpace', value : '22.13 GB'}, {name : 'Database Up?', value : 'Yes'} ] } ]
數據庫的作用是長時間持久化數據,但這是通過包含這些對象的數組實現的,而不是不斷地引用數據庫。這個策略的實現大大簡化了 Javascript 內的操作,並且最小化了數據進出數據庫的次數。然而,對於具有大量數據元素的應用程序,直接使用數據庫可能更具優勢,或者很可能使用某種 “分頁” 模式,其中每次從數據庫獲取大量元素,而一個 JavaScript 數組將包含元素的一個 “窗口”,表示全部數據項的其中一個子集。
圖 1 包含了數據庫結構的屏幕快照,其中包含一些記錄。可以通過 Web Inspector 查看數據庫,該工具是 Safari/WebKit 浏覽器平台的一部分。這也是為什麼 WebKit 開發具有強大功能的原因之一。我們將在桌面上查看 Web Inspector。所有代碼在 iPhone 和 android 上都表現良好。
注意:Android V2.0 是這些代碼的目標。android 中設置的 WebKit 特性隨著版本的發行而日趨成熟。
圖 1. 數據庫結構的屏幕快照,其中包含一些記錄
現在我們已經了解了數據元素的大致樣子,那麼如何使用我們的應用程序管理它們呢?
客戶端 — 默認視圖
應用程序的默認 UI 是一個列表,其中列出了正在管理中的站點,這些站點根據其狀態值排序。狀態為 not OK 的站點被列在了前面,如圖 2 所示。
圖 2. 狀態為 not OK 的站點
我們看到有三個站點處於管理中。目前,有兩個站點顯示出現問題。如果一個站點處於良好的狀態,那意味著它的狀態屬性為 OK,那麼我們將不會顯示摘要字段,並且將以黑色文本顯示。如果狀態為 BAD,我們將在站點名稱旁邊顯示摘要,並且 CSS 文件中名為 BAD 的樣式將指出呈現屬性 — 本例中為紅色文本。有關更多細節,參考文件 netmon.CSS;此應用程序的完整源代碼可以從 下載 部分獲得。
通過單擊某個條目,就可以隱藏或顯示有關該條目的細節。如 圖 2所示,每個條目都有三個可用鏈接,其中為主頁和 ping URL 位置,然後是包含了有關站點細節的部分,即表示該站點的狀態的名稱/值對列表。
在本例中,我們看到此站點的名稱為 ibm demo 2,其摘要為 “No more coffee?”,這當然不算什麼緊迫的技術事件,但是為我們提供了一個有趣的示例。跳到該條目的 Details 部分,我們看到這個服務器條件下隱藏的關鍵統計數據為:The Coffee Pot is empty。
我們可以點擊主頁鏈接,這將啟動一個新的浏覽器窗口。其次,我們可以通過進入 Refresh 鏈接刷新數據。我們稍後將查看 Refresh 步驟。
最後,我們通過選擇 Remove 鏈接刪除這個條目。一個簡單的window.confirm()
查詢要求我們確認是否希望執行這個不可恢復的任務。
客戶端 —— Html
要創建該列表,需要詳細了解兩個文件。第一個文件為 index.Html,如 清單 2 所示,第二個文件為 netmon.JS,如 清單 3 所示。在本文中,我們同時還將探查一些代碼片段,您可以從 下載 部分獲得完整的源代碼。
我們仍然指定移動 WebKit vIEwport meta 來幫助指導浏覽器按照我們希望的方式呈現頁面。我們還使用一些可選的 JavaScript 來指定一個針對特定設備的 CSS 文件。
新內容是包括了 JS 文件和一些 “邏輯” JavaScript 文件。該文件補充了一個新的 Html div
和 ID entryform
,其中包含直接從應用程序內添加新條目所需的元素。不需要加載不同的 HTML 頁面,這是完成此任務的傳統方法。回想一下,我們的設計目標之一就是我們的應用程序不依賴額外的服務器工具在所監視站點以外進行數據操作或存儲。清單 2 展示了 index.Html 中包含的語句。
清單 2. index.Html
<link rel="stylesheet" href="netmon.css" type="text/CSS" /> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="netmon.JS"></script> <script type="text/Javascript" src="json2.JS"></script> <script type="text/Javascript"> if (navigator.userAgent.indexOf('iPhone') != -1) { document.write('<link rel="stylesheet" href="iphone.css" type="text/css" />'); } else if (navigator.userAgent.indexOf('Android') != -1) { document.write('<link rel="stylesheet" href="android.css" type="text/css" />'); } else { document.write('<link rel="stylesheet" href="desktop.css" type="text/CSS" />'); }
客戶端 — 填充列表
數據從我們的本地數據庫獲取並顯示在一個服務器條目列表中。要獲得該列表,我們打開數據庫並發出一個 SQL 查詢,取回數據庫表中的所有數據項。
這類應用程序常用的一個技巧就是動態創建表。記住:沒有 DBA 可以幫助我們。我們的應用程序必須獨力完成全部工作。清單 3 包含了 netmon.JS 的源代碼:該文件實現了許多項,包括:
netmonResource
對象的數據類型構造函數的定義。dbHandle
— 用於管理到數據庫的連接的函數。initialize
— 用於創建/打開數據庫並檢索正在管理的服務器列表的函數。清單 3 包含有這個函數,它嘗試打開數據庫(如果尚未打開的話)並查詢數據庫。如果數據庫表沒有給出,該函數將發起創建表的過程。createResourcesTable
— 創建所需數據庫表並創建用於演示目的的單個默認條目的函數。addEntry
— 將新條目插入到數據庫表的函數。deleteEntry
— 從數據庫表刪除條目的函數。refreshEntry
— 通過使用 AJax 調用檢索條目的 ping URL,從而刷新條目的函數。Resources
— 包含數據庫條目的 “緩存” 的數組。某些其他功能作用於這個數組,而不是直接作用於數據庫。Render
— 返回 Html 字符串的函數,這些字符串根據我們顯示每個服務器條目的規則進行格式化。如果出現新的格式化想法,可以使用這個函數以及任何必要的 CSS 樣式實現。
清單 3. netmon.JS
initialize : function () { try { if (netmon.dbHandle == null) { netmon.dbHandle = openDatabase("netmondb","1.0","netmon",1000); } $("#mainContent").Html(""); netmon.dbHandle.transaction(function(tx) { tx.executeSql("SELECT * FROM tbl_resources order by status,nme", // parameters [], // good result function(tx,result){ try { var items = new Array(); for (var i = 0; i < result.rows.length; i++) { var row = result.rows.item(i); items[i] = new netmonResource(); items[i].name = row['nme']; items[i].homeurl = row['homeurl']; items[i].pingurl = row['pingurl']; items[i].status = row['status']; items[i].summary = row['summary']; items[i].items = eval(row['items']); if (items[i].items == undefined) { items[i].items = []; } } netmon.resources = items.slice(0); // setup gui with our data if (netmon.resources.length > 0) { jQuery.each(netmon.resources,function (index, value) { $("#mainContent").append(netmon.render(index,value)); }); $(".serverentry").click (function() \ {$(this).find(".serveritems").toggle();}); $(".serveritems").hide(); } } catch (e) { alert("Error fetching rows from database..." + e); } }, function(tx,error){ //alert("bad result on query: " + error.message + " let's try to create the db table"); netmon.createResourcesTable(); } ); }); }catch (e) { alert("error opening database. " + e); } }, ... render : function(index,itm) { try { var ret = ""; ret += "<div class='serverentry " + itm.status + " " + \ (index % 2 == 0 ? 'even' : 'odd') + "'>"; //ret += "<span class='name'>" + itm.name + "</span> <a target='_blank' href='" + itm.homeurl + "'>Show</a><br /><a target='_blank' href='javascript:netmon.deleteEntry(\"" + itm.name + "\");'>Remove</a><br />"; ret += "<span class='name'>" + itm.name + "</span>"; if (itm.status != "OK") { ret += "<span class='summary'>-" + itm.summary + "</span><br />"; } ret += "<div class='serveritems'>"; ret += "<a target='_blank' href='" + itm.homeurl + "'>Home</a> <a target='_blank' href='Javascript:netmon.refreshEntry(" + index + ",false); '>Refresh</a> <a target='_blank' href='Javascript:netmon.deleteEntry(\"" + itm.name + "\"); '>Remove</a><br />"; ret += "Home URL: " + itm.homeurl + "<br />"; ret += "PING URL: " + itm.pingurl + "<br />"; ret += "<hr />Details<br />"; jQuery.each(itm.items,function (j,itemdetail) { ret += ">>" + itemdetail.name + "=" + itemdetail.value + "<br />"; }); ret += "</div>"; ret += "</div>"; return ret; } catch (e) { return "<div class='error'>Error rendering item [" + itm.name + "] " + \ e + "</div>"; } } };
名稱空間 netmon
包含實現站點所需的大量邏輯,通過 index.Html 文件中的 helper 函數進行了增強。讓我們看看如何將一個新條目添加到數據庫中。
客戶端 — 添加一個站點
應用程序管理用戶需要的一個或多個站點的列表。對於每個站點,我們最初將收集並存儲以下內容:
為此,我們創建了一個簡單的表單,包含三個字符、字段標簽和幾個按鈕,如圖 3 所示。
圖 3. 簡單表單
此表單內容實際被包含在如 清單 2 所示的同一個 index.html 頁面中。通過使用一些 jQuery,我們可以在默認列表視圖和這個 add-new-server 表單之間切換。站點條目列表被包含在 Html div
中,其 ID 為 mainContent
。表單被包含在 Html div
中,其 ID 為entryform
,後者在最開始被隱藏。在任何時候,這兩個 div
元素的可見性都是相互排斥的。要切換它們的可見性,我們將對addNewEntry
函數中所示的每一個這些元素調用 jQuery 方法toggle
,如清單 4 所示。
清單 4. addNewEntry
函數
function addNewEntry() { $("#entry_name").val(""); $("#entry_homeurl").val(""); $("#entry_pingurl").val(""); $("#mainContent").toggle(); $("#entryform").toggle(); }
當生成一個新的服務器條目後,我們需要將其保存到數據庫並刷新服務器列表,以反映列表中新增加的條目。用戶可以通過選擇 Save按鈕來發起此操作,該按鈕將調用 index.Html 中實現的 saveEntry
JavaScript 函數。這個函數創建一個名為 netmonResource
的新 JavaScript 對象。我們將檢查所有字段是否都已正確填充,然後將其保存到數據庫中。我們通過調用 netmon.addEntry
保存新條目。此後,通過再次對兩個主要的 div
元素調用 toggle
來返回到條目列表。注意:我們並沒有實現完整的正則表達式來驗證 URL,這其實是一種好的做法。
接下來我們將討論 清單 3:netmon.JS。
要在 JavaScript 中使用 SQL 數據庫接口,需要兩個基本步驟。第一,如果數據庫尚未打開的話,我們需要打開它。在這個應用程序中,我們在初始化函數中打開了數據庫。要在數據庫中創建新記錄,讓我們看一看 addEntry
函數中的代碼,如 清單 5 所示。通過調用<databasehandle>.transaction(function (tx) {tx.executeSql()});
發起了一個 SQL 事務。在我們的例子中,數據庫句柄為 netmon.dbHandle
。
executeSql
函數接受 4 個參數:
?
表示。根據函數的第二個參數中包含的順序位置,每個 ?
被替換為一個值。?
占位符。清單 5 展示了函數 netmon.addentry
。
清單 5. netmon.addentry
函數
addEntry : function (entry) { try { netmon.dbHandle.transaction(function(tx) { tx.executeSql("insert into tbl_resources (nme,homeurl,pingurl,status,\ summary,items) values (?,?,?,?,?,?)", [ entry.name, entry.homeurl, entry.pingurl, entry.status, entry.summary, JSON.stringify(entry.items) ], function (tx,results) { // alert(entry.name + " added."); netmon.initialize(); }, function (tx,error) { alert("Error in addEntry [" + error.message + "]"); }); }); }catch (e) { alert("Error in netmon.addEntry " + e); } }
我們現在具備了添加條目的能力。從列表中刪除條目也十分簡單。參考 netmon.deleteEntry
函數,看看如何從數據庫中刪除條目。
現在讓我們看看如何使用 AJax 刷新條目。
客戶端 — 使用 AJax 刷新
如前所述,AJax 能夠在不刷新整個 Web 頁面的情況下檢索服務器端內容。這是通過浏覽器使用的不同底層技術實現的。然而,我們已經采用這種方法來依賴 Javascript 庫 jQuery 將這些細節隱藏起來。本文雖然關注的是 iPhone 和 android 浏覽器開發,但是通過這種方式,利用 JavaScript 庫變得非常普遍並可接受 — 甚至受到鼓勵。如果您了解到 jQuery 如何簡化工作的話,您也會同意這一點的。
netmon.refreshEntry
函數包含 AJax 代碼,如清單 6 所示,以及一些使用生成的數據更新數據庫的代碼。
清單 6. netmon.refreshEntry
函數
refreshEntry : function (entryidx,bfollow) { try { //alert("refresEntry [" + netmon.resources[entryidx].name + "][" + \ netmon.resources[entryidx].pingurl + "]"); $.get(netmon.resources[entryidx].pingurl, function (data,textstatus) { // alert("response is here : [" + data + "]"); try { var response = eval(data); netmon.dbHandle.transaction(function(tx) { tx.executeSql("update tbl_resources set status = ?,summary = ?,\ items = ? where nme = ?", [ response[0].status, response[0].summary, JSON.stringify(response[0].items), netmon.resources[entryidx].name ], function (tx,results) { netmon.initialize(); if (bfollow) { if (entryidx + 1 < netmon.resources.length) { netmon.refreshEntry(entryidx + 1,true); } } }, function (tx,error) { //alert("Error in refreshEntry [" + error.message + "]"); if (bfollow) { if (entryidx + 1 < netmon.resources.length) { netmon.refreshEntry(entryidx + 1,true); } } }); }); } catch (e) { alert("error handling response [" + e + "]"); } } ); } catch (e) { alert("Error refreshEntry " + e); } }
要從服務器獲取數據,只需調用 $.get()
。第一個參數是 URL,第二個參數是一個函數,將在完成調用後調用。在我們的例子中,通過使用前面提到的數據庫技巧,我們使用新檢索的數據更新數據庫(參見清單 3:netmon.refreshEntry
)。
jQuery 還提供了工具,為將參數傳遞給 Web 頁面提供 “表單編碼的數據”,並提供了有關期望返回的數據類型的暗示。參見 參考資料,獲得有關 jQuery 的 AJax 函數的介紹。根據您的應用程序需求,還可以實現額外的粒度。
注意,jQuery 還提供了一種直接訪問 JSON 對象的方法。然而,我們選擇使用 “通用的” AJax 函數,這樣就可以普遍應用於您可能希望構建的其他應用程序。
當從服務器接收回數據後,我們希望將其轉換為 JavaScript 對象以訪問其屬性。我們可以使用名為 eval
的 JavaScript 函數完成這個任務:var object = eval(<JSon text>);
。
此時,我們很好地處理了客戶端功能。現在還剩下一項內容需要討論:服務器端頁面,負責生成我們的應用程序要對之進行處理的 JSON。
服務器端 — 構建自己的監視頁面
我們已經介紹了我們的應用程序期望的 JSON 結構,但是如何生成這個結構?簡單來說,這取決於您。如果您希望使用 .Net 技術,則可以生成一個 .ASPx 頁面來動態生成數據。或者,可以使用一個定期調度的 shell 腳本來生成一個文本文件,您只需檢索這個文件來找出最近更新過的狀態。底線是應用程序必須能夠根據一個 URL 獲取一個 JSON 對象,但是執行生成 JSON 的任務完全取決於您。
清單 7 展示的樣例 PHP 文件生成了一個文件列表,並有選擇性地查看名為 error.txt 的文件。如果該文件不為空,那麼我們認為出現了某種問題並將構建 JSON 對象來表示一個 BAD 狀態值,然後使用 error.txt 文件的內容來更新 summary 屬性。
清單 7. 樣例 PHP 文件
<? $filecount = 0; $statusValue = "OK"; $summaryValue = "No Error"; ?> [ { items : [ { "name" : "Date", "value" : "<? echo date("m/d/y H:m:s"); ?>" }, <? foreach (new DirectoryIterator("./data") as $fileInfo) { if ($fileInfo->isDot() || $fileInfo->isDir()) continue; $filecount++; echo "{\"name\" : \"".$fileInfo->getFilename()."\",\"value\" : \"".$fileInfo->getSize()."\"},"; if ($fileInfo->getFilename() == "error.txt") { if ($fileInfo->getSize() > 0) { $statusValue = "BAD"; $fsize = $fileInfo->getSize(); $fh = fopen("./data/".$fileInfo->getFilename(),"r"); $summaryValue = fread($fh,$fsize); fclose($fh); } } } ?> {"name" : "FileCount","value" : "<? echo $filecount ?>"} ], "status" : "<? echo $statusValue ?>", "summary" : "<? print str_replace("\n","",$summaryValue) ?>" } ]
這些代碼生成如清單 8 所示的 JSON 數據,假設給出了一個錯誤條件。
清單 8. 清單 4 生成的 JSON 數據
[ { "items" : [ { "name" : "Date", "value" : "11/22/09 15:11:51" }, { "name" : "error.txt", "value" : "20" }, { "name" : "bad.html", "value" : "91" }, { "name" : "testfile.txt", "value" : "44" }, { "name" : "good.Html", "value" : "87" }, { "name" : "FileCount", "value" : "4" } ], "status" : "BAD", "summary" : "the sky is falling." } ]
在本例中,我們提供了生成數據的日期,文件的當前計數,以及由一組文件及其大小組成的列表。注意,根據您的具體位置,服務器上的時區可能有所不同。
由於 error.txt 的大小不為 0,因此我們將讀取該文件的內容並將它分配給 summary 屬性。就是這樣 — 一個非常基本的監視腳本。
在生成 JSON 的過程中,您可能會發現從基於浏覽器的應用程序中解析代碼會出現問題。如果是這種情況,您可能需要使用一個 JSON 驗證工具(參見 參考資料)。圖 4 展示了在桌面浏覽器中呈現的此條目。
圖 4. JSON 驗證工具
圖 5 展示了在 iPhone 中運行的應用程序。
圖 5. 運行在 iPhone 上的應用程序
圖 6 展示了運行在 android 上的應用程序,其中的服務器列表稍有不同。
圖 6. 運行在 android 上的應用程序
應用程序現在差不多已經全部完成。總是可以添加一些新的內容,因此在下面的小節中,我們來看一看可以將哪些內容留作練習。
下一步
總是可以實現一些新的想法來完善這個應用程序。下面列出了一些有趣的內容,可以添加到應用程序中:
結束語
本文以 android 和 iPhone 浏覽器之戰,第 1 部分:WebKit 成援兵 的內容為基礎,使用 SQL 本地存儲和 AJax 查詢構建了一個高度實用的移動 Web 應用程序。該應用程序本身可以用作一種工具來幫助監視您的當前網絡,並且在服務器端只需做少量的工作。
希望本文可以啟發您構建自己的針對 iPhone 和 android 的移動應用程序。
(2) RelativeLayout相對布局,它是依靠與父容器,同一容器中其它控件的相對位置來排列顯示的。主要常用的屬性如下:相對父容器的屬性:android:layo
開發出高效穩定的Android應用我們不得不需要了解下Java虛擬機的原理和內存分配機制,android使用的是Google經過優化的Dalvik Java VM。通常
簡介: HTML 5 針對移動 Web 應用程序引入了大量新特性,其中包括一些可視化特性,它們通常會帶來強烈的視覺沖擊。Canvas 是最引人注目的新 UI
可翻頁的產品細節屏幕為增強產品細節屏幕的可用性,我們定義了一個自定義視圖控制器(ProductScrollVIEwController 類)來支持用戶通過翻頁