FT WebApp团队:如何打造一个FT风格的离线HTML5 web App

标签: 技术与实践 WebApp,FT | 发表时间:2012-12-18 00:51 | 作者:byane
出处:http://www.webapptrend.com

为什么还有必要再写一个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.comjquery.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&hellip;</ 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扩展,使用PHP header 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&hellip;' ;
     }
     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  .ajax method,它首先从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 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的功能和用户体验。

 

文章来源: Tutorial: How to make an offline HTML5 web app, FT style

 

您可能也喜欢:

移动设备Web App标准:现状及发展路线

Financal Time产品主管谈FT Web App开发

讨论了那么多,究竟什么是Web App?

看好HTML5,Financial Times Web App开发公司被FT收购
无觅

相关 [ft webapp 团队] 推荐:

FT WebApp团队:如何打造一个FT风格的离线HTML5 web App

- - Web App Trend
为什么还有必要再写一个Offline HTML5 App指南. 现在已经有非常多的资源介绍如何写一个offline HTML5的网站了,但是仅仅让一个网站能够在离线情况下访问是远远不够的. 在这个指南中我们将搭建两个离线网站,用来向读者演示如何向一个已有的离线网站中增添功能,避免已有的用户觉得自己正在使用一个旧版本的网站.

webapp适配基准

- - 崔永键的博客
移动端网络流量已经超过pc. 基准a: android 4.x 占了60%左右,2.3x在30%左右. ios6.x的safari也占比很大. 基准b: 考虑一下 ios5. 基准c: 考虑下ios4,以及android2.x. 分辨率: 第一组 “520*xxx”以下的为一组, 第二组“640*xxx,800*xxx,720*xxx”等分为一组.

FT社评:美国觉醒

- 品味视界 - FT中文网_英国《金融时报》(Financial Times)
一个月前,当一群形形色色的抗议者在曼哈顿中心地带的祖科蒂公园(Zuccotti Park)安营扎寨,谴责资本主义的肆意妄为时,人们还仅仅把他们看作一群心怀理想主义的年轻人,做着年轻人通常做的事情. 今天,只有傻瓜才会对一场反映出全世界各行各业普通民众愤怒和失望情绪的运动视而不见. 到目前为止,美国的抗议活动在很大程度上是和平的.

FT社评:苹果之魂乔布斯

- 宋大妈 - FT中文网_英国《金融时报》(Financial Times)
史蒂夫•乔布斯(Steve Jobs)卸任首席执行官后,苹果公司(Apple)的命运将会如何. 围绕这个问题,人们无疑将辩论一段时间. 该公司是否将保住其在科技行业非同寻常的领导地位. 凭借这种地位,该公司已接近成为全球市值最高的上市公司. 乔布斯对企业的全神贯注,以及他所取得的成功,几乎没有一个老板(甚至创始人)比得上.

推荐几个Html5做的WebApp

- - CSS库
Everybody!喵~ 二当家出马,一个顶仨. 今天来不为别的,给大家推荐几点刚上线不久的WebApp. 最近涌现出了一批号称Html5版的WebApp,由于还处于社会主义初期阶段,所以鱼龙混杂,有虾米也有大鱼. 反正只要是触屏智能机打开浏览器输入以上网址然后全屏,怎么说呢. 反正你是感觉不到这是浏览器在跑的.

WebAPP ViewPort iPhone5 黑边解决方案

- - 大猫の意淫筆記
最好先仔细看一遍苹果官方文档  Configuring the Viewport. 容易被忽略的就是即使 width=640的时候,scale=1是按照device-width而不是按照640的大小. iPhone 的 device-width 等于320,如果我设置 width=640,scale=1.

webkit webApp 开发技术要点总结

- - ITeye博客
如果你是一名前端er,又想在移动设备上开发出自己的应用,那怎么实现呢. 幸好,webkit内核的浏览器能帮助我们完成这一切. 接触 webkit webApp的开发已经有一段时间了,现把一些技巧分享给大家 :. 对于桌面浏览器,我们都很清楚viewport是什么,就是出去了所有工具栏、状态栏、滚动条等等之后用于看网页的区域,.

移动webapp开发小贴士

- - 移动开发 - ITeye博客
1 创建主屏幕图标 (Creating a home screen icon ,for ios). 2 启动画面图像 (Creating a splash screen, for ios). 3 全屏 (Making it full-screen, for ios)– 更像本地App. 4 改变状态栏 (Changing the phone status bar, for ios).

宅男斗胆翻唱Love the way you lie – Eminem ft. Rihanna

- Linlun - 河蟹娱乐
这一切的亮点都是陪衬,对都是浮云,一切一切都被你不停张动的嘴所散发的能照亮宇宙的光芒和能穿透时间的发音所深深的掩盖,不的不说很牛B. 囧囧有神的兔斯基 love love love. 史上最蛋疼的一首歌I have no penis. 原文链接: http://hxyl.net/2011/04/13/love-the-way-you-lie/.