Pro ASP.NET Core MVC2(第7版)翻译

第25章:使用其它内置标签助手

作者:Adam Freeman
翻译:陈广
日期:2018-10-16


我在第24章中描述的标签助手侧重于生成 HTML 表单,但它们并不是 ASP.NET Core MVC 提供的唯一内置标签助手。在本章中,我将描述管理 JavaScript 和 CSS 样式表、为 anchor 元素创建 URL、为图像元素提供缓存破坏以及支持数据缓存的标签助手。我还描述了为应用程序相关 URL 提供支持的标签助手,这有助于确保当应用程序部署到与其他应用程序共享的环境中时,浏览器可以访问静态内容。表25-1为本章摘要。

表 25-1:本章摘要

问题 解决方案 清单
包括基于宿主环境的内容 使用environment元素 2、8
选择 JavaScript 文件 script元素应用asp-src-includeasp-src-exclude属性 3-7
为 JavaScript 文件使用 CDN script元素应用asp-fallback属性 9、10
选择 CSS 文件 link元素应用asp-href-includeasp-href-exclude属性 11
对 CSS 文件使用 CDN link元素应用asp-fallback属性 12
为 anchor 元素生成 URL 使用AnchorTagHelper助手 13
确保检测到对图像的更改 img元素应用asp-append-version属性 14
缓存数据 使用cache元素 15-23
创建应用程序相对 URL 使用~字符作为 URL 前缀 24-26

准备示例应用程序

我将继续使用第24章中的 Cities 项目。为了准备本章,我创建了 wwwroot/images 文件夹,并添加了一个名为 city.png 的图像文件。这是纽约市地平线公共域全景图,如图25-1所示。

图25-1 在项目中添加一张图片

此图片文件包含在本章的源代码中,可在本书的 GitHub 存储库中找到(https://github.com/apress/pro-asp.net-core-mvc-2)。如果不想下载示例项目,可以替换自己的图片。

本章所需的另一个更改是使用 LibMan 将 jQuery 添加到项目中,如清单25-1所示。

清单 25-1:Cities 文件夹下的 libman.json 文件,添加 jQuery

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    },
    {
      "library": "jquery@3.3.1",
      "destination": "wwwroot/lib/jquery/"
    }
  ]
}

如果运行应用程序,您将能够列出存储库中的对象并创建新的对象,如图25-2所示。

图25-2 运行示例应用程序

使用宿主环境标签助手

EnvironmentTagHelper类应用于自定义environment元素,并根据托管环境确定是否在发送到浏览器的 HTML 中包含内容区域,我在第14章中对此进行了描述。这似乎并不是最令人兴奋的起点,但这个标签助手是为了更好地利用我稍后描述的一些相关特性,environment元素依赖于names属性,我在表25-2中描述了这个属性,供之后快速参考。

表 25-2:environment 元素的内置标签助手属性

名称 描述
names 此属性用于指定以逗号分隔的宿主环境名称列表,其中environment元素中包含的内容将包含在发送给客户端的 HTML 中。

在清单25-2中,我在开发和生产托管环境的视图中将environment元素添加到共享布局中,以包括不同的内容。

清单 25-2:Views/Shared 文件夹下的 _Layout.cshtml 文件,使用 environment 元素

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <environment names="development">
        <div class="m-1 p-1 bg-info"><h2>This is Development</h2></div>
    </environment>
    <environment names="production">
        <div class="m-1 p-1 bg-danger"><h2>This is Production</h2></div>
    </environment>
    <div>@RenderBody()</div>
</body>
</html>

图25-3显示了在开发和生产托管环境中运行应用程序的效果。environment元素检查当前宿主环境的名称,或者包含它包含的内容,或者省略它(发送给客户端的 HTML 中总是省略了environment元素本身)。

图25-3 使用宿主环境管理内容

使用 JavaScript 和 CSS 标签助手

下一类内置标签助手用于通过scriptlink元素管理 JavaScript 文件和 CSS 样式表,这些元素通常包含在共享的布局中。正如您将在下面的章节中看到的那样,这些标签助手功能强大且灵活,但需要密切关注以避免产生意外的结果。

管理 JavaScript 文件

ScriptTagHelper类是script元素的内置标签助手,用于使用表25-3中描述的属性来管理视图中包含的 JavaScript 文件,我将在下面的章节中对此进行描述。

表 25-3:script 元素的内置标签助手属性

名称 描述
asp-src-include 此属性用于指定视图中将包含的 JavaScript 文件
asp-src-exclude 此属性用于指定将从视图中排除的 JavaScript 文件
asp-append-version 此属性用于缓存破坏,如《理解缓存破坏》侧边栏所述。
asp-fallback-src 此属性用于在内容传递网络出现问题时指定要使用的回退 JavaScript 文件。
asp-fallback-src-include 此属性用于选择 JavaScript 文件,如果存在内容传递网络问题,将使用这些文件。
asp-fallback-src-exclude 此属性用于排除 JavaScript 文件,以便在出现内容传递网络问题时不再使用它们。
asp-fallback-test 此属性用于指定 JavaScript 的片段,用于确定 JavaScript 代码是否已从内容传递网络中正确加载。

选择 JavaScript 文件

asp-src-include属性用于使用全局模式将 JavaScript 文件包含在视图中。全局模式支持用于匹配文件的一组通配符,表25-4描述了最常见的全局模式。

