最近更新: 2011-03-10

HTML5 - File API 簡易教學

HTML5 規劃了一組支援本地文件(客戶端文件)讀取的函數介面,即 File API。 透過 File API ,我們可以直接在瀏覽器中讀取客戶端的文件,而不需要先將檔案回傳到伺服端暫存後再讓瀏覽器取回顯示。

本文將設計兩個文件閱讀工具示範 File API 的用法。 整個工作僅需在瀏覽器(客戶端)使用 JavaScript 即可達成,完全不需要任何資料上傳到伺服端的工作。

在瀏覽器中,我們必須先得到一個 File 個體,才能調用 File API 所規範的各項行為。 但是基於安全性考量,HTML5 規範中只允許一種建立 File 個體的途徑,那就是透過「檔案選擇元件」(input type="file")。 當使用者在檔案選擇元件中選取檔案後,瀏覽器才會建立對應的 File 個體。 當瀏覽器將使用者選取的檔案配置為 File 個體之後,會儲存於檔案選擇元件的 files 屬性中。這是 HTML5 新增的元件屬性。 程序員可從 files 屬性中取得 File 個體。

上述規範,其目的在限制檔案讀取行為必須經過使用者之手。 瀏覽器不允許程序員直接建立 File 個體,藉此避免不經使用者察覺而讀取客戶端文件的惡意網頁程式行為。

下列是一個簡單的範例,展示如何從檔案選擇元件(input type="file")獲得 File 個體,並觀看其屬性值。

<html>
<script type="text/javascript">
function file_viewer_load(controller) {
    var s = "Type of files[0]: " + controller.files[0].toString() + "\n" +
        "File name: " + controller.files[0].name + "\n" +
        "File size: " + controller.files[0].size + "\n" +
        "File type: " + controller.files[0].type;
    alert(s);
}
</script>

<div>
<input id="file_selector" type="file" value=""
    onchange="file_viewer_load(this);"/>
</div>
</html>
HTML5 可以建立單選與多選的檔案選擇元件,所以給我們的是一個 files 陣列。 本文只使用檔案單選元件,所以使用者選擇的檔案,總是 files 的第一個項目(files[0])。
Read as Text

當使用者選取檔案後,瀏覽器僅先收集檔案資訊於 File 個體中,還不會實際讀取檔案內容。 故前一節的操作僅能得知檔案的名稱、大小與文件型態。尚無內容。 我們需再透過 FileReader 個體,才能要求瀏覽器載入檔案的內容,以便進一步地操作。

下列實作了一個簡單的文件閱讀器,先讓使用者透過檔案選擇元件選取檔案,再用 FileReader 讀取內容。 最後在頁面上顯示檔案內容。

<html>
<script type="text/javascript">
function FileViewer(args) {
    for (var p in args)
        this[p] = args[p];

    this.reader = new FileReader();

    this.reader.onloadend = (function(self) {
        return function() {
                    self.loaded();
                }
    })(this);
}
FileViewer.prototype.load = function() {
    this.file = this.controller.files[0];
    this.reader.readAsText(this.file);
}
FileViewer.prototype.loaded = function() {
    this.view_name.value = this.file.name;
    this.view_size.value = this.file.size;
    this.view.value = this.reader.result;
}

var file_viewer = undefined;

function init() {
    file_viewer = new FileViewer(
        {
            controller: document.getElementById('file_selector'),
            view_name: document.getElementById('show_filename'),
            view_size: document.getElementById('show_filesize'),
            view: document.getElementById('show_box')
        }
    );
}
</script>

<body onload="init();">

<div>
<input id="file_selector" type="file" value=""
    onchange="file_viewer.load();"/>
</div>

<div>
Name: <input id="show_filename" type="text"
            readonly="true" value=""/>
Size: <input id="show_filesize" type="text"
            readonly="true" value=""/>
<br/>
<textarea id="show_box" readonly="true" cols="60" rows="20">
</textarea>
</div>

</body>
</html>

FileReader 提供了不同地讀取結果,以配合不同的使用需求。 file-view.html 僅需要在文字區中顯示檔案內容,所以使用 FileReader::readAsText() 方法。 程序員必須將想讀取內容的 File 個體傳遞給 FileReader::readAsText()。 FileReader::readAsText() 讀取資料後,會將內容存於本身的 result 屬性。

FileReader 提供的讀取方法皆是非同步模式,所以程序員尚需指定各讀取階段的事件函數。 在本文中,我只關心讀取完成的事件,故僅指定了 onloadend 事件。

file-view.html 先等使用者選擇要顯示的檔案,觸發 load() 方法,調用 FileReader::readAsText() 讀取內容。 待讀取完成(onloadend),再將讀取結果(reader.result)指派給文字區顯示。

file-view.html 執行畫面
Read as Data URI

Data URI (RFC 2397) 是一種特殊的 URL 格式,可直接將文件內容編成一組 URL 字串。 這允許我們有途徑將其他的檔案內容嵌入 HTML 文件中。 維基百科的《Data URI scheme》條目記載了瀏覽器的支援度與一些使用範例。

舉例而言,在一般情形中,我們的 HTML 文件中並不直接包含圖片的資料內容,而是透過 img 的 src 屬性指向另一個圖片文件的遠端網址 URL。 由於 Data URI 也是一種正式的 URL 格式,於是我們可以直接將圖片的資料內容編成一個 URL 字串,指派給 img 的 src 屬性。 如此一來,便可以在一份 HTML 文件中同時包含文字、圖片等其他資料內容。

