1. 程式人生 > >用 FileSystem API 實現檔案下載器

用 FileSystem API 實現檔案下載器

提醒:本文最後更新於 1928 天前,文中所描述的資訊可能已發生改變,請謹慎使用。

各種基於 HTML5 的檔案上傳已經被大家玩得爛熟,常見的資料夾上傳、拖拽上傳以及拷貝剪下板資料上傳,都可以在各大網盤、WebIM 中見到。本文實現的純 JavaScript 多區塊併發下載器,要使用到 FileSystem 中相對不那麼常見的 FileWriter。

本文涉及知識較多,我並不打算全部介紹它們。示例程式碼中使用了 when.js 這個非同步框架,它的用法請看官方文件或我寫的《非同步程式設計:WHEN.JS 快速上手》;文中涉及到的 HTTP 知識可以參考 RFC2616,或者《HTTP 權威指南》;本文程式碼僅在 Chrome 29+ 測試過,FileSystem 的完整瀏覽器支援度請直接去

CanIUse 查;本文未涉及到的 FileSystem 其它知識,請參考 html5rocks 的這篇文章,不過需要注意的這篇文章完成時間很早,部分程式碼已失效(如文中的 BlobBuilder 已被廢棄);JavaScript 中的 Typed Array 也都可以在網上找到詳細介紹。

再說一點,文中示例程式碼做封裝,也完全沒有考慮異常流程。這是我寫部落格的一慣原則:示例程式碼只為了把事情講明白,如果要在實際專案中使用,需要讀者自己去思考並加工。

廢話說完,正式開始今天的主題。。。

獲取下載檔案資訊

我們的 HTTP 下載器目標是下載指定 URL 對應的檔案。為了方便後面的處理,首先,我們要獲取這個檔案的基本資訊。雖然實際應用中我們需要的資訊往往會由服務端提供,但本文目標是「完全不借助服務端語言(如 PHP),只使用瀏覽器 JavaScript 將 Nginx 託管的檔案下載到本地」,所以這裡採用純客戶端方案。

我們關心的是檔案大小(便於分配任務)和檔案型別(儲存時要用到)。這兩個資訊在 HTTP 響應頭中都有,分別是 Content-Length 和 Content-Type,可以通過 XHR(XMLHttpRequest)拿到它們。XHR 的 readyState 有 0 - 4 幾種狀態,等於 2 的時候就可以拿到響應頭了。

按照上面的分析,通過 XHR 給目標檔案傳送 GET 請求,並在 readyState 等於 2 時獲取響應頭資訊,再 abort() 掉請求就可以了。但實際上,HTTP 協議規定了「HEAD」這種請求方法,更適合做這件事情。文件對 HEAD 是這樣說明的:

HEAD 方法與 GET 方法的行為很類似,但伺服器在響應中只返回首部,不會返回實體的主體部分。這就允許客戶端在未獲取實際資源的情況下,對資源的首部進行檢查。伺服器開發者必須確保返回的首部與 GET 請求所返回的首部完全相同。遵循 HTTP/1.1 規範,就必須實現 HEAD 方法。

少數伺服器不支援 HEAD,本文直接忽略。現在開始編寫我們的 URL 分析工具,程式碼如下:

function UriAnalyser(url) {
    var deferred = when.defer();
    var xhr = new XMLHttpRequest();
    xhr.open('HEAD', url, true);
    xhr.onreadystatechange = function() {
        if(2 == this.readyState) {
            var ret = {
                    mimeType : xhr.getResponseHeader('Content-Type'),
                    size : xhr.getResponseHeader('Content-Length') | 0
                };

            deferred.resolve(ret);
        }
    };
    xhr.send();
    return deferred.promise;
}

程式碼很簡單,不用解釋了。UriAnalyser 接收 URL 引數並返回 promise,在獲取到相關資訊後 resolve。試用下:

var url = 'http://dl.qgy18.com/file.zip';
when(UriAnalyser(url)).then(function(o){ console.log(o) });

> Object {mimeType: "application/zip", size: 7992987}

在 FileSystem 建立檔案