表 25-4:公共全局模式

模式 示例 描述
? js/src?.js 该模式匹配除了/的任意单个字符。示例匹配 js 文件夹下文件名前面为 src,之后跟任意字符,之后是.js的所有文件,如 js/src1.js 和 js/srcX.js,但不包括 js/src123.js 或 js/mydir/src1.js。
* js/*.js 该模式匹配除了/的任意数量字符。示例匹配 js 文件夹中所有扩展名为.js的文件,如 js/src1.js 和 js/src123.js,但不包括 js/mydir/src1.js。
** js/**/*.js 该模式匹配包括/的任意数量字符。示例匹配 js 文件夹下或 js 文件夹下任意子文件夹下的扩展名为.js的文件,如 /js/src1.js 和 /js/mydir/src1.js

使用asp-src-include属性的全局模式意味着一个视图将始终包含应用程序中的 JavaScript 文件,即使文件的名称或路径更改了,或文件被添加或删除。在清单25-3中,我选择了 jQuery 的 JavaScript 文件,LibMan 将其安装到 wwwroot/lib/jquery 文件夹中。

清单 25-3:Views/Shared 文件夹下的 _Layout.cshtml 文件,选择 JavaScript 文件

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script asp-src-include="/lib/jquery/**/*.js"></script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

我在本例中使用的模式是一个常见的模式。模式是在 wwwroot 文件夹中计算的,jQuery库作为一个名为 jquery.js 的 javascript 文件交付。

全局模式尝试选择 jQuery 文件,同时适应 jQuery 分布方式的任何未来变化,例如更改 JavaScript 文件名。如果您运行这个示例并检查发送给客户端的 HTML,您会发现它包含了一个问题,如下所示:

<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script src="/lib/jquery/dist/core.js"></script>
    <script src="/lib/jquery/dist/jquery.js"></script>
    <script src="/lib/jquery/dist/jquery.min.js"></script>
    <script src="/lib/jquery/dist/jquery.slim.js"></script>
    <script src="/lib/jquery/dist/jquery.slim.min.js"></script>
    <link href="/lib/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
</head>

ScriptTagHelper类为每个与传递给asp-src-include属性的模式匹配的文件生成一个script元素。而不只是选择 jquery.js 文件,还有 jquery.min.js 文件,它是 jquery.js 文件的最小化版本,以及 jQuery 库的核心和缩减版本的常规和小型化版本。

您可能还没有意识到 jQuery 发行版包含了这么多文件,因为 Visual Studio 默认情况下会隐藏它们。要显示 wwwroot/lib/jquery 文件夹的全部内容,您必须在解决方案资源管理器中展开 jquery.js 项,然后对它包含的项执行同样的操作,如图25-4所示。

图25-4 显示【解决方案资源管理器】中目录的全部内容

我在清单25-3中使用的模式已多次将 jQuery 代码发送到浏览器,这浪费了带宽,减缓了应用程序的运行速度。对于某些库,它也可能导致错误或意外行为。有三种方法来解决这个问题,我将在下面的部分中描述。


使用源映射

JavaScript 文件被缩减以使其更小,这意味着它们可以更快地传递到客户端,并且使用更少的带宽。缩小过程从文件中删除所有空白,并重命名函数和变量,这样有意义的名称(如myHelpfullyNamedFunction)将由较小数量的字符(如x1)表示。当使用浏览器的 Javascript 调试器来跟踪最小化代码中的问题时,像x1这样的名称几乎不可能跟踪代码的进度。

jquery.min.map 文件是一个源代码映射,一些浏览器可以使用它来帮助调试小型化代码,方法是在最小化代码和开发人员可读的、不可缩小的源文件之间提供一个映射。

在我编写这篇文章时,源代码映射并不是一个普遍支持的特性,但是您可以在最新版本的 Chrome 和 Edge 上使用它们。例如,在 Chrome 中,如果开发者工具窗口打开,浏览器将自动请求源图文件,这意味着您不能始终将最小化版本的 Javascript 文件发送到浏览器,并且仍然能够轻松地调试它们。


收窄全局模式

许多包提供它们的 JavaScript 文件的常规和小型化版本,如果您只打算使用小型化版本,那么您可以限制全球化模式匹配的文件集,如清单25-4所示。如果您不需要调试 jQuery 库,这是一个很好的方法,因为 jQuery 库编写得很好,几乎没有什么问题,或者如果您知道目标浏览器支持源映射,那么这也是一种很好的方法。

清单 25-4:Views/Shared 文件夹下的 _Layout.cshtml 文件,仅选择小型化版本 JavaScript 文件

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script asp-src-include="/lib/jquery/**/*.min.js"></script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

如果运行该示例并检查发送到浏览器的 HTML,您将看到只包含了缩小的文件。

<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script src="/lib/jquery/jquery.min.js">
    </script><script src="/lib/jquery/jquery.slim.min.js"></script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>

收窄 JavaScript 文件的模式是有帮助的,但浏览器中最终还是会出现 jQuery 库的正常版本和瘦版本(瘦版本省略了一些不太常用的函数-请参阅 jquery.com 获取详细信息)。为了进一步缩小选择范围,我可以在全局模式中包括瘦代码,如清单25-5所示。

