HTML5数据存储

Published on 2016 - 11 - 11

Web应用中的数据可以保存到两个地方,一个是Web服务器,一个是Web客户端(用户的计算机)。这两个地方各自都有适合保存的数据。

Web服务器适合保存敏感信息,以及那些你不希望被别人篡改的数据。比如,在网上书店里购书时,所有交易信息都会保存到Web服务器上。你自己计算机中保存的数据只有那么一丁点,书店网站要用该数据判断你是谁(这样才能知道哪个购物车是你的)。即使有了HTML5,这种套路也不能改变——服务器安全、可靠、高效。

然而,服务器端存储也并非适合所有网站。有时候,把一些不太重要的信息放在用户计算机上相对会更方便。比如,把用户偏好(影响网页显示方式的设置)和应用状态(相当于Web应用某个瞬间的快照,保存后可以方便用户将来返回该状态)放在用户本地就比较合乎情理。

在HTML5之前,本地存储的唯一方案就是使用cookie。而最初发明cookie的目的,是为了在浏览器和服务器之间传送身份信息。利用cookie保存少量数据绝对方便,可是操作它们的JavaScript语法多少有点不够人性化。但cookie也有不好的一面,那就是必须处理过期数据,而且要跟着每一次请求来来回回地发送和接收这些没有用的数据。

HTML5新增了更好的本地存储功能,让我们在访客的计算机上保存数据更加方便。这些数据可以无限期地保存在用户计算机上,不会发送到服务器(除非你自己发送),有充裕的空间存储它们,而且能够通过几个简单的JavaScript对象对它们进行操作。这个叫做Web 存储(Web Storage)的新功能特别适合开发离线应用。离线应用的数据可以“自给自足”,无论用户能否上网,都可以在本地保存用户信息。

本文将带领大家探索Web存储功能的各个方面。另外,还会介绍一个更新的标准,支持该标准的浏览器能够从计算机硬盘的其他文件中读取数据。

Web存储简介

HTML5的这个Web存储功能,其实就是让网页在用户计算机上保存一些信息。这些信息可以是临时的(浏览器一关,就自动删除),也可以是长期的(多少天之后再打开网站,仍然可以访问它们)。

注意 Web存储这个名字误人不浅啊,本来网页保存的信息根本就不在Web(网)上,而是实实在在地保存在用户的计算机上,从来不会离开。

Web存储又分两种,分别对应两个JavaScript对象。

  • 本地存储,对应localStorage对象,用于长期保存整个网站的数据。也就是说,如果有一个网页利用本地存储保存了数据,那么访客在一天后、一星期后,甚至一年之后再上线,该数据仍然还会在那儿。当然,多数浏览器都会提供一种机制,让用户可以清除本地存储空间中的数据。有些浏览器只提供一个命令,要么一点不能删,要删就全都删除,就跟过去清除cookie的方法一样。(事实上,某些浏览器的实现是将本地存储和cookie放在一起的,因此要清除本地存储数据必须清除cookie。)另一些浏览器会让用户按照站点检查数据,然后有选择地清除本地存储的数据。

    • 会话存储,对应sessionStorage对象,用于临时保存针对一个窗口(或标签页)的数据。在访客关闭窗口或标签页之前,这些数据是存在的,而关闭之后就会被浏览器删除。不过,只要用户不关闭窗口或标签页,就算他从你的网站跑到人家的网站然后又回来,这些数据还会在。

提示 从页面代码的角度说,本地存储和会话存储的操作完全相同。它们的区别仅在于数据的寿命。本地存储主要用于保存访客将来还能看到的数据,而会话存储则用于保存那些需要从一个页面传递给下一个页面的数据。(当然,使用会话存储也可以保存只在一个页面中使用的数据,但这个任务就算普通的JavaScript变量也绝对可以胜任,又何必多此一举呢。)

无论本地存储还是会话存储,都是与网站所在的域联系在一起的。换句话说,如果利用本地存储保存数据的页面是www.GoatsCanFloat.org/game/zapper.html,那么另一个页面www.GoatsCanFloat.org/contact.html也可以访问该数据,因为这两个页面在同一个域中(www.GoatsCanFloat.org)。如果是另一个不同网站的页面,就不能访问该数据了。

