65.9K
CodeProject 正在变化。 阅读更多。
Home

HTML App Cache 实战,创建网页本地副本

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (28投票s)

2014年5月5日

CPOL

28分钟阅读

viewsIcon

45276

downloadIcon

870

一个 MVC 应用程序,使用 HTML5 的 AppCache 功能在浏览器缓存中创建 CodeProject 文章的副本

引言

本文是关于 HTML5 和 CSS3 学习系列 12 篇文章中的第 9 篇。

在本文中,我们将专门学习 HTML5 的 App Cache 功能及其使用方法。

为了演示 App Cache 的用法,我创建了一个简单的 MVC 应用程序,它列出了 CodeProject 最受欢迎的 10 篇文章并将其缓存,即使没有网络连接也能浏览它们。

选择这个特定示例的原因是我拥有一台只有 Wi-Fi 的 iPad,我只希望能够缓存我想在移动中阅读的文章。直到现在,我不得不将文章打印成 PDF 阅读。当我读到 App Cache 时,我首先想到的是“让我们缓存 Code Project” :)

本文将介绍以下概念并将其应用于我们的项目中。

什么是应用程序缓存?

应用程序缓存是 HTML5 的一个功能,用户即使没有活跃的网络连接也能查看网站的部分或全部网页。

Web 应用程序旨在适应不同的用户群和场景。随着移动技术成为访问网络信息最常用的方式之一,我们面临着新的网络可用性场景。要访问信息,我们需要一个活跃的网络连接,否则无法在线访问内容。应用程序缓存通过允许我们将部分(如果愿意,可以全部)内容离线提供给用户,而无需活跃的网络连接来帮助我们处理这种情况。

用户与 Web 应用程序交互的类型大致可分为 *Get/Fetch 操作* 或 *Set/Post 操作*。

大多数 Web 应用程序将包含这些操作的组合,有些则专门是 Get/Fetch 站点。

Get/Fetch 应用程序

Fetch 应用程序是指用户仅根据点击和选择查找信息的应用程序。不会向服务器发送任何信息进行存储。大多数情况下,这些页面是静态的,如果不是,它们会使用客户端加载的 Javascript 或 CSS 与用户交互。

一些示例应用程序包括照片库(不带点赞和评论功能)、活动信息站点、Codeproject 文章等。

Set/Post 应用程序

Set/Post 应用程序是指用户在 Web 应用程序上提交需要存储在服务器上或发送到另一台服务器的信息的应用程序。这些应用程序将与用户和服务器频繁交互,以呈现服务器信息并捕获用户输入的信息。

一个简单而熟悉的例子是在 Code project 网站上撰写并提交文章或回答问题。在这里,最终用户输入的信息存储在 Code Project 服务器上并进行维护,并在任何用户想要查看时发送回浏览器。

为什么要使用 App Cache?它有什么用?

HTML5 中的 App Cache 帮助我们使 Get/Fetch Web 应用程序可用于离线浏览。使用 App Cache,我们可以将必要的文件本地缓存到用户的机器上,当网络不可用时,浏览器会自动切换到存储的副本并加载它们以供显示。

Set/Post 应用程序需要将用户输入的信息本地存储,然后在网络可用时将其发送到服务器,这可以通过另一个新的 HTML5 功能 Web Storage 轻松实现,我们将在下一篇文章中讨论它。

浏览器需要什么才能呈现典型的 Get/Fetch Web 应用程序?

旨在向用户提供静态信息的 Web 应用程序主要包含以下各项

  • HTML/CSHTML/ASPX 等:这些文件包含布局信息,有时还包含关于布局外观的动态验证。
  • CSS:这些是包含布局应如何格式化和呈现的信息的样式。
  • 图像:网页上来自服务器的任何图像(非外部链接)。
  • Javascript:提供客户端功能的必要脚本,例如,一个根据用户鼠标和键盘输入在画布上绘图以支持游戏的脚本。

如果我们可以将这些文件本地存储,那么除非呈现的信息有更新,否则浏览器无需联系服务器来呈现这些信息。

应用程序使用 App Cache 可以获得多种优势。以下是其中一些。

  1. 速度:如果您的网站上有图片(尤其是大图片)不经常更改,那么您可以直接缓存它们,这样您的页面加载速度会快得多。类似地,缓存不经常更改的资源有助于提高应用程序加载和对用户可用的速度,因为现在这些图片和资源来自本地磁盘而不是远程服务器。
  2. 离线浏览:如前所述,您可以缓存必要的文件并使您的 Web 应用程序离线可用。这可以针对整个站点或特定页面,具体取决于要求。用户现在可以访问所需的信息,而无需等待活跃的网络连接(互联网)。
  3. 回退计划:即使我们的 Web 服务器因技术故障(或任何其他原因)而宕机,缓存也有助于保留对重要信息的访问。假设,出现问题并且我们的 Web 服务器宕机时间超过了可容忍的时间,那么用户将无法及时获取所需的信息。如果信息至关重要,这可能会变得非常不便。例如,您的网页是电影票的确认页面,而用户没有打印出来。如果您的 Web 服务器在用户需要出示或打印票之前变得不可用,那么肯定会让用户不开心 :)。或者一个提供驾驶路线的网页。在上述情况下,如果用户在服务器宕机之前已经访问了信息并且启用了 App Cache,那么即使服务器不可用,他也能访问信息。