接下來,我們要在瀏覽器的 FileSystem 中建立一個區域,用來儲存即將下載到的檔案。出於安全考慮,瀏覽器中的 FileSystem 執行在沙箱中。要使用它,首先需要請求許可權:

window.requestFileSystem(type, size, successCallback, errorCallback);

type 是檔案系統型別,有 window.TEMPORARY 和 window.PERSISTENT 兩種常量,區別是:TEMPORARY 型別的資料隨時可能會被瀏覽器刪掉;PERSISTENT 資料不會被瀏覽器清理,但需要使用者額外授權。size 是儲存大小,以位元組為單位。後兩個引數分別是請求檔案系統成功和失敗對應的回撥,successCallback 的第一個引數是對檔案系統的引用,我們用 fs 表示,後面還會用到。

下面兩個方法分別用來獲取檔案系統中的目錄和檔案(目標不存在則新建):

fs.root.getDirectory(dirName, {create: true}, function(dirEntry) { ... }, errorCallback);
fs.root.getFile(filePath, {create: true}, function(fileEntry) { ... }, errorCallback};

現在,我們編寫「根據指定的檔名,在檔案系統中建立對應空白檔案」的方法如下:

function CreateFile(name) {
    var deferred = when.defer();
    window.webkitRequestFileSystem(window.TEMPORARY, 10 * 1024 * 1024, function(fs) {
        var dir = (+ new Date).toString(36);
        fs.root.getDirectory(dir, {create: true}, function(dirEntry) {
            var file = dir + '/' + name;
            fs.root.getFile(file, {create: true}, function(fileEntry) {
                fileEntry.createWriter(function(fileWriter) {
                    var ret = {
                            fileEntry  : fileEntry,
                            fileWriter : fileWriter
                        };
                    deferred.resolve(ret);
                });
            });
        });
    });
    return deferred.promise;
};

這個 promise 會返回 fileEntry 和 fileWriter,後面會用到。試用下:

var file = 'a.js';
when(CreateFile(file)).then(function(o){ console.log(o) });

> Object {fileEntry: FileEntry, fileWriter: FileWriter}

開始下載

現在開始編寫下載程式碼。要實現併發下載,首先要合理分配任務。HTTP 協議中規定可以使用請求頭的 Range 欄位指定請求資源的範圍。例如服務端收到「Range : bytes=10-100」這樣的請求頭,只需要返回資源的 10-100 位元組這部分就可以了,這樣的響應狀態碼為 206。有些伺服器不支援 Range,本文繼續忽略。

現在,離最終目標越來越近了。我們只需要再實現支援指定 Range 的下載器和任務分配器就可以了。下載器比較簡單,直接看程式碼:

function Downloader(url, mimeType, range) {
    var deferred = when.defer();
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onreadystatechange = function() {
        if (this.readyState == 4) {
            if (this.response != null) {
                var blob = new Blob(
                        [ new Uint8Array(this.response) ], 
                        { type: mimeType }
                    );
                var ret = {
                        size : this.response.byteLength | 0,
                        blob : blob
                    };
                deferred.resolve(ret);
            }
        }
    };
    xhr.setRequestHeader("Range", "bytes=" + range);  
    xhr.responseType = 'arraybuffer'; 
    xhr.send();
    return deferred.promise;
};

下載器繼續使用 promise 實現,它會在獲取到響應資料時 resolve,返回指定 Range 的資料和大小。這樣,把所有下載器丟給 when.all(),就可以在所有任務完成後觸發下一步操作,順序還不會亂。

檔案全部下載完後,通過 fileWriter.write 方法就可以寫入前面獲得的 fileEntry 了。需要注意的是,當前資料寫完之後才能開始新的寫入,否則會產生異常。是否寫完可以通過 fileWriter.onwriteend 來獲得。下載完一部分資料就寫一部分當然也可以,原理是類似的。

我把下載器 XHR 的 responseType 設定為「arraybuffer」,是為了獲取每個下載器得到的長度。把全部下載器得到的長度加起來,跟分析器得到的檔案大小做對比,可以粗略地檢查檔案完整性。

任務分配和合並檔案的程式碼不貼了,太佔篇幅,請直接看最後 Demo 的原始碼。