同样,由于数据保存在用户的计算机上(或移动设备上),这些数据也是跟计算机绑定的;网页不能访问保存在其他用户计算机上的数据。类似地,如果你用不同的用户名登录自己的计算机,或者使用不同的浏览器,那么存取的也将是不同的本地存储数据。

注意 尽管HTML5没有硬性规定存储空间的上限,但大多数浏览器都把本地存储限制为5 MB以下。这已经足够保存很多数据了,但如果你想利用本地存储来缓存大图片或视频文件,恐怕这个限制会让你捉襟见肘(毕竟,这也不是设计本地存储的目的)。假如你需要更大的空间,可以考虑尚未定案的IndexedDB数据库标准。IndexedDB的起始空间就是50 MB,如果用户同意还可以扩展。

存储数据

要把一段信息保存到本地存储或会话存储中,首先要为该信息想一个名字。这个名字叫做键,将来要通过它来取回数据。

存储数据的语法如下:

localStorage[keyName] = data;

举个例子,假设你想保存用户名,那么这个键就可以叫做user_name:

localStorage["user_name"] = "Marky Mark";

当然,像这样保存硬编码的数据没有多大意思。更多情况下,可以保存动态数据,比如当前日期、数学计算的结果,或者用户在文本框中输入的某些文本,等等。下面就是一个保存动态数据的例子:

//取得文本框
var nameInput = document.getElementById("userName");

//保存文本框中的文本
localStorage["user_name"] = nameInput.value;

读取本地存储中的数据跟保存数据一样简单。例如,下面这行代码会读出前面保存的用户名,然后通过警告框显示出来:

alert("You stored: " + localStorage["user_name"]);

无论这个名字是5秒钟前保存的,还是5分钟前保存的,这行代码都管用。

当然,有可能这个键下面尚未保存任何数据。要检测某个键的值是否为空,可以直接测试它是否等于null。请看下面的例子:

if (localStorage["user_name"] == null) {
  alert ("You haven't entered a name yet.");
}
else {
  //把用户名放到文本框中
  document.getElementById("userName").value = localStorage["user_name"];
}

会话存储也一样简单。唯一的区别是要使用sessionStorage对象,而不是localStorage对象:

//取得当前日期
var today = new Date();

//以文本形式保存格式为HH:mm的时间
sessionStorage["lastUpdateTime"] = today.getHours() + ":" + today.get-
Minutes();

图1展示了一个页面,利用了刚才介绍的这些概念。

页面中有两个文本框,一个针对会话存储,另一个针对本地存储。单击Save Data,页面会保存方框中的值。再单击Load Data,又会把刚才保存的数据读回来。
注意 Web存储还支持以属性方式读写数据,但不太常用。在使用这种语法时,读取键为user_name的数据段,要使用localStorage.user_name,而不是localStorage["user_name"]。当然,两种语法都可以,具体使用哪种,就看你自己了。

没有Web服务器则不能使用Web存储

在测试Web存储时,可能会遇到一个意想不到的问题。在很多浏览器中,只有从Web服务器上打开的页面才能读写Web存储。无论这个Web服务器是远程的还是本地的——关键就是不能从本地硬盘打开页面。

这个问题的根源在于浏览器要限制Web存储的空间大小。如前所述,每个网站的存储上限是5 MB。为了达到这一目的,就必须把每个使用本地存储的页面与一个网站(域)关联起来。

那如果我就是从本地硬盘打开一个使用Web存储的页面,结果会怎么样?结果要视情况而定。在Internet Explorer中,浏览器好像会完全不支持Web存储功能一样。因为,localStorage和sessionStorage对象不见了,访问它们的JavaScript代码会报错。在Firefox中,localStorage和sessionStorage对象还在,似乎还支持Web存储(即使Modernizr也会认为支持),但任何读写操作都会静默地失败。而在Chrome中,结果又不一样——大多数Web存储的功能一如既往,只有少数功能(如onStorage事件)无效。在使用File API的时候,也会遇到类似的问题。因此,在测试之前最好先把页面放到你自己的Web服务器中,从而避免意外发生。

实战:保存游戏中的最后位置

此时此刻,或许你会想:Web存储不过如此啊,也就是能用方括号语法记录人名而已。你说的没错。但Web存储还有更实际的用途,而且照样不必多费力气。