清单 25-5:Views/Shared 文件夹下的 _Layout.cshtml 文件,收窄焦点

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script asp-src-include="/lib/jquery/**/*.slim.min.js"></script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

其结果是只将 jQuery 文件的一个版本发送到浏览器,同时仍然保留文件位置的灵活性:

<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script src="/lib/jquery/jquery.slim.min.js"></script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
排除文件

收窄 JavaScript 文件的模式有助于选择名称中包含特定单词的文件(如slim)。如果你想要的文件没有这个词,比如你想要缩小文件的完整版本,那就没什么用了。幸运的是,你可以使用asp-src-exclude属性从asp-src-include属性匹配的列表中删除文件,如清单25-6所示。

清单 25-6:Views/Shared 文件夹下的 _Layout.cshtml 文件,排除文件

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script asp-src-include="/lib/jquery/**/*.min.js"
            asp-src-exclude="**.slim.**">
    </script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

运行应用程序并检查发送到浏览器的 HTML,您将看到只包含了 jQuery 库的完整小型化版本:

<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script src="/lib/jquery/jquery.min.js"></script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>

当您想要文件的非小型化版本时,也可以使用相同的技术,这在开发过程中可能很有用,如清单25-7所示。

清单 25-7:Views/Shared 文件夹下的 _Layout.cshtml 文件,选择非小型化文件

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script asp-src-include="/lib/jquery/**/j*.js"
            asp-src-exclude="**.slim.**,**.min.**">
    </script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

请注意,我可以通过用逗号分隔它们来指定多个单词。如果运行应用程序并检查发送到浏览器的 HTML,您将看到只包含了 JavaScript 文件的非缩小版本:

<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script src="/lib/jquery/jquery.js"></script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
使用宿主环境选择文件

一种常见的方法是在开发过程中处理常规的 JavaScript 文件,这使得调试变得容易,并在生产中使用小型化的文件,这可以通过使用环境元素来有选择地包含基于宿主环境的script元素来实现,如清单25-8所示。

清单 25-8:Views/Shared 文件夹下的 _Layout.cshtml 文件,选择文件

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <environment names="development">
        <script asp-src-include="/lib/jquery/**/j*.js"
                asp-src-exclude="**.slim.**,**.min.**">
        </script>
    </environment>
    <environment names="staging, production">
        <script asp-src-include="/lib/jquery/**/*.min.js"
                asp-src-exclude="**.slim.**">
        </script>
    </environment>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

这种方法的优点是使应用程序适应宿主环境,但确实意味着您必须编写和维护多组script元素。


理解缓存破坏

静态内容(如图像、CSS 样式表和 JavaScript 文件)通常被缓存,以阻止对较少更改内容的请求到达应用程序服务器。缓存可以通过不同的方式进行:服务器可以告诉浏览器缓存内容,应用程序可以使用缓存服务器来补充应用服务器,或者可以使用内容分发网络发布内容。并非所有的缓存都在您的控制之下。例如,大公司通常会安装缓存以减少其带宽需求,因为很大比例的请求倾向于发送到相同的站点或应用程序。

缓存的一个问题是,客户端在部署静态文件时不会立即接收新版本的静态文件,因为它们的请求仍然由以前缓存的内容提供服务。最终,缓存的内容将过期,新的内容将被使用,但这就留下了一个时期,应用程序的控制器生成的动态内容与缓存传递的静态内容不同步。这可能导致布局问题或意外的应用程序行为,具体取决于已更新的内容。

解决这个问题被称为缓存破坏(cache busting)。其思想是允许缓存处理静态内容,但立即反映出在服务器上所做的任何更改。标签助手类通过向静态内容的 URL 中添加一个查询字符串来支持缓存破坏,该静态内容包括充当版本号的校验和。例如,对于 Javascript 文件,ScriptTagHelper类支持通过asp-append-version属性进行缓存破坏,如下所示:

...
<script asp-src-include="/lib/jquery/**/j*.js"
    asp-src-exclude="**.slim.**,**.min.**"
    asp-append-version="true">
</script>
...

启用缓存破坏功能会在发送到浏览器的 HTML 中生成类似以下元素:

...
<script src="/lib/jquery/jquery.min.js?v=3zRSQ1HF-ocUiVcdv9yKTXqM">
</script>
...

标签助手将使用相同的版本号,直到您更改文件的内容,例如更新 JavaScript 库,此时将计算不同的校验和。增加版本号意味着每次更改文件时,客户端将请求一个不同的 URL,它缓存被视为新内容的请求,而新内容不能满足以前缓存的内容并传递给应用程序服务器。


使用内容分发网络

内容分发网络(CDN)用于将应用程序内容的请求布署到离用户更近的服务器上。浏览器不是从服务器请求 JavaScript 文件,而是从解析到地理位置服务器的主机名请求该文件,从而减少加载文件所需的时间,并减少为应用程序提供的带宽。如果您有大量的分布于各地的用户集,那么注册 CDN 是有商业意义的,但是即使是最小和最简单的应用程序也可以受益于使用主要技术公司运营的免费 CDN 来交付通用的 JavaScript 包,比如 jQuery。

在本章中,我将使用 Microsoft CDN,它提供对流行软件包的免费访问,其列表可在www.asp.net/ajax/cdn找到。我使用 jQuery 3.2.1,此版本的 Microsoft CDN 有6个 URL:

这些 URL 提供了常规的 JavaScript 文件、小型化的 JavaScript 文件,以及 jQuery 的完整和瘦版本的小型化文件的源图。在清单25-9中,我修改了示例应用程序中的布局,将本地文件替换为从 CDN 获得的文件。

清单 25-9:Views/Shared 文件夹下的 _Layout.cshtml 文件,使用 CDN

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js"></script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

指定 CDN 意味着对 jQuery 的请求不会到达应用程序的服务器。CDN 的问题是它们不受您的组织的控制,这意味着它们可能会失败,导致应用程序运行,但由于 CDN 内容不可用而无法按预期工作。为了帮助解决这个问题,ScriptTagHelper类提供了当客户端无法加载 CDN 内容时返回到本地文件的能力,如清单25-10所示

清单 25-10:Views/Shared 文件夹下的 _Layout.cshtml 文件,使用 CDN 回退

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js"
            asp-fallback-src-include="/lib/jquery/dist/**/*.min.js"
            asp-fallback-src-exclude="**.slim.**"
            asp-fallback-test="window.jQuery">
    </script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

asp-fallback-src-includeasp-fallback-src-exclude属性用于选择和排除本地文件,如果 CDN 无法传递常规src属性指定的文件。为了确定 CDN 是否有效,asp-fallback-test属性用于定义 JavaScript 的片段,该片段将在浏览器中进行评估。如果片段的计算结果为false,则将请求回退为文件。

要了解这是如何工作的,请运行应用程序并检查发送给客户端的 HTML。您将看到ScriptTagHelper类从asp-fallback-test属性中获取了片段,并使用它创建了另一个脚本元素,如下所示:

