 | 级别: 中级 Uche Ogbuji (uche@ogbuji.net), 合伙人, Zepheira, LLC
2008 年 7 月 07 日 Mozilla Firefox 3 是一个提供了大量增强的重要版本,其中一些增强针对用户,而另一些则针对开发人员。其中一项最有趣的改进使 Web 开发人员能够构建即使在用户断开 Internet 的时候仍然可以工作的 Web 应用程序。请阅读本文以深入学习 Firefox 3 的这些新特性,尤其是全新的脱机应用程序支持。
Firefox 可能是 Mozilla 项目最为成功的产品。Firefox 在 2004 年首次发行并得到了广泛使用,而当时领先的 Web 浏览器 Microsoft® Internet Explorer(MSIE)则发展迟缓而且疲于应付大量安全问题。为了应对 Firefox 的异军突起,Microsoft 重新开始关注 MSIE,但是这并不能阻止这个新对手的不断发展壮大。根据 Mozilla Foundation 统计,全世界使用 Firefox 的用户有 2 亿左右。其中部分原因是因为它支持 45 种语言。Mozilla 正计划通过实现 24 小时内最大的软件下载量创造吉尼斯世界记录,从而制造更多新闻。这会成为一个新特性。尝试创造该项记录的软件是 Firefox 3.0,这是计划在 2008 夏季发行的重要版本,并且已经可以通过候选发行版获得,这意味着下一个版本将是完整的、最终的 3.0 版本。Firefox 3 令 Web 开发更加轻松;通过新增脱机支持,使浏览器成为更加实用的通用应用程序平台。本文将指导您如何充分利用 Firefox 3.0 的这些新改进。
全新的特性
Firefox 3.0 令人振奋的原因在于它向 Web 用户和开发人员承诺实现重要的改进。这是一个重要的里程碑,因为 Web 开发人员一直将 Firefox 用作首选的开发平台,尽管开发人员清楚在跨浏览器兼容性方面必须做些让步。Firefox 之所以在开发人员中间广受欢迎,源于其活跃的社区、严格的标准支持和持续的平台创新。平台创新使开发人员掌握 Web 的发展趋势;而对标准的支持意味着即使对于最新的趋势,Firefox 仍然提供透彻的支持特性,推动前沿技术的采用并提高兼容性。Firefox 3 提供了更多特性。如发行说明所述:
Firefox 3 基于 Gecko 1.9 Web 呈现平台,该平台的开发已经历时 33 个月。在先前版本的基础上,Gecko 1.9 进行了 14,000 处更新,包括几处重要的重新设计来提高性能、稳定性、呈现正确性,以及代码简化和可持续性。Firefox 3 就建立在这个新平台上,产生更加安全、更易于使用、更加个性化的产品,并且为网站和 Firefox 插件开发人员提供了更多内部特性。
“14,000” 仅仅是 Firefox 众多令人赞叹的特性的其中一种;在大量有用改进中,很多改进都值得深入探究。
这包括若干安全增强,例如关闭脚本漏洞和欺骗保护。而且,大部分增强不会影响您的工作,除非您正打算编写恶意代码。其他更新使用户的操作更加方便,例如更好的下载和密码管理器、出色的页面缩放功能,以及在 Mac、Windows® 或 Linux® 中更自然的行为。新的书签功能非常强大,支持使用多个标签,而不需要遵守严格的层次结构。书签功能结合了更加丰富的历史特性,提供了称为 Places 的特性,允许管理和搜索个性化的 Web 路径。
脱机支持
Firefox 3 基于 HTML5 工作草案,能够在用户脱机时继续工作。用户可以使用多种方式保持一个静态网页处于联机状态:使用浏览器缓存、使用一个本地缓存代理或者将 Web 内容下载到磁盘。越来越多的 Web 操作都具有动态特性,包括阅读 Web 邮件或 Web 提要、浏览最喜欢的商店或者使用最喜爱的社会网络。甚至最标准的生产力应用程序也喜欢适合 Web 的在线版本的文字处理程序、电子表格和演示工具,以及这些应用程序的新类型,例如 wiki。如果在无法上网时仍然可以继续使用这些应用程序,那么它们将更有价值。Firefox 3 使这一切成为可能,为您的 Web 应用程序提供了这种功能。让我们深入了解一下这个新特性,它可能是 Web 开发人员最感兴趣的新特性。
要解决联机与脱机问题,首先要求 Web 应用程序能够指示用户浏览器将应用程序需要的资源保存到称为应用程序缓存 的本地缓存中,该缓存在脱机期间是可用的。用户通常使用这个缓存保存主页面、样式表、脚本文件和其他不会经常变化的文件。您可以为您的应用程序注册一个 URI,它治理一组资源,Firefox 将这些资源与对应的联机内容保持同步。使用文档元素(例如 html)的 manifest 属性为应用程序建立缓存。
其次,了解用户何时联机又何时脱机,这样便可以执行应用程序自动缓存管理以外的任何任务。Firefox 2 引入了一个 Boolean 脚本属性 navigator.onLine。当用户手动切换到脱机模式时(通常通过 Firefox 菜单),该属性将得到更新。从 Firefox 3 开始,当浏览器自动检测到网络不可用时,这个属性也会更新。另外,当联机状态改变时,脚本事件将被调用。通过侦听这样的事件,开发人员能够完全控制脱机环境中的应用程序行为。
脱机应用程序示例
脱机支持出现在 Firefox 3 pre-alpha 版本之后不久,Mozilla 开发人员 Mark Finkle 为开发人员整理了一个小巧的示例。这个示例是一个 to-do 列表工具,它使用脚本将用户条目嵌入到主服务 Web 页面在应用程序中的缓存版本。该示例还将 to-do 列表保存至服务器端。这样,无论用户处于脱机还是联机,都可以使用这个页面并维护用户的数据。经过 Mark 的允许,本文对他的示例做了改进。清单 1(todo.html)是该应用程序的主 HTML 文件。
清单 1. To-do 列表应用程序的主 HTML(todo.js)
<html manifest="todo.manifest">
<head>
<title>Todo tool</title>
<script type="text/javascript" src="json2.js"></script>
<script type="text/javascript" src="todo.js"></script>
<style type="text/css">
body { font-family: verdana,tahoma, arial; }
div#container { width: 300px; }
div#title { font-size: 120%; }
div#subtitle { font-size: 80%; }
div#tasklist { margin-bottom: .5em; }
div#log { font-size: 90%; background-color: lightgray; margin-top: 1em;
white-space: pre; }
</style>
</head>
<body onload="loaded();">
<div id="container">
<div id="title">Todo tool <span id="status">online*</span></div>
<div id="subtitle">Simple online/offline demo for Firefox 3</div>
<hr />
<div id="tasklist">
</div>
<input type="text" id="data" size="35" />
<input type="button" value="Add" onclick="addItem();"/>
<hr />
<input type="button" value="Remove" onclick="removeItems();"/>
<input type="button" value="Complete" onclick="completeItems();"/>
<div id="log"><strong>Event Log</strong>
</div>
</div>
</body>
</html>
|
注意第一行中的 manifest="todo.manifest" 属性。该值是一个相对 URL,它结合 HTML 页面的基 URL 来指定清单(manifest) URL,其内容如清单 2 所示。
清单 2. to-do 列表应用程序(todo.manifest)的资源缓存清单文件
CACHE MANIFEST
# v1
todo.html
json2.js
|
文件格式比较简单。需要注意的主要问题是 v1 注释。如果更新该清单列出的任何文件,那么更新这个文本(例如,更新到 v2 等)。浏览器一旦发现清单文件发生变化,将自动获取所有列出文件的新版本(甚至那些没有改动的文件)。回到清单 1,to-do 列表条目出现在 <div id="tasklist"></div> 中,并通过脚本动态更新。其中一个载入脚本 json2.js 是一个流行的库,为 JSON 处理提供有用的代码。我将在后文详细介绍。清单 3 是第二个脚本,包含 to-do 列表工具的特定处理,包括脱机处理。
清单 3. To-do 列表应用程序的脚本(todo.js)
//Uses offline features from WHAT Web applications draft specification
//(Firefox 3-specific at present)
//JSON form of the task list items, for direct transport purposes
var taskStorage = "[]";
//Web app's domain is used as a key to the application cache data (globalStorage)
var storageDomain = location.hostname;
//Invoked when the browser page is loaded (i.e. onLoad attribute on the body)
function loaded() {
//Load the to-do list data from app cache or web app, depending on offline status
updateOnlineStatus("initial load", false);
//Set up listeners to handle online/offline transitions
document.body.addEventListener("offline",
function () { updateOnlineStatus("offline", true) }, false);
document.body.addEventListener("online",
function () { updateOnlineStatus("online", true) }, false);
//Load initial to-do list data saved in the application cache, if available
//This will cause the browser to prompt the user for permission
if (typeof globalStorage != "undefined") {
var storage = globalStorage[storageDomain];
if (storage && storage.taskStorage) {
taskStorage = storage.taskStorage;
}
}
//See if we can load an updated task list
fetchList();
}
//Invoked when the user's online/offline status changes
function updateOnlineStatus(msg, allowUpdate) {
//Update the online status indicator in the subtitle
var status = document.getElementById("status");
status.innerHTML = (navigator.onLine ? "[online]" : "[offline]");
//Record the change in the log area of the Web page
var log = document.getElementById("log");
log.appendChild(document.createTextNode("Event: " + msg + "\n"));
//If online, try to push any task list changes back to the server
if (navigator.onLine && allowUpdate) {
update();
log.appendChild(document.createTextNode("Updated server\n"));
}
}
//Execute HTTP request to the server, either to get the task list or to push updates
function httpRequest(type, data, callback) {
var httpreq = new XMLHttpRequest();
httpreq.onreadystatechange = function() {
if (httpreq.readyState == 4)
callback(httpreq.readyState, httpreq.status, httpreq.responseText);
}; //close function()
httpreq.open(type, "/todo-app", true);
if (type == "POST") {
httpreq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
}
httpreq.send(data);
}
//Put updated task list from the server on the Web page, and in the application cache
function loadList(readyState, status, responseText) {
if (readyState == 4) {
if (status == 200) {
taskStorage = responseText;
var tasks = JSON.parse(taskStorage);
var html = "";
//Update the fields on the Web page
for (var i=0; i<tasks.length; i++) {
html += "<input type='checkbox' id='" + tasks[i].name + "'/><label for='"
html += tasks[i].name + "'>" + tasks[i].data + "</label><br/>";
}
document.getElementById("tasklist").innerHTML = html;
//Update the application cache
if (typeof globalStorage != "undefined") {
globalStorage[storageDomain].taskStorage = taskStorage;
}
}
}
}
//Load an updated task list either from the server or from application cache
function fetchList() {
if (navigator.onLine) {
httpRequest("GET", null, loadList);
}
else {
loadList(4, 200, taskStorage);
}
}
//Invoked when the user adds a new task list item
function addItem() {
var data = document.getElementById("data").value;
document.getElementById("data").value = "";
//Turn the stored task list into a JavaScript list
//Use present timestamp as a simple ID ("name")
var tasks = JSON.parse(taskStorage);
tasks.push({"name": Date.now(), "data": data });
//Convert back to JSON and update the task list variable
taskStorage = JSON.stringify(tasks);
//Try to push any task list changes back to the server
update();
}
//Invoked when the user removes a task list item
function removeItems() {
var tasks = JSON.parse(taskStorage);
var newTasks = [];
//See which boxes are checked and only copy back those that are not
var items = document.getElementById("tasklist").getElementsByTagName("input");
for (var i=0; i<items.length; i++) {
if (items[i].checked == false) {
newTasks.push(tasks[i]);
}
}
taskStorage = JSON.stringify(newTasks);
update();
}
//Invoked when the user marks new task list item as completed
function completeItems() {
var tasks = JSON.parse(taskStorage);
//See which boxes are checked add strikeout tags to those that are
var items = document.getElementById("tasklist").getElementsByTagName("input");
for (var i=0; i<items.length; i++) {
if (items[i].checked) {
var task = tasks[i].data;
if (task.indexOf("<strike>") != -1) {
task = task.replace("<strike>", "");
task = task.replace("</strike>", "");
}
else {
task = "<strike>" + task + "</strike>";
}
tasks[i].data = task;
}
}
taskStorage = JSON.stringify(tasks);
update();
}
//Try to push any task list changes back to the server
function update() {
if (navigator.onLine) {
var post = "action=update&tododata=" + encodeURIComponent(taskStorage);
httpRequest("POST", post, function(readyState, status, json) { fetchList(); });
}
else {
loadList(4, 200, taskStorage);
}
}
|
虽然对代码添加了大量注释,但是本文仍将进行一些一般性说明。to-do 列表数据内嵌于页面中,但是这些数据在 JSON 表单内传输,保存在变量 taskStorage 中。当用户首次载入该 Web 应用程序的页面时,Firefox 尝试从应用程序缓存载入这些数据。如果其中没有该应用程序的条目,那么 Firefox 将提示用户允许建立一个条目。如图 1 所示。
图 1. 初始加载时的 To-do 列表工具
如果用户单击 allow,那么 Firefox 将建立该应用程序的缓存。开发人员有时可能需要进行重置,这样 Firefox 将再次请求脱机。重置操作可以在 Preferences 或 Options 窗口内完成,具体指 Advanced 选项卡和 Network 子选项卡。图 2 显示 Mac OS X 上的相关窗口。您可以看到本地主机的允许脱机条目。要重置该条目,则选中这个条目并单击 Remove...。
图 2. 管理脱机存储
实现 to-do 列表服务器
在用户打开该网页之后,可以添加或删除条目;或者将条目标记为已完成,这样相关条目虽然仍然显示在列表中,但是已经被删除。图 3 显示在联机时添加一些测试条目后页面如何呈现。
图 3:新增了一些条目并联机的 To-do 列表工具
在联机时,无论何时更新 To-do 列表,脚本都会向服务器发送一个更新。本文采用 Python 编写了一些代码,实现简单的服务器处理。Mark Finke 也采用 PHP 编写了代码来处理这种情况。因此,如果您更加青睐 PHP,那么请参阅 参考资料 中 Mark Finke 的原始代码的链接。但是仍然需要使用清单 3 的更新脚本,以便用于 Firefox 3 候选版本或者最终版本。清单 4 (todohandler.py)是 Python 服务器代码。
清单 4. To-do 列表应用程序的服务器代码(todohandler.py)
import os, sys
import cherrypy #http://www.cherrypy.org/
from webob import Request, Response #http://pythonpaste.org/webob/
#For now just use a single file for all sessions
DATAFILE = 'test.json'
#The handler function
def todo_handler_application(environ, start_response):
req = Request(environ)
if req.POST.get("action") == "update":
#Handle POSTs to update the to-do list entries. Write to the file
f = open(DATAFILE, 'w')
f.write(req.POST["tododata"])
f.close()
resp = Response(body='', content_type='application/json')
else:
#Handle GETs to pull the to-do list entries. Read from the file
f = open(DATAFILE, 'r')
data = f.read()
f.close()
resp = Response(body=data, content_type='application/json')
#Send the response
return resp(environ, start_response)
#Configure the server to handle the Web form in todo.html
cherrypy.tree.graft(todo_handler_application, '/todo-app')
#Configure the server to serve up the regular, static files from the current directory
server_config = {
'/': {'tools.staticdir.on': True, 'tools.staticdir.dir': os.getcwd() }
}
#Create a CherryPy handler. Doesn't do anything because the meat is in the WSGI app
class DummyHandler: pass
cherrypy.quickstart(DummyHandler(), '/', config=server_config)
|
这段代码的注释也颇为详细。惟一值得注意的是,为了简化这个示例,该代码为所有应用程序用户使用了同一文件(指定为 DATAFILE)。当然,对于实际应用程序应当进行一些修改。您应当为每个用户使用独立的文件或数据库行。我使用清单 5 中的文件(test.json)引导这些数据。
清单 5. To-do 列表应用程序的脚本(test.json)
[{"name":1171640861226,"data":"<strike>Example entry</strike>"},
{"name":1212604738536,"data":"Say \"Hello\""},{"name":1212604795352,"data":"
<strike>Say \"Good night\"</strike>"}]
|
目前为止我们已经练习了一些联机行为,下面谈一谈脱机问题。在 Firefox 中可以使用 Firefox 菜单中的 Firefox menu 实现脱机。这样,由于使用了联机/脱机转换的脚本处理程序,浏览器标题立即变成 “Todo tool [offline]”。用户仍然可以正常使用该应用程序。经过一些脱机操作之后(本例将任务标记为已完成),再切换回联机状态,显示结果大致如图 4 所示。
图 4. To-do 工具在脱机处理后返回联机状态
注意,我曾想在清单 3 中介绍 JSON 处理。我使用了 Douglas Crockford 的 json2.js 库,该库基于他为 JavaScript 增强(可能会包含在版本中)推荐的 API。清单 3 使用该 API 的 JSON.stringify 和 JSON.parse。Firefox 3 添加了新的高性能 JSON 处理 nsIJSON,但是目前该处理在浏览器默认设置下还不能使用。它不久就会内置于 Firefox,而且应当与 json2.js 兼容。当该处理就绪时,可以提高性能并通过删除单独的 JavaScript 文件消除一个外部依赖。
结束语
本文鼓励读者探究 Firefox 的其他一些新特性。基于 Web 的协议处理程序可以使开发人员在内置类型(例如http: 和 mailto:)之外定义新的 URI 类型。Firefox 3 为 XML 开发人员增添了许多 EXSLT 扩展支持,这大大增强了 XSLT 转换的能力。它还改进了对基于 XML 的矢量图形标准 SVG 的支持。我将在另一篇文章中深入探讨这项改进和 XML 处理的其他改进。Firefox 3 令 Web 开发更加轻松,通过添加脱机支持,使浏览器成为更加实用的通用应用程序平台。遗憾的是,该浏览器的大多数新功能显然还不能用于其他浏览器,但是至少大部分功能都基于标准。因此,完全有理由期待其他 Web 软件在不久之后提供支持。
参考资料 学习
获得产品和技术
讨论
关于作者  | 
|  | Uche Ogbuji 是
Zepheira, LLC 的合伙人,这家公司专门提供下一代 Web 技术解决方案。Ogbuji 是 4Suite 的首席开发人员,这是一种用于 XML、RDF 和知识管理应用程序的开放源代码平台;也是 Jacqard(用于 Web 团队开发的敏捷方法)和 Versa RDF 查询语言的首席开发人员。他是一位出生在尼日利亚的计算机工程师和技术作家,目前定居在科罗拉多的博尔德。可以通过他的博客 Copia 进一步了解 Ogbuji 先生。 |
对本文的评价
|  |