以我们讲画布时介绍的迷宫游戏为例。要想一次就走出迷宫并不容易,所以有必要在用户关闭窗口或打开新页面时记录当前位置。这样,用户再回来迷宫页面后,就可以把笑脸图标放在上一次的位置上。

要实现这个功能,有几种方案可以选择。可以每移动一次就保存一次新位置。本地存储的速度非常快,因此这样做没有问题。或者,可以响应页面的onBeforeUnload事件,询问游戏玩家是否要保存当前位置(如图2所示)。

以下就是实现页面建议保存位置的代码:

window.onbeforeunload = function(e) {
  //检测localStorage对象是否存在
  //(如果浏览器不支持Web存储,还怎么保存?)
  if (localStorage) {

    //询问是否保存位置
    if (confirm(
    "Do you want to save your current position in the maze, for next time?")) {
      //保存两个坐标值
      localStorage["mazeGame_currentX"] = x;
      localStorage["mazeGame_currentY"] = y;
    }
  }
}

提示 建议你也像这个例子一样选择长键名(如mazeGame_currentX)。因为必须保证键名的唯一性,这样网站的两个页面就不会意外地使用同一个键保存不同的数据。在只有一个存储空间的系统中,很容易发生命名冲突,而这也正是Web存储的一个弱点。为了避免这个问题,最好提前做个规划,选择一种有逻辑性、能自解释的键名命名方案。例如,假设另一个页面也有一个迷宫游戏,那么可以考虑将页面名也放到键名中,比如Maze01_currentX。

这样,当下次再打开迷宫游戏页面时,就可以检查该信息是否存在:

//支持本地存储功能吗?
if (localStorage) {
  //取得数据
  var savedX = localStorage["mazeGame_currentX"];
  var savedY = localStorage["mazeGame_currentY"];

  //如果变量为null,则说明没有保存的数据
  //否则,用保存的数据设置新坐标
  if (savedX != null) x = Number(savedX);
  if (savedY != null) y = Number(savedY);
}

这个例子展示了如何保存应用状态。如果你不想让用户每次离开游戏页面都看到同样的提示消息,可以添加一个“自动保存位置”复选框。然后,只要用户勾选复选框就保存位置信息。当然,你还得多保存一个复选框的值,而这也就是保存应用偏好的例子了。

浏览器对Web存储的支持情况

Web存储是支持情况比较好的HTML5功能,所有现代浏览器都支持它。表1列出了支持Web存储的浏览器及最低版本号。

要求 IE Firefox Chrome Safari Opera Safari iOS Android
最低版本 8 3.5 5 4 10.5 2 2