檔案合併完成後,通過 fileEntry.toURL() 得到檔案在 FileSystem 中地址,賦給 <a> 標籤的 href,再 click() 下就可以自動下載到本地了。

解決跨域問題

瀏覽器對同一個域名的併發連線數有限制,為了更好的實現併發下載,最好使用不同的域名訪問要下載的檔案,這用域名泛解析很容易實現。但是這樣又會導致 XHR 被同源策略所限制。

還好,我們有 CORS(Cross-Origin Resource Sharing),專門用來解決這個問題。在 Nginx 配置檔案中加上這幾行就可以了:

add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
add_header Access-Control-Allow-Headers Range;
add_header Access-Control-Expose-Headers Content-Type,Content-Length;

重點看下後兩行:Allow-Headers 用來設定允許 XHR 傳送哪些請求頭,必須加上前面提到的 Range;Expose-Headers 用來設定哪些響應頭可以被 XHR 的 getResponseHeader 方法獲得,必須加上分析器用到的 Content-Type 和 Content-Length。

Nginx 本身還有一個問題:對靜態資源發起 OPTIONS 請求會得到「405 Not Allowed」,這個問題可以改成用 PHP 讀取檔案再輸出來解決。但前面說過,本文討論內容不依賴服務端語言。通過 Google 找到了一個方案,在 Nginx 配置中增加下面這一段就可以了:

location / {
    if ($request_method = OPTIONS ) {
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
        add_header Access-Control-Allow-Headers Range;
        add_header Access-Control-Expose-Headers Content-Type,Content-Length;
        return 200;
    }
}

Demo

猛擊這裡觀看 Demo,僅在 Chrome 29+ 測試通過。

downloader

--EOF--

提醒:本文最後更新於 1928 天前,文中所描述的資訊可能已發生改變,請謹慎使用。

相關推薦

FileSystem API 實現檔案下載

提醒:本文最後更新於 1928 天前,文中所描述的資訊可能已發生改變,請謹慎使用。 各種基於 HTML5 的檔案上傳已經被大家玩得爛熟,常見的資料夾上傳、拖拽上傳以及拷貝剪下板資料上傳,都可以在各大網盤、WebIM 中見到。本文實現的純 JavaScript 多區塊併發下載器,要使用到 File

FileSystem API 實現檔案下載 2

提醒:本文最後更新於 1927 天前,文中所描述的資訊可能已發生改變,請謹慎使用。 上篇文章講到如何用 JavaScript 編寫下載器,不少小夥伴看了覺得神奇。在本篇文章裡,我把之前寫的一個 JS 遊戲全部資源打成 Zip 包,用上回寫的下載器載入,再解壓到 FileSystem,最後直接執行這個

iOS開發(OC)——iOS原生API實現檔案下載

新建繼承NSObject類Downloader Downloader.h程式碼 #import <Foundation/Foundation.h> #import <UIKit

VB+API實現網頁下載和資料提交功能。

        其實如果是單純的下載網頁,程式碼可以更簡單,但俺這段程式碼不僅可以下載WEB頁面,同時還可以向WEB頁傳遞引數,完全可以替換XMLHTTP的GET功能,俺本來還想加上POST功能的,後來專案中不需要,所以就沒加了,想了解WININET API的朋友,應該依此

Response實現檔案下載

這個由於自己的一個失誤導致瀏覽器一直讀取不到檔名和檔案型別,後來發現是一個筆誤。 "attachment;filename="+filename 少寫了一個等號。。。。把這個程式碼貼上來,以此記錄。 直接帖程式碼 /**  * Response實現檔案下載  *  */

javaweb實現檔案下載ajax請求,瀏覽器無響應的問題

最近做公司專案要實現使用者通過瀏覽器從服務端下載檔案的功能。於是,我寫好了後臺,然後前端用JQuery的ajax()方法傳送請求到後臺,按道理說是沒有問題的,包括相應頭的設定都沒問題,但是點選下載按鈕過後,瀏覽器沒任何反應,也沒有報錯,但是就是沒有按照我想的執行檔案下載的效果

AWS S3 API實現檔案上傳下載

