开发便携式移动 Web 应用程序
iPhone 和 Android 智能手机的移动应用程序是当今应用程序开发的热点。据 Canalys 称,2009 年 Android 手机销量接近 800 万部,iPhone 销量超过 2500 万部,这些用户经常在手机上加载应用程序(AdMob 表示,Android 和 iPhone 用户平均每月下载约 9 个应用程序,iTouch 用户平均每月下载约 12 个)。对移动应用程序的需求非常旺盛。
Linux 开发人员如何才能打入这个市场?iPhone 的原生应用程序必须使用 Apple iPhone SDK 开发,而 Apple iPhone SDK 只能在 Mac OS X 上运行。Android 开发在 Linux 上通过 Android SDK 得到支持,但理想情况下,您希望在 Linux 上开发可在 iPhone 和 Android 上运行的应用程序。
移动 Web 应用程序提供了一个解决方案。Web 应用程序使用浏览器作为跨不同平台的通用运行时环境。应用程序使用 HTML、JavaScript 和 CSS 编写,并在平台的原生浏览器上运行。这个想法并不新鲜,但从历史上看,Web 应用程序存在一些问题
浏览器安全阻止了本地数据的存储。
地理位置等平台功能无法从 HTML/JavaScript 访问。
用户必须启动浏览器才能使用 Web 应用程序,这与原生 UI 不符。
浏览器本身是碎片化的——不同的浏览器以不同的方式解释 JavaScript。
这些问题正在得到解决,幸运的是,Android 和 iPhone 都选择了 WebKit 作为其各自浏览器的布局引擎。HTML5 扩展了 Web 应用程序的功能,富有创造力的人们设计了 JavaScript 库,以最大限度地减少外观和感觉问题。该解决方案是一个持续的过程,尚未完成。
HTML5 和相关规范为 HTML 添加了画布、视频、本地存储、Web Workers、离线应用程序和地理位置等功能,而 WebKit 正在迅速将这些功能集成到布局引擎中。
存在专门的运行时和库,允许 JavaScript 访问底层手机功能,例如位置、加速度、声音、联系人、电池、摄像头、电话和日历。其中一些(例如 JIL、BONDI 和 WAC)由行业主导,涉及自定义 Web 运行时,旨在提供跨平台的通用小部件环境。另一些(例如 PhoneGap、Titanium 和 Rhomobile)专注于 iPhone 和 Android,并与原生 SDK 相关联,从而扩展了 Web 应用程序的功能。随着 HTML5 实现类似的功能,这些库通常使其 API 符合 HTML5 API。
已经开发了 JavaScript 库来解决外观和感觉问题,包括
iUi:一个小型可扩展库,模仿 iPhone 用户界面。
iWebKit:另一个用于 iPhone 风格应用程序的框架。
jQTouch:流行的 jQuery JavaScript 库的插件,它提供 iPhone 风格和更通用的 jQTouch 风格。jQTouch 的优势在于使用 jQuery 来隐藏浏览器差异。
Android 和 iPhone 都使用 WebKit 作为其布局引擎,但仍然存在差异,部分原因是选择了不同的 WebKit 版本
Android 1.6 (HTC G1) 使用 WebKit 525.20,仅实现了 Canvas、Canvas Text 和地理位置。
Android 2.1 (Motorola Droid) 使用 WebKit 530.17,并添加了 HTML5 的其余部分(视频、音频、本地存储、Web Workers 和离线应用程序)。
iPhone 3GS 和 iTouch 使用 WebKit 528.16 和 .18,并包含除 Web Workers(多线程)之外的所有功能。
iPad 使用 WebKit 531.21,并包含所有功能。
为智能手机开发 Web 应用程序非常简单,但您需要了解一些事项
它不像 C 语言:如果您习惯于使用 C 或 C++ 或 Java 或 Perl 开发 Linux 应用程序,那么这有所不同。Web 应用程序开发有点类似于 Android 开发环境,其中屏幕布局在 XML 文件中,功能是用 Java 编写的,但主要还是像 Web 开发。
原生应用程序需要原生 SDK:如果您希望用户能够从 iTunes 和 Android Market 加载您的应用程序,则必须使用相应的 SDK 启用该功能。我稍后会讨论一些解决方法。
开发 Web 应用程序所需的只是一个文本编辑器来编写 JavaScript、CSS 和 HTML,以及一个浏览器来测试结果。使用面向 Web 的 IDE、JavaScript 调试器和 Safari 浏览器,以及各种移动设备进行测试,这项工作会更容易一些。Safari 具有许多简化开发的功能。您可以选择浏览器模拟哪个用户代理(从“开发”菜单中选择),Web Inspector 允许您检查和调试 Web 元素,包括客户端数据库。Safari 在 Linux 上不受支持,但可以在 VirtualBox 下正常运行。本文的代码是使用以下工具开发的
Ubuntu 9.10 和 gedit 编辑器。
Windows XP 上 VirtualBox 中的 Safari 浏览器。
Apache httpd Web 服务器。
用于图标图形的 GIMP。
jQTouch 和 jQuery 库。
iPhone、iTouch、iPad 和 Android 设备。
工具的安装在其他地方有详细文档记录。本文的“资源”部分给出了下载 URL 的指针。要安装 jQTouch,只需将 JavaScript 和 CSS 文件放在 Web 应用程序的目录树中,并从 HTML <head> 元素指向它们。jQTouch 附带文件的最小化版本和最小化的 jQuery 库。
作为一个示例,让我们看看一个简单的笔记应用程序,我将其称为 Webnotes。使用它,用户可以编写笔记,并在以后查看、编辑或删除笔记。笔记将包含标题和任意长度的字符串。笔记将使用 HTML5 客户端数据库 API 本地存储在智能手机上,我们将测试它在各种 Apple 和 Android 设备上的运行情况。完成后,我们将将其与 Android SDK 附带的类似 Android 示例应用程序进行比较。因为我们使用的是 HTML5 的客户端数据库功能,所以我们期望它在 iPhone、Droid 和 iPad 上都能正常工作,而在 HTC G1 上无法工作(它不支持本地存储)。
我们的应用程序有三个屏幕
打开屏幕将显示现有笔记的列表,按标题列出,并按上次编辑日期排序。触摸标题将选择该笔记进行编辑。触摸“+”按钮将添加新笔记(图 1)。
编辑屏幕将允许查看、编辑或删除笔记。(图 2)。
添加屏幕将创建一个新笔记并将其存储在数据库中(图 3)。
列表 1 是 HTML 文件 index.html,主要关注布局。列表 2 是 JavaScript 文件 webnotes.js,其中包含我们需要的逻辑。让我们先浏览 HTML。
列表 1. index.html
<!DOCTYPE HTML PUBLIC> <head> <title>WebNotes</title> <link type="text/css" rel="stylesheet" media="screen" href="jqtouch/jqtouch.min.css"> <link type="text/css" rel="stylesheet" media="screen" href="themes/jqt/theme.min.css"> <script type="text/javascript" src="jqtouch/jquery.1.3.2.min.js"></script> <script type="text/javascript" src="jqtouch/jqtouch.min.js"></script> <script type="text/javascript" src="javascript/webnotes.js"></script> </head> <body> <div id="home"> <div class="toolbar"> <h1>Web Notes</h1> <a class="button add slideup" href="#addNote">+</a> </div> <ul class="metal"> <li id="noteTemplate" class="arrow" style="display:none"> <span class="title">Title</span> </li> </ul> </div> <div id="addNote"> <div class="toolbar"> <h1>Add Note</h1> <a class="button cancel" href="#">Cancel</a> </div> <form method="post"> <ul> <li><input type="text" class="title" /></li> <li><textarea class="note" ></textarea></li> <li> <input type="submit" class="submit" name="action" value="Save Note" /></li> </ul> </form> </div> <div id="editNote"> <div class="toolbar"> <h1>Edit Note</h1> <a class="button cancel" href="#">Cancel</a> <a class="button" onclick="deleteNoteById()">Delete</a> </div> <form method="post"> <ul> <li><input type="text" class="title" /></li> <li><textarea class="note" ></textarea></li> <li> <input type="submit" class="submit" name="action" value="Save Note" /></li> </ul> </form> </div> </body> </html>
列表 2. webnotes.js
var jQT = $.jQTouch({ icon: 'icon.png', }); var db; var currId; $(document).ready(function(){ $('#addNote form').submit(addNote); $('#editNote form').submit(replaceNoteById); db = openDatabase('WebNotes', '1.0', 'WebNotes', 524288); db.transaction( function(transaction) { transaction.executeSql( 'CREATE TABLE IF NOT EXISTS notes ' + ' (id INTEGER NOT NULL PRIMARY KEY ' + ' AUTOINCREMENT, ' + ' date DATE NOT NULL, title TEXT NOT NULL, ' + ' note TEXT NOT NULL);' ); } ); refreshNotes(); }); function addNote(){ var now = new Date(); var title = $('#addNote .title').val(); var note = $('#addNote .note').val(); db.transaction( function(transaction) { transaction.executeSql( 'INSERT INTO notes (date, title, note) VALUES' + ' (?,?,?)', [now, title, note], function(){ $('#addNote .title').attr('value', ""); $('#addNote .note').text(""); refreshNotes(); jQT.goBack(); }, errorHandler); } ); return false; } function errorHandler(transaction, err){ alert('SQL err: '+err.message+' ('+err.code+')'); return true; } function refreshNotes() { $('#home ul li:gt(0)').remove(); db.transaction( function(transaction) { transaction.executeSql( 'SELECT * from notes ORDER BY date;', null, function(transaction, result) { for (var i=0; i < result.rows.length; i++) { var row = result.rows.item(i); var newNote = $('#noteTemplate').clone(); newNote.removeAttr('id'); newNote.removeAttr('style'); newNote.data('noteId', row.id); newNote.appendTo('#home ul'); newNote.find('.title').text(row.title); newNote.click(function(){ editNoteById($(this).data('noteId')); jQT.goTo('#editNote', 'swap'); }); } }, errorHandler); } ); } function replaceNoteById() { db.transaction( function(transaction) { transaction.executeSql( 'UPDATE notes SET title=?, note=? WHERE id=?', [$('#editNote .title').val(), $('#editNote .note').val(), currId], function(transaction, result) { refreshNotes(); jQT.goTo('#home', 'swap'); }, errorHandler); } ); } function editNoteById(id) { db.transaction( function(transaction) { transaction.executeSql( 'SELECT * from notes WHERE id=?;', [id], function(transaction, result) { var res = result.rows.item(0); currId = res.id; $('#editNote .title').attr('value', res.title); $('#editNote .note').text(res.note); }); }, errorHandler); } function deleteNoteById() { db.transaction( function(transaction) { transaction.executeSql( 'DELETE FROM notes WHERE id=?;', [currId], function(transaction, result) { alert('Note deleted'); refreshNotes(); jQT.goBack(); }, errorHandler); } ); }
在 HTML 声明之后,是文档的 <head> 部分。<title> 元素是页面的 HTML 标题。iPhone 和 Android 浏览器将其显示为窗口标题,直到我们将应用程序设置为全屏。接下来的两个 <link> 元素告诉浏览器在哪里找到应用程序中引用的 CSS 文件。jQTouch 附带两种样式:“/themes/apple”和“themes/jqt”。我们在这里选择了后者,以使其更独立于设备。接下来的三个元素是 <script> 引用——前两个用于 jQuery 和 jQTouch,下一个用于 webnotes.js。顺序很重要——jQuery 的 <script> 元素必须在 jQTouch 的元素之前,并且它们都必须在任何使用它们的脚本之前。
在标头之后,列表 1 的 <body> 元素包含三个第一级 <div> 元素,每个屏幕一个。三个屏幕的顶层非常相似。每个屏幕都有一个唯一的 id 属性,我们使用该属性从 JavaScript 引用屏幕。每个屏幕还包括一个内部 <div>,其中包含class="toolbar",它定义了屏幕顶部的栏。这些 <div> 中的 <h1> 元素是屏幕标题。工具栏还各自具有一个锚元素,其class="button cancel"。这个 jQTouch 类定义了箭头形状的取消按钮,href 表示单击它会将我们带到“home”屏幕。该锚点还定义了按钮中显示的文本(“Cancel”)。在“home”屏幕上,我们添加了一个“+”按钮来添加笔记。我们为该按钮的操作指定了“slideup”动画,因此“addNote”屏幕将从屏幕底部滑入视图。
“home”屏幕还包含一个内部 <div>,其中包含一个包含一个列表项的列表。该项目是一个模板,用于定义笔记标题的显示。您将在下面看到我们如何使用它。
在“addNote”屏幕中,在“toolbar” <div> 之后,还有另一个 <div>,其中包含 <form>。此 <form> 包含一个列表,该列表具有三个列表项
用于笔记标题的文本输入。
用于笔记内容的文本区域。
用于提交 <form> 的按钮。
我们为列表项指定了类名,以便于使用 jQTouch 查找它们。
“editNote”屏幕看起来像“addNote”,只是增加了一个功能。工具栏中有一个 <input>,其class="button",为用户提供了一种删除笔记的方式。此按钮的 onclick 属性告诉浏览器调用 JavaScript 例程deleteNoteById(),我们在 webnotes.js 中定义了它。
WebKit 使用 SQLite 来实现 HTML5 的客户端数据库 API。该实现非常完整,包括对事务的支持,如果事务不成功,则会回滚。让我们看一下列表 2 webnotes.js 中的 JavaScript。
文件的前四行初始化 jQTouch 并将实例分配给变量 jQT。参数“.icon”是可以为 jQTouch 定义的许多参数之一。它指向应用程序的 57x57 像素图标(图 4)。