<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js">
    </script>
    <script>
        (window.jQuery||document.write("\u003Cscript
            src=\u0022\/lib\/jquery\/dist\/jquery.min.js
            \u0022\u003E\u003C\/script\u003E"));
    </script>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>

如果 CDN 中的文件已加载,则asp-fallback-test属性中指定的 JavaScript 片段必须返回true,否则返回false。最简单的方法通常是检查 JavaScript 代码提供的功能的入口点。jQuery 库在全局window对象上创建一个名为jQuery的函数,这就是我在清单25-10中测试的内容。您需要为从 CDN 加载的每个文件找到一个等效的测试。

测试回退设置非常重要,因为在 CDN 停止工作并且用户无法访问您的应用程序之前,您不会发现它们是否失败。检查回退的最简单方法是将src属性指定的文件名更改为不存在的文件名(我在文件名中添加了“Flow”),然后查看浏览器使用 F12 开发工具发出的网络请求。您应该会看到 CDN 文件的错误,后面是对后备文件的请求。

警告:CDN 回退功能依赖于浏览器同步加载和执行script元素的内容,以及它们定义的顺序。有许多技术用于通过使进程异步来加快 Javascript 的加载和执行,但这些技术可能导致在浏览器从 CDN 检索文件并执行其内容之前执行回退测试,从而导致对备用文件的请求,即使 CDN 运行良好。不要将异步加载脚本与 CDN 回退功能混合使用。

管理 CSS 样式表

LinkTagHelper类是link元素的内置标签助手,用于管理视图中包含的 CSS 样式表。这个标签助手支持表25-5中描述的属性,我在下面的章节中演示了这些属性。

表 25-5:link 元素的内置标签助手属性

名称 描述
asp-href-include 此属性用于为输出元素的href属性选择文件
asp-href-exclude 此属性用于从输出元素的href属性中排除文件
asp-append-version 此属性用于启用缓存破坏,如《理解缓存破坏》侧栏中所述
asp-fallback-href 如果 CDN 出现问题,则使用此属性指定回退文件
asp-fallback-href-include 此属性用于选择在出现 CDN 问题时将使用的文件
asp-fallback-href-exclude 此属性用于在出现 CDN 问题时将排除使用的文件
asp-fallback-href-test-class 此属性用于指定将用于测试 CDN 的 CSS 类
asp-fallback-href-test-property 此属性用于指定将用于测试 CDN 的 CSS 属性
asp-fallback-href-test-value 此属性用于指定用于测试 CDN 的 CSS 值

选择样式表

LinkTagHelperScriptTagHelper共享许多特性,包括支持全局模式来选择或排除 CSS 文件,因此不必单独指定它们。能够准确地选择 CSS 文件和 JavaScript 文件一样重要,因为样式表也有常规的和小型化的版本,而且还支持源映射。我在这本书中一直使用流行的 Bootstrap 包来样式化 HTML 元素,它在 wwwroot/lib/bootstrap/css 文件夹中包括了 CSS 样式表,如果您展开【解决方案资源管理器】中的所有项,您将看到有几个可用的文件,如图25-5所示。

图25-5 Bootstrap 发布文件

bootstrap.css 文件是常规样式表,bootstrap.min.css 文件是小型化版本,bootstrap.css.map 文件是源映射。其他文件提供了本章不感兴趣的特性。在清单25-11中,我使用了link元素上的asp-href-include属性来选择小型化样式表(我还删除了加载 jQuery 的脚本元素,现在已不需要它)。

清单 25-11:Views/Shared 文件夹下的 _Layout.cshtml 文件,选择样式表

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link rel="stylesheet"
          asp-href-include="/lib/bootstrap/**/*.min.css"
          asp-href-exclude="**/*-reboot*,**/*-grid*" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

选择 JavaScript 文件时同样需要注意细节,因为很容易为不需要的多个版本的文件生成link元素。您可以遵循相同的三种方法来控制选择的文件,正如我在上一节中为 JavaScript 文件描述的那样:收窄全局模式,使用asp-href-exclude属性排除文件,以及使用environment元素在重复的元素集合之间进行选择。

使用内容分发网络

LinkTaghelper类提供了一组属性,用于在 CDN 不可用时返回本地内容,尽管测试样式表是否已加载的过程比测试 JavaScript 文件要复杂一些。在清单25-12中,我使用 MaxCDN URL 作为 Bootstrap 库,只是为了显示 Microsoft 平台的替代方案(MaxCDN是 Bootstrap 项目推荐的CDN)。

清单 25-12:Views/Shared 文件夹下的 _Layout.cshtml 文件,使用 CDN 加载 CSS

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
          asp-fallback-href-include="/lib/bootstrap/**/*.min.css"
          asp-fallback-href-exclude="**/*-reboot*,**/*-grid*"
          asp-fallback-test-class="btn"
          asp-fallback-test-property="display"
          asp-fallback-test-value="inline-block"
          rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

href属性用于指定 CDN URL,我已经使用asp-fallback-href-includeasp-fallback-href-exclude属性来选择在 CDN 不可用时将使用的文件。然而,要测试 CDN 是否有效,需要使用三个不同的属性,并了解所使用的 CSS 样式表定义的 CSS 类。

CSS 回退功能的工作方式是将一个meta元素添加到由asp-fallback-test-class属性定义的类中。我在清单中指定了btn类,这意味着这样的元素将添加到发送到浏览器的 HTML 中:

<meta name="x-stylesheet-fallback-test" class="btn" />

您指定的 CSS 类必须在样式表中定义,样式表将从 CDN 中加载。我指定的btn类提供了 Bootstrap 按钮元素的基本格式设置。

asp-fallback-test-property属性用于指定由 CSS 类设置的 CSS 属性,asp-fallback-test-value属性用于指定将设置的值。标签助手将 JavaScript 添加到视图中,该视图测试meta元素上 CSS 属性的值,以确定样式表是否已加载,如果没有加载,则使用回退文件添加link元素。Bootstrap btn类将display属性设置为inline-block,这提供了测试,以查看浏览器是否能够从 CDN 加载 Bootstrap 样式表。

提示:了解如何测试第三方包(如 Bootstrap)的最简单方法是使用浏览器的 F12 开发工具。为了确定清单25-12中的测试,我给btn类分配了一个元素,然后在浏览器中检查它,查看类更改的各个 CSS 属性。我发现这比尝试阅读冗长而复杂的样式表更容易。

使用 Anchor 元素

a元素是导航应用程序和向应用程序发送 GET 请求以请求不同内容的基本工具。AnchorTagHelper类用于转换元素的href属性,以便它们针对使用路由系统生成的 URL,使用表25-6中描述的属性。

表 25-6:Anchor 元素的内置标签助手属性

名称 描述
asp-action 此属性指定 URL 将针对的 action 方法
asp-controller 此属性指定 URL 将针对的控制器
asp-area 此属性指定 URL 将针对的 area
asp-fragment 此属性用于指定 URL 片段(它在#字符后出现)
asp-host 此属性指定 URL 将针对的主机名称
asp-protocol 此属性指定 URL 将使用的协议
asp-route 此属性指定将用于生成 URL 的路由的名称
asp-route-* 名称以asp-route-开头的属性用于为 URL 指定附加值,以便asp-route-id属性用于向路由系统提供id段的值。

AnchorTagHelper简单且可预测,可以很容易地在使用应用程序的路由配置的元素中生成 URL。在清单25-13中,我更新了 Index.cshtml 视图中的a元素,以便它的href属性由标签助手生成。

清单 25-13:Views/Home 文件夹下的 Index.cshtml 文件,转换 Anchor 元素

@model IEnumerable<City>

@{ Layout = "_Layout"; }

<table class="table table-sm table-bordered">
    <thead class="bg-primary text-white">
        <tr>
            <th>Name</th>
            <th>Country</th>
            <th class="text-right">Population</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var city in Model)
        {
            <tr>
                <td>@city.Name</td>
                <td>@city.Country</td>
                <td class="text-right">@city.Population?.ToString("#,###")</td>
            </tr>
        }
    </tbody>
</table>
<a asp-action="Create" class="btn btn-primary">Create</a>

如果运行应用程序并请求 /Home/Index URL,您将看到标签助手将a元素进行了如下转换:

<a class="btn btn-primary" href="/Home/Create">Create</a>

使用 Image 元素

ImageTagHelper类用于通过img元素的src属性为图像提供缓存破坏,允许应用程序利用缓存,同时确保立即反映对图像的修改。ImageTagHelper类在img元素中操作,这些元素定义asp-append-version属性,表25-7对此进行了描述,以便快速参考。

表 25-7:Image 元素的内置标签助手

名称 描述
asp-append-version 此属性用于启用缓存破坏,如《理解缓存破坏》侧栏所述。

在清单25-14中,我向共享布局中的城市地平线图像添加了一个img元素,我在本章开始时添加了这个元素到项目中(我还重置了style元素以保持简洁,以便它使用本地文件)。

清单 25-14:Views/Shared 文件夹下的 _Layout.cshtml 文件,添加图像

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
          asp-fallback-href-include="/lib/bootstrap/**/*.min.css"
          asp-fallback-href-exclude="**/*-reboot*,**/*-grid*"
          asp-fallback-test-class="btn"
          asp-fallback-test-property="display"
          asp-fallback-test-value="inline-block"
          rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <img src="/images/city.png" asp-append-version="true" />
    <div>@RenderBody()</div>
</body>
</html>

如果运行应用程序,您将看到图像显示在每个页面的顶部。如果检查发送到浏览器的 HTML ,您将看到用于请求图像文件的 URL 中包含一个版本校验和,如下所示:

<img src="/images/city.png?v=KaMNDSZFbzNpE8Pkb30EXcAJufRcRDpKh0K_IIPNc7E" />

与 JavaScript 文件和 CSS 样式表的缓存破坏特性一样,URL 中包含的校验和将保持不变,直到文件被修改。

使用数据缓存

MVC 包括一个内存中的缓存,可以用来缓存内容片段,以加快视图渲染。要缓存的内容使用视图文件中的cache元素来表示,该元素由CacheTagHelper类使用表25-8中描述的属性进行处理。

注意:缓存是重用内容 sections 的有用工具,因此不必为每个请求生成缓存。但是有效地使用缓存需要仔细考虑和规划。虽然缓存可以提高应用程序的性能,但它也会产生奇怪的效果,例如接收陈旧内容的用户、包含不同版本内容的多个缓存,以及更新时由于从应用程序的前一个版本缓存的内容与新版本的内容混合在一起而中断的部署。除非您有一个明确定义的性能问题需要解决,否则不要启用缓存,并确保您理解缓存将产生的影响。

表 25-8:cache 元素的内置标签助手属性

名称 描述
enabled bool属性用于控制cache元素的内容是否缓存。省略此属性可启用缓存。
expires-on 此属性用于指定缓存内容将过期的绝对时间,表示为DateTime值。
expires-after 此属性用于指定缓存内容将过期的相对时间,表示为TimeSpan
expires-sliding 此属性用于指定缓存的内容将到期后,最后一次使用的时长,表示为TimeSpan值。
vary-by-header 此属性用于指定管理不同版本缓存内容的请求 header 的名称。
vary-by-query 此属性用于指定管理不同版本缓存内容的查询字符串键的名称。
vary-by-route 此属性用于指定管理不同版本缓存内容的 cookie 的名称。
vary-by-user bool属性用于指定身份验证用户的名称是否用于管理缓存内容的不同版本。
vary-by 使用此属性是为了计算用于管理内容的不同版本的键。
priority 此属性用于指定在内存缓存耗尽空间和清除未过期缓存内容时将考虑的相对优先级。

为了演示缓存属性的操作方式,我创建了 Components 文件夹,添加了一个名为 TimeViewComponent.cs 的类文件,并使用它定义了如清单25-15所示的视图组件。

清单 25-15:Components 文件夹下的 TimeViewComponent.cs 文件的内容

using System;
using Microsoft.AspNetCore.Mvc;

namespace Cities.Components
{
    public class TimeViewComponent : ViewComponent
    {
        public IViewComponentResult Invoke()
        {
            return View(DateTime.Now);
        }
    }
}

Invoke方法选择默认视图并提供一个DateTime对象作为视图模型。为了给视图组件提供一个视图,我创建了 Views/Home/Components/Time 文件夹,并添加了一个名为 Default.cshtml 的视图文件,其标记如清单25-16所示。

清单 25-16:Views/Home/Components/Time 文件夹下的 Default.cshtml 文件

@model DateTime

<div class="m-1 p-1 bg-info text-white">
    Rendered at @Model.ToString("HH:mm:ss")
</div>

DateTime模型对象用于显示当前时间,精确到第二个时间。在清单25-17中,我已经使用调用视图组件的@await Component.InvokeAsync表达式替换了上一节中的img元素。

清单 25-17:Views/Shared 文件夹下的 _Layout.cshtml 文件,使用视图组件

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
          asp-fallback-href-include="/lib/bootstrap/**/*.min.css"
          asp-fallback-href-exclude="**/*-reboot*,**/*-grid*"
          asp-fallback-test-class="btn"
          asp-fallback-test-property="display"
          asp-fallback-test-value="inline-block"
          rel="stylesheet" />
</head>
<body class="m-1 p-1">
    @await Component.InvokeAsync("Time")
    <div>@RenderBody()</div>
</body>
</html>

如果运行应用程序,您将看到网页横幅显示了内容渲染的时间。等待几秒钟,然后重新加载页面,您将看到显示的时间已经更改,如图25-6所示。

图25-6 示例应用程序中显示时间

cache元素用于包围应添加到缓存中的内容。在清单25-18中,我使用了cache属性将视图组件的输出添加到缓存中。

清单 25-18:Views/Shared 文件夹下的 _Layout.cshtml 文件,缓存内容

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
          asp-fallback-href-include="/lib/bootstrap/**/*.min.css"
          asp-fallback-href-exclude="**/*-reboot*,**/*-grid*"
          asp-fallback-test-class="btn"
          asp-fallback-test-property="display"
          asp-fallback-test-value="inline-block"
          rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <cache>
        @await Component.InvokeAsync("Time")
    </cache>
    <div>@RenderBody()</div>
</body>
</html>

在应用缓存元素是不带任何属性会告知 MVC 重用内容以满足所有未来的请求。如果启动该应用程序,则将缓存视图组件生成的内容,以便即使在重新加载页面时也会显示相同的时间。

提示CacheTagHelper类使用的缓存是基于内存的,这意味着它的容量受到可用 RAM 的限制。当可用容量不足时,内容将从缓存中弹出,而当应用程序停止或重新启动时,整个内容将丢失。

设置缓存过期

expires-*属性允许您指定缓存内容何时过期(表示为相对于当前时间的绝对时间),或者指定缓存内容未被请求的时间。在清单25-19中,我使用了expires-after属性来指定内容应该缓存15秒。

清单 25-19:Views/Shared 文件夹下的 _Layout.cshtml 文件,设置缓存过期

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
          asp-fallback-href-include="/lib/bootstrap/**/*.min.css"
          asp-fallback-href-exclude="**/*-reboot*,**/*-grid*"
          asp-fallback-test-class="btn"
          asp-fallback-test-property="display"
          asp-fallback-test-value="inline-block"
          rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <cache expires-after="@TimeSpan.FromSeconds(15)">
        @await Component.InvokeAsync("Time")
    </cache>
    <div>@RenderBody()</div>
</body>
</html>

如果运行应用程序,您将看到缓存的数据在15秒后过期,在此之后,重新加载页面将调用视图组件并创建一个新的缓存项,该条目将持续15秒。

设置固定的终止点

您可以使用expires-on属性指定缓存内容过期的固定时间,该属性接受DateTime值,如清单25-20所示。

清单 25-20:Views/Shared 文件夹下的 _Layout.cshtml 文件,指定固定缓存终止点

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
          asp-fallback-href-include="/lib/bootstrap/**/*.min.css"
          asp-fallback-href-exclude="**/*-reboot*,**/*-grid*"
          asp-fallback-test-class="btn"
          asp-fallback-test-property="display"
          asp-fallback-test-value="inline-block"
          rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <cache expires-on="@DateTime.Parse("2100-01-01")">
        @await Component.InvokeAsync("Time")
    </cache>
    <div>@RenderBody()</div>
</body>
</html>

我已经指定数据应该缓存到 2100 年。这不是一种有用的缓存策略,因为应用程序很可能在下个世纪开始之前重新启动,但是它确实说明了如何在将来指定一个不动点,而不是表示相对于缓存内容的那一刻的终止点。

设置最后一次使用的有效期

如果未从缓存中检索内容,应使用expires-sliding属性指定内容过期的时间段。在清单25-21中,我指定了10秒的滑动到期时间。

清单 25-21:Views/Shared 文件夹下的 _Layout.cshtml 文件,指定最后一次使用的有效期

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
          asp-fallback-href-include="/lib/bootstrap/**/*.min.css"
          asp-fallback-href-exclude="**/*-reboot*,**/*-grid*"
          asp-fallback-test-class="btn"
          asp-fallback-test-property="display"
          asp-fallback-test-value="inline-block"
          rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <cache expires-sliding="@TimeSpan.FromSeconds(10)">
        @await Component.InvokeAsync("Time")
    </cache>
    <div>@RenderBody()</div>
</body>
</html>

通过运行应用程序并定期重新加载页面,您可以看到express-sliding属性的效果。只要您在10秒内重新加载页面,缓存的内容就会被使用。如果您等待超过10秒才重新加载页面,则缓存的内容将被丢弃,视图组件将用于生成新内容,并且进程将重新开始。

使用缓存变体

默认情况下,所有请求都接收相同的缓存内容。CacheTagHelper类可以维护不同版本的缓存内容,并使用它们来满足不同类型的 HTTP 请求,使用其名称以vary-by开头的属性之一指定。清单25-22显示了使用vary-by-route属性来根据路由系统匹配的action值创建缓存变体的方法。

清单 25-22:Views/Shared 文件夹下的 _Layout.cshtml 文件,创建缓存变体

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
          asp-fallback-href-include="/lib/bootstrap/**/*.min.css"
          asp-fallback-href-exclude="**/*-reboot*,**/*-grid*"
          asp-fallback-test-class="btn"
          asp-fallback-test-property="display"
          asp-fallback-test-value="inline-block"
          rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <cache expires-sliding="@TimeSpan.FromSeconds(10)" vary-by-route="action">
        @await Component.InvokeAsync("Time")
    </cache>
    <div>@RenderBody()</div>
</body>
</html>

如果运行应用程序并使用两个浏览器选项卡或窗口请求 /Home/Index 和 /Home/Create URL,您将看到每个窗口在其过期时接收自己的缓存内容,因为每个请求产生不同的action路由值。CacheTagHelper类支持定义不同变体的一系列属性,包括为单个用户缓存内容。

还有一个vary-by header,允许您使用任何数据值定义任意的缓存变体。在清单25-23中,我通过指定直接从路由数据获得的值重新创建了vary-by-route属性的效果。

清单 25-23:Views/Shared 文件夹下的 _Layout.cshtml 文件,指定自定义缓存变体

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
          asp-fallback-href-include="/lib/bootstrap/**/*.min.css"
          asp-fallback-href-exclude="**/*-reboot*,**/*-grid*"
          asp-fallback-test-class="btn"
          asp-fallback-test-property="display"
          asp-fallback-test-value="inline-block"
          rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <cache expires-sliding="@TimeSpan.FromSeconds(10)"
           vary-by="@ViewContext.RouteData.Values["action"]">
        @await Component.InvokeAsync("Time")
    </cache>
    <div>@RenderBody()</div>
</body>
</html>

vary-by属性可以用来创建更复杂的缓存变体,但是应该小心,因为它很容易被抓走,最终产生的变体是如此具体,以至于缓存中的内容在过期之前从未被重用过。

使用应用程序相对 URL

最后一个内置标签助手是UrlResolutionTagHelper类,它用于提供对应用程序相对 URL 的支持,这些 URL 是以波浪符(~字符)为前缀的 URL。在清单25-24中,我更改了共享布局中的link元素,以便它使用显式定义的 URL,而不是使用标签助手从路由系统生成 URL。

清单 25-24:Views/Shared 文件夹下的 _Layout.cshtml 文件,使用显示 URL

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <cache expires-sliding="@TimeSpan.FromSeconds(10)"
           vary-by="@ViewContext.RouteData.Values["action"]">
        @await Component.InvokeAsync("Time")
    </cache>
    <div>@RenderBody()</div>
</body>
</html>

只要您明白,如果更改应用程序的 URL 架构,就必须更新它们,则显式 URL 是完全可以接受的。对于很多应用程序来说,这是您唯一需要考虑的问题。

但是,一些应用程序将部署到共享环境中,其中单个服务器支持通过向 URL 添加前缀而区分的多个应用程序。在清单25-25中,我更改了应用程序的配置,以便将请求管道设置为处理前缀为mvcapp的请求,模拟共享环境。

清单 25-25:Cities 文件夹下的 Startup.cs 文件,添加 URL 前缀

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Cities.Models;

namespace Cities
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IRepository, MemoryRepository>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.Map("/mvcapp", appBuilder => {
                appBuilder.UseStatusCodePages();
                appBuilder.UseDeveloperExceptionPage();
                appBuilder.UseStaticFiles();
                appBuilder.UseMvcWithDefaultRoute();
            });
        }
    }
}

