写点什么

使用 ASP.NET MVC 构建 HTML5 离线 web 应用程序

  • 2012 年 5 月 14 日
  • 本文字数:6472 字

    阅读完需:约 21 分钟

web 应用程序的主要制约之一就是连接性。在 HTML5 到来之前我们就曾想挖掘浏览器的能力,以使 web 应用程序能像桌面应用程序一样功能强大和易于使用,但浏览器始终让我们感到失望。虽然之前已出现了一些浏览器缓存技术,但这些缓存技术的设计初衷并不是为了使 web 应用程序能够完全地离线运行,令人遗憾的是,事实上使用这些技术的 web 应用程序很容易出问题,而且难于使用。HTML5 试图通过离线应用程序缓存 ( offline application cache) 技术来填补浏览器的能力空缺,该技术能更加可靠地使 web 应用程序离线工作。

为什么 web 应用程序需要离线运行呢?

老实讲,一般来说,桌面电脑的 web 应用程序即使能够完全离线运行也不能带来多大的好处,因为桌面电脑一般都是一直连线的。我特别期待看到的是,移动设备 web 应用程序能够从离线应用程序缓存技术得到多大的好处。

在许多地方,移动电话普及率都在持续增长。如果能够自然地填补网络断线的鸿沟,移动设备浏览器中的 web 应用程序对用户来说就更加友好了。

在一些特定场景中,使整个应用程序能够离线运行,意味着我们只需创建一个跨平台的浏览器解决方案,而不必创建多个内建应用程序。

试想一下,一位销售员需要随时随地向她的顾客展示商品目录单。她可以使用任何她想要的电子设备,她首次浏览商品目录单时需要连线,之后便能够随时随地离线浏览。

应用程序缓存技术并不只是在离线状态下才有用武之地。我们可以将应用程序缓存作为一个超级缓存,用于本地存储资源,这样可以加速应用程序启动。服务器上更新了的资源可以在后台线程重新加载,加载完成之后便替换掉本地旧的资源并更新到正在运行的应用程序上。这种方式非常适用于桌面电脑的重量级 web 应用程序。

清单文件

要使用应用程序缓存,你不需要编写大量代码。你可以在一个简单的文本文件中定义需要离线使用的资源,这个文件被称作清单 (manifest) 文件。

清单文件格式

一个简单的清单文件具有如下格式:

复制代码
CACHE MANIFEST
# Version 1.0
CACHE:
/home/index
/content/style.css
/scripts/main.js
NETWORK:
/service/status
FALLBACK:
/logo.png /logo_offline.png

其中,你必须将 CACHE MANIFEST 头放在清单文件的第一行。

以数字符号#开头的行是注释行。这通常用于显式地修改清单文件以通知浏览器更新缓存。比如,在你更新了一张图片但没有修改图片的名称时,这种方式非常有用,因为浏览器并没有其他方式可以检测到服务器上的图片已被更新。

接下来,清单文件包含了以下三节:CACHE,NETWORK 以及 FALLBACK。在 CACHE 节你可以指定需要缓存的资源。需要一直从服务器下载的资源(即使在断线的情况下)则在 NETWORK 中指定。如果有大量的资源需要一直从服务器下载,你可以在 NETWORK 节中使用通配字符(即一个星号 *)表示。在 FALLBACK 节中,你可以指定在离线状态下可以使用的备用资源。

清单文件的格式并不特别严格。以上介绍的几个部分可以是任意次序的,它们甚至可以在一个清单文件中多次使用。

在清单文件中你可以使用相对路径或者绝对路径来定位资源。如果你使用相对路径,则必须以清单文件的位置作为参考来定位资源。

引用清单文件

要将清单文件绑定到应用程序,需要将 manifest 属性添加到 html 标签上。每个引用清单文件的页面自身默认会被缓存。然而,还是建议在清单文件中显示列出你想要缓存的资源。如果某个页面没有在清单文件中被指定,同时也不曾被在线浏览过,则在离线状态下无法访问到这个页面,因为浏览器无法知道页面是否存在于本地缓存中。

复制代码
<html manifest="cache.manifest")/>

检查缓存状态

使用应用程序缓存 API,我们可以检查应用程序缓存的状态。使用 window.applicationCache 这个属性可以查询当前缓存的状态。该状态属性的值是一个介于 0 至 5 之间的数字,每个数字对应一个特定的缓存状态。

数字

状态

描述

复制代码
uncached

页面不在应用程序缓存中。应用程序缓存第一次加载时页面也处于这种状态。

1

idle

当应用程序缓存是最新的时,浏览器将状态设置为 idle。

2

checking

当应用程序检查是否有更新了的清单文件时,浏览器将状态设置为 checking。

3

downloading

