面向 Asp.NET 开发者的 HTML5 编程教程(三)
六、使用历史 API 和自定义数据属性
每当您在 web 应用的不同页面之间导航时,浏览器都会维护您访问过的页面的历史记录。您可以使用浏览器的后退和前进按钮浏览历史记录。JavaScript 代码可以通过History对象访问相同的历史。虽然History对象并不是 HTML5 中的新增内容,但是它有一些值得了解的增强功能。特别是在 Ajax 驱动的应用中,新的历史 API 可以证明是非常有用的。Ajax 驱动的应用通常会更改网页内容,而不会为页面中呈现的每个不同内容生成唯一的 URL。这不仅会导致书签网址和实际内容之间的不匹配,还会使搜索引擎难以跟踪内容。与其他 HTML5 APIs 相比,History API 提供了一个小的对象模型,但是当 web 应用需要同步浏览器地址栏中显示的 URL 和页面内容时,所提供的功能通常是可取的。
在讨论了传统历史管理的问题以及 HTML5 如何解决它们之后,本章继续讨论另一个有用的主题:定制数据属性,或data-*属性。标准 HTML 属性是由 HTML 规范预先定义的,通常会以某种方式影响元素的行为或外观。自定义数据属性是开发人员定义的属性,可用于存储元素的元数据信息。然后可以使用客户端脚本访问这些信息。
本章讨论的关键主题包括以下内容:
Historyobject and its HTML5 enhancement function- Scenes where historical tracking may be problematic
- Add history entries using HTML5 history API
- Define custom data attributes on HTML elements (
data-*- Use plain JavaScript and jQuery to access custom data attributes
历史对象
每当您浏览 web 应用的页面时,浏览器都会以History对象的形式跟踪您的访问。你可以通过 DOM 窗口对象的history属性访问History对象。在 HTML 4.01 中,History对象只有一个属性和三个方法,如表 6-1 所述。
开发人员不经常使用History对象,因为用户通常使用浏览器的后退和前进按钮来浏览历史。然而,如果出于某种原因需要,表 6-1 中描述的属性和方法可用于以编程方式导航至历史条目。
为了演示History对象的用法,让我们在 ASP.NET MVC 中开发一个简单的幻灯片放映应用。幻灯片应用显示一些木工工具的图像及其描述,如图图 6-1 所示。
***图 6-1。*幻灯片放映应用的用户界面
在图 6-1 中可以看到,页面底部有四个按钮,排列成两排。最上面一行按钮(上一页和下一页)使页面被提交回服务器,然后根据单击的按钮显示上一张幻灯片或下一张幻灯片。底部一排按钮(<和>)分别使用History对象来向后或向前导航。幻灯片放映应用使用的 URL 模式如下:
[localhost:1065/home/index/2](http://localhost:1065/home/index/2)
在这个 URL 中,home是 MVC 控制器名,index是处理获取幻灯片的服务器端逻辑的动作方法,2是正在显示的幻灯片的 ID。对于不同的幻灯片,URL 末尾的 ID 是不同的。
注意您可能想知道为什么您要构建一个完整的数据库驱动的应用,只是为了说明
History对象的几个方法。您将从这个项目开始,演示在非 Ajax 应用中使用History对象;然后将它转移到 Ajax 来查看出现的问题,最后您会看到History对象的新功能如何解决这个问题。
幻灯片放映应用将其数据存储在名为ImageDb的 SQL Server 数据库中。ImageDb数据库包含一个单独的表——Images——存储图像信息,比如标题、描述和图像 URL。图 6-2 显示了Images表的实体框架数据模型。
***图 6-2。*实体框架数据模型为Images表
如图 6-2 所示,图像数据模型类有四个属性:Id、Title、Description和ImageUrl。Id列是Images表的主键。
应用的主Controller类(HomeController)只包含一个动作方法— Index()。Index()动作方法的框架如图清单 6-1 所示。为了简单起见,省略了从数据库中检索图像信息的代码。
清单 6-1。 Index()动作方法
public ActionResult Index(int id=0) { ImageDbEntities db = new ImageDbEntities(); IQueryable<Image> data = null; ... ... ** return View(data.SingleOrDefault());** }
Index()动作方法接受一个可选的整数参数,该参数表示一个图像 ID。如果提供了该参数,将显示具有该特定 ID 的图像;否则,显示来自Images表的第一幅图像。
“上一张”和“下一张”按钮将表单提交给服务器,并显示上一张或下一张幻灯片。为了浏览浏览器历史,您需要处理<和>按钮的click事件,并分别调用History对象的back()和forward()方法。清单 6-2 展示了这是如何实现的。
***清单 6-2。*调用back()和forward()方法
$(document).ready(function () { $("#btnBackward").click(function () { window.history.back(); }); $("#btnForward").click(function () { window.history.forward(); }); });
可以看到,btnBackward按钮的click事件处理程序调用了History对象的back()方法,而btnForward按钮的click事件处理程序调用了History对象的forward()方法。
要查看back()和forward()方法是如何工作的,运行应用并使用上一页和下一页按钮浏览幻灯片。然后尝试点击<和>。浏览器地址栏会根据单击的按钮反映相应的幻灯片 URL。您也可以使用浏览器的后退和前进导航按钮来验证行为。
了解历史跟踪问题
上一节讨论的History对象实际上是通过浏览器地址栏、超链接、导航菜单或代码来跟踪您访问的 URL。例如,每次在幻灯片放映应用中单击“上一个”或“下一个”按钮时,浏览器地址栏都会显示不同的 URL。这是可能的,因为您将整个表单提交给服务器。换句话说,每张幻灯片都有一个唯一的 URL。但是,可能会出现这样的情况,即页面内容和它的 URL 之间的一对一映射无法保持。考虑以下情况:
- You are developing an application based on Web forms, and the logic for displaying different slides is embedded in the event handlers of
clickprevious and next buttons on the server side. You use a web form to display all the slides. Whenever you click the "Previous Page" or "Next Page" button, aImagecontrol on the page will be updated with a new picture. Therefore, all slides have the same URL. In this case, there is no one-to-one match between the slide and URL. If Ajax technology is used, such as jquery$.Ajax()method or ASP.NET Ajax extension, the complete page will not be submitted to the server. Instead, you make an Ajax call to the server and retrieve the next or previous slide. In this case, there is no unique URL for each slide.
在这两种情况下,浏览器历史记录为页面中显示的所有幻灯片记录一个 URL。因为一个 URL 代表所有的幻灯片,所以用户不能为特定的幻灯片添加书签。用户可以为页面添加书签,认为正在为特定幻灯片添加书签;但是如果书签被访问,它总是显示第一张幻灯片,因为所有幻灯片都有相同的 URL。为了更清楚地理解这个问题,让我们将之前开发的幻灯片放映应用转换成 Ajax 驱动的应用,以便使用 jQuery $.Ajax()方法加载幻灯片,而无需刷新整个页面。
幻灯片放映应用的 Ajax 版本由一个名为AjaxHomeController的控制器组成,该控制器包含Index()动作方法,如清单 6-3 所示。
*清单 6-3。Index()``AjaxHomeController*的动作方法
public ActionResult Index() { ImageDbEntities db = new ImageDbEntities(); IQueryable<Image> data = null; ... return View(data.SingleOrDefault()); }
这一次,Index()动作方法不接受图像 ID 作为参数,因为最初呈现索引视图时只调用一次Index()。之后,通过 Ajax 调用获取各种幻灯片。Index()动作方法只是从Images表中获取第一张图像,并将其传递给索引视图。
现在,“上一步”和“下一步”按钮不会将表单提交给服务器。相反,单击这些按钮会触发对服务器的 Ajax 调用来获取和显示幻灯片。
要显示上一张或下一张幻灯片,需要处理上一张和下一张按钮的click事件处理程序。这些事件处理器如清单 6-4 中的所示。
***清单 6-4。*客户端click上一步和下一步按钮的事件处理程序
`("#prev").click(function () { .Ajax({ type: "POST", url: "/AjaxHome/GetImage", data:'{ id : "' + $("#id").val() + '", direction : "P"}', contentType: "application/json; charset=utf-8", dataType: "json", success: OnSuccess, error: OnError }) });
("#next").click(function () { .Ajax({ type: "POST", url: "/AjaxHome/GetImage", data: '{ id : "' + $("#id").val() + '", direction : "N"}', contentType: "application/json; charset=utf-8", dataType: "json", success: OnSuccess, error: OnError }) });`
上一个和下一个按钮的这些click事件处理程序使用 jQuery $.Ajax()方法来调用GetImage()动作方法。GetImage()方法需要两个参数:当前图像的 ID 和方向(N=下一个或P=上一个)。GetImage()然后返回一个代表上一张或下一张图像的Image对象。OnSuccess()函数简单地读取Image对象属性并相应地显示一张幻灯片。清单 6-5 显示了OnSuccess()功能。
清单 6-5 . OnSuccess()功能
function OnSuccess(image) { $("#id").val(image.Id); $("#title").html(image.Title); $("#desc").html(image.Description); $("#img").attr("src",image.ImageUrl); $("#divMsg").html("History Length : " + history.length); }
OnSuccess()接收从GetImage()动作方法返回的Image对象。然后将Title、ImageUrl和Description分配给适当的 HTML 元素。一个<div>元素还显示了History对象中条目的数量来证明这一点。
使用$.Ajax()调用调用的GetImage()动作方法的框架如清单 6-6 所示。
清单 6-6。 GetImage()动作方法
public JsonResult GetImage(int id,string direction) { ImageDbEntities db = new ImageDbEntities(); IQueryable<Image> data = null; ... return Json(data.SingleOrDefault()); }
GetImage()接受图像的 ID 和方向。然后,它在指定的方向上查找下一个或上一个图像。如果没有指定方向,它将获取一个具有指定 ID 的图像。GetImage()的返回类型为JsonResult。Json()方法将一个Image对象转换成它的 JSON 等价物,并将其返回给调用者。
现在,要理解本节开始时讨论的问题,请运行新的幻灯片放映应用,并使用“上一页”和“下一页”按钮浏览幻灯片。图 6-3 显示了浏览器中显示的幻灯片 2,但是地址栏仍然显示了基本 URL。
***图 6-3。*改变当前幻灯片不会改变地址栏中的网址。
因为您正在使用$.Ajax()获取幻灯片,所以浏览器地址栏不会改变 URL。完成后,尝试点击<和>按钮。即使你浏览了所有的幻灯片,你也不会前进或后退,因为浏览器在History对象中只有一个条目:[localhost:1065/Ajaxhome](http://localhost:1065/Ajaxhome)。
对浏览器书签的影响
前一节中描述的历史跟踪问题也对书签有不适当的影响。假设用户在幻灯片 3 上,该幻灯片显示了关于扳手的信息。用户可能希望稍后重新阅读相同的信息,因此向该页面添加书签,以为幻灯片 3 已经被书签标记。但是,因为不同的幻灯片没有唯一的 URL,所以浏览器会在基本 URL ( [localhost:1065/Ajaxhome](http://localhost:1065/Ajaxhome))中添加一个书签。下次用户访问该书签时,将显示第一张幻灯片,而不是第三张,因为服务器从数据库中为 URL 发送第一张幻灯片。
对搜索引擎列表的影响
幻灯片应用也有一个缺点,没有一个网站管理员希望他们的应用有这个缺点。因为显示的所有幻灯片只有一个 URL,所以搜索引擎只捕获和列出这个 URL。如果每张幻灯片都有自己唯一的 URL(就像您之前开发的非 Ajax 版本一样),那么搜索引擎就可以列出数据库中的所有幻灯片。为了允许用户搜索和找到唯一的信息,您需要为每条独立的信息提供一个唯一的 URL 在本例中,是每张单独的幻灯片。当前形式的幻灯片放映应用的 Ajax 版本违反了这一建议,影响了应用的搜索引擎列表。
解决方案
既然您已经理解了 Ajax 驱动的应用在跟踪历史、书签和搜索引擎列表方面所面临的问题,那么让我们来讨论一下可能的解决方案。前面讨论的所有问题领域的基本原因是没有唯一的 URL 来标识独立的内容片段。要解决这些问题,您需要为每条这样的信息创建唯一的 URL。就 Ajax 应用而言,有两种方法可以生成这样的 URL:
- Use the hash fragment in the URL
- Use the new HTML5 history API
在 URL 中使用散列片段
这种技术包括通过在基本 URL 的末尾添加一个散列片段来生成 URL。例如,考虑以下 URL:
[localhost/slideshow.aspx#slideid=3](http://localhost/slideshow.aspx#slideid=3)
在该地址中,使用#将幻灯片 ID 附加在基本 URL 之后。浏览器忽略#之后的所有信息,不向服务器发送单独的请求。但是,这仍然是一个唯一的 URL。您可以在#片段中追加任意数量的键值对,就像查询字符串一样;唯一的限制是查询字符串的大小限制。一旦您创建了一个散列片段的 URL,您就可以使用标准的导航技术导航到它(例如,单击一个超链接或以编程方式调用window.location.href)。然后,您需要编写 JavaScript 代码来读取散列片段数据,并相应地显示页面内容。
虽然散列片段的 URL 似乎解决了这个问题,但是它们也引入了自己的问题:
- With the increase of the amount of data you want to transfer, processing hash fragments often becomes too complicated. Some JavaScript libraries can help you with this work, but in general, the code will become tricky for the URL of hash fragments.
- The website becomes too complicated to remember easily. Although most web applications don't want you to remember the URLs of individual pages (they have navigation menus), in some cases, easy-to-remember URLs are very useful. For example, consider a social networking application such as Facebook. Facebook provides a shorter profile URL, which is easier to remember than the traditional profile URL with complex ID in the query string. If you use the URL of the hash fragment, your processing logic may be tied to the hash fragment parameter. If you add, remove or change these parameters, you may need to update the code because of these changes.
- Although hash fragment URLs are widely used in Ajax-driven web applications, search engines may treat them differently. For example, Google expects the hash fragmented URL data to start with
#!(a hash symbol followed by an exclamation point). Once you do this, your application is considered graspable.
注意ASP.NET Ajax 扩展使用散列片段技术为多个 Ajax 请求生成 URL。
ScriptManager服务器控件通过它的EnableHistory属性使你很容易生成和处理散列片段的 URL。
HTML5 历史 API
当需要为单个 Ajax 请求创建不同的 URL 时,HTML5 可以帮上忙。使用 HTML5 历史 API,您可以以编程方式向浏览器历史添加条目。这些以编程方式添加的 URL 条目不需要实际存在于服务器上。当您导航到通过历史 API 添加的条目时,浏览器会引发一个事件,让您有机会同步 URL 和页面内容。通过处理该事件,您可以从服务器获取相关内容并将其呈现在页面上。如果您使用 HTML5 历史 API,就没有必要使用散列片段的 URL——您可以避免所有相关的复杂性。如果用户将这样一个以编程方式添加的 URL 加入书签,并在以后导航到该 URL,则该请求将作为一个新的请求发送到服务器,您需要在服务器上处理它。
了解历史 API
在本章的前面,你已经了解了History对象(见表 6-1 )。HTML5 History API 本质上向传统的History对象添加了两个方法和一个事件。这些新方法和事件在表 6-2 中列出。
要了解如何使用新的历史 API,您可以修改之前开发的 Ajax 版本的幻灯片放映(MVC)应用。回想一下,幻灯片放映应用使用 jQuery $.Ajax()方法向服务器端GetImage() action 方法发出 Ajax 请求并检索幻灯片。还记得浏览器地址栏显示相同的基本应用 URL,即使向用户显示不同的幻灯片。为了便于理解,幻灯片放映应用的OnSuccess()功能在清单 6-7 中再次给出。
清单 6-7。 OnSuccess()没有 HTML5 历史 API 的函数
function OnSuccess(image) { $("#id").val(image.Id); $("#title").html(image.Title); $("#desc").html(image.Description); $("#img").attr("src",image.ImageUrl); $("#divMsg").html("History Length : " + history.length); }
注意为了您的方便,源代码下载包含了三个版本的幻灯片放映 MVC 应用。控制器名称分别为
HomeController、AjaxHomeController和HTML5HomeController。在尝试这个例子时,您可以修改AjaxHomeController及其索引视图,而不是从头开始。
要使用 HTML5 历史 API,你需要修改OnSuccess()函数,如清单 6-8 所示。
清单 6-8。 OnSuccess()功能使用pushState()方法添加一个历史条目
`function OnSuccess(image) { ("#id").val(image.Id); ("#title").html(image.Title); ("#desc").html(image.Description); ("#img").attr("src", image.ImageUrl);
** history.pushState(image, image.Title, "/HTML5Home/index/" + image.Id);** $("#divMsg").html("History Length : " + history.length); }`
OnSuccess()函数接收由GetImage()动作方法返回的 JSON 对象,该对象表示来自数据库的图像。在分配了图像细节,如Title、Description和ImageUrl之后,使用pushState()方法向History对象添加一个条目。pushState()的第一个参数是您希望在popstate事件处理程序中访问的状态信息。这里,代码将整个Image JSON 对象作为状态信息传递。第二个参数是分配给这个历史条目的标题。该标题通常显示在浏览器的历史菜单或对话框中。第三个参数是与这个历史条目相关联的 URL。浏览器只是将这个 URL 放在地址栏中,而没有导航到它。此外,浏览器不会检查 URL 是否真的存在于服务器上。由您来决定 URL 应该是什么。幻灯片放映应用使用的 URL 的形式是/index/<image_id>。稍后您修改Index()动作方法来处理传递的图像 ID。
接下来,您需要处理popstate事件,以便当您导航到一个历史条目时,您可以根据历史状态信息生成幻灯片。这样,您可以将当前幻灯片与地址栏中显示的 URL 同步。清单 6-9 展示了如何处理popstate事件。
***清单 6-9。*处理popstate事件
`$(document).ready(function () { if (!Modernizr.history) { alert("This browser doesn't support the HTML5 History API!"); } else { window.onpopstate = OnPopState; } ... });
function OnPopState(evt) { .Ajax({ type: "POST", url: "/HTML5Home/GetImage", data: '{ id : "' + evt.state.Id + '", direction : "" }', contentType: "application/json; charset=utf-8", dataType: "json", success: function (image) { ("#id").val(image.Id); ("#title").html(image.Title); ("#desc").html(image.Description); $("#img").src = image.ImageUrl; }, error: OnError }) }`
记住,popstate事件是为window对象引发的,因此您将一个事件处理函数OnPopState附加到窗口。请注意如何检测对历史 API 的支持。OnPopState函数使用$.Ajax()对GetImage()进行 Ajax 调用。回想一下,在调用pushState()方法时,您将Image JSON 对象指定为历史状态。可以使用evt参数的state属性来检索同一个Image JSON 对象。然后将要获取的图像的 ID 传递给GetImage()方法。在成功完成 Ajax 调用后,诸如Title、Description和ImageUrl之类的Image细节被分配给各个<form>元素。
最后,更改Index()动作来处理可选的图像 ID。清单 6-10 显示了Index()的修改版本。
清单 6-10。 Index()动作方法,现在负责可选的图像 ID
public ActionResult Index(int id=0) { ImageDbEntities db = new ImageDbEntities(); IQueryable<Image> data = null; if (id == 0) { data = (from item in db.Images orderby item.Id ascending select item).Take(1); } else { data = from item in db.Images where item.Id == id select item; } return View(data.SingleOrDefault()); }
Index()动作方法现在检查是否传递了一个图像 ID。如果是,它返回一个具有指定 ID 的图像;否则,返回数据库中的第一幅图像。
图 6-4 显示了修改后的幻灯片放映应用的运行示例。请注意浏览器地址栏是如何为每张幻灯片显示唯一的 URL 的。
图 6-4。 HTML5 pushState()方法改变地址栏中的网址
浏览器的历史菜单或对话框也列出了通过pushState()方法添加的 URL,如图图 6-5 所示。
***图 6-5。*浏览器的历史对话框显示通过pushState()方法添加的网址
注意即使提到了,也不是所有的浏览器都使用
pushState()方法的title参数。例如,图 6-5 中的历史对话框显示所有 URL 的标题为“幻灯片-锤子”,即使它们的位置不同。未来版本的浏览器可能会使用此参数。
浏览器支持
除了 Internet Explorer 9,几乎所有主流浏览器都支持 HTML5 历史 API。与 HTML5 的任何其他特性一样,使用 Modernizr 检查浏览器是否支持历史 API 总是一个好的做法。幻灯片放映应用包括该检查,如清单 6-11 中的所示。
***清单 6-11。*使用 Modernizr 检查对历史 API 的支持
if (!Modernizr.history) { alert("This browser doesn't support HTML5 History API!"); }
如果一个浏览器不支持 HTML5 历史 API,你的应用可能还能工作,但是它不会在历史中添加任何条目;它也不会改变地址栏。如果您希望提供某种回退,您可以求助于以下两个选项之一:
像在第一个非 Ajax 版本的幻灯片放映应用中一样,使用全页面刷新。* Use the hash fragment URL technique discussed earlier.
自定义数据属性
HTML 标记元素使用属性来指定配置信息。例如,<canvas>元素的height和width属性决定了画布在浏览器中显示时的高度和宽度。HTML 元素的大多数属性都会影响用户界面或元素的视觉显示。
在开发 web 应用时,开发人员经常发现有必要向客户端浏览器发出有关元素的元数据。这样的元数据不会直接影响元素的显示。然而,元数据保存了与该元素相关的信息。
考虑一个使用定制 JavaScript 代码验证 HTML 表单的例子。当 JavaScript 代码验证表单字段时,如果任何字段包含无效值,就会显示验证错误。通常,错误消息文本直接嵌入在 JavaScript 代码中。但是,如果在部署之后要更改错误消息文本,该怎么办呢?将错误消息文本直接嵌入脚本需要编辑脚本。这相当于改变了应用的代码库。在这种情况下,如果错误消息文本与网页分开存储(比如说,存储在数据库表中)会很有帮助。在运行时,可以从数据库中检索错误消息,并发送给客户端。这些发出的消息构成了 HTML 元素的元数据。如果需要对错误消息进行任何更改,您可以更改数据库条目,应用将在下次运行时开始使用新值。
在 HTML5 之前,没有处理这种元数据信息的标准。HTML5 引入了自定义数据属性,可用于定义元素的元数据。
自定义数据属性概述
HTML5 自定义数据属性是采用以下形式的特殊属性:
data-<name>="value"
自定义数据属性总是以data-开头,后跟开发人员定义的名称。例如,对于前面讨论的定制验证场景,您可以创建一个名为data-errormessage的定制数据属性。
开发人员定义的名称可以包含连字符(-)。例如,data-customer-id和data-customer-name是有效的自定义数据属性。自定义数据属性也称为data-*属性,因为它们遵循命名约定。一个data-*属性可以像任何其他 HTML 属性一样被赋值。ASP.NET 广泛使用data-*属性在 web 表单和 MVC 应用中提供无障碍验证。
注意 无阻碍验证是 ASP.NET 实现的一种技术,使用数据注释属性和 jQuery。在这种模式下,数据模型用数据注释属性来修饰,这些属性对数据模型属性值执行验证。在运行时,ASP.NET 基于这些数据注释属性发出
data-*属性。客户端验证是在这些data-*属性和 jQuery 的帮助下执行的。
与标准的 HTML 属性不同,data-*属性不会影响元素的视觉外观。事实上,浏览器不会出于任何目的自动使用它们。您需要以编程方式访问它们,并对它们执行预期的逻辑。一个 HTML 元素可以定义任意数量的data-*属性。
尽管您可以为任何定制需求使用data-*属性,但是如果使用标准 HTML 属性可以达到同样的目的,您应该避免使用它们。例如,假设您希望在用户将鼠标指针悬停在某个元素上时显示该元素的工具提示。工具提示根据编程条件而变化。在这种情况下,最好使用 HTML title属性,而不是创建一个新的data-*属性。
清单 6-12 显示了一些使用自定义数据属性的 HTML 标记示例。
***清单 6-12。*使用自定义数据属性
`
...| Nancy | Davolio |
| Andrew | Fuller |
这个清单显示了一个包含雇员数据的 HTML 表。每个表格行包含雇员的姓名,另外还有两个自定义数据属性:data-employeeid and data-title。data-employeeid属性存储员工的EmployeeID,data-title属性存储员工的职位。
使用 JavaScript 访问自定义数据属性
现在您已经知道了什么是定制数据属性,让我们看看如何在 JavaScript 代码中访问它们。考虑一下清单 6-13 中显示的 JavaScript 代码,它说明了如何使用 JavaScript 代码访问清单 6-12 中使用的data-*属性。
***清单 6-13。*访问 JavaScript 代码中的data-*属性
`var emp = document.getElementById('emp1'); alert('Employee ID : ' + emp.getAttribute('data-employeeid')); alert('Title : ' + emp.getAttribute('data-title'));
emp.setAttribute('data-employeeid', '100'); emp.setAttribute('data-title', 'Senior Manager'); alert('New Employee ID : ' + emp.getAttribute('data-employeeid')); alert('New Title : ' + emp.getAttribute('data-title'));`
这段代码使用 DOM 文档的getElementById()方法获取 ID 为emp1的表格行。然后,它使用表 DOM 元素的getAttribute()方法检索data-employeeid和data-title自定义数据属性的值。这些值显示在一个警告框中。此外,代码使用setAttribute()方法更改data-employeeid和data-title的值。更改后的值也会显示在警告框中。
尽管从清单 6-13 中获取和设置data-*属性的技术像预期的那样工作,但是 HTML5 提供了一种专门为访问data-*属性而设计的技术。HTML5 data-*属性通过底层 DOM 元素的dataset属性对您的代码可用。清单 6-14 展示了如何使用dataset属性来访问data-*属性。
***清单 6-14。*使用dataset属性访问data-*属性
var emp = document.getElementById('emp1'); alert('Employee ID : ' + emp.dataset.employeeid); alert('Title : ' + emp.dataset.title); emp.dataset.employeeid = '200'; emp.dataset.title = 'Junior Manager'; alert('New Employee ID : ' + emp.dataset.employeeid); alert('New Title : ' + emp.dataset.title);
要检索一个data-*属性值,只需对dataset属性使用不带data-的属性名。类似地,要设置一个data-*属性值,您可以针对dataset属性使用它的名称,并给它赋值。
如果自定义数据属性包含连字符(-),您可以使用骆驼大小写来访问该属性。例如,要访问data-employee-birthdate属性,可以使用下面的代码:
alert('Employee ID : ' + emp.dataset.employeeBirthdate);
可以看到,属性名包含了employee-birthdate。但是当您使用dataset属性访问它时,它被称为employeeBirthdate。
使用 jQuery 访问自定义数据属性
jQuery 库还通过提供专门处理data-*属性的方法来支持访问定制数据属性。清单 6-15 展示了如何使用 jQuery 来检索和分配data-*属性值。
***清单 6-15。*使用 jQuery 访问data-*属性
`alert('Employee ID : ' + ("#emp1").data('employeeid')); alert('Title : ' + ("#emp1").data('title'));
("#emp1").data('employeeid', '100'); ("#emp1").data('title', 'Senior Manager');
alert('Employee ID : ' + ("#emp1").data('employeeid')); alert('Title : ' + ("#emp1").data('title'));`
jQuery 库提供了一个data()方法来访问定制的数据属性。要检索一个data-*属性,您可以在下面的 DOM 元素上调用data(),并传递不带data-的属性名。例如,清单 6-15 使用data('employeeid')来检索data-employeeid属性的值。为了给一个data-*属性赋值,您调用data()方法并提供不带data-的属性名及其新值。如果自定义数据属性在名称中包含连字符(-)(例如data-employee-birthdate,您可以使用相同的 camel-case 语法来访问它,如下所示:
alert('Title : ' + $("#emp1").data('employeeBirthdate'));
如果不带任何参数调用data(),它将返回一个包含该元素的所有data-*属性及其值的键值对对象。清单 6-16 展示了如何使用这种data()方法的变体。
***清单 6-16。*使用data()方法获取data-*属性作为对象
var obj = $("#emp1").data(); alert('Employee ID : ' + obj.employeeid); alert('Title : ' + obj.title); alert('Birth Date : ' + obj.employeeBirthdate);
如您所见,调用data()方法时没有任何参数。这样做将返回一个具有键值对的对象。要读取特定的data-*属性值,可以对返回的对象使用不带data-的data-*属性名。
注意您还可以使用 jQuery
attr()方法来获取或设置data-*属性。然而,最好使用data(),因为它是专门为处理定制数据属性而设计的。
使用自定义数据属性发出验证消息
现在您已经知道了如何使用定制数据属性,让我们开发一个 Web 表单应用,演示如何借助data-*属性实现本节开始时讨论的验证场景。在本节中,你开发一个如图图 6-6 所示的 web 表单。
***图 6-6。*员工列表网页表单
web 表单显示了来自Northwind数据库的Employees表中的雇员姓名以及他们的BirthDate和HireDate值。用户可以选择相应的日期,然后单击保存按钮。单击“保存”会触发验证逻辑,该逻辑会检查员工在雇用时是否至少年满 18 岁。否则,将在警告框中向用户显示一条错误消息。验证消息没有嵌入代码的任何地方;相反,它存储在一个名为ErrorMessages的数据库表中。ErrorMessages表结构简单,如其实体框架数据模型所示(图 6-7 )。
图 6-7。ErrorMessages表的数据模型
如您所见,ErrorMessages表包含三列:Id、ErrorCode和ErrorMessageText。ErrorCode列包含错误的简短代码(如INVALIDDATE),ErrorMessageText列包含描述性错误消息(例如,“雇佣日期必须晚于出生日期”)。清单 6-17 显示了呈现雇员列表的Repeater服务器控件的ItemTemplate的标记。
***清单 6-17。*显示员工列表的 Web 表单标记
<ItemTemplate> <tr id='<%# Eval("EmployeeID","emp{0}") %>' data-employeeid='<%# Eval("EmployeeID") %>'> <td><%# Eval("FirstName") %> <%# Eval("LastName") %></td> <td><asp:TextBox ID="txtBirthDate" runat="server" TextMode="Date" Text='<%# Eval("BirthDate") %>' data-error-invaliddate='<%# GetValidationMessage("INVALIDDATE") %>'> </asp:TextBox></td> <td><asp:TextBox ID="txtHireDate" runat="server" TextMode="Date" Text='<%# Eval("HireDate") %>' data-error-invaliddate='<%# GetValidationMessage("INVALIDDATE") %>'> </asp:TextBox></td> <td><input type="button" value="Save"/></td> </tr> </ItemTemplate>
这个标记由两个分别显示BirthDate和HireDate列值的TextBox服务器控件组成。这些文本框的TextMode属性被设置为Date,以便显示一个日期选择器来选择日期。每个文本框都有一个名为data-error-invaliddate的自定义数据属性。这个属性的值来自于GetValidationMessage()函数。GetValidationMessage()根据传递的ErrorCode从ErrorMessages台拾取ErrorMessageText:
`public string GetValidationMessage(string errorCode) { ValidationDbEntities db = new ValidationDbEntities();
var data = from item in db.ErrorMessages where item.ErrorCode == errorCode select item.ErrorMessageText; return data.SingleOrDefault(); }`
执行验证并显示验证错误(如果有的话)的 jQuery 代码驻留在 Save 按钮的click事件处理程序中。该代码如清单 6-18 中的所示。
***清单 6-18。*验证员工的年龄
`("input[value='Save']").click(function () { var birthDateTxtbox = (this).closest('tr').children().eq(1).children().eq(0); var hireDateTxtbox = (birthDateTxtbox).val()); var hireDate = ToDate($(hireDateTxtbox).val()); birthDate.setFullYear(birthDate.getFullYear() + 18);
if ((hireDate.getTime() - birthDate.getTime()) < 0) { alert($(birthDateTxtbox).data('errorInvaliddate'));
return;
}
//make $.Ajax() request to update the database alert('Data saved!'); });`
保存按钮的click事件处理程序首先获取对BirthDate和HireDate输入字段的引用。closest()方法返回保存输入字段和保存按钮的行。children()方法返回表格行的所有子元素。然后,eq()方法返回指定索引处的表格单元格。因为输入字段在表列中,所以需要再次调用children()和eq()方法来获取相应的输入字段。
输入字段保存 yyyy-MM-dd 格式的日期值。使用定制函数ToDate()将这些日期转换成 JavaScript date对象。ToDate()函数看起来像这样:
function ToDate(input) { var parts = input.match(/(\d+)/g); return new Date(parts[0], parts[1] - 1, parts[2]); }
ToDate()应该很熟悉,因为你在第五章里用过。它主要解析提供的输入值,并返回一个表示该值的 JavaScript date对象。
接下来,代码检查HireDate和BirthDate之间的差异,以确定雇员在被雇用时是否至少 18 岁。否则,在data-error-invaliddate自定义数据属性中指定的错误信息将在警告框中显示给用户。jQuery data()方法用于检索data-error-invaliddate属性的值。
虽然代码实际上没有将更改的数据保存到服务器,但是如果年龄验证成功,您可以添加该功能。在清单 6-18 中,年龄检查成功后会显示一条成功消息。
总结
HTML5 History API 是一个相对较小的改进领域,但它在某些情况下会派上用场。特别是,现代 web 应用广泛依赖 Ajax 技术来呈现页面内容,而无需刷新整个页面。同时,他们需要关注搜索引擎优化和用户体验。新的 HTML5 历史 API 允许您以编程方式在历史中添加条目。这样,您也可以更改浏览器地址栏中显示的 URL。当您在历史中导航时,历史 API 让您有机会将页面内容与导航到的 URL 同步。HTML5 历史 API 不仅提供了处理历史条目的标准方法,还消除了实现中的复杂性。
自定义数据属性(data-*)允许您嵌入关于 HTML 元素的元数据。它们不会被浏览器处理,也不会直接影响元素的行为。您可以使用 DOM 元素的dataset属性或 jQuery data()方法来访问data-*属性。
虽然 History API 和 custom data 属性允许您分别处理会话历史和元数据,但是也经常需要存储和检索会话数据。传统上,cookies 被用来在客户端存储这样的数据。下一章考察了所谓的 web 存储——在客户端本地存储和检索数据的能力。
七、在 Web 存储器中存储数据
今天开发的大多数网站都以这样或那样的形式处理数据。自然,这些应用数据需要某种存储机制。就服务器而言,有 SQL Server 等成熟的数据库引擎。然而,在客户端存储数据可能很棘手。传统上,开发人员使用 cookie 在客户端保存数据,但是 cookie 有其自身的局限性。为了在客户端提供简化的数据存储机制,HTML5 提供了 web 存储。本章探讨了什么是 web 存储以及可以使用它的情况。具体来说,您将了解以下内容:
- What is web storage?
- Style of web storage
- Store items in the web store, retrieve items from it, and delete items from the web store.
- Storing non-string data in web storage
- Transfer data from the web store to the server for further processing.
网络存储概述
术语 web 存储指的是 HTML5 的客户端数据存储机制。Web 存储允许您在客户端将数据存储为键值对。W3C 建议每个源的 web 存储大小限制为 5MB(参见“Web 存储的安全性考虑”一节以了解更多关于源的信息)。但是,个别浏览器可能会稍微偏离这一限制。例如,IE8 允许高达 10MB 的网络存储。
虽然 web 存储和 cookies 都在客户端存储数据,但它们的工作方式不同。对于来自给定网站的每个请求,Cookies 都在客户机和服务器之间传递。另一方面,web 存储永远不会自动传递给服务器。如果您需要将数据从 web 存储传输到服务器端代码,您必须求助于编程方法,比如 jQuery 调用服务器端代码或隐藏的表单字段。此外,与 cookies 不同,您不能为 web 存储设置过期时间。您要么需要编写代码来删除陈旧的项目,要么依靠用户使用浏览器中的选项来删除陈旧的项目。
Web 存储有两种风格:会话存储和本地存储。这两种类型分别作为window对象的sessionStorage和localStorage属性公开。正如您可能已经猜到的那样,只要当前浏览器(或其选项卡)实例正在运行,会话存储就会一直存在。当您关闭浏览器实例(或选项卡)时,数据将被删除。如果您稍后再次加载网站,它将无法访问任何先前存储的数据。会话存储适合于单个事务。与会话存储不同,本地存储跨浏览器的多个实例存储数据,也存储当前会话之外的数据。
总之,在以下情况下,web 存储是不错的选择
- You need to store data that exceeds the cookie-based storage size limit.
- You don't need to pass data back and forth to the server every time you request.
- You don't need to set any specific expiration time for data.
但是,在以下情况下,web 存储可能不是一个好的选择
- You want to store a large amount of data.
- Your data is not easily stored as key-value pairs (such as binary data or BLOBS).
- The data to be stored is sensitive.
会话存储和本地存储对象
如前所述,sessionStorage和localStorage对象将数据存储为键值对。这两个对象具有相似的属性和方法。表 7-1 列出了它们,供您快速参考。
现在您对会话存储和本地存储有了一些了解,让我们在 Web 窗体应用中尝试这些属性和方法。
注意您也可以使用普通的 HTML 页面测试
sessionStorage和localStorage对象,但是在这种情况下,您必须在互联网信息服务(IIS)中托管它们。除非您的页面是网站的一部分,否则浏览器无法确定它们的原始域,因此无法为它们分配存储空间。您将在本章的后面了解这些安全限制。
使用 sessionStorage 和 localStorage 对象
在本节中,您将创建如图 7-1 所示的简单 web 表单。
***图 7-1。*使用localStorage对象的简单 web 表单
web 表单允许您在localStorage对象中存储键值对。它由两个文本框组成,用户可以分别在其中输入键和值。点击存储数据按钮,将一个键及其值存储在localStorage对象中。底部表格中显示了所有键及其值的列表。点击清除数据按钮从localStorage中删除所有条目。清单 7-1 展示了两个按钮的click事件处理程序如何处理localStorage对象。
***清单 7-1。*使用localStorage对象
`var storage = window.localStorage;
(document).ready(function () { if (!Modernizr.localstorage) { alert("This browser doesn't support HTML5 Local Storage!"); } ("#store").click(OnStoreClick); ("#clear").click(OnClearClick); });` `function OnStoreClick(event) { var key = ("#keyName").val(); var value = ("#keyValue").val(); storage.setItem(key, value); ("#tblItems").empty(); for (var i = 0; i < storage.length; i++) { $("#tblItems").append("" + storage.key(i) + " = " + storage.getItem(storage.key(i)) + ""); } }
function OnClearClick(event) { storage.clear(); $("#tblItems").empty(); }`
这段代码首先在变量storage中存储对localStorage对象的引用。这样,您就不需要总是使用window.localStorage语法来调用localStorage对象的方法。另外,如果你决定测试sessionStorage而不是localStorage,你可以只修改这一行代码,其余的代码会自动使用sessionStorage对象。注意如何使用 Modernizr 检查对localStorage的支持。如果您希望检查对sessionStorage的支持,您可以使用Modernizr.sessionstorage属性。
jQuery ready事件处理程序用事件处理函数OnStoreClick()和OnClearClick()将两个按钮(存储数据和清除数据)的click事件连接起来。
OnStoreClick()事件处理函数使用localStorage对象的setItem()方法在storage对象中存储一个键值对。然后它遍历所有的键。在每次迭代中,代码使用key()和getItem()来检索一个键及其值。然后,键值对被添加到tblItems表中。
OnClearClick()事件处理函数使用clear()方法简单地从存储中删除所有的项目。
存储数字、日期和对象
虽然localStorage和sessionStorage对象允许您存储数据,但是就存储的项目的数据类型而言,它们有一个限制:所有数据都存储为字符串。即使添加数值或日期数据类型的值,它们仍会存储为普通字符串。这一点很重要,因为当您读回数据时,可能需要将其转换为适当的数据类型。在更复杂的情况下,您可能希望将对象存储在 web 存储中。显然,因为 web 存储本身只支持存储字符串数据,所以这些类型的转换是您的责任。
考虑清单 7-2 中的代码片段。
***清单 7-2。*将数字存储在localStorage
$(document).ready(function () { var storage = window.localStorage; storage["number1"] = 10; storage["number2"] = 20; var sum1 = storage["number1"] + storage["number2"]; var sum2 = Number(storage["number1"]) + Number(storage["number2"]); alert("Without conversion Sum = " + sum1 + "\r\n" + "With conversion Sum = " + sum2); });
正如你所看到的,两个数字分别用键number1和number2存储在localStorage中。注意,这次代码没有使用setItem()和getItem(),而是使用熟悉的字典访问语法来存储条目。sum1变量存储这两个键的值的和而不执行任何转换,而sum2通过首先使用Number()函数将它们转换成数字来存储它们的和。一个警告框显示sum1和sum2的值,如图 7-2 所示。
***图 7-2。*转换后添加数值
图 7-2 确认了localStorage以纯文本格式存储数据,在进一步处理之前,您需要将其转换为合适的数据格式。
存储日期值类似于存储数字,因为您需要将日期字符串转换成 JavaScript Date对象。清单 7-3 展示了如何做到这一点。
***清单 7-3。*将日期存储在localStorage
$(document).ready(function () { var storage = window.localStorage; storage["date"] = new Date(2012,5,15); var date1 = storage["date"]; try{ alert("Without conversion Year = " + date1.getFullYear()); } catch(e){ alert("Data is not of date type!"); } var date2 = new Date(storage["date"])); try { alert("With conversion Year = " + date2.getFullYear()); } catch (e) { alert("Data is not of date type!"); } });
该代码在localStorage中存储一个日期。然后,它尝试访问日期中的年份部分,而不执行任何转换。这将导致错误,如警告框所示。第二次尝试将存储的字符串解析成一个Date对象,然后输出它的年份。你可能已经猜到了,第二次尝试给出了正确的结果。
在清单 7-3 中,日期使用默认的 JavaScript 格式存储在localStorage对象中(例如,2012 年 3 月 1 日 00:00:00 GMT+0530[印度标准时间])。在许多情况下,您可能使用输入字段作为日期选择器(您可以在中通过将type属性设置为 date 来实现)。日期选择器以 yyyy-MM-dd 格式返回日期。这种格式直接存储在localStorage中也是安全的,因为以后可以很容易地解析回 JavaScript Date对象。
存储对象起初听起来很复杂,但幸运的是大多数浏览器都支持一种简便的方法将对象转换成字符串,反之亦然。就 JavaScript 而言,对象以 JSON 格式表示;使用JSON.stringify()和JSON.parse()方法,您可以轻松地将 JSON 对象转换成字符串,并将其字符串表示解析回对象。清单 7-4 展示了如何进行这种转换。
***清单 7-4。*转换 JSON 对象
$(document).ready(function () { var storage = window.localStorage; var object1 = { "Name": "Tom", "Age": 50 }; storage["object"] = JSON.stringify(object1); var object2 = JSON.parse(storage["object"]); alert(object2.Name + " (" + object2.Age + " years)"); });
这段代码用两个属性定义了一个 JSON 对象:Name和Age。在将 JSON 对象存储到localStorage时,JSON.stringify()方法将 JSON 对象转换成它的字符串表示。从localStorage中检索数据时,JSON.parse()方法重建对象。然后警告框正确输出Name和Age属性(图 7-3 )。
***图 7-3。*处理 JSON 对象
本例中使用的 JSON 对象包含了值汤姆的Name和值 50 的Age。产生的警告框向用户显示这些值。
会话存储和本地存储事件
sessionStorage和localStorage对象支持一个storage事件,每当底层存储区域改变时就会引发该事件。在处理这个事件时,你应该注意两件事。首先,在window对象上引发了storage事件。其次,对于除 IE 之外的大多数浏览器来说,除了改变了storage对象的浏览器之外,每个浏览器实例(或标签)都会触发storage事件。在 IE 中,浏览器的所有实例(或标签)都会引发storage事件。因此,如果Example1.aspx被加载到三个标签Tab1、Tab2和Tab3中,并且Tab1改变网络存储,Tab2和Tab3接收storage事件。在 IE 中,Tab2和Tab3以及Tab1接收storage事件。
storage事件处理器接收事件信息作为一个StorageEvent对象。StorageEvent的特性见表 7-2 。
为了检查storage事件是如何工作的,修改图 7-1 中的例子,如清单 7-5 中的所示。
***清单 7-5。*处理storage事件
`$(document).ready(function () { ... window.addEventListener('storage', OnStorage, false); ... });
function OnStorage (event) { alert("Storage event fired for key : " + event.key + " in page " + event.url); alert("Old Value - New Value : " + event.oldValue + " - " + event.newValue); }`
注意storage事件处理程序是如何使用window对象的addEventListener()方法附加的。OnStorage()函数使用事件参数的各种属性,并将它们显示在一个消息框中。为了测试storage事件,在两个 Firefox 选项卡中打开同一个 web 表单。切换到第一个选项卡,并添加一个键。您应该会在另一个选项卡上看到一个警告框,如图 7-4 中的所示。
如您所见,向第一个选项卡添加一个键会通知用户第二个选项卡已经引发了storage事件,键名为key1。
***图 7-4。*添加一个键时会引发storage事件。
手动清除 Web 存储器
如果您将键值对添加到localStorage中,然后关闭浏览器而不清除存储区域,数据会保留在磁盘上。如前所述,与 cookies 不同,您不能为本地存储设置特定的到期日期和时间。清除localStorage数据的一种方法是以编程方式为每一项调用removeItem()方法或调用clear()方法。或者,您可以使用浏览器对话框手动删除localStorage数据。例如,图 7-5 显示了 Firefox 中的清除所有历史对话框。
***图 7-5。*手动清除本地存储
确保选中 Cookies 复选框并点击 Clear Now 按钮。除了 cookies,这还会删除localStorage数据。
将数据从网络存储器传送到服务器
与 cookies 不同,web 存储数据不会随着每个请求在客户机和服务器之间传递。如果您希望将 web 存储数据发送到服务器,您必须设计一种编程方式来实现。完成此任务的一些选项如下:
- Hide form fields: In this method, the data is first stored in the
localStorageorsessionStorageobject as usual. When submitting the form, you transfer the web stored data to a hidden form field, and then submit the form. Then, the server-side code can read this hidden field and further process the data.- Ajax call: With this method, you send an Ajax call to the server and pass the web storage data to the server. Then, the server-side code further processes the data. In a Web forms application, you can make Ajax calls to web methods, web services or Windows Communication Foundation (WCF) services. In ASP.NET MVC application, an action method can be called by Ajax.
在下一节讨论的例子中,您使用 jQuery 来调用控制器动作方法。
在调查表格中使用本地存储的示例
这一节给出了一个比上一节更真实的例子,它使用了到目前为止讨论过的关于localStorage的所有信息。您开发了一个简单的调查表来收集用户反馈。您可能已经意识到,填写调查问卷可能不是最终用户在使用您的网站时的首要任务。他们可能会开始填写调查表,然后转到他们更感兴趣的网站的其他部分。他们甚至可能会关闭浏览器,稍后再回到您的网站。在这种情况下,当用户输入调查数据时,最好将数据保存在本地。稍后,当用户回来时,您可以重新加载持久化的数据并节省用户时间。调查应用使用 ASP.NET MVC 开发,其主视图如图 7-6 所示。
***图 7-6。*提交给用户的调查表
如您所见,调查表由两部分组成:用户信息和调查问题。用户信息部分捕获用户的详细信息,如名字、姓氏和电子邮件。调查问题部分显示调查问题及其答案选项的列表。调查问题和选项都是从 SQL Server 数据库中提取的(SurveyDb)。当用户开始填写调查表时,他们的输入被存储在本地存储器中。当他们完成调查并单击 Submit Answers 按钮时,用户详细信息和调查答案将使用 jQuery $.ajax()方法发送到服务器,并存储在数据库中。
SurveyDb数据库包含四个表:Questions、Choices、Results和Users。它们的用途在表 7-3 中列出。
实际的数据访问通过实体框架数据模型进行,如图 7-7 所示。
***图 7-7。*实体框架数据模型为 SurveyDb 数据库表
要从客户端访问驻留在SurveyDb数据库中的数据,可以使用 jQuery 代码调用某些动作方法。在控制器的所有三个动作方法中(HomeController)都是从 jQuery 代码中调用的。他们是GetQuestions()、GetChoices()、SaveResults()。当 jQuery 代码使用这些操作方法时,我们会对它们进行讨论。
当视图加载到浏览器中时,浏览器中会显示所有问题及其选项的列表。这个任务是在 jQuery ready()方法中完成的。ready()还为按钮和文本框连接事件处理程序,如清单 7-6 所示。
清单 7-6。 ready()事件处理程序
var storage = window.localStorage; $(document).ready(function () { if (!Modernizr.localstorage) { alert("This browser doesn't support HTML5 Local Storage!"); } $("#submit").click(SubmitData); $("#firstName").change(function () { storage["FirstName"] = $(this).val(); }); $("#lastName").change(function () { storage["LastName"] = $(this).val(); }); $("#email").change(function () { storage["Email"] = $(this).val(); }); $("#firstName").val(storage["FirstName"]); $("#lastName").val(storage["LastName"]); $("#email").val(storage["Email"]); GetQuestions(); })
为了方便起见,对localStorage对象的引用存储在一个全局变量storage中。提交答案按钮的click事件连接到SubmitData()函数。firstName、lastName和email文本框的change事件处理程序也被连接。这些change事件处理程序使用val()方法检索文本框值,并将其保存在localStorage中。这样,一旦用户开始在这些文本框中输入,他们的值就会自动保存在localStorage中。如果用户再次访问调查表,并且已经存储了firstName、lastName和email值,则从localStorage中检索这些值并填入文本框。
然后调用GetQuestions()函数来获取调查问题。GetQuestions()见清单 7-7 。
清单 7-7。 GetQuestions()客户端功能
function GetQuestions() { $.ajax({ type: "POST", url: "/Home/GetQuestions", dataType: "json", contentType: "application/json; charset=utf-8", success: function (results) { for (var i = 0; i < results.length; i++) { $("#container").append("<div data-questions-questionid='" + results[i].QuestionID + "'>" + results[i].QuestionText + "</div>"); $("div[data-questions-questionid]").addClass("paddedDiv"); } GetChoices(); }, error: function (err) { alert(err.status + " - " + err.statusText); } }) }
GetQuestions()函数使用$.ajax()调用GetQuestions()动作方法。GetQuestions()动作方法返回一个问题项数组。success处理函数遍历这个数组,每次迭代,一个<div>被动态添加到容器中。GetQuestions()动作方法如清单 7-8 所示。
清单 7-8。 GetQuestions()动作方法
public JsonResult GetQuestions() { SurveyDbEntities db = new SurveyDbEntities(); var data = from item in db.Questions select item; return Json(data.ToArray()); }
GetQuestions()动作方法从Questions表中选择所有的问题项,并将它们作为数组返回。因为要在 jQuery 代码中访问返回值,所以使用Json()方法将其转换为 JSON 格式,然后作为JsonResult返回。
注意清单 7-7 中属性的使用。正如在第六章中所讨论的那样,data-*属性不同于标准的 HTML 属性,因为它们是由开发人员定义的,不会以任何方式直接影响元素。所有的data-*属性都以前缀data-开头,后面是开发人员定义的属性名。一个元素可以有任意数量的data-*属性,您可以使用 jQuery 代码以编程方式访问这些属性。
动态生成的<div>元素定义了一个名为data-questions-questionid的data-*属性。该属性存储来自Questions表的问题的QuestionID。一个示例动态生成的<div>元素如下所示:
<div data-questions-questionid='1'>Which programming language do you use?</div>
然后,success处理函数调用GetChoices()函数来填充每个问题的选项。GetChoices()功能如清单 7-9 中的所示。
清单 7-9。 GetChoices()客户端功能
function GetChoices() { $.ajax({ type: "POST", url: "/Home/GetChoices", contentType: "application/json; charset=utf-8", dataType: "json", success: function(results){ for (var i = 0; i < results.length; i++) { $("div[data-questions-questionid='" + results[i].QuestionID + "']").append( "<br /><input type='checkbox' data-choices-questionid='" + results[i].QuestionID + "' data-choices-choiceid='" + results[i].ChoiceID + "'/><span>" + results[i].ChoiceText + "</span>"); if (storage[results[i].ChoiceID] != null) { var choiceId = results[i].ChoiceID; $("input[data-choices-choiceid='" + choiceId + "']").attr('checked', 'checked'); } } $("input[data-choices-questionid]").change(function (event) { var key = $(event.target).attr("data-choices-choiceid"); if ($(event.target).is(':checked') == true) { storage[key] = $(event.target).attr("data-choices-questionid"); } else { storage.removeItem(key); } }); }, error: function (err) { alert(err.status + " - " + err.statusText); } }) }
GetChoices()函数使用$.ajax()方法调用GetChoices()动作方法。GetChoices()动作方法返回一个选项数组。success处理函数遍历返回的所有选项项,每次迭代都通过动态生成复选框向问题添加一个选项。您需要将属于一个问题的所有选项添加到该问题的<div>元素中。请注意 jQuery 选择器选择了一个data-questions-questionid属性等于QuestionID的<div>。这样,选择的复选框被添加到显示他们问题的<div>中。还要注意代码如何设置<input>元素的data-choices-questionid和data-choices-choiceid属性以备后用。这两个属性分别代表选择的QuestionID和ChoiceID。添加了复选框的示例<div>如下所示:
`
C# VB.NET PHP
添加复选框后,从localStorage中确定它们的选中状态,相应地,它们被选中或保持未选中状态。如果用户切换选项选择,您需要将localStorage与新的选择同步。这可以在动态添加的复选框的change事件处理程序中完成。change事件处理程序本质上决定一个复选框是否被选中(:checked选择器),一个条目被添加到localStorage或者从localStorage中删除。当您在localStorage中添加一个项目时,一个ChoiceID充当一个键,它的QuestionID充当一个值。
前面讨论的GetChoices()函数使用的GetChoices()动作方法如清单 7-10 所示。
清单 7-10。 GetChoices()法
public JsonResult GetChoices() { SurveyDbEntities db = new SurveyDbEntities(); var data = from item in db.Choices select item; return Json(data.ToArray()); }
GetChoices()方法类似于GetQuestions()方法,但是返回来自Choices表的所有选择。
当用户填写调查表并点击提交答案按钮时,就会调用SubmitData()函数。这个函数又使用 jQuery $.ajax()方法调用SaveResults()动作方法。清单 7-11 中的代码展示了这是如何实现的。
清单 7-11。 SubmitData()客户端功能
function SubmitData(event) { var data = ''; for (var i = 0; i < storage.length; i++) { var key = storage.key(i); var value = storage[key]; var pair = '"' + key + '":"' + value + '"'; data = data + pair + ","; } if (data.charAt(data.length - 1) == ',') { data = data.substring(0, data.length - 1) } data = '{' + data + '}'; $.ajax({ type: "POST", url: "/Home/SaveResults", contentType: "application/json; charset=utf-8", data: data, dataType: "json", success: function(results){ alert('Results saved!'); window.localStorage.clear(); }, error: function (err) { alert(err.status + " - " + err.statusText); } }) }
SubmitData()函数通过遍历键来形成来自localStorage的所有键值对的 JSON 表示。该函数一次向服务器发送一个由多个问题-选项对组成的 JSON 字典,而不是一次发送一个问题-选项。然后,服务器端代码需要将这个 JSON 字典数据反序列化为. NET 字典,以便进一步处理。为了理解SubmitData()如何将数据发送到服务器,您需要知道 JSON 键值对是什么样子的。清单 7-12 显示了一些样本 JSON 数据。
***清单 7-12。*客户端正在发送 JSON 数据
{ "FirstName":"Tom", "LastName":"Jerry", "Email":"tom@somedomain.com", "5":"2", "7":"3", "1":"1", "9":"3" }
如您所见,发送到服务器的 JSON 数据中有几个键。这些键在表 7-4 中描述。
注意localStorage是一个键值集合,每个键都需要是惟一的。Choices表中的每个选项都有一个唯一的ChoiceID。这就是为什么你把ChoiceID作为键,把QuestionID作为值。如果你反过来做,你不能为一个问题存储多个选择——最新的ChoiceID将覆盖先前存储的ChoiceID,因为两者的QuestionID将是相同的。
一旦SaveResults()成功返回,来自localStorage的所有数据将使用clear()方法移除。将测量结果保存在Results表中的SaveResults()动作方法如清单 7-13 所示。
清单 7-13。 SaveResults()动作方法
`public JsonResult SaveResults() { string jsonData = string.Empty; using (StreamReader sr = new StreamReader(Request.InputStream)) { jsonData = sr.ReadToEnd(); } Dictionary<string, string> data = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonData);
SurveyDbEntities db = new SurveyDbEntities();
User usr = new User(); usr.FirstName = data["FirstName"]; usr.LastName = data["LastName"]; usr.Email = data["Email"]; db.Users.AddObject(usr); db.SaveChanges();
string userEmail = data["Email"]; int usrId = (from item in db.Users where item.Email == userEmail select item.UserID).SingleOrDefault();
data.Remove("FirstName"); data.Remove("LastName"); data.Remove("Email");
foreach (string str in data.Keys)
{
int choiceId = int.Parse(str); int questionId = int.Parse(data[str]);
Result result = new Result();
result.QuestionID = questionId;
result.ChoiceID = choiceId;
result.UserID = usrId;
db.Results.AddObject(result);
}
db.SaveChanges();
return Json("success");
}`
SaveResults()方法有点冗长,需要仔细观察。为了将从客户端传递的 JSON 数据转换成. NET 字典,它使用了Json.NET库。Json.NET是一个流行的高性能 JSON 框架。NET 提供了在 JSON 和。网络类型。SaveResults()首先将请求的InputStream读入一个字符串变量。然后,它使用Json.NET库的JsonConvert类将 JSON 字符串转换成. NET 字典。因为localStorage将所有数据存储为普通字符串,所以。NET 字典使用字符串键和值数据类型(Dictionary<string,string>)。
注
Json.NET是一个强大的开源框架,并提供许多其他特性。然而,在本例中,您只需要将 JSON 字典转换成. NET 字典。有关Json.NET的更多详情,请访问[json.codeplex.com](http://json.codeplex.com)。
SaveResults()然后将用户信息添加到Users表中。检索用户的UserID,因为在向Results表添加记录时也需要它。在Users表中添加一条记录后,FirstName、LastName和Email三个键被删除,只剩下ChoiceID键。这样,您可以简单地运行一个for-each循环,并将数据添加到Results表中。
图 7-8 显示了成功运行调查应用的示例。
***图 7-8。*调查应用的示例运行
您可以通过进行一些选择并关闭浏览器窗口而不保存数据来测试应用。如果您再次打开调查表,它应该会显示您之前的选择。然后,您可以提交答案并检查它们是否保存在SurveyDb数据库中。
将数据作为隐藏表单字段传递
在您刚刚开发的调查应用中,来自客户端的数据使用 jQuery $.ajax()方法传递给服务器。这样做很好,因为您不需要将整个页面提交给服务器。但是,在许多情况下,您可能希望整页回发到服务器。
假设您已经将调查应用开发为非 Ajax 应用。进一步假设调查像向导一样分成三个独立的网页,每个网页显示全部问题的一个小的子集。在这种情况下,当提交最终的向导页面时,需要将存储在localStorage中的数据发送到服务器。应用不使用 Ajax,那么如何将 web 存储数据传递给服务器呢?一个简单的方法是使用隐藏的表单域。您可以从 web 存储中读取键值对,将它们存储在隐藏字段中,然后将页面提交给服务器进行进一步处理。
假设调查表单上的提交答案按钮导致整页回发,您可以处理它的click事件,并使用清单 7-14 中的客户端脚本设置一个隐藏的表单字段。
***清单 7-14。*将localStorage数据转移到隐藏字段
function OnPostback() { var data = ''; for (var i = 0; i < storage.length; i++) { var key = storage.key(i); var value = storage[key]; var pair = '"' + key + '":"' + value + '"'; data = data + pair + ","; } if (data.charAt(data.length - 1) == ',') { data = data.substring(0, data.length - 1) } data = '{' + data + '}'; $("#hiddenAnswers").val(data); }
清单 7-14 本质上生成了与前一个案例相同的 JSON 键值对。然而,这次 JSON 数据被分配给一个 ID 为hiddenAnswers的隐藏字段。在服务器端,一个操作方法处理表单回发。清单 7-15 中的显示了一个这样的实现。
***清单 7-15。*在服务器上处理回发
[HttpPost] public ActionResult Index(FormCollection form) { string jsonData = Request.Form["hiddenAnswers"]; Dictionary<string, string> data = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonData); //save data here ... return Index(); }
如您所见,Index()动作方法接收了一个FormCollection。然后像以前一样,使用Json.NET库检索隐藏的字段数据并将其转换成. NET 字典。一旦你有了。NET dictionary ready,您可以轻松地将调查结果存储在数据库中,就像前面讨论的SaveResults() action 方法一样。
网络存储的安全考虑
当您使用 web 存储时,了解一些安全方面的知识很重要。网络存储不是用来存储敏感、机密数据的。因此,您不应该在 web 存储器中存储敏感信息,如密码、信用卡号、社会保险号等等。
浏览器为来自相同来源的所有数据分配相同的存储空间。一个源意味着你正在访问的网站的方案/主机/端口的组合。比如[www.domain1.com](http://www.domain1.com)和[blog.domain1.com](http://blog.domain1.com)被 web 存储当作两个不同的原点。同理,[www.domain1.com](http://www.domain1.com)和[www.domain1.com](https://www.domain1.com)也被认为是两个不同的网站。这样,恶意代码就不能欺骗网络存储器存储大量危险的数据。这种同源策略还可以防止恶意脚本使用随机子域来存储无限量的数据。
如前所述,web 存储基于每个原点分配存储空间。但是,有人可以使用 DNS 欺骗,假装一个可信的域正在尝试访问。这样,浏览器可能会允许恶意代码访问该域的存储区域。为了防止这种攻击,您可以使用安全套接字层(SSL)。一旦 SSL 就位,用户就可以放心,他们访问的站点来自真正的域,浏览器将为来自该域的所有页面分配相同的存储空间。
总结
Web 存储允许您在客户机上存储数据。它不受 cookies 的限制,允许在客户机上存储合理数量的数据。两个对象sessionStorage和localStorage存储字符串数据的键值对。sessionStorage只能存储当前浏览器会话的数据,而localStorage可以跨浏览器会话存储数据。
Web 存储不会随着每个请求自动传输到服务器。您需要设计一种编程方法,比如 Ajax 调用或隐藏表单字段,将 web 存储数据发送到服务器。
Web 存储处理实时 web 应用中使用的数据。下一章深入探讨了另一个特性——离线应用——它让你可以离线使用你的 web 应用。
八、开发离线 Web 应用
Web 应用通常被视为一直连接到网络的有线应用。web 应用的这种“永远在线”的特性是其流行和快速增长的原因之一。在当今的互联网时代,当网络连接不是一件大事时,web 应用的这一特性在大多数情况下不会造成问题。但是,有时候你不能保证网络的连通性。那么应用的用户应该怎么做呢?如果他们甚至可以在没有网络连接的情况下使用您的 web 应用,那不是很好吗?这正是 HTML5 离线 web 应用所提供的。
乍一看,您可能会发现 web 应用以不连接或独立的方式运行的概念有点奇怪;但是一旦你理解了它们可以提供帮助的场景,你就会欣赏 HTML5 的这个特性。本章解释了适用于 HTML5 的脱机 web 应用的概念。具体来说,您将了解以下内容:
- What are offline web applications and when to use them?
- Cache manifest file structure
- Create and use cache lists in Web forms and MVC applications
- Use Ajax technology when the offline application wants to talk to the server.
- Use
applicationCacheobjects and related events
何时使用离线应用
离线应用最适合在客户端下载应用文件后,客户端和服务器之间很少或没有通信的情况。顾名思义,离线应用是不能实时访问服务器端资源(如数据库和服务器端代码)的 web 应用。不是所有的应用都适合作为离线应用。因此,了解什么时候使用离线应用,什么时候避免使用它们是很重要的。在决定您的 web 应用是否适合脱机时,您应该考虑以下两个基本问题:
- Does your application depend on real-time data?
- How will network downtime affect your web application and its users?
假设您正在为一个体育门户构建一个显示实时板球比分的模块。显然,你的访问者将使用这个模块来查看板球比赛的最新状态。这样的模块下线是没有意义的,因为它依赖的是最新的数据。提供股票报价信息的网站也是如此。这样的 web 应用不适合作为离线应用来实现。
现在,考虑一个在线游戏。假设一旦下载,这个游戏在浏览器中运行,不需要来自服务器的数据。游戏使用的数据是在客户端创建和消费的。在这种情况下,应用不依赖于来自服务器的实时数据,因此可以作为离线应用开发。
另一个重要的考虑是网络停机时间。考虑一个销售主管使用的 web 应用。这些销售主管经常出差,通过无线互联网连接在笔记本电脑或移动设备上使用该应用。他们可能并不是一直都有网络连接;例如,他们可能会旅行到无线互联网服务提供商覆盖范围之外的偏远地区。网络连接的不可用性会妨碍他们的工作,因此开发离线应用是有意义的。另一方面,网络宕机可能不会对运营商用于某些日常任务的 web 应用产生太大影响,因此没有必要将其作为离线应用。
注意术语离线应用并不一定表示一个网站的所有页面都可以离线工作。一个网站可能有一些需要网络访问才能运行的网页和一些脱机工作的网页。离线应用指的是后一种类型的网页。
HTTP 缓存和离线应用
在深入研究离线应用的细节之前,有必要了解它们与传统的 HTTP 缓存不同。缓存网页、图像、样式表和 JavaScript 脚本文件等 web 资源并不是一项新发明。浏览器已经使用这些标准的 HTTP 缓存技术很多年了。无论何时访问网页,浏览器都会下载该网页及其相关资源,如图像和样式表,并将它们存储在缓存中。这个缓存在单独的客户机上维护。浏览器使用这种缓存是为了提高效率和性能。例如,假设您正在开发使用 jQuery 库的网页。您在所有的网页中都引用了来自微软 Ajax 内容交付网络(CDN)的 jQuery 库。如果浏览器的缓存中已经有了相同版本的 jQuery 库,就没有必要再从 CDN 上下载了。图像和样式表也是如此。
大多数时候,浏览器在后台使用传统的 HTTP 缓存。如果您希望检查浏览器是否在本地缓存中存储了文件,您可以使用浏览器的脱机模式测试该行为。几乎所有的浏览器都提供了脱机工作的选项。例如,图 8-1 显示了火狐的离线工作菜单选项。
如果您选择此菜单选项,Firefox 不会从服务器获取页面。相反,它使用本地缓存中已经存在的页面;否则,它会给出一条错误消息。
脱机工作菜单选项依赖于前面提到的标准 HTTP 缓存。通常,您不需要在服务器上编写任何代码来启用这种默认行为。如果您希望微调或禁止此行为,可以使用缓存控制头或 IIS 管理器来实现。
乍一看,传统的 HTTP 缓存可能类似于 HTML5 引入的离线应用缓存。但是,它们之间有显著的差异:
传统的 HTTP 缓存不需要您的任何代码或配置,除非您希望改变默认的浏览器行为。另一方面,HTML5 离线应用需要您的明确步骤。
***图 8-1。*在 Firefox 中离线工作
- Traditional HTTP cache can be fine-tuned by using cache-control header. HTML5 offline applications rely on manifest files and always work offline even if the network connection is available.
- The traditional HTTP cache is an implicit mechanism, which does not consider how the web page should behave when the browser is offline. The development of HTML5 offline application explicitly considers the requirements of data storage and network availability.
构建离线应用
离线应用本质上是获取服务器资源,如网页、图像、样式表和脚本文件,并将它们存储在浏览器的本地缓存中。一旦文件在本地机器上,就不需要网络连接。当然,稍后您可能需要上线同步本地和服务器端数据。
要开发脱机应用,您需要遵循以下基本步骤:
- Create a cache manifest file.
- Add a reference to the cached manifest file to all web pages that are part of the offline application.
- Configure the web server to recognize the cache manifest file extension.
- In JavaScript code, periodically check whether the network connection with the server is available.
缓存清单是一个文本文件,它列出了离线应用的所有基于文件的资源。缓存清单文件中列出的资源包括要缓存的文件、要通过网络获取而不是本地缓存的文件,以及由于某种原因无法缓存文件时的替代文件。一个 ASP.NET 网络应用可能包含许多网页,其中只有少数网页被用作离线应用。因此,ASP.NET 应用和脱机应用之间没有一对一的映射。一个 ASP.NET 应用可以包括多个独立的脱机应用。每个这样的离线应用都需要自己的缓存清单。对于缓存清单的文件扩展名没有严格的要求,但是常用的是.appcache和.manifest。
仅仅创建一个缓存清单文件是不够的。属于该脱机应用的所有网页都应该引用缓存清单。这样,每当浏览器下载网页时,它就知道该网页是脱机应用的一部分。此外,缓存清单告诉浏览器页面需要哪些资源(图像、脚本文件等),以便浏览器可以确保这些资源的可用性。
如上所述,缓存清单文件可以有任何开发人员定义的扩展名。您需要通知 IIS 您用于缓存清单文件的文件扩展名以及相关的 MIME 类型。如果没有这些信息,IIS 可能不会将缓存清单发送到浏览器,浏览器也无法缓存所需的文件。
尽管脱机应用的行为类似于独立的应用,但在它们生命周期的某个时刻,它们可能需要与 web 服务器进行交互。例如,假设您已经开发了一个 JavaScript 密集型游戏作为离线应用。这意味着最终用户在玩游戏时不需要网络连接。然而,在最后,游戏可能需要上线,以将用户的分数存储在在线帐户或个人资料中。在这种情况下,您需要检查网络连接是否可用,如果可用,则执行所需的数据传输。当然,这一步是可选的,主要取决于应用的性质。
以下部分详细分析了构建脱机应用的步骤。您开发了一个简单的基于 Web Forms 的脱机应用,它使用了您已经学习过的主题。这个离线应用在网页上显示一个 JavaScript 驱动的数字时钟(见图 8-2 )。还可以将时钟上显示的时间发送到服务器进行进一步处理。
***图 8-2。*时钟离线应用
创建缓存清单
如前所述,缓存清单是一个文本文件,它列出了要缓存的文件。在这个例子中,您使用.cachemanifest作为文件扩展名,并学习如何通知 IIS 这个扩展名。
缓存清单文件由一个缓存清单声明组成,后跟一个或多个部分:CACHE、NETWORK和FALLBACK。缓存清单声明如下所示:
CACHE MANIFEST
所有缓存清单文件都必须以此行开头。除了CACHE部分,其他部分是可选的,将在下面讨论。
注意缓存清单文件区分大小写。确保键入的节名和文件名与示例代码中显示的完全相同。
缓存清单的缓存部分
缓存清单的CACHE部分列出了要在客户端缓存的所有文件。这些文件可能包括网页、图像、样式表和 JavaScript 文件。例如,时钟应用使用表 8-1 中列出的文件。
在表 8-1 中显示的文件在缓存清单中指定,如清单 8-1 所示。
清单 8-1。缓存清单文件的 CACHE段
`CACHE MANIFEST
CACHE: Clock.aspx img/HTML5.png StyleSheet.css Scripts/jquery-1.7.2.min.js`
请注意,CACHE部分名称后面是一个冒号(:)字符。要缓存的文件每行列出一个。
CACHE是一个隐式的部分。这意味着如果你没有明确指定CACHE,这些文件被认为属于CACHE部分。所以,清单 8-1 和清单 8-2 是等价的。
***清单 8-2。*隐CACHE节
`CACHE MANIFEST
Clock.aspx img/HTML5.png StyleSheet.css Scripts/jquery-1.7.2.min.js`
到目前为止编写的缓存清单文件传达了在CACHE部分指定的四个文件将被下载到本地缓存。
**注意:**时钟应用是一个 ASP.NET Web 窗体应用;如果您将其开发为 MVC 应用,那么您将指定一个呈现相应视图的控制器动作,而不是
Clock.aspx(例如,Home/Index)。
缓存清单的网络部分
CACHE部分指定了浏览器应该缓存哪些文件以供离线使用。NETWORK部分做的正好相反:它列出了不应该被缓存的文件。假设您的应用除了显示页面内容之外还显示广告。它们很可能来自一个跟踪广告印象和点击的广告引擎。显然,这样的广告是无法线下存储的。如果你的离线应用使用了这样的资源,它们应该被列在NETWORK部分。
清单 8-3 显示了在NETWORK部分列出的Ads.js文件。
清单 8-3。缓存清单文件的 NETWORK段
`CACHE MANIFEST
CACHE: ...
NETWORK: Scripts/Ads.js`
这样,Ads.js就不会缓存在离线缓存中。相反,它总是通过网络访问。
在一个大的应用中,NETWORK部分可能有许多候选项。有时,您甚至不知道资源需要通过网络传输。在这种情况下,您可以使用*通配符来通知浏览器所有没有在CACHE部分列出的文件都应该通过网络访问。像这样使用*字符:
NETWORK: *
当您使用托管在不同服务器上并被您的应用引用的资源(图像、脚本等等)时,NETWORK部分也很有用。例如,您可能正在使用由 CDN 托管的图像和脚本文件,而不是将它们包含在您的应用中。如果您没有在NETWORK部分包含这些外部资源(无论是显式地还是通过使用*通配符),那么,奇怪的是,当您在线时,您的应用不会加载它们。
缓存清单的回退部分
缓存清单文件的FALLBACK部分指定了备用文件,以防资源无法缓存或无法通过网络访问。您可以将FALLBACK部分视为一种错误处理技术。如果原始资源不可用,您可以用其他资源替代。您可以使用FALLBACK部分提供替代内容或显示错误消息。例如,时钟应用使用FALLBACK部分来显示一个普通的错误信息(见清单 8-4 )。
清单 8-4。缓存清单文件的 FALLBACK段
`CACHE MANIFEST
CACHE: ...
NETWORK: ...
FALLBACK: / ErrorPage.aspx`
清单 8-4 使用ErrorPage.aspx作为所有不能被缓存的资源(/)的通用错误页面。注意/字符的使用:它表示对于任何不能访问的资源,应该显示ErrorPage.aspx。/和错误页面由空格分隔。
在继续下一节之前,请确保将缓存清单文件作为Clock.cachemanifest保存在 web 应用的根文件夹中。
引用 Web 表单和视图中的缓存清单
现在您已经为时钟应用创建了缓存清单,让我们看看如何在 web 表单和视图中引用它。看清单 8-5 。
***清单 8-5。*参考Clock.aspx 中的缓存清单
`<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Clock.aspx.cs" Inherits="BasicOfflineApp.WebForm1" %>
`
请注意以粗体显示的标记。标签包含一个指向Clock.cachemanifest文件的manifest属性。您需要在离线应用的所有 web 表单(或 MVC 视图)中添加manifest属性。
还要注意的是,<head>部分包含了对StyleSheet.css和 jQuery 库的引用。还有一个<script>块,包含负责显示时钟的 jQuery 代码(为了清楚起见,清单中没有显示该代码)。<body>部分使用Ads.js来显示广告,然后使用标记来显示时钟。回想一下,Clock.cachemanifest文件的CACHE部分包含所有这些样式表、脚本和图像文件。
来自<body>部分的剩余标记包括一些呈现小时、分钟和秒的<span>元素。时钟是使用 jQuery 代码显示的,如清单 8-6 所示。
***清单 8-6。*显示时钟的 jQuery 代码
`(document).ready(function () { if (!Modernizr.applicationcache) { alert("This browser doesn't support HTML5 Offline Applications!"); } var months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; var days= ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"] var today = new Date(); today.setDate(today.getDate()); ('#date').html(days[today.getDay()] + " " + today.getDate() + ' ' + months[today.getMonth()] + ' ' + today.getFullYear());
setInterval(function () { var seconds = new Date().getSeconds(); $("#sec").html(( seconds < 10 ? "0" : "" ) + seconds); },1000);
setInterval( function() { var minutes = new Date().getMinutes(); ("#min").html(( minutes < 10 ? "0" : "" ) + minutes); },1000);` ` setInterval( function() { var hours = new Date().getHours(); ("#hours").html(( hours < 10 ? "0" : "" ) + hours); }, 1000); });`
这段代码使用setInterval() JavaScript 函数每隔 1000 毫秒更新一次时间。使用 JavaScript Date对象及其方法,当前日期也显示在时钟的顶部。注意如何使用Modernizr.applicationcache属性检测浏览器对离线应用的支持。
配置 IIS 以识别缓存清单文件
为了让脱机应用按预期工作,浏览器应该能够从 web 服务器成功下载缓存清单文件。web 服务器(IIS)使用您使用的扩展名(在本例中为.cachemanifest)来提供缓存清单,这一点很重要。前面提到过.manifest和.appcache是缓存清单的常用文件扩展名;要知道.manifest也是由。NET ClickOnce 部署,IIS 可能已经有了它的入口(参见图 8-3 )。
***图 8-3。*IIS 中配置的 MIME 类型
考虑到这一点,您可能想要使用另一个文件扩展名(例如.appcache或.cachemanifest)来避免混淆。一旦决定了文件扩展名,您需要在 IIS 中将它的 MIME 内容类型添加为text/cache-manifest。这样,IIS 可以将缓存清单文件正确地提供给发出请求的浏览器。
还有一种使用 IIS 管理器的替代方法:您的 web 应用的web.config。您可以使用web.config的mimeMap元素配置缓存清单文件扩展名。清单 8-7 展示了如何将.cachemanifest文件扩展名映射到 MIME 类型text/cache-manifest。
***清单 8-7。*使用web.config 映射 MIME 类型
<system.webServer> <staticContent> <mimeMap fileExtension=".cachemanifest" mimeType="text/cache-manifest" /> </staticContent> </system.webServer>
<mimeMap>元素的fileExtension属性指定缓存清单文件的文件扩展名,而mimeType属性指定缓存清单的 MIME 类型。
测试离线应用
现在,您已经完成了创建离线应用所需的所有步骤,让我们使用 Chrome 测试时钟应用。假设您已经在 Visual Studio 中打开了项目,请按 Ctrl+F5 运行应用。Clock.aspx被加载到浏览器中,如前面的图 8-2 所示。为了确保应用真正从浏览器缓存中得到服务,打开 Visual Studio 开发 web 服务器(IIS Express)并停止应用(参见图 8-4 )。
图 8-4。 IIS Express 应用列表
现在,刷新Clock.aspx。通常,在这一步您会收到一个错误,因为 web 应用已停止;但是因为浏览器已经在本地缓存了应用,Clock.aspx是从缓存中刷新的。
打开另一个浏览器标签,在地址栏中键入chrome://appcache-internals,然后按回车键。您应该会看到一个类似于图 8-5 所示的页面。
图 8-5 展示了 Chrome 如何显示离线应用的细节。您可以看到缓存清单中列出的所有文件。您也可以通过单击删除来手动清除缓存。
图 8-5。 Chrome 显示离线申请详情
注意不同的浏览器有不同的方式展示离线应用的细节。例如,Firefox 在工具
选项
高级
网络对话框中显示了这些细节。此外,Firefox 会在首次访问应用时通知您某个 web 应用正在请求离线存储。
使用 AJAX 上网
尽管脱机应用与服务器断开连接,但在应用的生命周期中,它可能需要联机并与服务器通信。假设您希望将时钟应用中显示的当前时间保存在数据库中。只有在网络连接可用的情况下,您才能这样做。判断网络连接是否可用的可靠方法是向服务器发出请求,并查看请求是否成功。jQuery $.ajax()方法可以有效地用于这个目的。清单 8-8 展示了如何使用$.ajax()周期性地 ping 服务器。
***清单 8-8。*使用$.ajax() 检查网络连接是否可用
$(document).ready(function () { ... setTimeout(CheckOnline, 5000); }); function CheckOnline() { $.ajax({ type: "POST", url: 'Clock.aspx/IsOnline', contentType: "application/json; charset=utf-8", dataType: "json", success: function (result) { if (result.d == true) { $("#status").html("You are Online!"); setTimeout(CheckOnline, 5000); } }, error: function () { $("#status").html("You are Offline!"); setTimeout(CheckOnline, 5000); } });
这段代码显示了一个函数——CheckOnline()——它向一个名为IsOnline()的 web 方法发出一个$.ajax()请求。web 方法返回一个布尔标志。如果$.ajax()方法能够成功调用IsOnline(),则表明网络连接可用。然后,success方法将<div>的内容设置为“您在线了!”如果没有网络连接,调用IsOnline()失败,调用error函数。error函数将<div>的内容设置为“您离线了!”您需要定期检查联机状态;因此,使用setTimeout()函数每 5000 毫秒调用一次CheckOnline()函数。
$.ajax()调用使用的IsOnline() web 方法如清单 8-9 所示。
清单 8-9。 IsOnline() Web 方法
[WebMethod] public static bool IsOnline() { return true; }
IsOnline()方法返回true,表示到服务器的网络连接可用。
更新离线应用
一旦高速缓存清单文件被提供给浏览器,浏览器就开始使用来自高速缓存的应用文件。但是您可能需要在 web 应用被缓存后对其进行更改。在这种情况下有两种可能性:
- You can add or delete files in the web application and make changes to the cache manifest file.
- You can change one or more application files (their contents or codes), but you cannot change the cache manifest file.
当您开始使用脱机应用时,浏览器通常会检查服务器,看是否有比已缓存的文件更新的缓存清单文件。如果是,浏览器将下载新的缓存清单文件,并根据该缓存清单下载文件。这个浏览器行为处理列出的第一个场景,因为修改的缓存清单文件的时间戳不同于先前下载的缓存清单的时间戳。
然而,第二种情况有点棘手。在这种情况下,您并没有更改缓存清单文件:您更改的是组成文件,并且您希望浏览器使用这些文件的新版本。不幸的是,浏览器不检查每个组成文件的时间戳,它一直使用文件的缓存版本。要纠正这个问题,您需要更改驻留在服务器上的缓存清单文件的时间戳。有不同的方法可以做到这一点:
- Whenever one or more component files change, run a utility to change the timestamp of the cache manifest file (you can easily develop such a utility using the
System.IOclass).- Open the cache manifest file and make pseudo changes to it (for example, add blank space and delete it; Or cut the entire contents of the file, save the file, paste the contents, and save the file back to disk), so it has a new time stamp.
- Maintain a version number in the cache manifest file, and change it whenever the component file changes.
在这些选项中,最后一个最容易实现。如果您计划构建一种自动方式来更新缓存清单文件,那么其他选项更合适。
假设Clock.cachemanifest中列出的Clock.aspx文件由于代码级别的修订而改变,并且您希望使用版本号作为改变缓存清单的一种方式。你可以这样做,如清单 8-10 所示。
**清单 8-10。**版本号为的缓存清单
`CACHE MANIFEST # version 2.0
CACHE: ...
NETWORK: ...
FALLBACK: ...`
请注意粗体代码。缓存清单文件将跟在#字符后面的内容视为注释。您可以在注释中指定版本号。每当您更改任何组成文件时,都需要更改版本号并保存修改后的缓存清单文件。这样,缓存清单的时间戳也会更改,浏览器会再次下载缓存清单及其组成文件。
注意检查新版本的缓存清单文件的方式可能会有一些不同。如果由于某种原因(例如 HTTP 缓存)缓存清单文件被缓存在客户端,浏览器会检查文件是否被修改。如果是这样,用户需要手动删除离线应用文件,如前所述。
离线应用事件
在前面几节中,您通过创建缓存清单创建了一个脱机应用。浏览器完成了下载应用文件并将其存储在本地缓存中的所有工作。当浏览器忙于下载和缓存应用时,它会引发几个applicationCache对象事件。applicationCache是一个对象,它的属性、方法和事件可以使用 JavaScript 代码来处理。使用applicationCache事件,您可以跟踪离线应用生命周期的各个阶段。让我们看看applicationCache对象触发了什么样的事件:
- Whenever you request a web page whose
manifestattribute points to the cached list file, thecheckingevent of theapplicationCacheobject is triggered. Whether the application has been cached or not, thecheckingevent will be raised.- If the browser has never cached the application before, it will download the application cache list indicated in the
manifestattribute and start downloading the files listed in the cache list. At the same time, thedownloadingevent of theapplicationCacheobject is raised.- The cache list may contain many files, and the download operation may be very long. This is why the
applicationCacheobject will periodically raise aprogressevent to inform you of the progress of the download operation.- After downloading all the files listed in the cache list,
applicationCacheobject raisescachedevent. This event indicates that the entire application is now available offline.- If you are accessing an application that has been cached, the browser will check whether the cache manifest file of the application has changed since the last download.
- If the cache list file is unchanged, there is no need to update the offline cache, and the
noupdateevent of theapplicationCacheobject will be raised.- If the cache list has changed since the last download, the browser starts downloading application files, and as before,
downloadingandprogressevents ofapplicationCacheobject are raised.- When all the application files are successfully downloaded, the
updatereadyevent of theapplicationCacheobject is triggered.- It is also possible that the previously cached application is no longer an offline application, that is, its cache list has been deleted from the server. In this case, the browser raises the
obsoleteevent of theapplicationCacheobject and deletes the existing file from the cache. This application is now regarded as a application connected with , and it needs a real-time network connection with the server to run normally.- If there is an error in any of the previous steps, the
errorevent of theapplicationCacheobject will be raised.
为了方便起见,刚才讨论的applicationCache对象事件在表 8-2 中列出。
为了理解如何使用这些事件,让我们将其中一些连接到时钟应用中(见清单 8-11 )。时钟应用主要使用这些事件来通知最终用户相关的操作。
***清单 8-11。*使用applicationCache对象事件
`(document).ready(function () { ... (applicationCache).bind("checking",NotifyUser); (applicationCache).bind("progress", NotifyUser); (applicationCache).bind("updateready", NotifyUser); (applicationCache).bind("obsolete", NotifyUser); $(applicationCache).bind("error", NotifyUser); });
function NotifyUser(evt) { alert(evt.type); if (evt.type == 'updateready') { if (confirm('An updated version of this application is available.' + 'Do you wish to use the new version now?')) { applicationCache.swapCache(); } } }`
这段代码将表 8-1 中列出的applicationCache事件绑定到一个公共事件处理函数NotifyUser()。NotifyUser()功能显示事件类型(checking、downloading、progress等)。注意if块:如果事件类型是updateready,这意味着应用的更新版本是可用的。在大多数情况下,您希望用户立即使用更新的版本。实现这一点的一种方法是调用window.location.reload()方法。或者,您可以调用applicationCache对象的swapCache()方法。swapCache()用新下载的缓存替换旧的缓存,并开始使用新的缓存,而不需要像reload()的情况那样刷新页面。
注意
applicationCache对象还公开了一个status属性,该属性根据对象的状态返回一个数字状态代码(1 表示空闲,2 表示检查,等等)。然而,在大多数情况下,使用applicationCache对象事件比使用status属性提供了更多的灵活性和控制。
ASP.NET MVC 示例:重新审视调查应用
在前面的部分中,您使用了基于 Web 窗体的时钟应用。要使用 ASP.NET MVC 创建一个离线应用,您需要遵循相同的步骤。为了完整起见,让我们将您在第七章中开发的调查应用转换为离线应用。Survey 应用可能不是脱机使用的最佳选择,但是可以考虑它的一个微小的变体。
假设一家大型软件开发公司经常招聘初级和高级软件开发人员。该公司收到了数百份感兴趣的求职者的求职申请,不可能对所有求职者进行一对一的个人面试。因此,该公司希望开发一个在线考试引擎,向考生呈现一系列单项选择、多项选择和描述性问题。考生可以在任何方便的地方参加在线考试。这种在线测试成为筛选候选人的基础。只有成功的候选人才会被邀请参加面试。在在线考试引擎的情况下,考生可能会在一个页面上停留很长时间,因为他们在思考正确的答案。这样的应用可以很好地开发成离线应用。到目前为止,您一定已经猜到了这个调查应用,尽管它并不完全是一个在线考试引擎,但它类似于这个场景。
让我们执行将调查应用转换为离线应用所需的步骤。调查应用的离线版本如图 8-6 所示。
***图 8-6。*调查应用转换为离线应用
修改后的调查应用在外观和感觉上与原始应用几乎完全相同,只有一处不同。在“提交答案”按钮下面,您现在显示网络状态,以便用户知道调查数据是否可以保存到数据库中。
创建缓存清单文件
若要开始将调查应用转换为脱机应用,请创建调查应用的副本,并在 Visual Studio 中打开它。接下来,您需要为应用创建缓存清单文件。因此,创建一个名为Survey.cachemanifest的文本文件,并添加清单 8-12 中的条目。
***清单 8-12。*文件Survey.cachemanifest文件
`CACHE MANIFEST
version 1.0
CACHE: Home/Index Content/Site.css Scripts/jquery-1.6.2.js Scripts/modernizr-2.0.6-development-only.js
FALLBACK: / Home/ErrorPage`
仔细看看这个清单。CACHE部分列出了四个条目:一个路由 URL、一个 CSS 文件和两个脚本文件。FALLBACK部分指定了一个通用错误页面:ErrorPage视图向用户显示一个通用错误消息。
接下来,打开索引视图,使用manifest属性指定缓存清单文件,如下所示:
<html manifest="/Survey.cachemanifest">...</html>
获得问题和选择
即使将调查应用转换为脱机应用,其整体功能也保持不变。但是,因为应用依赖于存储在数据库中的问题和选择,所以您需要在客户端缓存它们。回想一下,GetQuestions()和GetChoices()函数分别调用控制器动作方法来检索问题和选择。您应该将这些数据存储到本地存储中,以便在脱机模式下也可以使用。
注现实世界的调查或在线考试申请有许多问题和选择。此外,问题可能会经常变化,在客户端缓存所有问题及其选择是不切实际的。更好的方法是每次都在实时模式下获取问题和选项,然后通知用户他们可以离线。
GetQuestions()和GetChoices()功能的修改版本如清单 8-13 所示。
***清单 8-13。*在本地存储器中存储数据
function GetQuestions() { $.ajax({ ... ** GetChoices();** ** },** error: function (err) { ** if (storage["container"] != null) {** ** $("#container").html(storage["container"]);** ** }** ** else {** ** alert(err.status + " - " + err.statusText);** ** }** ` }
})
}
function GetChoices() { ("#container").html();** }, error: function (err) { alert(err.status + " - " + err.statusText); } }) }`
请注意粗体代码。GetQuestions()函数获取问题,然后调用GetChoices()函数。一旦GetChoices()成功完成,容器<div>元素就会被问题和它们的选择填充。该代码将整个动态生成的 HTML 标记存储在本地存储中。这样,即使没有网络连接,代码也可以使用问题和选择。从下一次开始,如果GetQuestions()由于网络不可用而无法访问数据库,调用$.ajax()(属于GetQuestions())的错误函数将检索存储在本地存储中的 HTML 标记,并填充容器<div>元素。
检查网络连接
最后,调查应用需要将答案存储在数据库中。每当单击 Submit Answers 按钮时,它都会向服务器发出一个 Ajax 调用。当用户点击提交答案时,调用SubmitData()函数;它调用SaveResults()控制器动作方法来保存测量数据。只有在有网络连接的情况下,这个 Ajax 调用才会成功。
通知用户网络连接的可用性并相应地启用或禁用提交回答按钮将是有益的。为此,您需要定期检查网络连接是否可用。清单 8-14 显示了这是如何实现的。
***清单 8-14。*启用或禁用提交答案按钮
`$(document).ready(function () { ... setTimeout(CheckOnline, 5000); })
function CheckOnline() { .ajax({ type: "POST", url: 'Home/IsOnline', contentType: "application/json; charset=utf-8", dataType: "json", success: function (result) {` ` if (result == true) { ("#status").html("You are Online!"); ("#submit").removeAttr('disabled'); setTimeout(CheckOnline, 5000); } }, error: function () { ("#status").html("You are Offline!"); $("#submit").attr('disabled', 'disabled'); setTimeout(CheckOnline, 5000); } }); }`
这段代码看起来应该很熟悉:您在时钟应用中使用了类似的技术。使用setTimeout()方法,每五秒钟调用一次CheckOnline()。CheckOnline()然后调用IsOnline()控制器的动作方法。如果此调用成功,则表明网络连接可用;否则,网络不可用。相应地,通过添加或删除disabled属性来启用或禁用提交答案按钮。IsOnline()如清单 8-15 所示。
清单 8-15。 IsOnline()动作方法 public JsonResult IsOnline()
{ return Json(true); }
IsOnline()以 JSON 格式返回true。您现在可以运行调查应用,并通过在 IIS Express 中停止该应用来测试它,如前所述。
总结
大多数 web 应用需要一个到 web 服务器的实时网络连接才能正常运行。这种应用涉及客户端和服务器之间的大量交互。然而,一些 web 应用可以在没有到 web 服务器的活动网络连接的情况下工作。这种应用通常涉及大量的客户端功能。HTML5 让你可以轻松开发这样的离线应用。并不是每个应用都是好的候选,但是如果需要的话,您可以获得对这种离线应用的本机支持。
脱机应用的核心是一个缓存清单文件,它列出了脱机模式下需要的所有文件。如果需要,当网络连接可用时,脱机应用可以与 web 服务器连接,并传输数据或执行服务器端代码。HTML5 中引入的applicationCache对象表示离线应用缓存,并通过引发事件来帮助您跟踪各种应用生命周期事件。
HTML5 为您的 web 应用添加了丰富的客户端功能。另一个这样的领域是客户端文件访问。传统上,JavaScript 不能以任何方式访问本地文件。HTML5 文件 API 提供了一种处理本地文件的标准化方法。下一章将详细讨论这个特性。
九、使用文件 API 处理本地文件
众所周知,为了安全和隐私,浏览器不允许 web 应用篡改本地文件系统。只有当用户决定使用 file 类型的 HTML 元素将本地文件上传到服务器时,才在 web 应用中使用本地文件。本章的标题一开始可能会让你感到惊讶,因为术语文件 API 给人的印象是一个成熟的文件系统操作对象模型,就像。NET 框架。显然,HTML5 背后的人意识到了这种对象模型可能带来的安全问题。因此,文件 API 本质上是一个文件处理系统的精简版本,其中文件只能被读取,不能被修改或删除。此外,文件 API 不能读取机器上的任何随机文件。要读取的文件必须由用户明确提供。因此,在用户同意的情况下,文件 API 是读取和可选地上传本地文件的安全方式。
本章研究了文件 API 能为您做什么,以及如何在 ASP.NET web 应用中使用它。具体来说,您将了解以下内容:
- Class can be used as part of the file API.
- The technology of selecting files should be used with the file API.
- Use HTML5 native drag and drop
- Reading files with file API
- Upload the file to the server
了解文件 API
HTML5 文件 API 由一组三个对象组成(见表 9-1 ),允许你读取驻留在客户端计算机上的文件。要读取的文件必须由用户使用后面章节中讨论的支持技术之一来明确选择。一旦选中,您就可以使用 JavaScript 代码读取文件。
文件 API 的对象模型非常小,但是您可以以创造性的方式使用这三个对象,这在 HTML5 之前是不可能的(或者至少是非常困难的)。使用文件 API 的一些创造性方法如下:
- In traditional HTML, when you upload a file using the file field, the client does not check the file size. Only when the file arrives at the server can you check in the server-side code and accept or reject the file. Now, you can reject files that exceed a specific file size at the client.
- You can read the image file at the client and present a preview or thumbnail before uploading it to the server. This facility can also be used for social networking applications or photo album applications that process images.
- You can verify the contents of the file before uploading it to the server. For example, if your application should upload XML files, you can validate the XML documents before they reach the server. You can develop client images or file directories for users to view before uploading to the server. You can develop an album application that allows users to drag and drop image files instead of picking them up one by one.
注意未来,HTML5 可能会增加对文件系统导航的支持。可在
[dev.w3.org/2009/dap/file-system/pub/FileSystem/](http://dev.w3.org/2009/dap/file-system/pub/FileSystem/)获得一份规范草案。然而,在撰写本文时,主流浏览器很少或根本不支持这些特性。
文件列表对象
FileList对象表示一列File对象。从网页上的文件字段(<input type="file">)或者通过用户将本地文件从 Windows 资源管理器拖放到网页的可拖放区域来返回FileList。通常,你通过一个FileList来访问单个的File对象。FileList对象只公开一个属性和一个方法,如表 9-2 所述。
文件对象
一个File对象代表一个文件,并提供关于它的信息,比如它的名称、大小和 MIME 类型。如果你想读取一个文件的内容,你还需要一个File对象。表 9-3 列出了File对象的属性。
FileReader 对象
一个FileReader对象允许你读取一个File的内容。读取操作以异步方式执行。这样,即使非常大的文件也可以读取,而不会阻塞其他操作。FileReader对象可以以文本、Base64、二进制或ArrayBuffer的形式读取File内容。表 9-4 列出了负责读取文件的FileReader对象的方法。
readAsText()方法旨在用于基于文本的文件,比如纯文本文件、CSV 文件和 XML 文件。readAsDataURL()方法以 Base64 格式对文件内容进行编码,并将其作为数据 URL 返回。正如在第四章中更详细描述的,您知道数据 URL 格式由 Base64 编码的数据组成,并以文件的 MIME 类型为前缀,如下例所示:
data:image/png;base64,iVBORw0KGgoAAAANSUh…
在二进制数据无法传输到服务器的情况下,readAsDataURL()方法也很方便。一种这样的情况是将文本数据发送到服务器的 jQuery $.ajax()方法。readAsBinaryString()方法将文件作为原始二进制数据读取。readAsArrayBuffer()方法以ArrayBuffer的形式读取文件内容;一个ArrayBuffer是固定长度的二进制数据缓冲器。
这里讨论的文件读取方法会影响FileReader的某些属性。然后,这些属性可用于处理文件内容或向最终用户标记错误。表 9-5 列出了这些属性。
在读取文件时,FileReader会引发某些事件。您可以为这些事件连接处理程序,并拦截读取操作的各个阶段。FileReader对象引发的事件在表 9-6 中给出。
在表 9-6 中列出的所有事件中,您必须处理load事件,因为这是您可以访问文件内容的地方。
选择要与文件 API 一起使用的文件
如前所述,要使用文件 API 访问的文件必须由用户以下列方式之一明确选择:
- Users can use the open file dialog box displayed by the file field control to select files.
- Users can drag files from Windows Explorer and place them in a predefined area of the webpage.
第一种方式简单而传统。这种技术的一个变体是在页面上显示没有可见文件字段的打开文件对话框。在这种情况下,你需要耍花招来达到预期的行为。第二种方式是特定于 HTML5 的,由于对拖放的本机支持,很容易在网页中实现。
以下部分讨论了这两种选择文件的方法。请注意,HTML5 拖放功能不仅限于文件选择,还可以在应用中独立使用。
使用文件字段选择文件
使用文件字段来选择一个或多个文件是选择要与文件 API 一起使用的文件的最基本技术。在 HTML5 之前,文件字段控件只允许一次选择一个文件。如果你想让允许用户选择五个文件,你必须在网页上放置五个独立的文件域控件。然而,在 HTML5 中,用户可以使用单个文件字段控件选择多个文件。这可以通过<input>元素的新multiple属性来实现。清单 9-1 展示了如何配置一个文件字段控件来选择单个或多个文件。
***清单 9-1。*文件字段控制标记
<input id="File1" type="file" /> <input id="File2" type="file" multiple="multiple" />
这个清单显示了两个属性设置为file的<input>元素。注意,第二个文件字段具有multiple属性。当您指定multiple属性时,产生的打开文件对话框允许您使用标准的 Windows 技术(Ctrl/Shift 键和鼠标点击的组合)选择多个文件。
不同的浏览器会显示不同的文件字段。例如,图 9-1 显示了文件字段在 Chrome、Opera 和 Firefox 中是如何显示的。
***图 9-1。*不同浏览器的文件字段
注意 Chrome 和 Opera 如何显示带有multiple属性的文件字段,以表示可以选择多个文件。点击浏览器,选择文件,或者添加文件按钮打开一个标准的 Windows 打开对话框,如图图 9-2 所示。
***图 9-2。*使用打开对话框选择多个文件
注意图 9-2 显示选择了多个文件。当您选择多个文件并点按“打开文件”对话框中的“打开”按钮时,所有选定的文件会一个接一个地添加到同一个文件栏中。Firefox 和 Opera 在文件字段控件的文本框中显示所有选中的文件,而在 Chrome 中你需要将鼠标悬停在文件字段上才能看到选中文件的列表(参见图 9-3 )。
***图 9-3。*选择文件后的文件字段控件
如果你正在开发一个基于 ASP.NET 网络表单的应用,你可以使用FileUpload服务器控件,而不是使用清单 9-1 中的原始标记。清单 9-2 中的标记展示了如何使用FileUpload服务器控件。
***清单 9-2。*使用FileUpload服务器控件
<asp:FileUpload ID="FileUpload1" runat="server" /> <asp:FileUpload ID="FileUpload2" runat="server" AllowMultiple="True" />
这里显示的第一个FileUpload服务器控件允许您选择单个文件。第二个FileUpload服务器控件的AllowMultiple属性被设置为True,并允许您选择多个文件。
如果你正在开发一个 ASP.NET MVC 应用,你可以使用如清单 9-1 所示的普通 HTML 或者使用 HTML TextBox助手来呈现一个文件域。清单 9-3 展示了如何使用 HTML TextBox助手来呈现文件字段。
***清单 9-3。*使用TextBox助手渲染文件域
<% using (Html.BeginForm("Index","Home","POST")) { %> <%= Html.TextBox("file1", "",new {type="file"})%> <br /> <%= Html.TextBox("file2", "",new {type="file",multiple="multiple"})%> <%}%>
正如您所看到的,为了呈现一个文件字段,您使用了TextBox helper 并提供了一个type属性值file。对TextBox助手的第一次调用呈现了一个文件字段,只允许选择一个文件,而第二次调用设置了multiple属性,允许选择多个文件。
使用自定义按钮选择文件
有时,出于美观的原因,使用文件字段来选择文件是不可取的。虽然您可以使用 CSS 来改变文件字段的外观,但是它的外观不能被彻底改变。例如,假设您希望在网页上显示一个图像,当用户单击该图像时,您希望提示他们选择一个或多个文件。使用文件字段是不可能的,因为即使应用了 CSS,字段的基本外观也是由浏览器控制的。
如果您希望在应用中提供这样一种选择文件的定制技术,您需要使用一个编程技巧。实际上,您需要在网页中添加一个文件字段,但要隐藏起来。当用户单击用于文件选择的自定义图形或按钮时,您通过 JavaScript 触发隐藏文件字段上的click事件。这样,向用户显示打开文件对话框。一旦用户选择了一个或多个文件,这些文件将被分配到隐藏文件字段。然后,您可以访问这些文件进行进一步处理。
图 9-4 显示了用于选择文件的自定义图像。
***图 9-4。*使用自定义图像按钮选择文件
web 表单由一个Label控件、一个FileUpload控件和一个ImageButton控件组成。清单 9-4 展示了网络表单的标记。
***清单 9-4。*使用自定义图像提示文件选择
`
<asp:FileUpload ID="FileUpload1" runat="server" AllowMultiple="true" CssClass="hidden" /> <asp:Label ID="Label1" runat="server" CssClass="message" Text="Click on the image below to select files" > </asp:Label><asp:ImageButton ID="ImageButton1" runat="server" ImageUrl="~/img/UploadFile.jpg" /> `
请注意标记中的一些内容。首先,FileUpload控件的AllowMultiple属性被设置为True,以允许多文件选择。其次,它的CssClass属性被设置为一个名为hidden的 CSS 类。CSS 类看起来像这样:
.hidden { display:none; }
hidden CSS 类简单地将display CSS 属性设置为none,这样在运行时,FileUpload控件就不可见了。一个ImageButton控件用于向用户显示一个可点击的图像。为了捕获ImageButton的客户端click事件,并以编程方式触发FileUpload控件的click事件,您需要编写一些 jQuery 代码。清单 9-5 显示了添加到 web 表单的 jQuery 代码。
**清单 9-5。**编程触发文件字段的click事件
$(document).ready(function () { $("#FileUpload1").change(function (evt) { alert(evt.target.files.length + " file(s) were selected!"); }); $("#ImageButton1").click(function (evt) { $("#FileUpload1").click(); evt.preventDefault(); }); });
这段代码主要处理ImageButton控件的客户端click事件。在click事件处理程序中,它触发FileUpload控件(文件字段)的click事件。这样,向用户显示一个打开文件对话框。因为ImageButton控件没有任何服务器端功能,所以调用preventDefault()方法来取消默认操作。选择文件后,会引发文件字段的change事件。请注意files属性的使用,它允许您访问使用打开文件对话框选择的所有文件。change事件处理程序显示使用files对象的length属性选择的文件数量。
使用拖放选择文件
从打开的文件对话框中选择文件并不是使用文件 API 抓取要读取的文件的唯一选项。一个更高级的选项允许用户从 Windows 资源管理器或桌面拖动文件,并将它们放到网页的某个区域。这种拖放选项比以前的选项需要更多的编码,因为您需要指定网页中可以放置文件的特定区域;此外,您必须处理与拖放相关的某些事件。过去,开发人员使用第三方 JavaScript 库或插件来实现拖放。然而,HTML5 提供了对拖放的本地支持。尽管实现基于拖放的文件选择比其他技术需要更多的代码,但是与传统的 HTML 相比,整个过程很简单,也很容易实现。
HTML5 中的本地拖放支持并不局限于使用文件 API。它是一个独立的特性,可以在任何需要拖放的情况下使用。在接下来的部分中,您将学习如何在网页中实现拖放。
注意严格来说,使用拖放选择文件只需要抓取拖放操作即可。然而,为了完整起见,下一节将介绍如何实现拖放操作。您可以在许多情况下使用本机拖放支持来提供更好的用户体验。
使用拖放操作
拖放操作在桌面应用中很常见。现代 web 应用还试图利用拖放操作的便利性和强大功能来提供增强的用户体验。Web 开发人员经常求助于第三方基于 JavaScript 的库或定制技术,以便在他们的 web 应用中实现拖放行为。幸运的是,HTML5 内置了对拖放的支持。
使用拖放功能,您可以将一个 HTML 元素拖放到另一个元素上。您也可以从 Windows 资源管理器或桌面拖动文件,并将其放到网页上进行进一步处理。在拖放操作过程中,您可以将数据从源元素传递到目标元素。
实现拖放主要包括以下步骤:
- Allows you to drag one or more HTML elements from a Web page.
- Decide a drag-and-drop target to handle the drag-and-drop of draggable elements or files.
- Handling drag-and-drop related events.
- If necessary, transfer data between the drag-and-drop source and the drag-and-drop target.
启用 HTML 元素的拖动
在页面中使用拖放的第一步是使一个或多个元素可拖动。通过将 HTML 元素的draggable属性设置为true可以做到这一点。例如,下面这段标记使一个<div>元素可拖动:
<div class="myclass" draggable="true">Some content</div>
拖放事件
将一个或多个 DOM 元素标记为可拖动只是故事的一部分。为了让您的拖放功能对最终用户更具吸引力,您需要处理某些事件。这些事件在表 9-7 中列出。
注意,拖动源是一个正在被拖动的元素,而拖放目标是一个可拖动元素将要被拖放到其上的元素。来自表 9-7 的事件如dragstart、drag和dragend由拖动源处理,而事件如dragenter、dragleave、dragover和drop由拖放目标处理。您可以使用 JavaScript 将事件处理程序连接到这些事件,如清单 9-6 所示。
***清单 9-6。*为拖放事件连接事件处理程序
$("div").each(function () { this.addEventListener('dragstart', OnDragStart, false); this.addEventListener('drop', OnDrop, false); });
在这段代码中,<div>元素的dragstart和drop事件分别通过addEventListener()方法连接到OnDragStart和OnDrop函数。
在拖放操作之间传输数据
大多数时候,将某个东西拖放到另一个东西上也需要在源元素和目标元素之间传输一些数据。为了完成这个数据传输,HTML5 提供了dataTransfer对象。通过传递给事件处理程序的事件对象,可以在各种拖放事件中访问dataTransfer对象。表 9-8 列出了dataTransfer对象的一些重要属性和方法。
通常在dragstart和drop事件处理程序中使用dataTransfer对象的属性和方法。
实现拖放:一个购物车
现在让我们将你的知识运用到一个简单而实用的购物车网络表单中,如图 9-5 所示。
***图 9-5。*购物车网页表单
如您所见,web 表单代表了一个简单的购物车。各种产品由放置在Repeater控件中的<div>元素表示。产品可以拖放到购物袋上。添加完所有需要的产品后,用户可以单击 Place Order 按钮将产品数据发送到服务器进行订购。
实体框架数据模型
购物车示例将数据存储在 SQL Server Express 数据库中。为了从数据库中获取数据,应用使用实体框架数据模型,如图 9-6 所示。
***图 9-6。*购物车数据库的实体框架数据模型
数据模型由两个类组成:Product和Order。Product类捕获细节,如ProductId、Name、Description、Cost和ImageUrl。Order类捕获细节,如OrderId、ProductName和Qty。当然,真实世界的购物车系统会捕获更多的细节,但是这个数据模型足以说明这个主题。
产品目录和购物车
产品目录是一个Repeater控件,它的ItemTemplate包含一个可拖动的<div>元素。这个<div>元素包装了所有的产品细节,比如单个产品的Name、Description和Cost。Repeater控件通过EntityDataSource控件接收其数据。清单 9-7 中给出了Repeater控件的标记。
***清单 9-7。*产品目录的标注
<asp:Repeater ID="Repeater1" runat="server" DataSourceID="EntityDataSource1"> <ItemTemplate> ** <div class="product" draggable="true">** <header><%# Eval("Name") %></header> <div> <asp:Image runat="server" ID="img1" ImageUrl='<%# Eval("ImageUrl") %>' /> </div> <div><%# Eval("Description") %></div> <br /> <div class="cost"><%# Eval("Cost","Cost : ${0}") %></div> <input type="hidden" value="<%# Eval("ProductId") %>" /> </div> </ItemTemplate> </asp:Repeater>
注意粗体的代码。代表产品的<div>元素用设置为true的draggable属性来标记。清单 9-7 中的剩余标记实际上是使用 ASP.NETEval()数据绑定表达式将Products表的各列与 HTML 元素绑定在一起。
处理拖放事件
下一步是将拖放事件处理程序连接到各种元素。连接各种事件处理程序的 jQuery 代码如清单 9-8 所示。
***清单 9-8。*连线拖放事件处理程序
`(document).ready(function () { ("div .product").each(function () { this.addEventListener('dragstart', OnDragStart, false); });
var cart = $("#divCart").get(0); cart.addEventListener('dragenter', OnDragEnter, false); cart.addEventListener('dragleave', OnDragLeave, false); cart.addEventListener('dragover', OnDragOver, false); cart.addEventListener('drop', OnDrop, false); cart.addEventListener('dragend', OnDragEnd, false); })`
如您所见,首先使用 jQuery 选择器选择所有应用了product CSS 类的<div>元素(即所有产品的容器<div>元素)。然后对结果集调用each()方法,为dragstart事件添加一个事件监听器。addEventListener()方法将一个事件处理函数连接到一个事件。在这种情况下,dragstart事件由OnDragStart函数处理。
包含购物袋和按钮的<div>元素的 ID 为divCart。divCart元素应该处理其他事件,如dragenter、dragleave和drop。一系列的addEventListener()方法调用将事件处理函数附加到这些事件上。事件处理函数采用On*XXXX*的形式,其中 XXXX 是事件的名称。
总共有六个事件处理函数:OnDragStart、OnDragEnter、OnDragLeave、OnDragOver、OnDrop和OnDragEnd。让我们一个一个地检查它们。
OnDragStart
dragstart事件处理程序降低了被拖动元素的不透明度,这样最终用户就能得到拖动操作的视觉线索。处理dragstart事件的OnDragStart事件处理函数如清单 9-9 所示。
清单 9-9。 OnDragStart事件处理函数
function OnDragStart(e) { this.style.opacity = '0.3'; srcElement = this; e.dataTransfer.effectAllowed = 'move'; var product=$(this).find("header")[0].innerHTML; e.dataTransfer.setData('text/html', product); }
OnDragStart事件处理函数将拖动操作的源存储在一个全局变量srcElement中,因为稍后在drop事件处理函数中会用到它。dataTransfer对象的effectAllowed属性被设置为move。当用户拖动一个产品并将其放到购物袋上时,您需要将相应的产品名称传输到拖放目标。在这种情况下,产品名称被放在<header>元素中;因此,find()方法找到所有的 header 元素,然后获取 header 元素的innerHTML(产品名称)。setData()方法将产品名称设置为要通过dataTransfer对象传输的数据。这样,drop事件处理程序就知道哪个产品将被添加到购物车中。setData()的第一个参数表示正在传输的数据的 MIME 类型(本例中为'text/html')。
昂格洛夫
OnDragOver事件处理函数向拖放目标添加一个 CSS 类,以便向用户提供关于操作的视觉线索。OnDragOver函数如清单 9-10 所示。
清单 9-10。 OnDragOver功能
function OnDragOver(e) { ... $(this).addClass('highlight'); e.dataTransfer.dropEffect = 'move'; }
CSS 类本质上是通过改变背景色来给拖放目标(购物袋元素)添加一个亮点。这里显示了:
.highlight { background-color:Yellow; }
dataTransfer对象的dropEffect属性被设置为move。
软骨和软骨叶
OnDragEnter和OnDragLeave事件处理程序只是将highlight CSS 类添加到 drop target 元素中,并从其中移除。这些事件处理程序如清单 9-11 中的所示。
清单 9-11。 OnDragEnter和OnDragLeave功能
`function OnDragEnter(e) { $(this).addClass('highlight'); }
function OnDragLeave(e) { $(this).removeClass('highlight'); }`
滴上
OnDrop事件处理函数是主要的事件处理程序,您可以在其中将产品名称从DataTransfer对象传输到购物车。OnDrop功能如清单 9-12 中的所示。
清单 9-12。 OnDrop功能
function OnDrop(e) { ... srcElement.style.opacity = '1'; $(this).removeClass('highlight'); var count = $(this).find("div[data-product-name='" + e.dataTransfer.getData('text/html') + "']").length; if (count <= 0) { $(this).append("<div class='selectedproduct' data-product-name='" + e.dataTransfer.getData('text/html') + "'>" + e.dataTransfer.getData('text/html') + "</div>"); } else { alert("This product is already added to your cart!"); } return false; }
因为拖放操作已经完成,所以drop事件处理程序将源元素的不透明度设置回1。它还从目标元素中移除了highlight CSS 类。然后,它将被拖动的产品附加到目标元素上。
注意使用了getData()方法来检索之前在OnDragStart事件处理函数中设置的数据。还有一个检查,所以相同的产品不能添加到购物车多次。图 9-7 显示了如果同一产品被多次拖放,错误信息是如何显示给用户的。
***图 9-7。*跌落后检查重复产品
如您所见,鼠标已经被添加到购物车中。再次尝试将鼠标拖放到购物车上会出现一个警告框,其中有一条消息通知用户存在重复。
不可忍受
OnDragEnd事件处理函数简单地从拖放目标中移除了highlight CSS 类,如清单 9-13 所示。
清单 9-13。 OnDragEnd功能
function OnDragEnd(e) { $("div .bag").removeClass('highlight'); this.style.opacity = '1'; }
将数据从客户端传递到服务器
要将购物车中的商品传输到服务器端代码,需要使用 jQuery $.ajax()方法。下单按钮的click事件处理程序有相关代码,如清单 9-14 所示。
***清单 9-14。*在服务器上保存订单数据
$("#Button1").click(function () { var data = new Array(); $("div .bag div").each(function (index) { data[index] = "'" + this.innerHTML + "'"; }); $.ajax({ type: 'POST', url: 'shoppingcart.aspx/PlaceOrder', contentType: "application/json; charset=utf-8", data: '{ products:[' + data.join() + ']}', dataType: 'json', success: function (results) { alert(results.d); }, error: function () { alert('error'); } }); });
如您所见,首先购物袋中的产品被存储到一个 JavaScript 数组中。这样,很容易通过连接数组元素将它们传递给服务器。通过使用each()方法并提取各个<div>元素的innerHTML来创建一个Array。然后,$.ajax()方法调用驻留在ShoppingCart.aspx web 表单中的 web 方法PlaceOrder()。web 方法如清单 9-15 所示。
清单 9-15。 PlaceOrder Web 方法
[WebMethod] public static string PlaceOrder(string[] products) { Guid orderId = Guid.NewGuid(); ShoppingCartEntities db = new ShoppingCartEntities(); foreach (string p in products) { Order order = new Order(); order.OrderId = orderId; order.ProductName = p; order.Qty = 1; db.Orders.AddObject(order); } db.SaveChanges(); return "Order with " + products.Length.ToString() + " products has been added!"; }
PlaceOrder() web 方法将订单放入Orders表中。方法接受表示产品名称的字符串数组。注意$.ajax()如何以 JSON 格式传递products参数。web 方法成功完成后,success处理函数向最终用户显示一个警告。
要测试拖放行为,请运行 web 表单并尝试在购物袋上拖动产品。图 9-8 显示了网络表单的运行示例。
***图 9-8。*购物车 web 表单的运行示例
请注意被拖动的产品(鼠标)是如何以较低的不透明度显示的,以及购物袋是如何突出显示的。如果您单击“下订单”按钮,所选产品将保存在数据库中。
拖拽文件
现在,您已经知道了如何将 HTML 元素拖放到目标元素上,让我们看看如何将文件拖放到网页上。在文件拖放的情况下,您不必担心将任何 HTML 元素标记为可拖动的,因为文件是外部实体,您可以从浏览器外部拖动它们。你需要处理掉文件。
一个页面可能包含许多能够作为放置目标的 HTML 元素。但是,如果您想要访问从 Windows 资源管理器或桌面拖放的文件,您必须“监听”指定用于该目的的已知元素。为了理解如何做到这一点,让我们开发一个 web 表单,如图 9-9 所示。
***图 9-9。*在网页表单上拖拽文件
web 表单由一个<div>元素组成,其背景图像被设置为一个篮子的图像。您可以将文件从 Windows 资源管理器或桌面拖放到这个<div>元素上。<div>处理drop事件,并显示一个警告框,指示有多少文件被丢弃。清单 9-16 中的代码展示了如何处理所需的拖放事件。
***清单 9-16。*处理掉的文件
`$(document).ready(function () { var container; container = document.getElementById("container"); container.addEventListener("dragenter", OnDragEnter, false); container.addEventListener("dragover", OnDragOver, false); container.addEventListener("dragleave", OnDragLeave, false); container.addEventListener("drop", OnDrop, false); });
function OnDragEnter(e) {
e.stopPropagation();
e.preventDefault();
} function OnDragLeave(e) {
e.stopPropagation();
e.preventDefault();
}
function OnDragOver(e) { e.stopPropagation(); e.preventDefault(); }
function OnDrop(e) { e.stopPropagation(); e.preventDefault(); var files = e.dataTransfer.files; alert(files.length + " file(s) dropped!"); }`
这段代码使用addEventListener()方法连接了四个事件的事件处理程序— dragenter、dragover、dragleave和drop。与购物车示例不同,函数OnDragEnter、OnDragLeave和OnDragOver除了调用stopPropagation()和preventDefault()方法之外,不做任何特殊的事情。jQuery stopPropagation()方法阻止事件在 DOM 树中冒泡,从而防止任何父处理程序收到事件通知。类似地,jQuery preventDefault()方法阻止了事件的默认动作。
如果您愿意,可以在这些事件处理程序中设置dropEffect或一个可视指示器。另外,请注意,您没有在任何地方设置dataTransfer对象。这是因为数据(本例中是文件)来自外部来源。注意OnDrop()事件处理函数:它使用dataTransfer对象的files属性来访问放在元素上的文件。然后显示使用length属性删除的文件数量。
要测试 web 表单,运行它:从 Windows 资源管理器中拖动几个文件,并把它们放到篮子里。一旦您删除了文件,您应该会看到一个包含已删除文件数量的警告。
读取文件并显示文件信息
现在您已经知道了选择文件的技术——文件字段、定制按钮和拖放——让我们看看如何使用文件 API 读取所选文件。为了理解这个过程,你开发一个类似于图 9-10 的 web 表单。
***图 9-10。*使用文件 API 读取文件
web 表单分为两部分。顶部允许您使用所有三种技术选择图像文件。然后,所选文件的详细信息(如文件名、大小和 MIME 类型)会显示在底部的表格中。将鼠标悬停在表格每一行中提供的显示链接上,会在表格右侧显示该图像。web 表单的标记如清单 9-17 所示。
***清单 9-17。*显示图像预览的 Web 表单的标记
`
| ` ` | ||
|---|---|---|
| ** ** | ** ** ** ** | ** ** |
清单中的粗体元素对于 web 表单的功能非常重要。文件字段FileUpload1用于直接选择文件,而FileUpload2保持隐藏,当点击ImageButton时显示打开文件对话框。<div>元素divBasket是拖放目标,从 Windows 资源管理器拖动的文件可以放在这里。HTML 表Table1根据所选的文件使用 jQuery 动态填充。图像预览显示在filePreview图像元素中。
元素的各种事件被连接在文档的ready()事件处理程序中,如清单 9-18 所示。
***清单 9-18。*连接 HTML 元素的事件处理程序
`var files;
(document).ready(function () { ("#FileUpload1").change(OnChange); $("#FileUpload2").change(OnChange);
("#ImageButton1").click(function (evt) { ("#FileUpload2").click(); evt.preventDefault(); });
var basket;
basket = document.getElementById("divBasket"); basket.addEventListener("dragenter", OnDragEnter, false);
basket.addEventListener("dragover", OnDragOver, false);
basket.addEventListener("drop", OnDrop, false);
});`
这段代码声明了一个全局变量files,在代码的后面使用它来存储对所选文件的引用。两个文件字段控件的change事件连接到OnChange函数。类似地,dragenter、dragover和drop事件分别连接到OnDragEnter、OnDragOver和OnDrop函数。在所有这些事件处理程序中,OnChange和OnDrop非常重要,因为它们启动了生成文件表的过程。这两个事件处理程序如清单 9-19 所示。
清单 9-19。 OnChange和OnDrop事件处理程序
`function OnChange(evt) { files = evt.target.files; ShowFileDetails(files); }
function OnDrop(evt) { evt.stopPropagation(); evt.preventDefault(); files = evt.dataTransfer.files; ShowFileDetails(files); }`
OnChange事件处理函数使用文件字段控件的files属性获取选定的文件。注意FileUpload1和FileUpload2的change事件处理程序都是由OnChange处理的。所以,evt.target指的是各自的文件字段控件。OnChange然后调用一个助手函数ShowFileDetails(),它更新页面中显示的文件表。
OnDrop事件处理函数使用dataTransfer对象的files属性抓取文件并取消事件冒泡。然后调用ShowFileDetails()生成一个文件表。
注意,文件字段控件的files属性和dataTransfer对象的类型是FileList(参见前面的表 9-2 以快速回顾FileList对象)。OnChange和OnDrop事件处理程序使用的ShowFileDetails()函数如清单 9-20 所示。
**清单 9-20。**使用File对象显示文件信息
function ShowFileDetails(files) { $("#Table1").empty(); $("#Table1").append("<tr><th>File Name</th><th>Size</th><th>MIME Type</th><th>Preview</th></ tr>"); for (var i = 0; i < files.length; i++) { if (files[i].type == "image/jpeg" || files[i].type == "image/png" || files[i].type == "image/gif") { $("#Table1").append("<tr><td>" + files.item(i).name + "</td><td>" + files[i].size + "</td><td>" + files[i].type + "</td><td><a href='#' data-file-index='" + i + "'>Show</a></td></tr>"); } else { alert("Only image files are allowed. Other files will be ignored!"); } } $("a").hover(ShowPreview, HidePreview); }
ShowFileDetails()首先通过使用 jQuery empty()方法删除表中的所有行来清空表。然后,它遍历FileList并访问每个File对象来获取文件细节。因为该应用仅用于图像文件,所以会检查每个文件的扩展名。如果文件是图像文件(.jpg、.jpeg、.png或.gif),则表格中会添加一个新行。这种检查是在File对象的type属性的帮助下完成的,该属性返回文件的 MIME 类型(image/jpeg、image/png等等)。name属性返回带有扩展名的文件名,但不包括路径信息。属性返回文件的大小,以字节为单位。
您可以使用典型的集合语法(files[i])或使用FileList对象的item()方法来访问单个的File对象。显示超链接使用自定义的data-*属性data-file-index存储文件的索引。通过这种方式,您可以确定要显示哪个图像。
当您将鼠标悬停在显示链接上时,会显示实际的图像预览。jQuery hover()方法将两个处理函数绑定到当鼠标指针进入和离开超链接时调用的hyperlink元素。ShowPreview()和HidePreview()功能如清单 9-21 所示。
***清单 9-21。*显示图像预览
`function ShowPreview(evt) { var reader = new FileReader(); (reader).bind("load",function (e) { var imgSrc = e.target.result; ("#filePreview").attr('src',imgSrc); }); var fileIndex = $(evt.target).attr('data-file-index'); reader.readAsDataURL(files[fileIndex]); }
function HidePreview(evt) { $("#imgPreview").attr('src', ''); }`
ShowPreview()函数创建一个FileReader的实例。FileReader对象以异步方式读取文件,并在成功读取文件时引发一个load事件。这就是为什么在读取文件之前需要附加load事件的事件处理程序。使用FileReader对象的readAsDataURL()方法读取文件。readAsDataURL()以数据 URL (Base64 编码)的形式将文件内容提供给load事件处理程序。可以使用e.target对象的result属性来访问这些内容。load事件处理程序使用FileReader对象的result属性检索文件内容,然后将图像的src属性设置为图像内容。
HidePreview()方法只是删除图像的src属性。
上传文件到服务器
文件 API 读取的文件不一定要上传到服务器。然而,在大多数情况下,您将它们上传到服务器进行处理或存储。当然,您可以根据处理逻辑丢弃一些选定的文件,并上传选定文件的子集。就文件 API 而言,它在将文件上传到服务器的过程中不起任何作用。您有责任设计一种机制,负责将所需的文件上传到服务器。
使用文件字段控件或自定义按钮上传选定的文件很容易,因为您需要做的只是将 web 表单POST到服务器。在服务器端代码中,你可以访问所选择的文件,如清单 9-22 所示。
***清单 9-22。*通过POST网页形式上传文件
foreach (HttpPostedFile file in FileUpload1.PostedFiles) { string fileName = file.FileName; fileName = Server.MapPath("~/uploads/" + fileName); file.SaveAs(fileName); }
在服务器端代码中,使用了FileUpload控件的PostedFiles集合。PostedFiles的每个元素都属于HttpPostedFile类型。HttpPostedFile类的SaveAs()方法允许你将上传的文件保存到服务器。
上传使用拖放技术选择的文件有点棘手。这是因为选择的文件不属于一个<form>控件,因此它们不会POST到服务器。您需要以编程方式将它们发送到服务器。jQuery $.ajax()方法在这里也派上了用场。清单 9-23 展示了如何使用$.ajax()上传文件。
***清单 9-23。*使用$.ajax()上传文件
function UploadFiles() { var data = new FormData(); for (var i = 0; i < files.length; i++) { data.append(files[i].name, files[i]); } $.ajax({ type: "POST", url: "UploadFiles.ashx", contentType: false, processData: false, data: data, success: function (result) { alert(result); }, error: function () { alert("There was error uploading files!"); } }); }
这个清单显示了将所选文件上传到服务器的UploadFiles()函数。您不能将File对象从FileList直接发送到服务器;你首先需要把它们转换成FormData 对象。顾名思义,FormData对象表示应该伴随请求的表单数据。FormData对象的append()方法允许你添加你想要上传的单个文件。代码向通用处理程序UploadFiles.ashx发出一个POST请求,该处理程序负责接受发布的文件并将它们保存在服务器上。成功上传文件后,会向用户显示一条成功消息。
注意,$.ajax()调用将contentType和processData选项设置为false。您不需要提供内容类型,因为FormData对象默认为内容类型multipart/form-data。如果没有将processData选项设置为false,$.ajax()会自动将发布的数据转换为 URL 编码的形式,这是不希望的。
清单 9-24 展示了使用$.ajax()方法编辑的文件POST是如何在服务器端使用通用处理程序UploadFiles.ashx处理的。
***清单 9-24。*使用通用处理程序保存上传的文件
public void ProcessRequest(HttpContext context) { if (context.Request.Files.Count > 0) { HttpFileCollection files = context.Request.Files; foreach (string key in files) { HttpPostedFile file = files[key]; string fileName = file.FileName; fileName = context.Server.MapPath("~/uploads/" + fileName); file.SaveAs(fileName); } } context.Response.ContentType = "text/plain"; context.Response.Write("File Uploaded Successfully!"); }
通用处理程序的ProcessRequest()方法将发布的文件保存在服务器上。ProcessRequest()接收HttpContext作为参数。这可用于访问固有对象,如Request、Response和Server。
使用Request.Files集合访问上传的文件。集合中的每个元素都属于类型HttpPostedFile。HttpPostedFile类的SaveAs()方法允许你在服务器上保存文件。一旦保存了所有文件,就会向客户端发送一条成功消息。该消息显示在$.ajax()调用的success功能中。
在 ASP.NET MVC 中使用文件 API
在本节中,您将开发一个使用文件 API 的 ASP.NET MVC 应用。该应用有两个目的:将 XML 文件上传到服务器,并根据 XSD 模式验证上传的文件。
假设一个桌面应用将其数据作为 XML 文件存储在本地机器上。您需要定期将这样生成的 XML 文件上传到中央 web 服务器进行进一步处理。这种应用可以使用文件 API 来完成以下任务:
- Ensure that only XML files are uploaded to the server.
- Check whether the XML file contains specific XML tags. Although this kind of verification can't replace XSD schema verification, it can be used as the first-level verification.
- Displays a preview of the XML file being uploaded to the server.
您在本节开发的应用类似于图 9-11 。
图 9-11。【ASP.NET 上传 XML 文件的 MVC 应用
该应用允许您使用文件字段控件或通过从 Windows 资源管理器中拖动来选择文件。选择文件后,所选文件的数量会显示在购物篮下方,同时会显示一个文件名列表作为链接。如果将鼠标悬停在文件名上,链接的工具提示中将显示文件内容的简短预览(最多 500 个字符)。单击“上传”按钮时,所有文件都会上传到服务器,并根据 XSD 模式进行验证。如果上传的 XML 文件与模式规范不匹配,则会返回一个错误。
XML 文件上传应用的工作原理与您之前开发的图像预览应用相同;因此,本节仅关注不同的领域。清单 9-25 显示了应用索引视图的标记。
***清单 9-25。*XML 文件上传应用的索引视图
<% using (Html.BeginForm()) { %> <div class="message"> Select files using a field field or drop them on the basket </div> <%= Html.TextBox("file1", "",new {type="file",multiple="multiple"})%> <div class="message">OR</div> <div id="basket" class="dropDiv"></div> <div id="filecount" class="message"></div> <div id="errors" class="error"></div> <input id="upload" type="button" value="Upload" /> <%}%>
<form>由一个文件字段控件和一个作为拖放目标的篮子<div>元素组成。上传按钮触发文件上传操作。连接各种事件处理程序的 jQuery 代码如清单 9-26 所示。
***清单 9-26。*为拖放事件连接事件处理程序
`var files; $(document).ready(OnChange); var basket; basket = document.getElementById("basket"); basket.addEventListener("dragenter", OnDragEnter, false); basket.addEventListener("dragleave", OnDragLeave, false); basket.addEventListener("dragover", OnDragOver, false); basket.addEventListener("drop", OnDrop, false);
$("#upload").click(UploadFiles); });`
这段代码声明了一个全局变量files,在代码的后面使用它来存储对所选文件的引用。文件字段的change事件处理程序连接到OnChange函数。类似地,dragenter、dragleave、dragover和drop分别连接到OnDragEnter、OnDragLeave、OnDragOver和OnDrop。OnChange和OnDrop事件很重要,如清单 9-27 所示。
清单 9-27。 OnChange和OnDrop事件处理程序
`function OnChange(evt) { files = evt.target.files; ShowFileDetails(); }
function OnDrop(e) { e.stopPropagation(); e.preventDefault(); files = e.dataTransfer.files; ShowFileDetails(); }`
OnChange事件处理函数使用文件字段控件的files属性抓取选定的文件。OnChange然后调用一个助手函数ShowFileDetails(),该函数显示被选作锚元素的文件列表。
OnDrop使用dataTransfer对象的files属性抓取文件并取消事件冒泡。ShowFileDetails()是后来的称呼。
ShowFileDetails()函数如清单 9-28 中的所示。
清单 9-28。 ShowFileDetails()功能
function ShowFileDetails() { var html = ""; html += files.length + " files selected!"; html += "<div class='fileName'>" for(var i=0;i<files.length;i++) { if (files[i].type == "text/xml") { html += "<a href='#' data-file-index='" + i + "'>" + files[i].name + "</a> "; } else { html += "<span data-file-index='" + i + "'>" + files[i].name + "</span> "; } } html += "</div>"; $("#filecount").html(html); $("a").hover(ShowPreview,HidePreview); }
该应用仅用于上传 XML 文件,因此只能预览 XML 文件。ShowFileDetails()遍历选定的文件(files全局变量)并检查每个File对象的type属性。对于 XML 文件,type 属性返回text/xml;只有这样的文件名才会显示为锚点。其他文件类型显示为<span>元素,因此无法预览。
jQuery hover()方法将鼠标指针进入和离开超链接时调用的两个处理函数绑定到超链接元素。ShowPreview()函数负责在工具提示中显示 XML 文件的简短预览。这是通过设置相应锚元素的title属性来实现的。清单 9-29 显示ShowPreview()。
清单 9-29。 ShowPreview()功能
function ShowPreview(evt) { evt.stopPropagation(); evt.preventDefault(); var reader = new FileReader(); $(reader).bind("load", function (e) { var xmlData = e.target.result; if (xmlData.length > 500) { xmlData = xmlData.substr(0, 500); } $(evt.target).attr('title', xmlData); }); var fileIndex = $(evt.target).attr('data-file-index'); reader.readAsText(files[fileIndex]); }
ShowPreview()创建一个FileReader对象。因为 XML 文件本质上是文本文件,所以使用了readAsText()方法来读取文件。FileReader的load事件处理程序使用result属性访问 XML 文件内容。XML 文件可能非常大,出于预览目的,只提取文件的一部分(最多 500 个字符)。然后将底层锚元素的title属性设置为提取的 XML 数据。
在这种情况下,HidePreview()函数不会做任何特殊的事情,因为当鼠标指针离开一个超链接时,浏览器会自动隐藏工具提示。
将 XML 文件上传到服务器的任务由UploadFiles()函数完成,如清单 9-30 所示。
清单 9-30。 UploadFiles()功能
function UploadFiles() { var data = new FormData(); for (var i = 0; i < files.length; i++) { if (files[i].type == "text/xml") { data.append(files[i].name, files[i]); } } $.ajax({ type: "POST", url: "/Upload/UploadFiles", contentType: false, processData: false, data: data, success: function (result) { $("#errors").empty(); $("#errors").html(result); }, error: function () { alert("There was error uploading files!"); } }); }
UploadFile()遍历所选文件,仅将 XML 文件追加到一个FormData对象。然后,$.ajax()方法向Upload控制器的UploadFiles()动作方法发出POST请求。contentType和processData选项和以前一样设置为false。如果有任何模式验证错误,它们会显示在一个<div>元素中。
保存和验证 XML 文件的UploadFiles()动作方法如清单 9-31 所示。
清单 9-31。 UploadFiles()动作方法
[HttpPost] public JsonResult UploadFiles() { if (Request.Files.Count > 0) { HttpFileCollectionBase files = Request.Files; foreach (string key in files) { HttpPostedFileBase file = files[key]; ` string fileName = file.FileName;
fileName = Server.MapPath("~/Content/Uploads/" + fileName);
file.SaveAs(fileName);
** XmlReaderSettings settings = new XmlReaderSettings();** ** settings.Schemas.Add("", Server.MapPath("~/Content/Employees.xsd"));** ** settings.ValidationType = ValidationType.Schema;** ** settings.ValidationEventHandler += OnValidationError;** ** XmlReader reader = XmlReader.Create(fileName, settings);** ** while (reader.Read())** ** {** ** }** ** reader.Close();** } } Response.ContentType = "text/plain"; StringBuilder sb = new StringBuilder(); sb.Append("
- ");
foreach (string error in errors)
{
sb.Append("
- " + error + " "); } sb.Append("
void OnValidationError(object sender, ValidationEventArgs e) { string fileName = Path.GetFileName(((XmlReader)sender).BaseURI); errors.Add(fileName + " encountered an error - " + e.Exception.Message); }`
清单 9-31 中保存上传的 XML 文件的代码与前面的例子相同。粗体标记的代码负责根据 XSD 模式文件来验证 XML 文件:Employees.xsd。Employees.xsd模式文件期望 XML 标记的格式如清单 9-32 所示。
***清单 9-32。*样本 XML 文件
<?xml version="1.0" encoding="utf-8" ?> <employees> <employee employeeid="1"> <firstname>Nancy</firstname> <lastname>Davolio</lastname> <homephone>(206) 555-9857</homephone> <notes> <![CDATA[...]]> </notes> </employee>
虽然这个例子使用了一个固定的模式文件,但是您可以根据条件选择一个模式文件。XmlReader类用于读取 XML 文档。XmlReaderSettings类将Employees.xsd附加到XmlReader上。当您调用XmlReader类的Read()方法时,会读取 XML 文档并根据模式对进行验证。如果有任何验证错误,就会引发ValidationEventHandler事件。事件处理函数OnValidationError将 XML 文件名和错误消息存储在一个通用列表中。
一旦所有上传的 XML 文件都通过了验证,代码就会遍历一般的错误列表,并创建一个错误消息列表。错误列表通过转换成 JSON 格式返回给 jQuery success函数。
要测试这个应用,运行它并尝试上传 XML 文件——一些匹配模式,一些违反模式(例如,保留一些没有employeeid属性的<employee>元素)。对于任何无效的 XML 文件,您都应该得到错误消息。
总结
HTML5 的文件 API 允许您读取驻留在用户本地文件系统中的文件。但是,用户必须使用文件字段控件的“打开文件”对话框或通过将文件从 Windows 资源管理器拖放到网页的预定义区域来显式选择文件。
文件 API 由三个主要对象组成:File、FileList和FileReader。File对象给出了关于文件的信息,比如它的名称、大小和 MIME 类型。FileList对象是File对象的集合,通过文件字段的files属性或dataTransfer对象的files属性获得。FileReader对象让您以异步方式读取选定的文件。
虽然拖放是 HTML5 的一个独立功能,但它可以与文件 API 结合使用,以增强用户体验。可以使用$.ajax()方法将选定的文件上传到服务器。
下一章介绍了另一个有趣的特性——Web 工作器——它允许您在后台运行代码。Web 工作器 就像多线程桌面应用中使用的线程,它们允许您在后台运行冗长的进程,而不会妨碍用户界面。