HTML5新增了很多特性,其中File API是非常重要的部分。在肉大师中,我大量使用了HTML5的文件API,这样一来可以给予用户近乎桌面软件的体验,二来还能减少服务器和带宽的消耗。今天终于把最后几个问题解决了,在这里总结下HTML5 File API的使用。
用途
在W3C页面上,列出了File API可能用到的场合(以下为意译,可能有所偏颇,欢迎对比原文阅读):
- 断点续传
- 上传时,先把目标文件复制到本地沙箱,然后分解逐块上传
- 浏览器崩溃或者网络中断也没关系,因为恢复后可以续传
- 需要大量媒体素材的应用,比如视频游戏
- 下载压缩包,在本地解压,就能恢复之前目录结构
- 跨平台
- 通过渐进式下载,进入新关卡或者开启新功能均无需等待,因为玩的时候所需素材已经通过后台下载完成了
- 从本地缓存中直接读取素材,速度飞快
- 二进制文件也不在话下
- 使用压缩包可以大大减轻带宽和服务器消耗,也避免了频繁下载碎片文件带来的检索问题
- 离线图片/音频编辑器通
- 不怕频繁读写大量数据
- 只想重写文件的某些部分也能做到(比如修改ID3或者EXIF信息)
- 创建目录组织项目后用起来舒服多了
- 编辑完的文件还能被iTunes、Picasa之类的本地应用访问
- 离线视频播放器
- 下载超过1G的大文件,将来想看再看
- 可以在不同时间点间来回跳转播放
- 能够给Video标签提供URL
- 即便片子还没下完,也能把下载到的部分先睹为快
- 还能任意截取一段视频交给Video标签播放
- 离线邮件客户端
- 下载保存附件到本地自不必说
- 断网的情况下,可以缓存用户要上传的附件,以后再上传
- 需要时可以列出缓存里的附件,通过缩略图显示,预览后上传
- 能像正常服务器那样触发标准的下载动作
- 不仅能使用XHR一次性上传全部内容,还可以把邮件和附件拆解成小块依次发送
听起来都是些令人振奋的功能,实际用起来还是要踩点坑。下面就把我的经验分享一下。
FileReader
很多浏览器都实现了FileReader,关于它的教程和文章很多,而且常与同样被HTML5引入的DND(Drag & Drop)API连用,以支持“上传图片文件前先预览”的功能。比如:
- NATIVE HTML5 DRAG AND DROP
- Reading local files in JavaScript
- HTML5 Drag and Drop Upload and File API Tutorial
所以这个主题我就不再多着笔墨去写了,值得注意的有以下几点:
- 只有<input type=”file” />和拖拽文件后可以获得FileList,无法通过URL直接读取文件(后面要说到的本地文件可以)
- FileList形似数组,可以用脚标取元素,而且有length属性,但它本身并不是数组,不支持concat和slice等方法,要操作只能遍历
本地文件
这里必须解释一下“本地文件”的概念。本地文件是HTML5本地存储的一部分(不特指那个localStorage),本地存储现在分为4个等级:
- Cookie,最古老,100K限制,通用
- localStorage,新增,各浏览器的容量限制不等,采用“键-值”对应的方式存储,按域划分沙箱,不能跨域读写数据
- 数据库,新增,容量限制不等,标准不一
- 本地文件,新增,目前只有Chrome 12+支持;全体域的临时文件共享1G空间,每个域需单独请求持久化存储空间;不能跨域操作,但同域下本地文件和远程文件之间不算跨域
这里的“本地文件”并不是我们通常意义上说的文件,没有直接存放在操作系统的文件体系中,直接搜索文件名是找不到的。Windows下可以在C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\File System找到它的真身,是索引文件+大文件,其读写只能用JavaScrip操作。可以用“filesystem:http://domain.com/temporary/文件名”(假设域名是domain)来访问,比如:
1
2
3
4
|
// 经本地环境下的js写入的文件
// 经远程环境下的js写入的文件
|
本地环境下,这个API同样受到限制,需要在Chrome启动时添加参数: –allow-file-access-from-files 方可正常使用。
后面几节的内容基本都是我从 EXPLORING THE FILESYSTEM APIS 和 File API: Directories and System 中学到的。目前关于本地文件API的介绍很少,应用也不多(毕竟仅限Chrome),不过看看那些令人心动的应用场景,相信不久我们就能在更多浏览器里看到它的身影了。
接下来,我们还是边看代码边进行吧。操作本地文件前需要请求空间:
1
2
3
4
5
6
|
// 判断是何种浏览器,使用不同的函数
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
// type 类型,<code>TEMPORARY</code> 临时,所有文件共享1G空间;或者PERSISTENT 永久,需单独请求
// size 容量,单位是字节
// 成功失败两个回调函数,后面仍会大量出现
window.requestFileSystem(type, size, successCallback, opt_errorCallback);
|
请求空间成功后,会调用成功的回调函数,并传入FileSystem的实例,我们可以把它存起来,以备后用。
1
2
3
|
function
fileSystemReadyHandler(fs) {
fileSystem = fs;
}
|
错误的回调函数写一个通用的就行了,以后几乎每次都要用到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
function
errorHandler(e) {
var
msg =
''
;
switch
(e.code) {
case
FileError.QUOTA_EXCEEDED_ERR:
msg =
'QUOTA_EXCEEDED_ERR'
;
break
;
case
FileError.NOT_FOUND_ERR:
msg =
'NOT_FOUND_ERR'
;
break
;
case
FileError.SECURITY_ERR:
msg =
'SECURITY_ERR'
;
break
;
case
FileError.INVALID_MODIFICATION_ERR:
msg =
'INVALID_MODIFICATION_ERR'
;
break
;
case
FileError.INVALID_STATE_ERR:
msg =
'INVALID_STATE_ERR'
;
break
;
default
:
msg =
'Unknown Error'
;
break
;
};
console.log(
'Error: '
+ msg);
}
|
请求临时空间(TEMPORARY)就这么简单;请求持久化存储空间(PERSISTENT)稍微复杂些,因为临时存储是所有应用共享1G空间,而持久化存储则是按照域来单独授予空间,所以后者后必须经过用户许可才行。代码方面要在requestFileSystem之前,先请求空间,用户许可后方可继续:
1
2
3
4
5
|
window.webkitStorageInfo.requestQuota(PERSISTENT, 1024*1024,
function
(grantedBytes) {
window.requestFileSystem(PERSISTENT, grantedBytes, fileSystemReadyHandler, errorHandler);
},
function
(e) {
console.log(
'Error'
, e);
});
|
Chrome会降下黄条,询问用户是否允许该域使用持久化存储,用户同意后才会请求FileSystem。
复制文件
复制文件指把文件从操作系统的文件系统复制到本地文件的文件系统中。这个操作很简单,也很有代表性,请先看代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
this
.clone =
function
(file) {
// file即通过拖拽或者<input type="file" />选择产生的File实例
targetFile = file;
// create true表示如果该文件不存在,则创建之
fileSystem.root.getFile(file.name, {create:
true
}, fileEntry_cloneReadyHandler, errorHandler);
}
function
fileEntry_cloneReadyHandler(fileEntry) {
// 获取本地文件的URL,可以赋给img的src属性,一般是filesystem://domain://temporary或者persistent/路径/文件名
fileURL = fileEntry.toURL();
fileEntry.createWriter(fileWriter_cloneReadyHandler, errorHandler);
}
function
fileWriter_cloneReadyHandler(fileWriter) {
// onwrite的话可能文件还没写完,所以最好用onwriteend,这点我参考的教程中没有提到
fileWriter.onwriteend =
function
(e) {
console.log(
'Write completed.'
);
};
fileWriter.onerror =
function
(e) {
console.log(
'Write failed: '
+ e.toString());
};
fileWriter.write(targetFile);
targetFile =
null
;
}
|
几乎所有的文件型操作都包括以上三步:获取文件、创建FileWriter、写入。与我们日常的文件操作不太一样,HTML5的File API要先找到或创建文件,然后再对文件进行操作,所以文件的URL在getFile之后就已经确定下来。
getFile有4个参数,分别是文件名(文件路径我这次没用到,所以这篇文章中不会提及)、文件处理策略、成功回调函数、错误回调函数。按照w3c规范,文件处理策略有两个参数,分别是create和exclusive。前者代表如果目标文件不存在,是否创建;后者代表如果目标文件已存在,是否抛出异常,在后面“写文件”一节里会特别讲解这个参数的用法。
写文件
与复制文件不同,为了保证输出的文件可用,我们需要选择合适的文件格式和文件类型。这里我姑且假设输入的内容都是字符串,直接以文本文件来保存就可以了。至于二进制文件后面再讨论。继续看代码吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
this
.save =
function
(name, content, type) {
fileName = name;
fileContent = content;
fileType = type ||
'text/plain'
;
fileSystem.root.getFile(fileName, {create:
true
, exclusive:
true
}, fileEntry_saveReadyHandler, errorHandler);
}
function
fileEntry_saveReadyHandler(fileEntry) {
fileURL = fileEntry.toURL();
fileEntry.createWriter(fileWriter_saveReadyHandler, errorHandler);
}
function
errorHandler(error) {
// 前面可以照搬,后面需要增加一个处理
if
(error.code == FileError.INVALID_MODIFICATION_ERR) {
fileSystem.root.getFile(fileName, {create:
false
}, fileEntry_removeReadyHandler, errorHandler);
}
}
function
fileWriter_saveReadyHandler(fileWriter) {
fileWriter.onwriteend =
function
(event) {
console.log(
'Write completed.'
);
};
fileWriter.onerror =
function
(error) {
console.log(
'Write failed: '
+ error.toString());
};
var
bb =
new
WebKitBlobBuilder();
bb.append(fileContent);
fileWriter.write(builder.getBlob(fileType));
fileContent =
null
;
}
function
fileEntry_removeReadyHandler(fileEntry) {
fileEntry.remove(fileRemoveHandler, errorHandler);
}
function
fileRemoveHandler() {
self.save(fileName, fileContent, fileType);
}
|
可以看到,大体上还是三步:获取文件,创建FileWriter、写入内容,不过增加了很多异常处理。这就要回到前面提到的那个getFile,它的第二个参数“文件处理策略”,里面有个字段叫exclusive,代表如果目标文件已经存在,是否抛出异常。因为FileWriter写入内容时以文件指针为标准,从0开始,逐字节写入,直到fileContent写完;当目标文件存在时,它仍会这么做,这种逐字节覆盖的方式导致如果先前的内容比后写入的内容要长,文件内容就会是新老相接的。这明显不是我们希望的结果,所以我要修改errorHandler,当遇到FileError.INVALID_MODIFICATION_ERR时就先把目标文件删除,然后再重新写入。
删除文件的操作是两步:获取文件、删除文件。完成之后,再次调用save方法,写入内容。
另外需要注意的是BlobBuilder(Chrome下是WebKitBlobBuilder)。Blob是HTML5中引入的新类型,代表不可变的原始数据。它是所有文件的基础,可以是任何一种文件类型。BlobBuilder则是生成这种数据格式的工具,它提供一个append方法,用来将数据添加到Blob中,还提供了一个getBlob方法,用来提取Blob。这两个方法配合,就可以将内存中的数据保存到硬盘上了。
JSZip与BlobBuilder
我在项目中主要用到的操作有:
- 用户通过拖拽或者选择文件的方式,将文件复制到本地
- 将用户生成的内容保存为本地文件
- 将用户生成的所有内容(html、js、css、图片、视频等)存入一个压缩包,交给用户下载
zip压缩方面,我选择了JSZip这个类库(官网:http://stuartk.com/jszip/,Github:https://github.com/Stuk/jszip)。最开始,我按照官网介绍使用location触发下载,小容量内容测试时一切都好,正式导出时Chrome就反复崩溃。Google之,原来Chrome的URL上限是2M,当压缩后的内容超过2M后,就不能再通过location触发下载了。没办法继续Google,从国外一个大侠的博客中找到了克敌制胜的法宝:Uint8Array 和 ArrayBuffer。
Uint8Array 和 ArrayBuffer 也是新标准带来的好东西。前者表示一个由8位无符号整数组成的数组,后者则是二进制数据缓冲,这样说大家可能不明白,按照我的理解和用法,就是Uint8Array(JavaScript 类型数组的一种)的数据可以通过ArrayBuffer来转化成二进制对象,而前者可以用来转化对象。具体的做法还得参考代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
function
fileWriter_saveReadyHandler(fileWriter) {
fileWriter.onwriteend =
function
(event) {
console.log(
'Write completed.'
);
};
fileWriter.onerror =
function
(error) {
console.log(
'Write failed: '
+ error.toString());
};
// 处理二进制数据,暂时只考虑zip,其它应该也通用
var
builder =
new
WebKitBlobBuilder();
if
(fileType ==
'application/zip'
) {
var
byteArray =
new
Uint8Array(fileContent.length);
for
(
var
i = 0, len = fileContent.length; i < len; i++) {
// 把内容逐字节转化为无符号8位整数
byteArray[i] = fileContent.charCodeAt(i) & 0xFF;
}
builder.append(byteArray.buffer);
}
else
{
builder.append(fileContent);
}
fileWriter.write(builder.getBlob(fileType));
fileContent =
null
;
}
|
前面的函数修改后,就可以妥善保存二进制文件了,非常神奇。相比外国大侠的解决方案,这样将zip文件保存到本地的做法,牺牲了部分浏览器兼容性(目前只有Chrome支持File API),但是文件名可读性要好的多,也方便XHR将文件上传到服务器,相信日后会有更多应用选择我的这种方式。
类库下载和最后
我把这些操作封装在一个类里,方便使用,有兴趣的同学可以下载。大家如果有问题和建议,也欢迎在留言里跟我交流。
用法:
1
2
3
4
5
|
var
file =
new
FileReferrence();
// 复制文件到本地
file.clone(file);
// 保存内容到指定文件
file.save(
'temp.txt'
,
'text'
);
|