最大的问题是IE7,因为它根本不支持Web存储。而要解决这个问题,可以用cookie来模拟Web存储。虽然不完美,但却可行。虽然没有官方的脚本可以帮你实现模拟,但在GitHub“腻子脚本”页面(http://tinyurl.com/polyfill)的“Web Storage”部分,还是可以找到不少有用的东西。

深入Web存储

现在我们已经掌握了Web存储的基础知识,包括保存和读取数据。可是,在学以致用之前,还有一些重要的知识点以及有用的技术需要再掌握一下。接下来的几节会介绍怎么从Web存储中删除数据项,怎么检索当前保存的所有数据。另外,还会介绍如何处理不同的数据类型、保存自定义对象和响应存储数据变化。

删除数据项

这个任务已经简单得不可能再简单了。只要调用removeItem()方法,传入键名,就可以删除不想要的数据项:

localStorage.removeItem("user_name");

要不然,就调用更厉害的clear()方法,清空网站在本地保存的会话数据:

localStorage.clear();

查找所有数据项

要搜索某一个数据项,只要知道键名即可。但这里会再告诉你一个更有意思的技巧:不必知道任何键名,使用key()方法从本地或会话存储中取得(当前网站保存的)所有数据项。这个技巧非常适合调试排错。当然如果你想知道其他页面都保存了哪些数据,或者都使用了什么样的键名,也可以使用它。

图3展示了一个实际使用这个技巧的例子。

在这个例子中,单击按钮会执行findAllItems()函数,该函数会遍历本地存储中的所有数据项,其代码如下:

function findAllItems() {
  //取得用于保存数据项的<ul>元素
  var itemList = document.getElementById("itemList");

  //清除列表
  itemList.innerHTML = "";

  //遍历所有数据项
  for (var i=0; i<localStorage.length; i++) {
    //取得当前位置数据项的键
    var key = localStorage.key(i);

    //取得以该键保存的数据值
    var item = localStorage[key];

    //用以上数据创建一个列表项
    //添加到页面中
    var newItem = document.createElement("li");
    newItem.innerHTML = key + ": " + item;
    itemList.appendChild(newItem);
  }
}

保存数值和日期

到目前为止,我们还遗漏了关于Web存储的一个重要细节。那就是在通过localStorage和sessionStorage保存数据时,该数据会自动被转换为文本。

对于本来就是文本的数据(如在文本框中输入的用户名),这当然没问题。但数值就不一样了。如果忘了把文本转换成数值,那么就会碰到下面所示的问题:

//取得x坐标
//假设保存为文本"35"
x = localStorage["mazeGame_currentX"];

//给坐标加上一个数值
//然而,JavaScript会把"35"+"5"转换成"355"
x += 10;

这显然不是你想要的结果。因为这会导致笑脸跳到一个错误的位置上,甚至会跳出迷宫。

问题在于,JavaScript认为你是想把两段文本拼起来,而不是要执行数学计算。要解决这个问题,就需要给JavaScript一个提示,告诉它你想计算两个数值的加法。办法有很多,但使用Number()函数就很好:

x = Number(localStorage[mazeGame_currentX]);

//这样,JavaScript就可以正确地计算35+5,返回40
x += 10;

文本和数值还算容易处理的。如果你想在Web存储中保存其他类型的数据,就要多加留意。有些数据类型有方便的转换方法。比如,像下面这样保存日期:

var today = new Date();

结果并不会保存日期对象,而是会保存一个文本字符串。比如Sat Jun 09 2011 13:30:46。可是,要把这样的文本转换回日期对象可不容易。若没有日期对象,也就不能以相同方式来操作日期,比如不能调用日期对象的方法执行日期计算。

为解决这个问题,可以先按照既定的格式把日期转换成相应的文本,然后再根据取得的文本创建日期对象。下面就是一个例子:

//创建日期对象
var today = new Date();

//按照YYYY/MM/DD的标准格式把日期转换成文本字符串
//然后保存为文本
sessionStorage["session_started"] = today.getFullYear() + "/" +
 today.getMonth() + "/" + today.getDate();

...

//取得日期文本,并基于该文本创建新的日期对象
//这是因为文本格式是有效的日期形式
today = new Date(sessionStorage["session_started"]);

//使用日期对象的方法,比如getFullYear()
alert(today.getFullYear());

运行以上代码,会弹出一个显示年份的消息框,也就说明你重新创建了日期对象。

保存对象

上一节介绍了在Web存储中保存数值和日期时会把它们转换成文本,而将来使用时还要再转换回去。这些转换都有JavaScript函数的帮助,首先是Number()函数,然后是文本到日期转换时的一些技巧,依靠日期对象固有的方法。然而,还有很多其他对象不能这样转换,比如自定义对象。

比如人格测试,那个例子使用了两个页面。测试者在第一个页面回答问题然后得到一个分数,而第二页会显示测试结果。当时为在两个页面间传递数据,使用了嵌入在URL中的查询字符串参数。这是传统HTML中的做法(当然使用传统的cookie也可以)。但有了HTML5,利用本地存储共享数据才是最佳方案。

可是也有一个难题。测试数据包含5个数据,分别对应五个人格因素。当然,可以分别保存这5个数值,但要是能把所有人格因素保存到一个自定义对象中,岂不更简单明了?为此我们可以定义一个PersonalityScore对象:

function PersonalityScore(o, c, e, a, n) {
  this.openness = o;
  this.conscientiousness = c;
  this.extraversion = e;
  this.agreeableness = a;
  this.neuroticism = n;
}

定义了PersonalityScore对象之后,只要1个数据项(而非5个)就可以保存所有数据。

为了把自定义对象保存到Web存储中,必须先把对象转换成文本形式。要自己写转换代码,那麻烦可就大了。好在JavaScript有一个更简单的、标准化的机制叫JSON 编码。

JSON(JavaScript Object Notation,JavaScript对象表示法)是把结构化数据——类似封装在对象中的那些值——转换为文本的一种简便格式。而且浏览器原生支持JSON编码。也就是说,直接调用JSON.stringify(),就可以把任何对象连同其数据转换为文本形式。调用JSON.parse()则可以把文本转换回对象。以下就是在测试中转换PersonalityScore对象的代码。在测试者提交答案时,页面会计算分数(但不显示),创建对象,保存它,然后再打开新页面:

//创建PersonalityScore对象
var score = new PersonalityScore(o, c, e, a, n);

//将其保存为方便的JSON格式
sessionStore["personalityScore"] = JSON.stringify(score);

//转到结果页
window.location = "PersonalityTest_Score.html";

到新页面后,再从会话存储中取出JSON文本,使用JSON.parse()方法将其转换回对象。以下就是相应的代码:

//JSON文本转换为原来的对象
var score = JSON.parse(localStorage["personalityScore"]);

//从对象中取得数据
lblScoreInfo.innerHTML = "Your extraversion score is " + score.extraversion;

响应存储变化

Web存储也为我们提供了在不同浏览器窗口间通信的机制。具体来说,就是在本地存储或会话存储发生变化时,其他查看同一页面或者同一站点中其他页面的窗口就会触发window.onStorage事件。因此,如果你在www.GoatsCanFloat.org/storeStuff.html页面中改变了本地存储,那么打开www.GoatsCanFloat.org/checkStorage.html页面的窗口会触发onStorage事件。(当然,必须是同一台计算机中相同的浏览器打开的页面,这一点你已经知道了。)

所谓存储变化,指的就是向存储中添加新数据项,修改既有数据项,删除数据项或清除所有数据。但是,那些对存储不产生任何影响的操作(比如用既有的键名保存相同的值,或者清除原本就是空的存储空间),不会引发onStorage事件。

下面看看图4所示的页面。可以在这个页面中向本地存储添加任何数据项,只要在相应的文本框中输入键和值即可。保存新数据项时,第二个页面就会报告保存了什么。

为了体验onStorage事件,同时打开StorageEvents1.html和StorageEvents2.html。当你在第一个页面(上)中添加或修改数据项时,第二个页面(下)会响应该事件,并报告结果(底部)

图4所示的示例涉及两个页面,第一个页面负责保存数据。在这个页面中,单击Add按钮会触发一个小函数addValue(),其代码如下:

function addValue() {
  //取得两个文本框中的值
  var key = document.getElementById("key").value;
  var item = document.getElementById("item").value;

  //在本地存储中保存数据项
  //(如果同名键已经存在,则用新值替换旧值)
  localStorage[key] = item;
}

第二个页面很简单,就是在页面加载后为window.onStorage事件添加一个处理函数,代码如下:

window.onload = function() {
  //把onStorage事件与storageChanged()函数联系起来
  window.addEventListener("storage", storageChanged, false);
};

以上代码与我们前面展示的添加事件处理程序的代码有所不同。在此,我们没有设置window.onstorage事件,而是调用了window.addEventListener()。这是为了确保代码在所有浏览器中都能运行,而这样写是最简单的形式。如果直接设置window.onstorage事件,那么这个例子在Firefox中就不能运行(因为Firefox的window对象没有onstorage属性)。

storageChanged()函数的任务很简单,只是取得更新的信息,然后通过页面中的<div>元素显示出来:

function storageChanged(e) {
  var message = document.getElementById("updateMessage");
  message.innerHTML = "Local storage updated.";
  message.innerHTML += "<br>Key: " + e.key;
  message.innerHTML += "<br>Old Value: " + e.oldValue;
  message.innerHTML += "<br>New Value: " + e.newValue;
  message.innerHTML += "<br>URL: " + e.url;
}

可见,onStorage事件提供了不少信息,包括发生变化的键和值、原来的值(oldValue)、新值(newValue)和导致此次变化的页面URL。如果onStorage事件反映的是插入新数据项,那么e.oldValue属性要么是null(在大多数浏览器中)或者空字符串(在Internet Explorer中)。

注意 如果同时打开了同一站点的多个页面,那么这些页面会依次发生onStorage事件,只有导致变化的页面(即前面例子中的StorageEvents1.html)不会发生该事件。不过,IE是个例外,它也会在导致变化的页面触发onStorage事件。

读取文件

作为HTML5的一部分,Web存储得到了很好的支持。但这并不是存取数据的唯一方式。为了实现与存储相关的不同任务,也出现了其他几种不同的标准。其中一个就是File API,从技术角度讲,它并不是HTML5规范的内容,但得到了现代浏览器较好的支持(IE除外)。

File API,这名字听起来蛮大气嘛!不知道的,还以为它是针对浏览器读写硬盘文件而制定的一个全方位的标准。然而,它可没有那么高远的志向,或者说没有那么强大。简单地说,File API只是规定怎么从硬盘上提取文件,直接交给在网页中运行的JavaScript代码。然后代码可以打开文件探究数据,无论是文本文件还是其他文件。注意,关键在于文件会被直接交给JavaScript代码。与以往的文件上传不一样,File API不是为了向服务器提交文件设计的。

另外,关于File API 不能做什么,也非常值得注意。很明显,它不能修改文件,也不能创建新文件。想保存任何数据,你都要采用其他办法,比如通过XMLHttpRequest把数据发送到服务器,或者把它保存在本地存储空间中。

说到这儿,有读者可能会认为File API不如本地存储有用。嗯,对于大多数站点而言,的确如此。可是,从某种意义上讲,File API却为HTML扩展了疆界,至少在没有插件的情况下,通过它能够走得更远。

注意 目前,File API对于某些专门网站是不可或缺的。将来,随着其功能的增强,还会变得越来越重要。比如,将来的某个版本可能会允许网页在本地硬盘上写文件,让用户通过“保存对话框”控制文件名和保存位置。Flash浏览器插件已经具备了这种能力。

取得文件

在通过File API操作文件之前,首先必须取得文件。为此,有三种方式可以选择;实际上,归根结底只有一种方式,那就是必须由访客自己选择文件然后提交给你。

这三种方式如下。

  • 使用<input>元素。将其type属性设置为file,这样就能得到一个标准的上传文件框。不过,编写一点JavaScript来利用File API,就可以在本地打开文件。

    • 隐藏的<input>元素。嫌<input>元素太难看?为了保证风格一致,可以把<input>元素隐藏起来,显示一个漂亮的按钮。用户单击按钮,就通过JavaScript调用隐藏的<input>元素的click()方法。这样就会显示标准的文件选择对话框。
    • 拖放。如果浏览器支持拖放,可以从桌面或资源管理器中把文件拖放到网页上。

接下来几节就讨论这几种方式。不过首先,有必要看看浏览器当前对File API的支持情况。只有这样才能知道是否能在自己的网站中使用它。

浏览器对File API的支持情况

File API可不像Web存储那么广受支持。表2展示了浏览器对它的支持情况。

要求 IE Firefox Chrome Safari Opera Safari iOS Android
最低版本 10 3.6 8 6 11.1 3

表中所示版本下的浏览器可以运行本文的所有例子。但是,这些浏览器几乎没有一个实现了File API的全部功能。原因是这个标准的某些部分(即那些与处理二进制Blob数据及“切分”数据块有关的功能)还有可能变化。

由于File API需要一些比普通网页更高的权限,所以通过JavaScript来填充这些“空白的”功能是不现实的。为此,可以选择Flash或Silverlight插件。比如,访问https://github.com/MrSwitch/dropfile,可以找到一个“腻子脚本”,该脚本利用Silverlight拦截拖放过来的文件,打开并将其内容交给网页中的JavaScript代码。

读取文本文件

使用File API可以直接读取文本文件的内容。图5展示了一个例子,是一个页面读取了一个网页文件中的标准,然后显示出来。

单击Browse按钮(或Choose File,在Chrome中),选择一个文件,然后单击OK。无需上传,网页中的JavaScript就能取得文本文件,把内容复制到页面中

要创建这个例子,首先要使用一个<input type="file">元素,这样就能得到文本框和浏览器按钮:

<input id="fileInput" type="file" onchange="processFiles(this.files)">

不过,与往常属于<form>元素并且会将文件发送给Web服务器的<input>元素不同,这个<input>有自己处理文件的方式。访客选择了一个文件后,就会触发这个<input>元素的onChange事件,因而就会执行processFiles()函数。这个函数将会通过JavaScript来打开文件。

下面我们就来一行一行地分析processFiles()函数。这个函数首先必须从<input>元素提供的文件集合中取得第一个文件。除非你允许用户选择多个文件(使用multiple属性),否则文件集合中只会有一个文件,而该文件在集合中的索引就是0:

function processFiles(files) {
  var file = files[0];

注意 每个文件对象都有三个有用的属性:name属性保存文件名(不包含路径),size属性保存文件的字节大小,而(如果可以确定的话)type属性保存文件的MIME类型。可以分别读取这三个属性,然后加入判断,比如拒绝处理超过一定大小的文件,或者只允许某种类型的文件。

然后,创建FileReader对象,以便处理文件:

  var reader = new FileReader();

紧接着,差不多就可以调用FileReader的方法来提取文件内容了。但这个对象的方法都是异步的,也就是说可以不必等待数据而立即读取。要取得文件内容,首先要处理onLoad事件:

  reader.onload = function (e) {
    //这个事件发生,意味着数据准备好了
    //把它复制到页面的<div>元素中
    var output = document.getElementById("fileOutput");
    output.textContent = e.target.result;
  };

最后,在这个事件处理程序之后,调用FileReader的readAsText()方法:

  reader.readAsText(file);
}

这个方法会把文件内容转换成一个长字符串,保存在发送给onLoad事件的e.target.result中。

readAsText()方法只能处理包含文本内容(而不是二进制内容)的文件。HTML文件当然没有问题,图5展示的就是读取HTML文件的结果。CSV格式也是各种有用的纯文本格式中的一种,它是所有电子表格程序都支持的一种导出格式。XML也是纯文本格式,它是程序间交换数据的一种标准。(XML也是Office XML格式的基础,因此可以使用readAsText()方法直接处理.docx和.xlsx文件。)

readAsText()只是众多读取文件的方法之一。FileReader对象提供的方法还有:readAsBinaryString()、readAsDataURL()和readAsArrayBuffer();Firefox尚未支持最后一个方法。

其中,readAsBinaryString()方法可以让应用处理二进制编码的数据,但基本上就是把数据保存到一个文本字符串中,效率不高。如果你真想要解释二进制数据,恐怕就要处理好特别复杂的编码问题。规范中更好的支持方案是“切分”出一小段二进制数据,以便每次只处理一部分内容。

而readAsDataURL()方法则让我们能方便地取得图片数据。

 替换标准上传控件

Web开发人员一致认为,用于提交文件的标准<input>控件非常难看。虽然必须得用它,但实际上可以不让任何人看见它。换句话说,就是像下面这样把它隐藏起来:

#fileInput {
  display: none;
}

