既然要用到File API以及URL(或者叫做Blob URL),還是對他做一個簡單的了解。
File API目前主要定義了幾個介面,主要是:Blob、File、FileList、URL。
Blob是一個代表二進位資料的結構。除了透過像是File Input或是Drag and Drop事件取得,也可以透過他的建構式來建立(可以利用ArrayBuffer、字串、其他Blob來建立)。取得Blob物件後,透過size屬性可以知道他的大小,透過type屬性可以知道他的mime type。另外,他提供了一個slice方法,可以用來切割出另一個Blob物件。
File繼承了Blob,多增加了兩個屬性,透過name可以取得他的檔名、透過lastModifiedDate可以取得最後修改日期。
FileList是一個File物件的list,通常透過File Input或Drag and Drop事件,可以取得這個物件,然後在透過他取得檔案。使用這個方式,就可以一次操作多個檔案。透過length屬性,可以知道FileList中有多少File物件,透過item(index)方法則可取得這些File物件。
FileReader的用途就是讀取File/Blob物件。Blob與File物件中,只有內容的資訊(size、type、name...),並不能取得他的內容。要讀取內容,就得透過FileReader。FileReader目前提供了三種讀取Blob的非同步方法:readAsArrayBuffer(Blob)、readAsText(Blob, encoding)、readAdDataURL(Blob),當onload事件被觸發時,代表讀取完畢,這時可以在事件處理函數中讀取FileReader的result屬性,裡面就是指定讀取的資料。
URL是比較新加入File API中的東西,他主要是用一個新的URL Scheme來到表Blob資料,然後透過這個URL讓一些元素可以直接存取,不用再做複雜的讀取動作。在沒有這個介面前,如果想要實作選取圖檔時,不必上傳就產生預覽圖片,那在透過File Input元素取得選取的File後,通常會使用到FileReader的readAsDataURL把它轉成DataURL(以圖檔來說,會轉成代表內容的Base64編碼字串,這個作法效率不太好...),然後把這個DataURL指派給預覽用的img元素的src屬性。
有了URL以及新的Blob URL Scheme後,就不用這麼麻煩。直接使用URL.createObjectURL(Blob),就會產生一個URL,代表Blob物件,直接把這個URL指配給img元素的src屬性就可以了。這樣效率比較好,而且操作也比較簡單。
除了處理Blob,其實如果瀏覽器開始支援WebRTC規格,那就可以用URL.createObjectURL(MediaStream),來把MediaStream直接用Video/Audio元素播放。
昨天的簡單概念驗證,其實就是利用File API,來在使用File Input選取檔案後產生可以直接存取的連結。簡單地說,從File Input取得File物件後,就在下方動態產生一個連結,連結的href就是由URL.createObjectURL產生的。點選連結,就可以在新視窗開啟。基本上在Firefox16都可以正確執行,不過在Chrome22,他無法處理的檔案內容,就會在瀏覽器視窗印出來...這樣會有點怪,還是Firefox產生下載檔案動作的方式可能好一點。
接下來,先搭配node.js的ws這個WebSocket伺服器套件,來做簡單的資源分享的概念驗證。之前做的程式都是搭配Socket.IO,不過他沒辦法傳輸Blob、ArrayBuffer等代表二進位的資料,所以先用ws來做驗證。之後可能需要擴展Socket.IO或是包裝ws,來讓操作行為比較像,然後可以把之前的程式跟目前做資源分享的需求可以結合在一起。
先來看伺服器:
var Server = require('ws').Server;
var app = new Server({port:8443});
var sockets = {};
app.on('connection', function(ws) {
var id = Math.random()*10000 + '' + new Date().getTime();
sockets[id] = ws;
var filename = '';
ws.on('message', function(data, flags) {
for(var i in sockets) {
if(i !== id) {
sockets[i].send(data, {binary:flags.binary, mask:false});
}
}
});
});
為了可以管理所有的連線,這裡在每個連線建立(connection事件)時,同時產生一個unique的id,透過他把ws存到sockets物件中,之後需要群播時,就可以透過這個物件來做。這裡群播的方法也很簡單,就是只要跟自己id不一樣就把訊息轉送出去。ws送給message事件處理函數的flags物件參數,會有一個成員叫做binary,透過他就可以得知傳來的訊息是否是binary的。
接下來看網頁端的程式:
<meta charset='utf-8'>
<style>
#panel {
border: solid 1px #336699;
width: 240;
}
</style>
<input type='file' id='files'><br>
<div id='panel'></div>
<div id='show'></div>
<script>
var files = document.getElementById('files');
var ws = new WebSocket('ws://localhost:8443');
function getUrl() {
if(!!window.URL) {
return window.URL;
}
if(!!window.webkitURL) {
return window.webkitURL;
}
}
files.addEventListener('change', function(e) {
var URL = getUrl();
if(files.files.length>0) {
var file = files.files[0];
var src = URL.createObjectURL(file);
var a = document.createElement('a');
a.href = src;
a.innerHTML = file.name;
a.target = '_blank';
document.getElementById('panel').appendChild(a);
document.getElementById('panel').appendChild(document.createElement('br'));
ws.send(JSON.stringify({name: file.name,type: file.type}));
ws.send(file);
}
});
var fileinfo;
ws.onmessage = function(e) {
if(typeof e.data === 'object') {
var URL = getUrl();
var a = document.createElement('a');
var file = new Blob([e.data], {type:fileinfo.type});
a.href = URL.createObjectURL(file);
a.innerHTML = fileinfo.name;
a.target = '_blank';
document.getElementById('panel').appendChild(a);
document.getElementById('panel').appendChild(document.createElement('br'));
} else {
fileinfo = JSON.parse(e.data);
}
}
</script>
WebSocket的Data Frame雖然可以支援UTF-8字串以及Blob/ArrayBuffer,但是沒辦法同時傳,為了可以把檔名及mime type也傳出去,只好先傳一次檔名及mime type資訊,然後才把Blob傳出去。透過WebSocket接收到的Blob物件有一個問題,就是他的type屬性是控字串,這會導致Chrome在顯示這個Blob時出問題。所以在這裡不直接用接收到的Blob物件,而是利用他產生一個新的Blob物件,把它的mime type屬性透過先收到的fileinfo設定好,然後才呼叫URL.createObjectURL()來產生Blob物件的URL。
再來看一下畫面,跟前一個範例看起來差不多,不過現在是透過WebSocket在Chrome與Firefox間傳送Blob。同時開啟Chrome與Firefox後,先用Chrome選一個檔案:
然後用firefox也選一個檔案:
在Chrome點選Firefox送過來Blob產生的連結,在Firefox則點選Chrome送過來的Blob產生的連結,分別跑出不同Blob所包含的圖片:
大致上就這樣。明天先來包裝ws跟改寫之前的伺服器程式,另外也加上Client使用的簡單Library,讓ws的操作感覺像Socket.IO,但是又可以傳送二進位的資料。