当应用程序正在下载新缓存时,浏览器将状态设置为 downloading。

4

updateready

当新缓存已下载完毕,可以替换旧有资源时,浏览器将状态设置为 updateready。

5

obsolete

当找不到清单文件时,浏览器将状态设置为 obsolete。

你可以使用 setInterval 函数来快速显示状态变化。

复制代码
setInterval(function () {
      console.log(window.applicationCache.status)
}, 500);

事件处理

除了检查缓存状态,我们还可以处理特定事件。

事件

描述

checking

当浏览器在检查是否有清单文件被更新过时这个事件被触发。这通常是第一个被触发的事件。

downloading

当浏览器开始下载新资源时该事件被触发。

cached

当所有资源下载完毕并提交到缓存时,该事件被触发。

error

当应用程序缓存机制出问题时该事件被触发。这可能是因为找不到清单文件,或者找不到清单文件中指定的某个资源。也可能是超过了浏览器离线缓存限额。通常来说,每当发生致命错误时该事件被触发。

noupdate

第一次下载清单文件时该事件被触发。

progress

每当应用程序缓存下载完一项资源时该事件被触发。

updateready

当新资源下载完毕并可以更新旧缓存时该事件被触发。

obsolete

当找不到清单文件时该事件被触发。

缓存替换

当新缓存下载完成之后,它并不会立即替换掉旧的缓存,而是直到我们通知应用程序使用新缓存时它才进行替换。我们可以通过处理 updateready 事件,使用 swapCache 将旧缓存替换为新缓存。更新的资源要在刷新页面后才能见到。

复制代码
window.applicationCache.onupdateready = function(){
      window.applicationCache.swapCache();
});

怎样让用户知道你的应用程序可以离线运行呢?

据我所知,没有哪种浏览器会通知用户当前应用程序是能离线运行的。不过,我们可以自己通知用户:通过监听应用程序缓存的特定事件,当应用程序已经可以离线工作时通知用户。我们甚至可以将应用程序缓存生命周期的每个阶段都通知用户。

应用程序缓存相关事件的处理是直截了当的。其中一个特别有用的事件是 progress 事件。每当一个资源下载完毕时这个事件被触发,其包含三个非常有用的属性,我们可以用这三个属性来显示下载进度:lengthComputable、loaded 以及 total。首先,我们需检查 lengthComputable 属性来判断 loaded 和 total 属性是否可用,接着我们使用 loaded 和 total 属性计算出资源下载的百分比进度。

复制代码
window.applicationCache.onchecking = function (e) {
    updateCacheStatus('Checking for a new version of the application.');
};
<p>window.applicationCache.ondownloading = function (e) {<br></br>    updateCacheStatus('Downloading a new offline version of the application');<br></br>};</p><p>window.applicationCache.oncached = function (e) {<br></br>    updateCacheStatus('The application is available offline.');<br></br>};</p><p>window.applicationCache.onerror = function (e) {<br></br>    updateCacheStatus('Something went wrong while updating the offline version of the application. It will not be available offline.');<br></br>};</p><p>window.applicationCache.onupdateready = function (e) {<br></br>    window.applicationCache.swapCache();<br></br>    updateCacheStatus('The application was updated. Refresh for the changes to take place.');<br></br>};</p><p>window.applicationCache.onnoupdate = function (e) {<br></br>    updateCacheStatus('The application is also available offline.');<br></br>};</p><p>window.applicationCache.onobsolete = function (e) {<br></br>    updateCacheStatus('The application cannot be updated, no manifest file was found.');<br></br>};</p><p>window.applicationCache.onprogress = function (e) {<br></br>    var message = 'Downloading offline resources.. ';<br></br>    if (e.lengthComputable) {<br></br>        updateCacheStatus(message + Math.round(e.loaded / e.total * 100) + '%');<br></br>    } else {<br></br>        updateCacheStatus(message);<br></br>    };<br></br>};</p>

怎样检测浏览器是处于在线状态还是离线状态呢?

你需要知道浏览器是在线的还是离线的有以下几个原因:也许是因为你想通知用户其正在离线工作,也许是因为你想在网络断开时禁用应用程序的某些功能,还或许是因为你想通过本地存储 (local storage) 技术以支持离线用户输入,然后在上线时将用户输入的文本同步到服务器。要实现这些需求,你可以通过自造基础架构,也可以通过使用开源项目或第三方项目。

检测在线状态

从原理上讲检测在线状态应该是非常简单的,比如在标准状况下,你使用 navigator 单件的 onLine 属性就可以检测出当前浏览器是否在线。

复制代码
console.log(navigator.onLine)

