FT WebApp团队:如何打造一个FT风格的离线HTML5 web App
为什么还有必要再写一个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 resourcesecho 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扩展,使用PHP header function设置content type。很多地方都推荐使用*.appcache,但这并非是必须的,所以我们在这里并没有采用这种主流的配置。
|
1
2
3
4
5
6
7
8
9
|
<?phpheader( "Content-Type: text/cache-manifest" );?>CACHE MANIFEST# 2012-07-14 v2jquery.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并调用一个模板将文章的标题显示在订阅列表中。
/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 resourcesecho 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的功能和用户体验。
文章来源: Tutorial: How to make an offline HTML5 web app, FT style
| 您可能也喜欢: | |||
讨论了那么多,究竟什么是Web App? |
Financal Time产品主管谈FT Web App开发 |
移动设备上的Web应用标准:2011年8月发展现状以及未来规划(下) |
看好HTML5,Financial Times Web App开发公司被FT收购 |
| 无觅 | |||