Map方法允许使用不同的前缀设置多个请求管道。这在日常 MVC 开发中并不是一个特别有用的特性,因为您可以使用路由系统在 MVC 应用程序中创建 URL 前缀。但是对于本章,它是一个有用的特性,因为它意味着每个 URL 都是由客户端请求的,包括静态内容的请求。

通过启动应用程序并请求 /mvcapp URL,您可以看到出现的问题,它现在是应用程序的默认 URL,并针对 Home 控制器上的Index action。现在所有的 URL 都必须从 /mvcapp 开始,链接元素中样式表的显式 URL 不能工作,这意味着应用程序中的内容不能被样式化,如图25-7所示(您可能必须清除浏览器的缓存才能看到问题)。

图25-7 Bootstrap 发布文件

我可以通过更新为显式 URL 以包含前缀来解决这个问题,但这并不总有用,因为前缀在部署中可能会更改,或者在开发时可能不知道。一个更好的解决方案是使用一个应用程序相对 URL,其中静态内容的路径是相对于可能已经配置的任何前缀表示的,如清单25-26所示。

清单 25-26:Views/Shared 文件夹下的 _Layout.cshtml 文件,使用应用程序相对 URL

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="~/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <cache expires-sliding="@TimeSpan.FromSeconds(10)"
           vary-by="@ViewContext.RouteData.Values["action"]">
        @await Component.InvokeAsync("Time")
    </cache>
    <div>@RenderBody()</div>
