为什么还有必要再写一个Offline HTML5 App指南
现在已经有非常多的资源介绍如何写一个offline HTML5的网站了,但是仅仅让一个网站能够在离线情况下访问是远远不够的。
在这个指南中我们将搭建两个离线网站,用来向读者演示如何向一个已有的离线网站中增添功能,避免已有的用户觉得自己正在使用一个旧版本的网站。
许多现有的指南都只会特别关注某一种技术。而这个指南则有所不同,它不会具体地介绍某一种技术,而是站在更高的层次,告诉读者怎样用最少的代码、花最少的时间、将各种技术融合在一起,打造一个真正有用的web app,并且支持以后的功能扩展。
介绍
我们将要开发一个RSS订阅阅读器,它能够为离线用户显示最新的新闻列表。而这个项目已经可以真实运行了,读者可以在github访问到它。
需求分析
- 支持用户下载最近的文章。
- 当我们想在客户端代码中加入新的功能或者更正bug的话,需要能够简单可靠地获取用户在本地缓存的信息。
- 支持用户预览文章的标题,并且能够通过选择文章或者点击图标阅读全文。
- 支持离线访问。
- 能够支持iPhone,iPad和iPod touch(以及一些其他的平台,比如Blackberry Playbook,Chrome 的Android,Android Browser,Opera Mobile,Opera和Safari 。)
该项目使用PHP和jQuery开发,因为它具有很好的简洁性和通用性。
application cache简介
通过指定一个文件列表,能够使用app cache 离线访问网站,当用户的网络连接断开后,可以将用户更新的数据保存在本地。但是,正如网上已经广泛讨论的,app cache技术真的不怎么样。
- 如果你在app cache manifest中指定了100个文件,那它就会尽快将这100个文件下载下来—这将影响到用户访问app的性能—在浏览器正在下载文件时,app可能会显得有些响应不及时。
- 除此以外,如果你修改了这些资源,哪怕仅仅只是修改了浏览器中某个CSS文件的某一行代码,都将导致manifest中的所有文件被再重新下载一次。它无法做到增量更新,只能将缓存中的所有内容整个替换。
- 如果某一个文件下载失败,都将导致所有已经成功下载的文件被抛弃,缓存的内容回滚到之前的版本。
- 所以,如果你只修改了某个文件的一行代码,而且浏览器已经将所有更新的文件都下载成功了,但是下载没有更新的文件时失败了,这也还是会导致整个更新失败,这显然并不合理。
- 所以我们使用application cache的一个重要原则是尽可能精简放入其中资源的数量,尽量不要将经常更新的资源放入其中,比如:
- 字体
- 子图
- 悬浮图片
- 单个引导页面(后面会介绍)
- 并且在下面的场景我们不建议采用application cache:
- 我们的Javascript,HTML&CSS的主体
- 内容(包括图片在内)
可以参考Fixing app cache这篇文章。
不用application cache,我们用什么呢?
我们只用appcache保存最基本的Javascript,CSS和HTML,只让它能够支持web app启动就足够了(我们称之为引导程序),后面的工作就交给ajax,eval() 来完成,然后把它保存在localStorage*中。
这种策略很棒,不论何种原因导致app无法正常启动(比如Javascript代码中引入了错误),这些受感染的Javascript代码都不会被缓存住,当用户下次启动app时,浏览器将从服务器上获得一份最新的代码副本。
这一技术也存在不少争议,因为localStorage意味着数据更新是同步的,在用户保存数据或是检索数据时,整个网站都会被锁定,不能做任何访问。但我们测试在我们的目标平台上,这一过程是非常快的,比WebSQL还要快(iOS和Blackberry平台上提供的客户端数据库)。而当我们保存或者访问RSS订阅的文章时,我们选择使用客户端数据库技术WebSQL。
1.引导程序(bootstrap)
为了开发一个简单的Hello World web app,需要以下一些文件。
| /index.html | 引导程序中的HTML, Javascript & CSS |
| /api/resources/index.php | 和我们的Javascript&CSS源文件链接,并向他们发送一个JSON string。 |
| /css/global.css | |
| /source/application/applicationcontroller.js | 首先为我们的应用程序编写一个Javascript文件,然后再做其他的工作。 |
| /jquery.min.js | jquery.com从jquery.com上下载最新的版本 |
| /offline.manifest.php | app cache manifest file. |
/index.html
首先编写引导程序中的html文件。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
<!DOCTYPE html>
<
html
lang
=
"en"
manifest
=
"offline.manifest.php"
>
<
head
>
<
meta
name
=
"viewport"
content
=
"width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no"
/>
<
script
type
=
"text/javascript"
src
=
"jquery.min.js"
></
script
>
<
script
type
=
"text/javascript"
>
$(document).ready(function () {
var APP_START_FAILED = "I'm sorry, the app can't start right now.";
function startWithResources(resources, storeResources) {
// Try to execute the Javascript
try {
eval(resources.js);
APP.applicationController.start(resources, storeResources);
// If the Javascript fails to launch, stop execution!
} catch (e) {
alert(APP_START_FAILED);
}
}
function startWithOnlineResources(resources) {
startWithResources(resources, true);
}
function startWithOfflineResources() {
var resources;
// If we have resources saved from a previous visit, use them
if (localStorage && localStorage.resources) {
resources = JSON.parse(localStorage.resources);
startWithResources(resources, false);
// Otherwise, apologize and let the user know the app cannot start
} else {
alert(APP_START_FAILED);
}
}
// If we know the device is offline, don't try to load new resources
if (navigator && navigator.onLine === false) {
startWithOfflineResources();
// Otherwise, download resources, eval them, if successful push them into local storage.
} else {
$.ajax({
url: 'api/resources/',
success: startWithOnlineResources,
error: startWithOfflineResources,
dataType: 'json'
});
}
});
</
script
>
<
title
>News</
title
>
</
head
>
<
body
>
<
div
id
=
"loading"
>Loading…</
div
>
</
body
>
</
html
>
|
总的来说,这个文件所做的工作就是:
- 通过在html标签中添加一个指向manifest 文件的应用,告诉浏览器网站支持离线访问:<html manifest=”offline.manifest.php”>
- 一旦app没有检测到设备处于离线状态(通过window.navigator.onLine检测),它将尝试下载最新的Javascript和CSS文件。
- 如果app无法获取最新的资源,则它将尝试访问保存在本地的内容。
- Eval the Javascript。
- 通过调用evaled中的代码(在本文中的代码是APP.applicationController.start())启动app。
- 一旦成功下载了最新的资源,将它保存在本地。
- 一旦app加载失败,尽量显示一个友好的出错界面。
- 在应用程序加载时,向用户显示一个Loading…提示信息。
/api/resources/index.php
现在来实现服务器端的处理工作(处理在上一个文件/index.html的#47行代码中发起的请求):
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?php
// Concatenate the files in the /source/ directory
// This would be a sensible point to compress your Javascript
$js
=
''
;
$js
=
$js
.
'var APP={}; (function (APP) {'
;
$js
=
$js
.
file_get_contents
(
'../../source/application/applicationcontroller.js'
);
$js
=
$js
.
'}(APP));'
;
$output
[
'js'
] =
$js
;
// Concatenate the files in the /css/ directory
// This would be a sensible point to compress your css
$css
=
''
;
$css
=
$css
.
file_get_contents
(
'../../css/global.css'
);
$output
[
'css'
] =
$css
;
// Encode with JSON (PHP 5.2.0+) and output the resources
echo
json_encode(
$output
);
|
/css/global.css
在当前阶段,这个文件还没有实现任何真正的功能,只是用来说明我们如何使用CSS的。
|
1
2
3
|
body {
background
:
#d6fab2
;
/* garish green */
}
|
/source/application/applicationcontroller.js
这个文件后面将会进一步扩展,但是现在只是实现了一个最简单的示例,其中的Javascript代码用来请求CSS资源,清空显示窗口,并显示一个Hello World消息。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
APP.applicationController = (
function
() {
'use strict'
;
function
start(resources, storeResources) {
// Inject CSS into the DOM
$(
"head"
).append(
"<style>"
+ resources.css +
"</style>"
);
// Create app elements
$(
"body"
).html(
'<div id="window"><div id="header"><h1>My News</h1></div><div id="body">Hello World!</div>'
);
// Remove our loading splash screen
$(
"#loading"
).remove();
if
(storeResources) {
localStorage.resources = JSON.stringify(resources);
}
}
return
{
start: start
};
}());
|
/offline.manifest.php
其他的教程也会教你在apache配置文件中为*.appcache增加一个content-type 。这么做确实没错,但是我想让这个示例app有更好的可移植性,希望只要简单的加载标准的PHP server就可以启动,而无需额外配置.htaccess或是需要服务器端的配置文件,所以我在代码中加入了一个 *.php扩展,使用PHPheader function设置content type。很多地方都推荐使用*.appcache,但这并非是必须的,所以我们在这里并没有采用这种主流的配置。
|
1
2
3
4
5
6
7
8
9
|
<?php
header(
"Content-Type: text/cache-manifest"
);
?>
CACHE MANIFEST
# 2012-07-14 v2
jquery.min.js
/
NETWORK:
*
|
通过上面示例应用的代码就能看出,我们前文所推荐的,尽量精简app cache中保存的内容,只要它能够支持web app启动就足够了。
将这些文件上传到一个标准的PHP web服务器上(所有的文件都应该放在一个能够被其他用户访问的目录下,或者是public_html(有的服务器上是在httpdocs)目录下),然后下载app,他就能离线访问了。目前为止,这个app还只能显示Hello World——因为我们还没有编写任何Javascript代码。
目前为止,这个web app已经能够实现自动更新——而且在后文中将不会再讨论app cache了。
2. 打造真正意义上的app
到目前为止,我们还在介绍一些非常通用的代码——大部分的app都会采用上面的代码,它可以构成一个计算器,火车时刻表,甚至是一个游戏。而我们准备开发一个简单的新闻类app,所以我们还需要以下一些代码:
- 一个客户端的数据库,用来保存从RSS订阅列表中下载的文章。
- 更新这些文章的方法。
- 一个文章列表。
- 单独呈现每篇文章的方法。
我们使用标准的Model-View-Controller (MVC) approach来组织我们的代码,并且尽量保持代码的整洁。这将减轻测试和后期开发的工作。
说了这么多,来看看我们用到的文件吧:
| /source/database.js | 一些简化客户端(WebSQL)数据库操作的方法 |
| /source/templates.js | MVC中的V,这里有视图的逻辑 |
| /source/articles/article.js | 文章的模型——算是一种数据库方法 |
| /source/articles/articlescontroller.js | 对文章的控制 |
| /api/articles/index.php | 获取新闻的API |
我们还需要修改 api/resources/index.php 文件和/source/application/applicationcontroller.js文件。
/source/database.js
我们选择WebSQL在客户端保存文章内容,尽管现在它正逐渐被IndexedDB所取代,但是我们最终还是选择了WebSQL,因为IndexedDB目前还不支持iOS平台,而我们的app主要到定位在iOS平台。到了后期,我们将考虑如何同时支持这两种数据库。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
APP.database = (
function
() {
'use strict'
;
var
smallDatabase;
function
runQuery(query, data, successCallback) {
var
i, l, remaining;
if
(!(data[0]
instanceof
Array)) {
data = [data];
}
remaining = data.length;
function
innerSuccessCallback(tx, rs) {
var
i, l, output = [];
remaining = remaining - 1;
if
(!remaining) {
// HACK Convert row object to an array to make our lives easier
for
(i = 0, l = rs.rows.length; i < l; i = i + 1) {
output.push(rs.rows.item(i));
}
if
(successCallback) {
successCallback(output);
}
}
}
function
errorCallback(tx, e) {
alert(
"An error has occurred"
);
}
smallDatabase.transaction(
function
(tx) {
for
(i = 0, l = data.length; i < l; i = i + 1) {
tx.executeSql(query, data[i], innerSuccessCallback, errorCallback);
}
});
}
function
open(successCallback) {
smallDatabase = openDatabase(
"APP"
,
"1.0"
,
"Not The FT Web App"
, (5 * 1024 * 1024));
runQuery(
"CREATE TABLE IF NOT EXISTS articles(id INTEGER PRIMARY KEY ASC, date TIMESTAMP, author TEXT, headline TEXT, body TEXT)"
, [], successCallback);
}
return
{
open: open,
runQuery: runQuery
};
}());
|
这个模块提供了两个接口供其他模块调用:
- open操作将打开一个5MB*的数据库,并且确保articles表单有足够的空间供app保存离线阅读的文章。
runQuery是一个简单的帮助方法,能够简化数据库的访问操作。
* 想进一步了解数据库大小的限制,可以访问这里。
/source/templates.js
我们把所有view和template的代码都放到了这个文件中。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
APP.templates = (
function
() {
'use strict'
;
function
application() {
return
'<div id="window"><div id="header"><h1>Guardian Technology News</h1></div><div id="body"></div></div>'
;
}
function
home() {
return
'<button id="refreshButton">Refresh the news!</button><div id="headlines"></div></div>'
;
}
function
articleList(articles) {
var
i, l, output =
''
;
if
(!articles.length) {
return
'<p><i>No articles have been found, maybe you haven\'t <b>refreshed the news</b>?</i></p>'
;
}
for
(i = 0, l = articles.length; i < l; i = i + 1) {
output = output +
'<li><a href="#'
+ articles[i].id +
'"><b>'
+ articles[i].headline +
'</b><br />By '
+ articles[i].author +
' on '
+ articles[i].date +
'</a></li>'
;
}
return
'<ul>'
+ output +
'</ul>'
;
}
function
article(articles) {
// If the data is not in the right form, redirect to an error
if
(!articles[0]) {
window.location =
'#error'
;
}
return
'<a href="#">Go back home</a><h2>'
+ articles[0].headline +
'</h2><h3>By '
+ articles[0].author +
' on '
+ articles[0].date +
'</h3>'
+ articles[0].body;
}
function
articleLoading() {
return
'<a href="#">Go back home</a><br /><br />Please wait…'
;
}
return
{
application: application,
home: home,
articleList: articleList,
article: article,
articleLoading: articleLoading
};
}());
|
这个文件中,我们只实现了一些非常简单的功能(尽可能不用任何复杂的逻辑),生成一些HTML字符串。这里唯一略显奇特的事情是:可能你已经注意到了,无论你期待的结果是什么,哪怕只是一个简单的结果,database.js runQuery函数都会返回一个数组。这意味着,APP.templates.article()需要处理一个数组,可能里面只包含了一篇文章。其实很容易扩展数据库的处理操作,在运行查询操作时值返回结果的第一个对象,但是现在我们还不打算实现这个接口。
随着app的功能扩展,我们可能需要把这个文件分割成多个文件实现,比如可以把处理文章的函数放在/source/articles/articlesview.js文件中。
/source/articles/article.js
这个文件的主要功能是实现文章操作和数据库之间的通讯。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
APP.article = (
function
() {
'use strict'
;
function
deleteArticles(successCallback) {
APP.database.runQuery(
"DELETE FROM articles"
, [], successCallback);
}
function
insertArticles(articles, successCallback) {
var
remaining = articles.length, i, l, data = [];
if
(remaining === 0) {
successCallback();
}
// Convert article array of objects to array of arrays
for
(i = 0, l = articles.length; i < l; i = i + 1) {
data[i] = [articles[i].id, articles[i].date, articles[i].headline, articles[i].author, articles[i].body];
}
APP.database.runQuery(
"INSERT INTO articles (id, date, headline, author, body) VALUES (?, ?, ?, ?, ?);"
, data, successCallback);
}
function
selectBasicArticles(successCallback) {
APP.database.runQuery(
"SELECT id, headline, date, author FROM articles"
, [], successCallback);
}
function
selectFullArticle(id, successCallback) {
APP.database.runQuery(
"SELECT id, headline, date, author, body FROM articles WHERE id = ?"
, [id], successCallback);
}
return
{
insertArticles: insertArticles,
selectBasicArticles: selectBasicArticles,
selectFullArticle: selectFullArticle,
deleteArticles: deleteArticles
};
}());
|
关于代码的一些注释:
- 在这个简单的示例app程序中,文章都被作为一个对象在各个接口间传递(通过
var article = { headline: 'Something has happened!', author: 'Matt Andrews',等形式访问)。为了将这种形式的文章插入WebSQL数据库,需要将他们转换成一个数组——这正是代码中#17行所做的工作。 - 由于WebSQL的速度很慢(有时甚至比网络的速度还要慢),因此当我们获得了app主页上文章列表中列出的文章后,我们将不再从WebSQL中查找文章的内容。这就是为什么我们用了两个查询语句实现选择文章这一功能:
selectBasicArticles(plural)和selectFullArticle。
/sources/articles/articlescontroller.js
接着来实现对文章的控制操作:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
APP.articlesController = (
function
() {
'use strict'
;
function
showArticleList() {
APP.article.selectBasicArticles(
function
(articles) {
$(
"#headlines"
).html(APP.templates.articleList(articles));
});
}
function
showArticle(id) {
APP.article.selectFullArticle(id,
function
(article) {
$(
"#body"
).html(APP.templates.article(article));
});
}
function
synchronizeWithServer(failureCallback) {
$.ajax({
dataType:
'json'
,
url:
'api/articles'
,
success:
function
(articles) {
APP.article.deleteArticles(
function
() {
APP.article.insertArticles(articles,
function
() {
/*
* Instead of the line below we *could* just run showArticeList() but since
* we already have the articles in scope we needn't make another call to the
* database and instead just render the articles straight away.
*/
$(
"#headlines"
).html(APP.templates.articleList(articles));
});
});
},
type:
"GET"
,
error:
function
() {
if
(failureCallback) {
failureCallback();
}
}
});
}
return
{
synchronizeWithServer: synchronizeWithServer,
showArticleList: showArticleList,
showArticle: showArticle
};
}());
|
article controller 的功能是:
- 引导模块从数据库中获取文章,并将获得的数据传递给view显示在屏幕上。(#4和#10)
- 将从RSS订阅列表中获得的最新内容同步到数据库中。
- 使用 jQuery’s
.ajaxmethod,它首先从RSS订阅列表中下载最新的文章(使用JSON格式)。 - 当整个内容下载成功后,它将调用
APP.articles.deleteArticles函数清空数据库中已有的内容。 - 然后调用
APP.article.insertArticles函数将最新下载的文章存入数据库。 - 最后,它使用jQuery并调用一个模板将文章的标题显示在订阅列表中。
- 使用 jQuery’s
/api/articles/index.php
这个文件的功能是下载并解析 RSS订阅的信息(使用xpath)。然后去除每篇文章中的HTML标签(除了<p>’s 和<br>’s),然后用json_encode显示处理后的结果。
我们订阅了 Guardian Technology 作为一个示例。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?php
// Convert RSS feed to JSON, stripping out all but basic HTML
// Using Guardian Technology feed as it contains the full content
$rss
=
new
SimpleXMLElement(
file_get_contents
(
'http://www.guardian.co.uk/technology/mobilephones/rss'
));
$xpath
=
'/rss/channel/item'
;
$items
=
$rss
->xpath(
$xpath
);
if
(
$items
) {
$output
=
array
();
foreach
(
$items
as
$id
=>
$item
) {
// This will be encoded as an object, not an array, by json_encode
$output
[] =
array
(
'id'
=>
$id
+ 1,
'headline'
=>
strval
(
$item
->title),
'date'
=>
strval
(
$item
->pubDate),
'body'
=>
strval
(
strip_tags
(
$item
->description,
'<p><br>'
)),
'author'
=>
strval
(
$item
->children(
'http://purl.org/dc/elements/1.1/'
)->creator)
);
}
}
echo
json_encode(
$output
);
|
尽管我们已经完成了添加新文件的功能,但是开发还没结束。
/api/resources/index.php
我们需要更新资源编译器,让它知道我们最新添加的 Javascript 文件的位置,而 /api/resources/index.php文件也需要更新:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?php
// Concatenate the files in the /source/ directory
// This would be a sensible point to compress your Javascript.
$js
=
''
;
$js
=
$js
.
'var APP={}; (function (APP) {'
;
$js
=
$js
.
file_get_contents
(
'../../source/application/applicationcontroller.js'
);
$js
=
$js
.
file_get_contents
(
'../../source/articles/articlescontroller.js'
);
$js
=
$js
.
file_get_contents
(
'../../source/articles/article.js'
);
$js
=
$js
.
file_get_contents
(
'../../source/database.js'
);
$js
=
$js
.
file_get_contents
(
'../../source/templates.js'
);
$js
=
$js
.
'}(APP));'
;
$output
[
'js'
] =
$js
;
// Concatenate the files in the /css/ directory
// This would be a sensible point to compress your css
$css
=
''
;
$css
=
$css
.
file_get_contents
(
'../../css/global.css'
);
$output
[
'css'
] =
$css
;
// Encode with JSON (PHP 5.2.0+) & output the resources
echo
json_encode(
$output
);
|
/source/application/applicationcontroller.js
最后,我们需要更新 applicationcontroller.js 文件,这样我们所做的更新工作才能真正生效。
|
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
APP.applicationController = (
function
() {
'use strict'
;
function
offlineWarning() {
alert(
"This feature is only available online."
);
}
function
pageNotFound() {
alert(
"That page you were looking for cannot be found."
);
}
function
showHome() {
$(
"#body"
).html(APP.templates.home());
// Load up the last cached copy of the news
APP.articlesController.showArticleList();
$(
'#refreshButton'
).click(
function
() {
// If the user is offline, don't bother trying to synchronize
if
(navigator && navigator.onLine ===
false
) {
offlineWarning();
}
else
{
APP.articlesController.synchronizeWithServer(
function
() {
alert(
"This feature is not available offline"
);
});
}
});
}
function
showArticle(id) {
$(
"#body"
).html(APP.templates.articleLoading());
APP.articlesController.showArticle(id);
}
function
route() {
var
page = window.location.hash;
if
(page) {
page = page.substring(1);
if
(parseInt(page, 10) > 0) {
showArticle(page);
}
else
{
pageNotFound();
}
}
else
{
showHome();
}
}
// This is to our webapp what main() is to C, $(document).ready is to jQuery, etc
function
start(resources, start) {
APP.database.open(
function
() {
// Listen to the hash tag changing
$(window).bind(
"hashchange"
, route);
// Inject CSS Into the DOM
$(
"head"
).append(
"<style>"
+ resources.css +
"</style>"
);
// Create app elements
$(
"body"
).html(APP.templates.application());
// Remove our loading splash screen
$(
"#loading"
).remove();
route();
});
if
(storeResources) {
localStorage.resources = JSON.stringify(resources);
}
}
return
{
start: start
};
}());
|
(工作从下至上)这个文件能够完成如下功能:
APP.applicationController.start()函数:- 监听hash tag的改变,当发现变化时,运行route函数。
- 向DOM中注入CSS,构造最基本的app元素(和之前的方法一样,但是我们将HTML字符串移到了 templates.js 文件中)。
- 删除加载启动画面。
- 调用route函数。
route函数能够实时获取hash tag:- 如果该标签为空,则运行showHome函数。
- 如果第一个字符不是删除标记(通常是“#”)——并且如果它是一个正整数,那么将会被当做一个文章id,并调用
showArticle(id)将指定id的文章下载到本地。 - 如果它既不为空,又不是一个正整数,则向用户显示一个友好的 Page not found提示信息。
- 最后,showHome 和
showArticle(id)函数可能会向页面中加入一些简单的HTML,并调用articleContrshowHome的showArticleList和showArticle(id)函数。showHome函数也会设置一个事件监听器,用了监测刷新按钮,并触发articleController的synchronizeWithServer方法。
接下来的工作
- 目前开发的app必须在支持Javascript的环境下运行。
- 不支持搜索——还没有可以抓取的内容。
- 还没有考虑可用性。
- 我们将页面渲染工作全部放在了客户端完成(有可能是一个旧款的移动手机)。
- 它的操作感觉并不像是一个app。如果你使用触屏设备访问这个app时,你可能发现它的响应不够及时——它可能会有300ms的延迟。
- 它的外观看起来也不像一个app——因为它没有针对各种不同尺寸的屏幕做适应性设计…
- 目前还不支持离线访问图片。
- 在引导程序上,我们还有以下工作可以改进:
- 在这个示例新闻app中,每当我们运行这个app时,就会下载所有的CSS和Javascript程序,并处理这些信息(JSON编码解码,保存到本地)。通过为下载的资源指定版本号能够提高程序的效率。因为,app会首先检查自己的版本号,如果是最新的版本,则可以直接跳过下载步骤。
- 当设备连入网络后,应用程序还是要求用户等待服务器响应请求。但是,这个app可以使用本地保存的文件启动应用——直到下次重启时才运行最新的内容。FT web app正是这么做的。
结束语
显然,我们的web app示例还有很多提升的空间。但是我们开发的这个代码组织结构能够支持任何一种应用程序,只要使用一个简单的脚本(我们称之为引导程序)下载所需的资源,然后eval自己的程序代码就ok了,我们还不用操心app缓存管理的问题。这使得我们能够更加专注于如何丰富web app的功能和用户体验。
本文介绍了一种构建离线HTML5应用的方法,通过避免使用传统的appcache技术,利用Ajax和localStorage实现应用资源的高效管理和更新。文章详细展示了如何创建一个RSS阅读器,包括其前端和后端的设计与实现。
2293

被折叠的 条评论
为什么被折叠?