哪些浏览器支持 HTML5 App Cache 功能?

大多数现代浏览器都支持 App 缓存。

以下是关于哪些浏览器版本开始支持 App Cache 的快速参考表。

浏览器 最早支持的版本
火狐 3.5
安卓版火狐 26.0
Chrome 桌面版 4.0
Safari 桌面版 4.0
Opera 10.6
Internet Explorer 10.0 *不相信我? 点击这里
Android 2.3
移动版 Chrome 33.0
Opera Mini 尚未支持
Safari iOS 3.2

App 缓存能容纳多少数据?

每个浏览器为此功能分配了不同的配额。以下是截至今天可用的信息表。

浏览器 限制
Safari 无限
Safari 移动版 10 MB
Chrome 5 MB
安卓浏览器 无限
火狐桌面版 无限
Opera 默认= 50 MB,但用户可以管理

希望很快我们能实现浏览器类型和版本无关的统一性。

App 缓存与浏览器/HTTP 缓存相同吗?

不,浏览器/HTTP 缓存和 App 缓存的预期用途和工作方式非常不同。引入浏览器缓存是为了通过在客户端缓存带有过期日期的资源来提高 Web 应用程序的性能,这会减少每次从服务器获取资源的往返次数。这通常用于缓存不经常更改的 CSS、Javascript 和图像。此外,即使资源被缓存,用户也无法离线使用页面。

要启用浏览器缓存,我们必须为每个文件添加带有过期日期的 HTTP 头。根据这些过期日期,浏览器会返回服务器以获取资源的最新/新鲜版本。如前所述,这是一种旨在提高站点性能而非提供离线能力的技术。通常也将这些浏览器缓存文件的过期日期设置得相对较长。

另一方面,App 缓存的目的不是为了提高性能而缓存资源(至少不是主要目的)。它用于使您的 Web 应用程序的一部分离线可用。

我们还需要注意,因为这两个功能结合起来可能会导致意想不到的问题或产生意外结果。

例如,一个配置为使用 App Cache 离线可用的页面也带有一个缓存一周的头部。现在,如果您更改清单以强制缓存新版本,那么这个有效期为一周的页面将不会被缓存,直到下周。始终建议在使用 App Cache 的页面上禁用 HTTP 缓存。

让我们开始缓存

既然我们知道 App Cache 是新的超级英雄,那么让我们看看如何在我们的应用程序中真正实现/使用 App Cache。

为了理解 App Cache 的概念和用途,我们将创建一个 MVC 应用程序,它将列出 Code Project 网站上当天最热门的文章,并在点击链接时显示文章内容。我们的目标是缓存这些文章以供离线查看。这样我们就可以看到 App Cache 的实际应用。我还会把我们之前开发的游戏也放进去,以确保你在离线阅读文章时想休息一下时有东西可以玩 :)

我们的项目包含以下页面

  • Index – 此页面是所有热门文章的索引
  • Article – 显示文章内容的页面
  • Balloon_Bob – 游戏 Balloon_Bob 的页面
  • Bobby Carter – 游戏 Bobby_Carter 的页面

为了支持这些页面,我们还将有一些 Javascript、CSS 和图像文件。

在进入本文的下一部分之前,让我们准备好我们的项目。以下是一些简单的入门步骤,或者您可以直接下载此处附带的解决方案,并随着我们的讲解回顾文件。

创建示例应用

创建一个空的 MVC 项目,并添加 4 个视图,即 Index.cshtml、Article.cshtml、Balloon_Bob.cshtml 和 Bobby_Carter.cshtml。

创建一个 Home 控制器,并为这 4 个视图创建如下操作。我们的操作将只返回视图。

        public ActionResult Index()
        {
            return View();
        }
        public ActionResult Article()
        {
            return View();
        }
        public ActionResult Balloon_Bob()
        {
            return View();
        }
        public ActionResult Bobby_Carter()
        {
            return View();
        }

我没有添加视图代码,以缩短本文的长度,但这些代码已在上面提供下载。

我们还将添加一些图片、javascript 和 css 来支持这些视图,这些也包含在下载包中。

在我们的 Index 视图中,我们使用 SyndicationFeed 对象读取 https://codeproject.org.cn/WebServices/ArticleRSS.aspx 上公开的 codeproject 热门文章的 RSS,并将其列在一个表格中。

表格将包含 Title,它将是带有查询字符串的 Article 视图的链接以及作者姓名。

下面是视图中的代码以及它在页面上的样子。

<div style="margin: auto;">
    <article class="blog-post">
        <div>
            @using System.Xml;
            @using System.ServiceModel.Syndication;
            @{
                var feed = SyndicationFeed.Load(new XmlTextReader("https://codeproject.org.cn/WebServices/ArticleRSS.aspx"));
            }
            <table class="results">
                <tr>
                    <th>Article
                    </th>
                    <th>Author</th>
                </tr>
                @foreach (var item in feed.Items)
                {
                    <tr>
                        <td><a href="/Home/Article?article=@item.Id&Author=@item.Authors[0].Email.ToString()&Title=@item.Title.Text.ToString()">@item.Title.Text.ToString()</a></td>
                        <td>@item.Authors[0].Email.ToString()</td>
                    </tr>
                }
            </table>
        </div>
    </article>