</body>
</html>

波浪号由UrlResolutionTagHelper类检测,该类用到达 wwwroot 文件夹的内容所需的路径替换该波浪号。如果您运行应用程序,您将看到内容被样式化,并且检查发送到浏览器的 HTML,将显示链接元素包含mvcapp前缀的 URL。

<link href="/mvcapp/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />

UrlResolutionTag助手在多种元素中查找 URL,如表25-9所述。

提示:如果使用另一个内置标签助手从路由系统生成 URL,则它们生成的 HTML 将自动包含任何所需的前缀,该前缀来自HttpRequest.PathBase context 属性,其值由承载应用程序的服务器提供。

表 25-9:由 UrlResolutionTagHelper 转换的元素和属性

元素 属性
a href
applet archive
area href
audio src
base href
blockquote cite
button formaction
del cite
embed src
form action
html manifest
iframe src
img src,srcset
input src,formaction
ins cite
link href
menuitem icon
object archive,data
q cite
script src
source src,srcset
track src
video src,poster

总结

在本章中,我描述了与 HTML 表单无关的内置标签助手。这些标签助手帮助管理对 JavaScript 文件和 CSS 样式表的访问,为 anchor 元素创建 URL,对图像执行缓存破坏,缓存数据,以及转换应用程序相对 URL。在下一章中,我将介绍模型绑定系统,该系统用于处理 HTTP 请求中的数据,以便在 MVC 应用程序中使用。

;

© 2018 - IOT小分队文章发布系统 v0.3