接着再添加一个新的按钮,用于触发提交操作。一个普通的按钮就可以胜任,而且可以任意修改其外观:

<button onclick="showFileInput()">Analyze a File</button>

最后一步是处理按钮单击事件,通过该事件来手工调用隐藏的<input>元素的click()方法:

function showFileInput() {
  var fileInput = document.getElementById("fileInput");
  fileInput.click();
}

这样,单击按钮就可以运行showFileInput()函数,该函数会模拟单击隐藏的Browse按钮,并显示出对话框供访客选择文件。访客选择了文件后,又会触发隐藏的<input>元素的onChange事件,于是processFiles()函数运行,一切跟以前一样。

一次读取多个文件

没有理由限制用户一次只能提交一个文件。HTML5也支持一次提交多个文件,只要为<input>元素添加multiple属性即可:

<input id="fileInput" type="file" onchange="processFiles(this.files)"
multiple>

这样,用户就可以在打开的对话框中一次选择多个文件了(比如在Windows中按Ctrl键并单击多个文件,或者用鼠标拖出一个选择框)。支持选择多个文件,代码也要相应修改。换句话说,不能再像前面例子中那样,只取得集合中的第一个文件了。这次,要使用for循环来依次处理每个文件:

for (var i=0; i<files.length; i++) {
  //取得下一个文件
  var file = files[i];

  //为这个文件创建FileReader对象,然后运行相同的代码
  var reader = new FileReader();
  reader.onload = function (e) {
    ...
  };
  reader.readAsText(file);
}

