[Node.js] 使用File API 异步上传文件

页面导航:首页 > 网络编程 > JavaScript > [Node.js] 使用File API 异步上传文件

[Node.js] 使用File API 异步上传文件

来源: 作者: 时间:2016-02-19 09:19 【

问题分解似乎是老生常谈,几年前我做过类似的功能模块(基于 NET平台),方案思路:基于表单提交Server端根据上传文件分配标识符(GUID)并进行流式读取Browser端发起Ajax拉取文件上传
问题分解
似乎是老生常谈,几年前我做过类似的功能模块(基于.NET平台),方案思路:
 
基于表单提交
Server端根据上传文件分配标识符(GUID)并进行流式读取
Browser端发起Ajax拉取文件上传状态
这种方案的问题是受制于文件大小(最大2G)。所谓文件上传进度的实时显示,个人觉得比较理想的方案是:
 
Browser 端需要告诉Server文件的大小
Browser 端需要能对文件分块读取
Server 端需要根据接收到的块及文件大小计算出进度,并告知Browser端
Browser 端在进度未完成时,继续读取分块上传
HTML5 File API
上述方案中,最大的难点在于Browser端分块读取文件。好在HTML5 File API提供了这样的接口:FileReader
 
使用FileReader对象,web应用程序可以异步的读取存储在用户计算机上的文件(或者原始数据缓冲)内容,可以使用File对象或者Blob对象来指定所要读取的文件或数据。其中File对象可以是来自用户在一个<input>元素上选择文件后返回的FileList对象……
 
有意思的是Blob接口,它只有一个方法:slice()——不难想象,它是用进行数据分块的,方法签名形如:
 
1
2
3
4
5
Blob slice(
  optional long long start,
  optional long long end,
  optional DOMString contentType
};
从W3C Draft 可以看出,File 接口实际上是继承自Blob接口的,意味着File.slice(start, end) 可以返回文件的块数据,结合FileReader.readAsBinaryString方法,我们在Browser端能读取到本地文件的任意部分数据。
 
关于FileReader
首先,FileReader并不是每个浏览器都支持的,兼容性测试情况(很不幸,巨硬的IE又拖后腿了……:
 
操作系统 Firefox Chrome Internet Explorer Opera Safari
Windows 支持 支持 不支持 支持 不支持
MAC OS X 支持 支持 N/A 支持 支持
其次,使用readAsBinaryString的方法,需要对FileReader的 onloadend事件进行订阅处理,即读取块数据操作完成时,这个事件订阅方法将得到已读取的二进制块数据:
 
currentFileReader.onload = function (evnt) {
    console.log('Data content length: ', evnt.target.result.length);
};
B/S通信
拿到了块数据,剩下的问题是怎么发出去,有这么些选项:AJAX,富客户端,WebSocket。由于网盘项目基于Node开发,我选用了Socket.IO 做为B/S两端通信的框架。
 
从B端开始
页面准备和引用:
 
复制代码
<div>
 <progress id="progressBar" value="0" max="100"></progress>
 </div>
 <input type="button" id="choose-button" value="选择文件">
 <input type="file" id="choose-file" class="hidden"/>
</div>
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
复制代码
兼容性测试先行:
 
复制代码
if (!window.File && !window.FileReader) {
    alert('Your browser does not support the File API. Please use modern browser');
    return;
} else {
     var socket = io.connect();
     var currentFile = null;
     var currentFileReader = null;
}
复制代码
在用户选择了文件后,对相应事件进行处理:
 
复制代码
$('#choose-file').on('change', function () {
    currentFile = document.getElementById('choose-file').files[0];
    if (currentFile) {
        currentFileReader = new FileReader();
        currentFileReader.onload = function (evnt) {
            socket.emit('upload', {
                'Name': currentFile.name,
                'Segment': evnt.target.result
            });
        };
        socket.emit('start', {
            'Name': currentFile.name,
            'Size': currentFile.size
        });
    }
});
复制代码
从上边的代码可以看出,socket.emit('start') 是整个交互流程的开始,它告诉Server端文件信息;FileReader.onload 则按块向Server端 emit 数据。还缺一段触发 FileReader的代码:
 
复制代码
socket.on('moreData', function (data) { 
    updateProgressBar(data.percent);
    var position = data.position * 524288;
    var newFile = null;
    if (currentFile.slice)
        newFile = currentFile.slice(position, position + Math.min(524288, currentFile.size - position));
    else if (currentFile.webkitSlice)
        newFile = currentFile.webkitSlice(position, position + Math.min(524288, currentFile.size - position));
    else if (currentFile.mozSlice)
        newFile = currentFile.mozSlice(position, position + Math.min(524288, currentFile.size - position));
    if (newFile)
        currentFileReader.readAsBinaryString(newFile); // trigger upload event
});
复制代码
Browser端这个moreData消息,是由Server端触发的,在收到start消息后,Server端将向Browser端发送这个moreData消息。这里需要注意的是,各家浏览器对于Blob.slice接口实现不一 (Firefox 12之前的版本上为blob.mozSlice(), Safari上为blob.webkitSlice()
 
上传完成的收尾工作:
 
socket.on('done', function (data) {
    delete currentFileReader;
    delete currentFile;
    updateProgressBar(100);
});
Server端实现
首先,需要一个全局数据结构,来保存每一个上传文件的描述符(传完后从作用域删除):
 
  var Files = {};
然后是Socket.IO的初始化,准备文件描述符:
 
复制代码
var io = require('socket.io').listen(server);
io.sockets.on('connection', function (socket) {
    //prepare for uploading
    socket.on('start', function (data) { 
        var name = data.Name;
        var size = data.Size;
        var filePath = '/tmp';
        var position = 0;
        Files[name] = { // define storage structure
            fileSize: size,
            data: '',
            downloaded: 0,
            handler: null,
            filePath: filePath,
        };
        Files[name].getPercent = function () {
            return parseInt((this.downloaded / this.fileSize) * 100);
        };
        Files[name].getPosition = function () {
            return this.downloaded / 524288;
        };
        fs.open(Files[name].filePath, 'a', 0755, function (err, fd) {
            if (err)
                console.log('[start] file open error: ' + err.toString());
            else {
                Files[name].handler = fd; // the file descriptor
                socket.emit('moreData', { 'position': position, 'percent': 0 });
            }
        });        
    });
});
复制代码
Server端收到upload消息时,并不立即写入,而是进行缓冲,以10M分批进行写入:
 
复制代码
socket.on('upload', function (data) {
    var name = data.Name;
    var segment = data.Segment;
 
    Files[name].downloaded += segment.length;
    Files[name].data += segment;
    if (Files[name].downloaded === Files[name].fileSize) {
        fs.write(Files[name].handler, Files[name].data, null, 'Binary', 
           function (err, written) {
            //uploading completed
            delete Files[name];
            socket.emit('done', { file: file });
        });
    } else if (Files[name].data.length > 10485760) { //buffer >= 10MB
        fs.write(Files[name].handler, Files[name].data, null, 'Binary', 
           function (err, Writen) {
            Files[name].data = ''; //reset the buffer
            socket.emit('moreData', {
                'position': Files[name].getPosition(),
                'percent': Files[name].getPercent()
            });
        });
    }
    else {
        socket.emit('moreData', {
            'position': Files[name].getPosition(),
            'percent': Files[name].getPercent()
        });
    }
});
Tags:

文章评论

最 近 更 新
热 点 排 行
Js与CSS工具
代码转换工具

<