但事实上并非如此简单,因为各种浏览器对在线和离线的定义不尽相同。比如,旧版本的火狐浏览器只当用户显示地进行在线和离线状态切换时才更新 onLine 属性的值,而忽略了实际的网络状况。抛开实现上的不一致,检测网络连接状况本身就不是一件微不足道的事情。比如,假设你的电脑是连接上了的,但是你的路由器出问题了,这时应该显示什么状态呢?

一种流行的 hack 方法是检查每个 AJAX 请求的状态码,然后当状态码为不成功时则回退到离线机制。

事件处理

如果你想在浏览器改变连线状态时做一些事情,你可以通过处理 offline 和 online 事件来实现。但是请注意,和检查 onLine 属性一样,使用这两个事件也有类似问题。

复制代码
window.addEventListener('offline', function(e) {
  console.log('offline');
}, false);
window.addEventListener('online', function(e) {
  alert('online');
}, false);

浏览器支持

除了 Internet Explorer,所有主流现代浏览器都支持离线 web 应用程序。Internet Explorer 10 也实现了相关规范,只是目前它还未发布。在 caniuse.com 上可以查看到每种浏览器及其版本对这一规范的支持情况。

对于大部分实现,各主流浏览器基本上是相一致的。但在实现存储限额以及对限额的管理(这两项没有定义在规范中)上,各浏览器差异比较大。在测试你的 web 应用程序时应该考虑这个问题,移动设备中的浏览器在缓存大小上可是斤斤计较的。

使用 ASP.NET MVC 生成和提供清单文件

生成清单文件

利用 ASP.NET MVC 创建和提供清单文件有几种方式。最简单地方式就是让 ASP.NET MVC 提供静态文本文件。然而,如果我们想要使用内建的 ASP.NET MVC 特性来解析路由,或者想编写代码来动态操控清单文件,我们最好使用自定义的 action result。

我把这个自定义的 action result 命名为 ManifestResult,它继承自 MVC 框架中的 FileResult 类。提供清单文件服务时应该使用’text/cache-manifest’ MIME 类型,我把这个字符串传递给了父类的构造函数。

复制代码
public class ManifestResult : FileResult
{
    public ManifestResult(string version)
        : base("text/cache-manifest") { }
}

ManifestResult 类具有四个属性,其中三个属性对应清单文件的三个节,另外一个属性对应版本号。表示 CACHE 节和 NETWORK 节的两个属性仅仅是字符串枚举,而表示 FALLBACK 节的属性是字典类型的,用于将资源映射到 FALLBACK 指定的资源。

复制代码
public class ManifestResult : FileResult
{
    public ManifestResult(string version)
        : base("text/cache-manifest")
    {
            Version = version;
        CacheResources = new List<string>();
        NetworkResources = new List<string>();
        FallbackResources = new Dictionary<string, string>();
    }
    public string Version { get; set; }
    public IEnumerable<string> CacheResources { get; set; }
    public IEnumerable<string> NetworkResources { get; set; }
    public Dictionary<string, string> FallbackResources { get; set; }
}

要将格式化的清单文件输出到响应流,需要重写 WriteFile 方法。

复制代码
protected override void WriteFile(HttpResponseBase response)
{
WriteManifestHeader(response);
    WriteCacheResources(response);
    WriteNetwork(response);
    WriteFallback(response);
}
private void WriteManifestHeader(HttpResponseBase response)
{
    response.Output.WriteLine("CACHE MANIFEST");
    response.Output.WriteLine("#V" + Version ?? string.Empty);
}
private void WriteCacheResources(HttpResponseBase response)
{
    response.Output.WriteLine("CACHE:");
    foreach (var cacheResource in CacheResources)
        response.Output.WriteLine(cacheResource);
}
private void WriteNetwork(HttpResponseBase response)
{
    response.Output.WriteLine();
    response.Output.WriteLine("NETWORK:");
    foreach (var networkResource in NetworkResources)
        response.Output.WriteLine(networkResource);
}
private void WriteFallback(HttpResponseBase response)
{
    response.Output.WriteLine();
    response.Output.WriteLine("FALLBACK:");
    foreach (var fallbackResource in FallbackResources)
        response.Output.WriteLine(fallbackResource.Key + " " + fallbackResource.Value);
}

提供清单文件服务

为了提供清单文件服务,我们要将相应的 action 添加到相应的控制器类中,以生成和返回清单文件的动作结果(action result)。在该 action 中,我们利用 MVC 的 UrlHelper 对象来正确地解析路由。

复制代码
public ActionResult Manifest()
{
     var manifestResult = new ManifestResult("1.0")
    {
         CacheResources = new List<string>()
             {
                   Url.Action("Index", "Home"),
                   "/content/style.css",
                   "/scripts/main.js"
             },
             NetworkResources = new string[] { Url.Action("Status", "Service")},
             FallbackResources = { { "/logo.png", "/logo_offline.png" } }
    };
    return manifestResult;
}