近日專案需要使用AWS S3的API實現檔案的上傳和下載功能,才發現網上關於.net實現該功能的文章不多,有幾篇也都是很老版本的AWS的使用描述了,寫法和封裝的類等等都基本上報廢了。雖然這樣,但還是很感謝那幾篇文章給我指點了方向。下面我就把實現API的方法提供給大家: Am

socket http檔案下載c語言實現

socket是網路程式設計的基石, 本文介紹如何使用c語言使用socket實現一個http檔案下載器. 下載分為以下幾個過程 解析出下載地址中的域名和檔名 通過域名獲取伺服器的IP地址 與目標伺服器建立連線 構建http請求頭並將其傳送到伺服器 等待

post或者get實現檔案下載

package test; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStr

在Java中使用多執行緒結合斷點續傳實現一個簡單的檔案下載

這篇部落格介紹在android中使用多執行緒和斷點續傳實現一個簡單的檔案下載器 第一步:啟動Tomcat伺服器,將需要下載的檔案部署到Tomcat伺服器上 第二步:使用eclipse建立一個Java工程,並且在工程中新增下面的程式碼 package com.fyt.mul

HttpListener實現檔案下載

和asp.net中一樣,如果要實現url重定向,使用response.Redirect()方法即可,在中使用如下: string desUrl = "http://www.google.com";response.Redirect(desUrl);response.Outp

electron 實現檔案下載管理

檔案下載是我們開發中比較常見的業務需求,比如:匯出 excel。 web 應用檔案下載存在一些侷限性,通常是讓後端將響應的頭資訊改成 `Content-Disposition: attachment; filename=xxx.pdf`,觸發瀏覽器的下載行為。 在 electron 中的下載行為,都會觸發

Tcl腳本調高層API實現儀表使用和主機創建配置的自己主動化測試用例

sub ret eat ati 包含 lin name ref config #設置Chassis的基本參數,包含IP地址。port的數量等等 set chassisAddr 10.132.238.190 set islot 1 set portList {11 12

調天氣api實現查詢各城市天氣

mgo inpu gfw scp bpa avd tpc ops sel 調用的api數據為haoservice.com網站提供的天氣數據。 如下圖,我們需要向其傳遞的參數有兩個,一個為我們自己申請的key,一個為城市名字。 首先定義兩個變量,一個存儲key,一個存儲

C語言實現websocket服務

sockaddr extend ++i set strlen ner ace == perl Websocket Echo Server Demo 背景 嵌入式設備的應用開發大都依靠C語言來完成,我去研究如何用c語言實現websocket服務器也是為了在嵌入式設備中實現一個

Vue來實現音樂播放(十六):滾動列表的實現

com 作用 efault nor 大小 -s stylus BE ack 滾動列表是一個基礎組件 他是基於scroll組件實現的 在base文件夾下面創建一個list-view文件夾 裏面有list-view.vue組件 <template>

Vue來實現音樂播放(十八):右側快速入口點擊高亮

為我 UC 沒有 short cut this 必須 左右 png 問題一:當我們點擊右側快速入口的時候 被點擊的地方高亮 首先我們要知道右側快速入口是為什麽高亮??因為當watch()監控到scrollY的變化了的時候 將scrollY的值和listHeight相比較

Vue來實現音樂播放(八):自動輪播圖啊

-s AR better hold ons ntp next start upd slider.vue組件的模板部分 <template> <div class="slider" ref="slider"> <div class=

Vue來實現音樂播放(九):歌單數據接口分析

QQ 插件 但是 之間 nbsp 跨域問題 前端 代理服務 一點 z這裏如果我們和之前獲取輪播圖的數據一樣來獲取表單的數據 發現根本獲取不到 原因是qq音樂在請求頭裏面加了authority和refer等 但是如果我們通過jsonp實現跨域

Vue來實現音樂播放(三十八):歌詞滾動列表的問題

vue 三十八 pla -s toggle 情況 TP 解決辦法 暫停 1、頻繁切換歌曲時,歌詞會跳來跳去 原因: // 歌詞跳躍是因為內部有一個currentLyric對像內部有一些功能來完成歌詞的跳躍 //每個currentLyric能實現歌曲的播放跳到相應的位置 是