FileReader 提供了直接將檔案內容讀取為 Data URI 字串的方法,即 FileReader::readAsDataURL()。這令我們得以實現在頁面中嵌入讀取結果的目標。

image-view.html 利用 Data URI 機制,直接在頁面上顯示使用者選取的本地圖片內容,而不須上傳到伺服端處理。

<html>
<script type="text/javascript">
function ImageViewer(args) {
    for (var p in args)
        this[p] = args[p];

    this.reader = new FileReader();

    function _event_handler(self, method) {
        return function(){
            method.call(self);
        };
    }
    this.reader.onloadend = _event_handler(this, this.loaded);
}
ImageViewer.prototype.load = function() {
    this.file = this.controller.files[0];
    this.reader.readAsDataURL(this.file);
}
ImageViewer.prototype.loaded = function() {
    this.view_name.value = this.file.name;
    this.view_size.value = this.file.size;
    this.view_data.value = this.reader.result.substring(0,100);

    if ( ! /^image/.test(this.file.type) )
        alert("This is not an image file. Type: " + this.file.type);
    else
        this.view.src = this.reader.result;
}

var file_viewer = undefined;

function init() {
    file_viewer = new ImageViewer({
        controller: document.getElementById('file_selector'),
        view_name: document.getElementById('show_filename'),
        view_size: document.getElementById('show_filesize'),
        view: document.getElementById('show_image'),
        view_data: document.getElementById('show_data')
    });
}
</script>

<body onload="init();">

<div>
<input id="file_selector" type="file" value=""
    onchange="file_viewer.load();"/>
</div>

<div>
Name: <input id="show_filename" type="text"
            readonly="true" value=""/>
Size: <input id="show_filesize" type="text"
            readonly="true" value=""/>
<br/>
<textarea id="show_data" readonly="true" cols="60" rows="2">
</textarea>
<br/>
<img id="show_image" alt="Show Image" />
</div>

</body>
</html>

image-view.html 大致與 file-view.html 相似,僅差別在其顯示的是圖片。 由於 img 只能透過 src 屬性指定圖片內容,故我們需要利用 Data URI 機制,讓它顯示 FileReader 所讀入的圖片內容。 據此需求,我們調用 FileReader::readAsDataURL() 而不是 FileReader::readAsText()。

image-view.html 執行畫面

image-view.html 示範的內容,通常可應用於照片上傳到伺服器之前的預覽動作,更進一步地達成多照片同時上傳與進度呈現的效果。 對此有興趣的人,可閱讀《使用 JavaScript File API 实现文件上传》。 不過該文章在讀取檔案內容時,使用了 FireFox 專有的 File 讀取方法,而不是 W3C 規範的 FileReader 方法。 Mozilla 官方文件中已經標注捨棄專有的 File 讀取方法。各位宜參考本文內容,改用W3C 規範的方式。

FileReaderSync

為了配合 HTML5 新增的多線作業機制(Web Worker),File API 又規劃了另一組同步化的 FileReaderSync 方法。 FileReaderSync 提供的讀取方法採同步讀取模式,所以它們會將讀取的檔案內容作為函數值回傳。

下列是採用 FileReaderSync 讀取方法的 file-view.html。

<html>
<script type="text/javascript">
function FileViewer(args) {
    for (var p in args)
        this[p] = args[p];

    this.reader = new FileReaderSync();
}
FileViewer.prototype.load = function() {
    this.view_name.value = this.file.name;
    this.view_size.value = this.file.size;

    this.file = this.controller.files[0];
    this.view.value = this.reader.readAsText(this.file);
}

var file_viewer = undefined;

function init() {
    file_viewer = new FileViewer(
        {
            controller: document.getElementById('file_selector'),
            view_name: document.getElementById('show_filename'),
            view_size: document.getElementById('show_filesize'),
            view: document.getElementById('show_box')
        }
    );
}
</script>

<body onload="init();">

<div>
<input id="file_selector" type="file" value=""
    onchange="file_viewer.load();"/>
</div>

<div>
Name: <input id="show_filename" type="text" readonly="true" value=""/>
Size: <input id="show_filesize" type="text" readonly="true" value=""/>
<br/>
<textarea id="show_box" readonly="true" cols="60" rows="20">
</textarea>
</div>

</body>
</html>

非同步模式與同步模式的程式架構,主要差異在於同步模式可自讀取函數返回讀取結果, 而非同步模式須指定事件處理函數捕抓讀取完成事件。

瀏覽器相容性

在我的環境中,FireFox 3.5 與 Chrome 9 皆可運作,但尚不支援 FileReaderSync 方法。 亦即本文最後一個範例程式無法在 FireFox 3.5 與 Chrome 9 上執行。

至於 IE8 與 Opera 10 ,則尚未支援 HTML5 的 files 屬性與 File API, 所以不能執行本文的範例程式。

參考資料
樂多舊網址: http://blog.roodo.com/rocksaying/archives/15328315.html

樂多舊回應
未留名 (#comment-21798589)
Tue, 07 Jun 2011 14:52:56 +0800
謝謝你超級詳盡的解說