为清单文件设置路由

我们应该为清单文件设置特定的路由。大多数浏览器对清单文件的位置没有严格的规定,而最可靠的跨浏览器方式是将清单文件放在根目录,并将其命名为 cache.manifest。在应用程序启动时,下面的代码将这个新的“cache.manifest”路由添加到路由表中。

复制代码
routes.MapRoute("cache.manifest", "cache.manifest", new { controller = "Resources", action = "Manifest" });

结论

离线 web 应用程序是正处于不断发展中的 HTML 规范的重要内容之一。根据实际用例,你可能仅仅是利用这个特性来缓存资源或者让 web 应用程序完全离线运行。这个特性的中心就是清单文件。清单文件的格式和要求一点也不复杂,使用 ASP.NET MVC 或其他服务端技术可以直截了当地生成和提供清单文件服务。编写好清单文件之后,使用应用程序缓存 API 就可以很容易地进行缓存更新。你也可以使用这组 API 来查询缓存状态和处理应用程序缓存的特定事件。想知道浏览器处于在线状态还是离线状态,你可以通过检查 navigator 对象的 onLine 属性,或者处理特定的在线和离线事件来判断。

关于作者

Jef Claes 热衷于创建新玩意。在日常工作中,他是 Euricom 的一名软件工程师,负责构建基于微软技术的企业应用软件。在业余时间,他喜欢将随机的想法或创新的方案付诸实践。除了这些,Jef 一般会在他的博客上写他感兴趣的事情。Jef 不羞于演讲,他在一些本地会议上谈论和web 相关的技术和问题。你可以通过 Twitter 和他联系。

查看英文原文: HTML5 offline web applications using ASP.NET MVC


感谢侯伯薇对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012 年 5 月 14 日 00:006746

评论

发布
暂无评论
发现更多内容

# 架构师训练营Week1总结

lggl

极客大学架构师训练营 UML

ARTS打卡 第17周

引花眠

微服务 ARTS 打卡计划

架构师训练营第一周课程笔记及心得

Airs

腾讯PCG数据中台专场介绍&招聘报名

Geek_c46970

数据中台 腾讯 招聘

拆分链表、图解HTTPS、Zookeeper原理、如何成为技术专家、架构师三板斧 John 易筋 ARTS 打卡 Week 18

John(易筋)

ARTS 打卡计划 图解https ZooKeeper原理 架构师三板斧 拆分链表

第一周:学习总结

王建军

第1周 作业

Pyr0man1ac

第二周 - 框架设计

Arthur云剑

java安全编码指南之:输入校验

程序那些事

java安全编码 安全编码规范 java安全编码指南

科大讯飞再握一国产核心技术,可高精细拾取30分贝超小音量

Talk A.I.

【架构师训练营1期】第一周作业

诺乐

架构师训练营第 1 期第一次作业

Geek_a01290

极客大学架构师训练营

架构师训练营第 1 期第一周总结

Geek_a01290

极客大学架构师训练营

Java新特性:数据类型可以扔掉了?

王磊

Java 新特性 Java新特性 var 局部类型推导

深入理解JVM垃圾回收算法 - 标记清理算法

CHEN川

GC算法 标记清理 位图标记 懒惰标记

超全面分布式缓存高可用方案:哨兵机制

架构精进之路

redis哨兵模式

Java ConcurrentHashMap 高并发安全实现原理解析

vivo互联网技术

Java hashmap 多线程 高并发

SpringBoot系列(1)-初识SpringBoot

引花眠

学习 springboot

week-1-part2 学习总结

陈龙

第1周 作业

wgl

UML

RDS、DDS和GaussDB理不清?看这一篇足够了!

华为云开发者社区

数据库 华为云 RDS

架构师第一周笔记

Geek_Gu

课程大作业

小胖子

git 常用操作及 git 工作流介绍

hepingfly

git git分支操作 git工作流

oeasy 教您玩转linux 010303文件管理器 nautilus

o

早知道这 8 个锦囊,我的程序人生一定更精彩

沉默王二

程序员

高效程序员的45个习惯:敏捷开发修炼之道(8)

石云升

敏捷开发 技术分享 轮换制

从linux源码看socket的阻塞和非阻塞

无毁的湖光

Linux TCP socket Linux Kenel

信任环:口碑传播的关键环节

boshi

用户增长 运营创新

程序员为什么热衷于造轮子,升职加薪吗?

小傅哥

Java 小傅哥 代码质量 编程开发 编程经验

智能商业时代的思考(三)数据驱动

刘旭东

大数据 数据驱动 智能商业

使用ASP.NET MVC构建HTML5离线web应用程序-InfoQ