</div>

截图

我们还将更新 Article 视图,使其获取查询字符串,获取文章内容并将其显示在页面上。

在这里,我们将使用 HtmlWeb 对象读取文章页面的 html 内容,并使用 HTML Agility 包更新 HTML 源,以便在我们的页面上呈现,这至少不难看,即使不如原始 CodeProject 页面那么漂亮。

下面是相应的代码,以及一张 GIF 图像,展示了点击链接后文章的显示方式。

<div style="margin: auto;">
    <article class="blog-post">
        <div>
            @using System.Xml;
            @using HtmlAgilityPack;
            @{
                HtmlWeb hw = new HtmlWeb();
                HtmlDocument doc = new HtmlDocument();
                try
                {
                <h2 style="color: #f90;">@Request["Title"]</h2>
                <h3>Author : @Request["Author"]</h3>
                    doc = hw.Load(@Request["article"]);
                    HtmlNode node = doc.DocumentNode.SelectSingleNode("//div[@id='contentdiv']");
                    var nodesToRemove = node
                    .SelectNodes("//img")
                    .ToList();
                    foreach (var node1 in nodesToRemove)
                    {
                        var varAtt = node1.Attributes["src"].Value;
                        node1.SetAttributeValue("src", "https://codeproject.org.cn" + node1.Attributes["src"].Value);
                        node1.Remove();
                    }
                @Html.Raw(@node.InnerHtml.ToString());
                }
                catch (Exception ex)
                {
                <h2>Invalid Article Link</h2>
                }
            }
        </div>
    </article>
</div>

截图

正如预期的那样,当我们无法连接到网络时,页面将无法加载。为了测试这种情况,我们不关闭网络连接,而是使用 Fiddler,这将是测试此情况的更简单更好的方法。

要使用 Fiddler 测试此功能,您只需通过创建一个简单规则来配置您的自动响应器,以阻止我们应用程序内的访问。下面的 GIF 演示了如何逐步配置您的 Fiddler。

在上述步骤中,我们创建了一个新规则,规定如果请求的 URL 包含 localhost:3701,则通过关闭 TCP/IP 连接发送响应。使用 *drop 将关闭连接,如果使用 *reset,则会使用 TCP/IP RST 终止连接。

创建缓存清单的基础知识

要为我们的 Web 应用程序启用应用程序缓存,我们需要指定哪些文件需要缓存,以便在无需等待服务器提供资源的情况下呈现页面。这将是站点的浏览器指令手册。

缓存清单是一个扩展名为 *.APPCACHE 的文本文件,它列出了所有需要缓存以在网络不可用时启用离线访问的资源,不应该缓存因为这些资源应该始终来自服务器的资源,一些注释以及一个回退页面指针,它告诉浏览器当用户访问未缓存(这似乎是一个杜撰的词J)的页面时该怎么做。

到现在你可能已经意识到,创建这个文件最简单最好的方法是打开记事本并开始输入。嗯,我完全同意。

但是,在创建此文件时,我们必须小心,因为浏览器会以*区分大小写*的方式读取这些文件,并且文件名和路径必须与服务器上的完全一致。

我找到了一个有趣的工具来创建这个文件,或者至少在创建后验证我们的清单。

它叫做 manifestR,它是一个书签工具,可以拖到书签栏中,当你加载页面时,你只需点击该书签工具,它就会为你创建一个清单文件。然后你可以复制粘贴它生成的文本到文件中。

但是您需要小心,因为这个书签工具会列出您页面中几乎所有引用的资源,而您可能不需要所有这些资源,因此需要进行一些微调。下面是它列出的内容。

  • 查找页面中的所有图片元素,无论它们是否位于同一域。
  • 指向同一域中所有页面的链接
  • 引用的样式表
  • 引用的脚本

一旦收集到上述详细信息,它将以正确的格式快速整理出一个包含 CACHE 部分的清单文件。然后,您需要列出 FALLBACK 和 NETWORK 部分。

以下是我在 CodeProject 主页上执行此操作时得到的结果。

清单文件(缓存清单、缓存、网络、回退)

此清单文件是主文件,它决定何时、缓存什么以及不缓存什么,因此清单文件本身绝不应被缓存。让我们看看此文件包含什么。

正如图片本身所解释的,该文件包含以下五个部分

  • CACHE MANIFEST:这必须是文件的第一行,告诉浏览器这是一个清单文件。这是强制性的。
  • COMMENTS:注释以 # 字符开头,在文件中是可选的。它们通常用于提及版本和日期。
  • CACHE:这些是浏览器需要缓存的文件 URL。
  • FALLBACK:这是当用户尝试访问未缓存页面时将显示的文件的名称。此处提及的页面将被缓存。
  • NETWORK:一个明确的白名单,告诉浏览器哪些资源不应被缓存,并且应始终从服务器请求。