图 4. 应用程序图标
在第五行,我们声明数据库实例的变量“db”。从$(document).ready开始的代码块是一个 jQuery 函数,当浏览器完成加载 DOM 时执行,即使页面内容可能仍在加载。匿名函数首先重定向“addNote”和“editNote”表单中的提交按钮,将它们指向 JavaScript 函数。然后我们使用 openDatabase() 来执行此操作,向其传递四个参数
数据库的短名称。
数据库版本号。
数据库的显示名称。
数据库的最大大小。
如果数据库不存在,SQLite 会创建数据库。匿名函数执行一个 SQLite 事务,该事务创建或打开一个名为“notes”的表。该表的每一行代表一个笔记,其中包含以下列
INTEGER id KEY:SQLite 自动递增的唯一标识符。
DATE lastedit:笔记上次编辑的日期。
TEXT title:笔记的标题。
TEXT note:笔记的内容。
打开表后,我们调用一个函数 refreshNotes(),如下所述,它将更新“home”屏幕上显示的列表。
接下来,我们定义“addNote()”函数,当用户在“addNote”屏幕上触摸“保存笔记”时,将调用该函数。用户已经输入了标题和笔记的文本,所以我们现在想在“notes”表中插入一条合适的记录。我们从 Date() 函数获取日期,并使用 jQuery 查找标题和笔记输入元素。如果您不熟悉 jQuery,它使用类似 CSS 的语法来标识 DOM 元素。在本例中,它找到类为“title”和“note”的元素,.val() 函数将其值分配给 JavaScript 变量“title”和“note”。使用客户端数据库 API 启动事务,transaction.executeSql() 接受四个参数
SQL 字符串:要执行的 SQL 的模板。
参数数组,其值替换 SQL 模板中的 ? 标记。
如果操作成功,则执行的函数——在本例中,是一个匿名函数。
如果出现错误,则执行的函数——在本例中,是 errorHandler()。
如果成功,我们清除输入元素中的值(为下一次添加做好准备),刷新笔记列表,以便新笔记显示出来,并使用 jQTouch 函数 jQT.goBack() 返回到“home”屏幕。由于我们使用“slideup”来显示“addNote”屏幕,因此 jQTouch 非常智能,可以将其向下滑动以返回到“home”屏幕。然后我们返回“false”给浏览器,因为我们不需要它继续。
我们现在定义 errorHandler() 函数,我们在脚本中的所有数据库事务中重用该函数。它显示一个带有错误消息的警报框并返回。
接下来是 refreshNotes() 函数。我们使用 jQuery 查找“home”屏幕上的所有 <li> 元素并删除它们。然后,我们执行数据库事务以查找所有笔记记录,并使用“home”屏幕模板为每个笔记创建一个列表项并将其插入到“home”屏幕中。我们为每个列表项添加一个 .click 函数,当用户单击笔记标题时,该函数会将用户带到“editNote”屏幕。
replaceNoteById()、editNoteById() 和 deleteNoteById() 函数都与 addNote() 非常相似,只是 SQL 模板进行了相应的更改。
该应用程序可以从 iPhone 或 Android 设备上的浏览器运行。正如从不同手机上的 HTML5 功能所预期的那样,Webnotes 在 iPhone、iTouch、iPad 和 Droid 上都能正常工作。它在 G1 上无法工作,因为该手机不支持客户端数据库事务。
如果您想打包 Web 应用程序以在 iTunes 或 Android Market 上分发,一种方法是使用相应的 SDK 并编写一个小型的包装器应用程序。该应用程序创建一个浏览器 Intent (Android) 或 UIWebView (iPhone),并为其提供 index.html 的位置。我们没有空间在这里深入研究 SDK,但这些应用程序实际上只有几行代码。
或者,对于 iTunes,您可以让像 PhoneGap 这样的软件包为您完成这项工作。您仍然需要 iPhone SDK,因此您必须在 Mac 上创建软件包,但 PhoneGap 使该过程更简单。创建完成后,您可以像上传任何其他 iPhone 应用程序一样将其上传到 iTunes。
如果您不关心 iTunes 或 Android Market,还有另一种方法——将您的应用程序打包为 HTML5 离线应用程序。列表 3 是一个清单文件 webnotes.manifest,您将其放在应用程序的主目录中。您还需要在 index.html 中的 <html> 元素中添加一个属性
manifest="webnotes.manifest"
还有一件事——如果您从 Apache Web 服务器提供文件,则 Web 目录中的 .htaccess 文件需要像这样的一行
AddType text/cache-manifest .manifest
这告诉 Apache 使用正确的 MIME 类型提供 .manifest 文件。当用户首次访问网站时,服务器将下载清单中列出的文件并将其保存在设备上。在后续访问中,如果清单发生更改,则会重新加载该文件——即使更改是在注释字段中。
列表 3. webnotes.manifest
CACHE MANIFEST index.html icon.png jqtouch/jqtouch.min.css themes/jqt/theme.min.css jqtouch/jquery.1.3.2.min.js jqtouch/jqtouch.min.js javascript/webnotes.js
在 Apple 设备上,当用户访问您的网站时,他们可以触摸浏览器底部的“+”来将该 URL 的图标放在他们的主屏幕上(还记得我们初始化 jQTouch 时的 .icon 属性吗?)。如果您创建了一个离线应用程序,它会从本地存储加载和执行,很像原生应用程序。
我们将 Webnotes 定义为类似于 Android SDK 附带的示例应用程序 NotePad。有关代码行数的比较,请参见表 1。
如果用任何语言编写一行代码的工作量大致相同,那么将应用程序编写为 Web 应用程序将花费大约四分之一的时间——并且它可以在大多数基于移动 WebKit 的浏览器上运行。当您计划下一个移动应用程序开发时,这值得考虑。
资源
Canalys 2009 年智能手机市场分析:www.canalys.com/pr/2010/r2010021.html
AdMob 2010 年 1 月移动指标:metrics.admob.com/wp-content/uploads/2010/02/AdMob-Mobile-Metrics-Jan-10.pdf
布局引擎比较 (HTML5):en.wikipedia.org/wiki/Comparison_of_layout_engines_(HTML5)#cite_note-114
深入 HTML5 站点以检查浏览器中的 HTML5 功能:diveintohtml5.org/past.html
我的用户代理是什么?:whatsmyuseragent.com
jQTouch:www.jqtouch.com
PhoneGap:phonegap.com
iWebkit:iwebkit.net
Jon Stark 关于使用 HTML、CSS 和 JavaScript 构建 iPhone 应用程序的优秀书籍:building-iphone-apps.labs.oreilly.com
Rick Rogers 是一位拥有 30 多年专业经验的嵌入式软件开发人员(从 FORTRAN 到 JavaScript)。他目前是 Wind River Systems 的移动解决方案架构师。他欢迎对本文的反馈,请发送至 portmobileapps@gmail.com。