读取图片文件

前面我们看到了,FileReader处理文本内容只需要一步。同样,处理图片内容也这么简单,而这就要归功于readAsDataURL()方法。

图6展示了一个涉及两项功能的例子:处理图片和文件拖放。提交的图片文件用于绘制元素的背景。当然也可以把图片绘制到画布中,然后利用画布的原始像素处理功能来修改图片。综合利用该技术,可以让用户把图片拖到页面中,然后在图片上绘制或者修改图片,最后再使用XMLHttpRequest调用把结果上传到服务器。
 

要创建这个页面,首先要添加一个元素,以捕获拖放过来的文件。在我们的例子中,这个元素是一个名为dropBox的<div>

<div id="dropBox">
  <div>Drop your image here...</div>
</div>

写几行简单的样式声明,就可以为其设置好指定大小、边框和颜色:

#dropBox {
  margin: 15px;
  width: 300px;
  height: 300px;
  border: 5px dashed gray;
  border-radius: 8px;
  background: lightyellow;
  background-size: 100%;
  background-repeat: no-repeat;
  text-align: center;
}

#dropBox div {
  margin: 100px 70px;
  color: orange;
  font-size: 25px;
  font-family: Verdana, Arial, sans-serif;
}

眼光敏锐的读者肯定已经发现了background-size和background-repeat属性。这两个属性是为了接下来的功能作准备的。当把图片拖放到这个<div>上时,图片会作为它的背景。而background-size属性是为了缩小图片以全部显示,background-repeat属性则是为了不让图片重复显示。