缓存

此部分也称为**_显式部分_**,是文件中的默认部分,这意味着任何没有标题的部分都将被视为 CACHE 部分。

我们列出所有在服务器上带有路径的文件,这些文件将被缓存以支持预期的离线功能。此列表也可以包含外部资源。

所有下载的资源将在浏览器下次加载此页面时替代其在线对应项。您可能不需要明确列出要缓存的当前页面,因为浏览器会隐式缓存其 html 标签中包含清单属性的页面。

但是,如果您需要缓存其他 html 或 aspx 文件,则必须明确列出它们。

示例

CACHE MANIFEST

CACHE:

/css/siteCSS.css
/css/common.css
/js/index.js
/js/Bobby_Carter.js
/images/logo.png
/images/banner.jpg
http://s.codeproject.com/App_Themes/CodeProject/Img/logo250x135.gif

在上面的示例中,我们将 CACHE Manifest 作为第一行来通知浏览器这是一个清单文件,之后我们有 CACHE 部分,这可以省略,因为没有标题的部分默认将被视为 CACHE。

上面指定了 7 个路径要下载和缓存,其中还有一个来自 Code Project 网站的外部图像,它也将被下载和缓存。

这里的文件名必须与服务器上的完全相同,这意味着名称必须区分大小写。因此,建议以编程方式填充此文件以避免错误。此外,每个需要缓存的文件都必须明确列出,不允许使用通配符(如 js/*.js)的快捷方式。

回退

回退部分每行始终包含一对路径。第一个路径将是文件的在线版本,第二个路径是回退页面,如果在线页面无法访问,则将显示该页面。

示例

CACHE MANIFEST

FALLBACK:
/Index.cshtml     /offline.cshtml
/Article.cshtml   /noArticle.cshtml

这里我们告诉浏览器,如果用户尝试访问 Index.cs 页面,并且该页面之前未被缓存,则显示 offline.cshtml 页面。如果用户尝试访问未缓存的 Article.cshtml 页面,则显示 noArticle.cshtml 页面。这两个路径必须用空格分隔。

或者,我们可以为所有未在此处提及的其他资源配置一个全局回退页面,方法是在全局回退页面路径之前以反斜杠和空格开头。下面是一个示例

示例

CACHE MANIFEST

FALLBACK:

/Index.cshtml     /offline.cshtml 
/Article.cshtml   /noArticle.cshtml 
/   /globalOffline.cshtml

Fallback 指令适用于所有类型的资源,而不仅仅是页面。例如,当原始图像未缓存时,您可以分配一个回退图像。

定义回退的另一种方法是按目录定义回退资源。在指定目录下请求的任何未缓存资源都将导致回退资源。

以下是当图片目录下任何图片未缓存时提供默认图片的示例。

示例

CACHE MANIFEST

FALLBACK:

/Index.cshtml     /offline.cshtml

/Article.cshtml   /noArticle.cshtml

/   /globalOffline.cshtml

/images/     /imageUnavailable.png

网络

如前所述,网络部分只是一个白名单,列出了所有不应缓存的页面。我们可以提供一个特定的文件列表,这些文件应明确从服务器请求,如下例所示。

CACHE MANIFEST

NETWORK:

/Events.cshtml

/statistics.svc

或者像回退一样,您可以限制整个目录的资源不被缓存,如下所示

CACHE MANIFEST

NETWORK:

/statisticImages/

或者一个非常常见的方法是只提供一个星号,这意味着任何未明确列出要缓存的资源都应从服务器请求

CACHE MANIFEST

NETWORK:
*

链接和提供清单文件

如前所述,需要创建清单文件并将其放置在服务器上,以便浏览器请求它。因此,我们可以在根目录中创建一个扩展名为 .appcache 的清单文件并将其链接。

但是,由于我们可能希望控制哪些文件应该缓存,哪些不应该缓存,因此我们可以在请求时以编程方式创建文件内容并提供它们。

提供清单文件

我们这是一个 MVC 应用程序,所以让我们创建一个动作来在请求时提供清单文件内容。

我将添加一个循环以包含站点中的所有脚本和 CSS,但在实际应用中,我们应该始终确保只加载该页面所需的资源。

public ActionResult Manifest()
{
    Response.ContentType = "text/cache-manifest";
    Response.ContentEncoding = System.Text.Encoding.UTF8;
    Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);

    var manifest = "CACHE MANIFEST" + Environment.NewLine +
    "CACHE:" + Environment.NewLine+
"NETWORK:" + Environment.NewLine +
    "*" + Environment.NewLine 
;

    string startupPath = System.AppDomain.CurrentDomain.BaseDirectory;
    string[] JSfiles = System.IO.Directory.GetFiles(startupPath, "*.js",SearchOption.AllDirectories);
    string[] CSSfiles = System.IO.Directory.GetFiles(startupPath, "*.css",SearchOption.AllDirectories);

    for(int i=0;i<JSfiles.Length;i++)
    {

        manifest = manifest + "\\" + Url.Content(JSfiles[i].ToString().Replace(startupPath, "")) +
            Environment.NewLine;
                
    }
        for(int j=0;j<CSSfiles.Length;j++)
    {
        manifest = manifest + "\\" + Url.Content(CSSfiles[j].ToString().Replace(startupPath, "")) +
            Environment.NewLine;
    }

    return Content(manifest, "text/cache-manifest");
}

查看上面的代码,您会发现我们正在将内容类型设置为 text/cache-manifest。这是因为清单文件可以有任何扩展名(推荐使用 *.appcache),但它应该始终以 MIME 类型 *“text/cache-manifest”* 提供。

如果您要从服务器上的文件提供此服务,那么我们可能需要向 Web 服务器添加自定义文件类型。

例如,在 IIS 中,您可以像下面的 GIF 图像所示那样设置 mime-type。

Google App Engine 的 app.yaml 文件需要添加以下行。

- url: /mystaticdir/(.*\.appcache)
  static_files: mystaticdir/\1
  mime_type: text/cache-manifest
  upload: mystaticdir/(.*\.appcache)

在 Apache 中,以下内容需要添加到配置文件中。

AddType text/cache-manifest .appcache

在我们的代码中,我们通过将内容类型显式设置为正确的 MIME 类型来管理此问题。

Response.ContentType = "text/cache-manifest";

在此之后,我们还将编码设置为 UTF 8,如下所示。

    Response.ContentEncoding = System.Text.Encoding.UTF8;

另一个重要的点是,将此文件的可缓存性设置为 NoCache,因为如前所述,清单文件本身绝不应被缓存,因为如果清单没有更改,那么您缓存的资源将永远不会看到互联网的光明 :) 我们将通过一个示例详细说明这一点。

Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);

链接清单文件

一旦这个动作准备好,我们所需要做的就是在我们页面的 html 标签中链接动作 URL,如下所示。现在,我们只将其添加到我们的 Index 页面。

<html manifest="/Home/Manifest">

如果您是从文件提供服务,那么您可以在操作链接的位置提供文件的路径。下面是一个例子

<html manifest=”~/Content/Manifests/Manifest.appcache”>

就是这样,您已在索引页面上启用了 App Cache。

现在让我们看看当我们访问我们的网页时会发生什么。请记住,我们尚未为任何其他网页(包括文章)启用应用程序缓存。以下是显示结果的 GIF。

如上例所示,即使没有网络连接,首页也能加载,而文章页面则不能。

这是因为我们只在 Index 页面上启用了 App Cache,而不是在 Article 页面上。现在您可以继续在所有页面上启用 App Cache。

App Cache 事件流

我们现在知道 App Cache 有助于提供离线浏览功能。现在让我们看看幕后到底发生了什么,浏览器如何做出决定以及在此过程中触发了哪些事件。

当启用 App Cache 的 Web 应用程序加载时,浏览器会创建 Window.ApplicationCache 对象。当浏览器处理缓存或更新站点资源时,会触发事件到此对象。

为了简洁明了,让我们列出所有触发的事件并快速浏览它们。

事件

事件 流程中的下一个事件
检查中
  • [无更新] 或
  • [正在下载] 或
  • [已过时] 或
  • [错误]
下载
  • [进度] 或
  • [错误] 或
  • [已缓存] 或
  • [更新就绪]
进度
  • [进度] 或
  • [错误] 或
  • [已缓存] 或
  • [更新就绪]
已缓存 最后事件
更新就绪
无更新
已过时
Error(错误)
在线 不适用
离线 不适用

需要记住的重要一点是,浏览器甚至在请求清单并更新缓存之前就加载了页面的缓存版本。

Checking 是当浏览器通过查看页面 HTML 属性中的清单属性注意到页面上启用了 App Cache 时触发的第一个事件。即使页面之前是否被缓存,也会触发此事件。

一旦从服务器获取到清单文件,浏览器就会将其内容与缓存的对应文件进行比较,如果检测到任何更改,则会触发 Downloading 事件。即使清单是第一次接收,也会触发 Downloading 事件。

通常,当我们更改某些代码并重新部署页面时,清单本身不会更改,因为它只包含文件名。因此,每当我们希望浏览器缓存最新版本时,我们应该在清单文件中包含一个注释行,该行会在每次新构建时更改。

在我们的示例中,为了测试目的,我们包含一个版本号,即当前日期时间(YYYYMMDDHHmm),这将强制浏览器每分钟刷新缓存。

public ActionResult Manifest()
{
    string version = DateTime.Now.ToString("yyyyMMddHHmm");
    Response.ContentType = "text/cache-manifest";
    Response.ContentEncoding = System.Text.Encoding.UTF8;
    Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);

    var manifest = "CACHE MANIFEST" + Environment.NewLine +
    "# Version: " + version + Environment.NewLine +
    "CACHE:" + Environment.NewLine;

    string startupPath = System.AppDomain.CurrentDomain.BaseDirectory;
    string[] JSfiles = System.IO.Directory.GetFiles(startupPath, "*.js",SearchOption.AllDirectories);
    string[] CSSfiles = System.IO.Directory.GetFiles(startupPath, "*.css",SearchOption.AllDirectories);

    for(int i=0;i<JSfiles.Length;i++)
    {

        manifest = manifest + "\\" + Url.Content(JSfiles[i].ToString().Replace(startupPath, "")) +
            Environment.NewLine;
                
    }

        for(int j=0;j<CSSfiles.Length;j++)
    {
        manifest = manifest + "\\" + Url.Content(CSSfiles[j].ToString().Replace(startupPath, "")) +
            Environment.NewLine;
    }

        manifest = manifest + "#File_Count=" + (JSfiles.Length+CSSfiles.Length) + Environment.NewLine +
       "NETWORK:" + Environment.NewLine +
       "*" + Environment.NewLine;

    return Content(manifest, "text/cache-manifest");
}

在下载过程中,每次资源下载完成后都会触发一个进度事件。假设我们正在缓存 20 个文件,那么浏览器将触发 20 个进度事件,每个事件都在成功下载后触发。进度事件的参数具有可以帮助我们跟踪已加载文件数量和文件总数的属性,即 **event.loaded** 和 **event.total** 属性。

现在,如果在下载这些文件中的任何一个时出现问题,则会触发**错误事件**,并且缓存中的任何资源都不会更新。

一旦所有资源下载完成,就会根据下载开始前缓存的状态触发以下两个事件之一。

已缓存事件:当页面首次被缓存且在下载开始之前不存在该页面的缓存时,会触发此事件。

UpdateReady 事件:当文件之前已被缓存且新版本的资源刚刚下载时,会触发此事件。我们必须在此事件中处理旧缓存与新缓存的交换。为此,ApplicationCache 对象为我们提供了一个名为 appCache.swapCache() 的方法。*注意:某些浏览器不需要显式调用此方法,每次下载成功完成后都会发生交换。

另外请记住,一旦缓存更新,页面不会自动刷新,因此我们必须向用户提供消息以手动刷新页面以获取最新版本,或者我们需要使用脚本在 UpdateReady 事件中以编程方式完成此操作。

检查事件之后,如果浏览器发现现在收到的清单与之前的清单完全相同,则会触发 NoUpdate 事件。如果服务器上未找到清单文件,则会触发 Obsolete 事件

每当网络状态从在线更改为离线或反之亦然时,AppCache 也会触发一个事件(**Online 事件和 Offline 事件**)。事件名称将是目标状态。例如,从在线更改为离线时,它会触发离线事件。

下面是一个显示这些事件流程的图表。

除了事件之外,我们还可以访问 ApplicationCache 对象的 STATUS 属性以了解最新状态。

以下是状态码及其含义。

状态码 描述
0 - 未缓存 applicationCache 对象的缓存宿主未与缓存关联,这是未缓存页面在第一次触发检查事件之前的状态。
1 – 空闲 当前关联的 applicationcache 是最新的
2 - 正在检查 清单目前正在请求和比较中
3 – 正在下载 资源正在下载中
4 - 更新就绪 新版本的资源可用并已准备好进行交换。一旦交换完成,状态将更改为“空闲”。
5 – 已废弃 清单文件不可下载,应用程序缓存现在被标记为已废弃。

让我们在网页上跟踪这些事件和状态,并启用通过按钮手动更新缓存。

步骤1:添加一个进度条以显示下载事件的进度

<div style="text-align: center;margin-left:5%;margin-right:5%">

            <span id="progText" style="width: 80%; color: orangered"></span>
            <progress id="progressBar" max="100" value="0"></progress>

        </div>

步骤2:让我们添加一个按钮以启用手动更新

       <div style="text-align: center;">
            <span style="display: inline-block;">
                <input type="button" id="manualUpdate" value="Update Cache" style="font-weight:bold;border-radius: 25px;padding-top:2px;margin:5px;padding-bottom:2px;" />
            </span>

步骤3:为网络状态和应用程序缓存状态添加一些 span。我们将有两个 CSS 类,分别命名为 eventNotTriggered 和 eventTriggered,它们的背景颜色分别设置为灰色和绿色。

            <span class="eventNotTriggered" id="networkStatus"></span>

            <span class="Status" id="Status"></span>

步骤4:添加5个表示不同事件的span,每当一个事件被触发时,我们都会将其颜色更改为绿色,这将快速参考在该特定更新期间触发了哪些事件。

           <span class="eventNotTriggered" id="CHECKING">Checking</span>
            <span class="eventNotTriggered" id="DOWNLOADING">Downloading</span>
            <span class="eventNotTriggered" id="UPDATEREADY">UpdateReady</span>
            <span class="eventNotTriggered" id="noupdate">noupdate</span>
            <span class="eventNotTriggered" id="progress">Progress</span>
        </div>

步骤5:除了这些,我们还将添加一个区域,用于记录触发的事件,这样我们就可以有一个事件发生的时间顺序文本。

<div style="float: right; width: 50%; height: 500px; overflow: hidden">
<div style="overflow: auto; height: 490px">
<ul id="applicationEvents" style="color: black;">
<!-- List items will be created dynalically using  Javascript. -->
</ul>
</div>
</div>

步骤6:将一个名为 AppcacheEvents.js 的 Javascript 文件添加到项目中,并创建一个每 10 毫秒运行一次的方法,轮询 AppCache 状态并在上面创建的一个 span 中更新它。

$(document).ready(function () {

    var appStatus = $("#applicationStatus");
    var appEvents = $("#applicationEvents");
    var manualUpdate = $("#manualUpdate");
    var progBar = $("#progressBar");
    var progText = $("#progText");
    var Status = $("#Status");
    var appCache = window.applicationCache;
    var timer = setInterval(updateStatus, 10);

    function updateStatus() {
        var txt = "Status : Code( " + appCache.status + " )";
        switch (appCache.status) {
            case 0:
                txt = txt + " : Uncached";
                break;
            case 1:
                txt = txt + " : Idle";
                break;
            case 2:
                txt = txt + " : Checking";
                break;
            case 3:
                txt = txt + " : Downloading";
                break;
            case 4:
                txt = txt + " : UpdateReady";
                break;
            case 5:
                txt = txt + " : Obsolete";
                break;
            default:
                txt = txt + " : Unknown";
                break;

        }
        Status.text(txt);

    }
})

步骤7:添加一个名为 LogEvent 的方法,用于为我们上面创建的 UL 动态添加列表项。

function logEvent(event) {
        appEvents.prepend(
            "<li>" +
                ((new Date()).toLocaleTimeString()) + ":Curr_status:" + appCache.status + ": " + event + "     " +
            "</li>"
        );
    }

步骤8:添加点击事件方法,以便在点击按钮时手动更新缓存。

// Update button Click
    manualUpdate.click(
        function (event) {
            $(".eventTriggered").addClass("eventNotTriggered");
            $(".eventTriggered").removeClass("eventTriggered");
            progBar.val(0);
            progText.text("");
            event.preventDefault();
            // Status of the network.
            appStatus.text(navigator.onLine ? "Online" : "Offline");
            appStatus.addClass(navigator.onLine ? "eventTriggered" : "eventNotTriggered");
            appStatus.removeClass(navigator.onLine ? "eventNotTriggered" : "eventTriggered");
            // Manually update the Cache.
            appCache.update();
        }
    );

步骤9:添加在线/离线、检查和下载事件的事件监听器,我们只会记录事件并更改这些事件指定 span 的 CSS 类。

    appCache.addEventListener('online offline', function (event) {
        appStatus.text(navigator.onLine ? "Online" : "Offline");
        appStatus.addClass(navigator.onLine ? "eventTriggered" : "eventNotTriggered");
        appStatus.removeClass(navigator.onLine ? "eventNotTriggered" : "eventTriggered");
        logEvent("<span class='event'>" + event.type + "</span>" + " : " + "Network Status Changed");
    }, false);

    appCache.addEventListener('checking', function (event) {
        logEvent("<span class='event'>" + event.type + "</span>" + " : " + "Checking for manifest");
        $("#CHECKING").addClass("eventTriggered");
        $("#CHECKING").removeClass("eventNotTriggered");
    }, false);

    appCache.addEventListener('downloading', function (e) {
        

        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "Downloading cache");

        $("#DOWNLOADING").addClass("eventTriggered");
        $("#DOWNLOADING").removeClass("eventNotTriggered");
    }, false);

步骤10:添加进度事件的事件监听器,由于我们可以访问已加载和总文件属性,我们还将通过更改进度条值和文本来跟踪进度。并重复记录和类更改的步骤。

    appCache.addEventListener('progress', function (e) {
        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "File "+e.loaded+" downloaded");
        progBar.val((e.loaded / e.total) * 100);
        progText.text("Total files : " + e.total + " - Downloaded Files : " + e.loaded + " - Progress : " + Math.round(((e.loaded / e.total) * 100)*100/100) + "%");
        $("#PROGRESS").addClass("eventTriggered");
        $("#PROGRESS").removeClass("eventNotTriggered");

    }, false);

步骤11:为 Cached、UpdateReady、Error 和 noUpdate 事件添加事件监听器。所有这些都执行相同的记录和类更改步骤。但是,UpdateReady 事件将有一个额外的步骤来交换缓存。

appCache.addEventListener('cached', function (e) {
        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "All files downloaded");
        $("#UPDATEREADY").text("Cached");
        $("#UPDATEREADY").addClass("eventTriggered");
        $("#UPDATEREADY").removeClass("eventNotTriggered");
    }, false);

    appCache.addEventListener('updateready', function (e) {
        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "New cache available");
        appCache.swapCache();
        $("#UPDATEREADY").text("UpdateReady");
        $("#UPDATEREADY").addClass("eventTriggered");
        $("#UPDATEREADY").removeClass("eventNotTriggered");
    }, false);

    appCache.addEventListener('noupdate', function (e) {
        logEvent("<span class='event'>" + event.type + "</span>" + " : " + "No cache updates");
        $("#noupdate").addClass("eventTriggered");
        $("#noupdate").removeClass("eventNotTriggered");
    }, false);

    appCache.addEventListener('obsolete', function (e) {
        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "Manifest cannot be found");
    }, false);

    appCache.addEventListener('error', function (e) {
        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "An error occurred");
    }, false);

步骤12:好了,就这样,让我们将此 js 文件包含在页面的 head 部分,然后运行应用程序。

<script src="@Url.Content("~/Home/js/jquery.min.js")"></script>
<script src="@Url.Content("~/Home/js/config.js")"></script>
<script src="@Url.Content("~/Home/js/skel.min.js")"></script>
<script src="@Url.Content("~/Home/js/skel-panels.min.js")"></script>
<script src="@Url.Content("~/Home/js/AppCacheEvents.js")"></script>

下面是 Chrome 浏览器首次运行页面的日志,以及一张显示页面进度的图片(我使用了一个断点来减慢页面更新速度,这样我们就可以实时看到事件的发生)。

如下所示,第一次状态停留在“已缓存”,而下一次以“更新就绪”状态结束。

为了模拟 Obsolete 状态和事件,我们从 Fiddler 返回一个 404 错误,然后查看 Obsolete 事件是否被触发并在页面上更新。

作为最后一步,让我们在清单中添加一个 FallBack 部分,以返回一个静态页面,看看它如何工作。

清单操作

manifest = manifest + "#File_Count=" + (JSfiles.Length+CSSfiles.Length) + Environment.NewLine +
        Environment.NewLine+"FALLBACK:"+Environment.NewLine+"/ /Home/FallBack"+Environment.NewLine + "NETWORK:" + Environment.NewLine +
       "*";

回退操作

public ActionResult FallBack()
        {
            return View();
        }

现在让我们通过以下步骤测试一下

步骤1:加载页面

步骤2:点击第一篇文章,使其被缓存

步骤3:使用 fiddler 切断网络

步骤4:点击第二篇未缓存的文章

步骤5:验证回退页面已加载。

就是这样,网页现在缓存了所有以前访问过的文章,您可以在离线时随时阅读。

您已准备好**_缓存并携带_**您**_最喜欢的 CodeProject 文章_**。

一些提示、技巧和最佳实践

在您的项目中使用 App Cache 之前,请查看以下一些注意事项和提示。*(其中一些可能在前面重复过,但为了快速参考而列出)

  1. 浏览器总是先加载页面的缓存版本,然后再查找更新的版本,您需要在 UpdateReady 事件上手动或以编程方式刷新才能获取页面的最新更新。
  2. 如果项目中的所有页面都需要缓存,则每个页面都需要 manifest 属性。
  3. 被缓存的页面本身不需要在清单文件中列出,它们将自动缓存
  4. 所有资源都必须成功下载才能更新缓存,否则它们都不会更新。生效的是“全部或无”策略。
  5. 有些浏览器需要在 UpdateReady 事件上显式调用 SwapCache,无论如何都包含它是个好主意。
  6. HTTP 缓存和应用程序缓存不同,应避免同时使用。
  7. 即使您的应用程序已更改而清单未更改,浏览器也不会将其视为更改。只有更改的清单文件才会触发更新。
  8. 使用 Fiddler 测试您的网站。上面包含了快速的逐步说明。
  9. 如果您想查看您的 Chrome 浏览器中当前缓存了哪些站点,请按照以下步骤操作
    1. 在地址栏中输入 chrome://chrome-urls/
    2. 点击 chrome://appcache-internals/
    3. 这将列出所有缓存的站点
    4. 如果您想查看特定站点的条目,请点击“查看条目”
    5. 要清除站点的缓存,只需单击“删除”链接。
    6. 下面是一个快速参考图
  10. 浏览器根据 URL 进行缓存,并且区分大小写,因此如果您的 URL 包含查询字符串等更改,它将被单独缓存。
  11. 如果您的缓存页面上有链接图像,则它们将不会被缓存,因此在离线浏览期间也不会加载。
  12. 使用 Chrome 或 Firefox 控制台调试或查看事件日志和任何错误。Chrome 中的快捷键 [Ctrl + Shift + J]。下面是带有事件日志的控制台快速截图。

  1. 有效应用程序缓存*永不过期*。
  2. 当启用 App Cache 时,明确指示不通过 HTTP Headers(如 *Cache-Control: No-Cache*)缓存此页面的指令将被忽略。
  3. 清单文件应始终以 MIME 类型“text/cache-manifest”提供。
  4. 请务必在您的清单中添加带有版本号的注释,此版本号甚至可以来自您的应用程序构建时间戳。
  5. 始终以 No-Cache 设置提供您的清单。清单文件本身绝不应被缓存。确保这一点的另一种方法是,在您的 Web 服务器上将 *.appcache 文件的过期头设置为立即过期。这将消除缓存清单文件的可能性。
  6. 如果清单文件本身的请求返回 404,则状态会更新为“已过时”,并且缓存会被删除。
  7. 在 Chrome 的隐身模式下,浏览器无法写入缓存,您的网站将不会被缓存,使用 modernizer 可以在一定程度上解决这个问题。

使用代码

下载 Zip 文件并解压内容,在 Visual Studio 2010/2012 中打开解决方案文件。由于我没有在下载的 zip 中包含引用的 DLL,您第一次需要安装 HTMLAgility pack 和 Json.Net。

完成后,您就可以开始了。点击*运行*并**_缓存_**。

历史

初始版本 - 2014 年 5 月 3 日 - Guruprasad K Basavaraju

© . All rights reserved.