为了处理放置文件的操作,需要处理三个事件:onDragEnter、onDragOver和onDrop。页面一加载完成,就会为这三个事件添加处理程序:

var dropBox ;

window.onload = function() {
  dropBox = document.getElementById("dropBox");
  dropBox.ondragenter = ignoreDrag;
  dropBox.ondragover = ignoreDrag;
  dropBox.ondrop = drop;
};

其中,ignoreDrag()函数同时处理onDragEnter和onDragOver事件,前者在鼠标指针进入放置区时发生,后者在拖动文件的鼠标指针位于放置区之上时发生。之所以用同一个函数处理两个事件,原因就是不必对这两个事件作出反应,只要告诉浏览器自己什么也不做即可。这个函数的代码如下:

function ignoreDrag(e) {
  //因为我们在处理拖放,所以应该
  //确保没有其他元素会取得这个事件
  e.stopPropagation();
  e.preventDefault();
}

我们要响应的事件是onDrop,这个事件一发生,就说明要取得和处理文件了。不过,由于存在两种向页面提交文件的方式,所以drop()函数调用了实际上负责处理的processFiles()函数:

function drop(e) {
  //取消事件传播及默认行为
  e.stopPropagation();
  e.preventDefault();

  //取得拖进来的文件
  var data = e.dataTransfer;
  var files = data.files;
  //将其传给真正的处理文件的函数
  processFiles(files);
}

最后一个函数就是processFiles(),它会创建一个FileReader,为其onload事件添加一个函数,然后调用readAsDataURL()将图片转换为数据URL。

注意 正如在学习Canvas时所提到的,数据URL是一种用长字符串表示图片的方式。这种方式让传递图片数据变得十分方便。为了在网页中显示图片,可以将<img>元素的src属性设置为图片URL,也可以将CSS的background-image属性设置为图片URL(像这个例子中一样)。

function processFiles(files) {
  var file = files[0];

  //创建FileReader
  var reader = new FileReader();

  //告诉它在准备好数据URL之后做什么
  reader.onload = function (e) {
    //使用图像URL来绘制dropBox的背景
    dropBox.style.backgroundImage = "url('" + e.target.result + "')";
  };

  //读取图片
  reader.readAsDataURL(file);
}

FileReader还有其他事件,在读取图片文件的过程中可以选择使用。如果读取图片的时间比较长,可以通过onProgress事件(间歇性地触发)来确定已经加载了多大比例。(可以调用FileReader的abort()方法取消未完成的操作。)如果打开或读取文件时发生错误,会触发onError事件。而在操作完成时,则触发onLoadEnd事件(包括由于错误导致的终止)。

参考文档