講透前端文件切片上傳格二、斷點續(xù)傳网缝、拖拽上傳、圖片預(yù)覽蟋定、文件下載

github倉庫

要了解文件上傳粉臊,首先需要了解blob對象。

Blob

全稱是 binary large object,二進(jìn)制大文件對象驶兜,非 Javascript 特有扼仲,計算機(jī)通用對象,不可修改抄淑。

構(gòu)造函數(shù):Blob

new Blob(blobBits[, options])

blobBits:一個數(shù)組屠凶,成員可以是二進(jìn)制對象或字符串,表示 Blob 的內(nèi)容肆资。
options:可選參數(shù)矗愧,可以設(shè)置以下兩個屬性:

  • type:默認(rèn)值為 "",它代表了將會被放入到 blob 中的數(shù)組內(nèi)容的 MIME 類型郑原。
  • endings:表示換行符的類型唉韭,可以是"transparent"、"native"或"lf"犯犁、"cr"属愤、"crlf"。默認(rèn)為"transparent"酸役。

Blob的核心方法:
slice([start[, end[, contentType]]])
返回一個新的 Blob 對象住诸,包含原 Blob 對象的一部分?jǐn)?shù)據(jù)驾胆。

  • start:可選,類型為 Number贱呐,表示開始位置的索引丧诺,默認(rèn)值為 0。
  • end:可選奄薇,類型為 Number锅必,表示結(jié)束位置的索引,默認(rèn)值為 blob 的大小惕艳。
  • contentType:可選搞隐,類型為 String,表示新 Blob 對象的 MIME 類型远搪,默認(rèn)值為原 Blob 對象的 MIME 類型劣纲。

例子:

const myBlob = new Blob(["了解Blob"], { type: "text/plain" });
blob.png

使用場景:

1、分片上傳
File 對象是特殊類型的 Blob谁鳍,且可以用在任意的 Blob 類型的上下文中癞季。所以針對大文件傳輸?shù)膱鼍埃覀兛梢允褂?slice 方法對大文件進(jìn)行切割倘潜,然后分片進(jìn)行上傳绷柒,具體示例如下:

const file = new File(["a".repeat(1000000)], "test.txt");
const chunkSize = 40000;
const url = "https://httpbin.org/post";
async function chunkedUpload() {
  for (let start = 0; start < file.size; start += chunkSize) {
    const chunk = file.slice(start, start + chunkSize + 1);
    const fd = new FormData();
    fd.append("data", chunk);
    await fetch(url, { method: "post", body: fd }).then((res) =>
      res.text()
    );
  }
}

2、預(yù)覽圖片

/* 使用blob預(yù)覽 */
const fileDom = document.getElementById("file");
const file = e.target.files[0];
const img = document.getElementById("img");
// File是Blob的子類涮因,所以可以直接使用Blob的方法
// const blob = new Blob([file], { type: "image/png" });
img.src = URL.createObjectURL(blob);


/* 使用base64預(yù)覽 */
const fileDom = document.getElementById("file");
const file = e.target.files[0];
const img = document.getElementById("img");
// FileReader將file轉(zhuǎn)換為base64
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = (e) => {
  img.src = e.target.result
};

3废睦、下載文件

fetch('xxxx')
  .then(function (response) {
    // 使用fetch獲取到的response對象的blob方法,將文件轉(zhuǎn)換為blob對象
    return response.blob();
  })
  .then(function (myBlob) {
    /* 
    URL.createObjectURL(blob)生成的 URL 存儲了一個 URL → Blob 映射养泡。
    
    但實際上它也有副作用嗜湃。雖然存儲了 URL → Blob 的映射,但 Blob 本身仍駐留在內(nèi)存中澜掩,瀏覽器無法釋放它购披。映射在文檔卸載時自動清除,因此 Blob 對象隨后被釋放肩榕。

    但是刚陡,如果應(yīng)用程序壽命很長,那不會很快發(fā)生株汉。因此筐乳,如果我們創(chuàng)建一個 Blob URL,即使不再需要該 Blob郎逃,它也會存在內(nèi)存中哥童。

    對這個問題,我們可以調(diào)用 URL.revokeObjectURL(url) 方法褒翰,從內(nèi)部映射中刪除引用,從而允許刪除 Blob
     */

    let objectURL = URL.createObjectURL(myBlob);
    const link = document.createElement("a");
    // 設(shè)置下載鏈接
     link.href = objectURL;
    // 文件名稱(類型)
    ink.download = 'test.png';
    ink.click();
    ink.remove();
    URL.revokeObjectURL(link.href);
  });

File

const file = document.getElementById("file");
// 獲取文件
const file = input.current.files[0];

// file文件是File類型
console.log(file instanceof File); // true

File 對象代表一個文件,用來讀寫文件信息优训。它繼承了 Blob 對象朵你,或者說是一種特殊的 Blob 對象,所有可以使用 Blob 對象的場合都可以使用它揣非。

構(gòu)造函數(shù):File

new File(fileBits, fileName[, options])

fileBits:一個數(shù)組抡医,成員可以是二進(jìn)制對象或字符串,表示文件的內(nèi)容早敬。
fileName:文件名忌傻。
options:可選參數(shù),可以設(shè)置以下兩個屬性:

  • lastModified:文件最后修改時間搞监,單位為毫秒水孩,如果不傳入該參數(shù),會使用當(dāng)前時間琐驴。
  • type:文件類型俘种,如果不傳入該參數(shù),會根據(jù)文件名自動判斷绝淡。(MIME)

例子:

var file = new File(["hello world"], "hello.txt", {
  type: "text/plain",
});

適用于Blob的所有方法宙刘,F(xiàn)ile也可以使用

  • 切片上傳
  • 預(yù)覽圖片
  • 下載文件

FileReader

FileReader是一種異步讀取文件機(jī)制。

FileReader提供了如下方法:

  • readAsDataURL(file):讀取文件內(nèi)容牢酵,結(jié)果用data:url的字符串形式表示(base64)
  • readAsText(file, '編碼方式'):按字符讀取文件內(nèi)容悬包,結(jié)果用字符串形式表示
  • abort():終止文件讀取操作

例子:

var reader = new FileReader();
reader.readAsText(file);
reader.onload = function () {
     var txt = reader.result;
}

FormData

FormData提供了一種表示表單數(shù)據(jù)的鍵值對 key/value 的構(gòu)造方式,我們可以異步上傳一個二進(jìn)制文件(Blob)馍乙。

const formData = new FormData();
formData.append("file", file);
...
  • formData.append():添向 FormData 中添加新的屬性值玉罐,F(xiàn)ormData 對應(yīng)的屬性值存在也不會覆蓋原值,而是新增一個值潘拨,如果屬性不存在則新增一項屬性值吊输。
  • formData.get():獲取一個鍵值對
  • formData.getAll():獲取所有鍵值對
  • formData.has():判斷是否存在某個鍵值對
  • formData.delete():刪除某個鍵值對
  • formData.set():設(shè)置某個鍵值對

上傳文件主要分為兩種方式:點擊上傳以及拖拽上傳。

點擊上傳

// 單文件上傳
const App = () => {
  const change = e => {
    const file = e.target.files[0];
    // 我們可以通過file.type來獲取文件的類型铁追,從而判斷是否是圖片或者限制上傳的文件類型
    // const type = file.type;
    const formData = new FormData();
    formData.append('file', file);
    axios.post('http://localhost:1111/upload', formData).then(res => {
      console.log(res);
    }
    );
  };
  // accept屬性用來限制上傳的文件類型季蚂,可以是圖片、視頻琅束、音頻等
  return (
    <div className={styles.root}>
      <input type="file" onChange={change} /* accept=".jpeg,.png" */ />
    </div>
  );
};

// 多文件上傳

/* 
  遍歷上傳
  我們可以給input標(biāo)簽添加multiple屬性扭屁,這樣就可以上傳多個文件了。
  然后再通過e.target.files獲取到所有的文件涩禀,然后遍歷上傳即可料滥。
*/

/* 
  合并上傳
  我們可以將所有的文件合并成一個FormData對象,然后一次性上傳艾船。
  但是這樣會有一個問題葵腹,就是后端無法區(qū)分每個文件高每,所以我們需要在FormData對象中添加一個字段,用來區(qū)分每個文件践宴。
  例如:formData.append('file', file, file.name);
  這樣后端就可以通過file.name來區(qū)分每個文件了鲸匿。
*/

/* 
  我們也可以不設(shè)置multiple屬性,達(dá)到多文件上傳的目的(一個一個的添加阻肩,緩存進(jìn)數(shù)組里面再一次性上傳)
*/

圖片預(yù)覽

// 圖片預(yù)覽
const App = () => {
  const [base64, setBase64] = useState('');
  const change = e => {
    // file轉(zhuǎn)base64預(yù)覽

    // const file = e.target.files[0];
    // const fileReader = new FileReader();
    // fileReader.readAsDataURL(file);
    // fileReader.onload = e => {
    //   setBase64(e.target.result);
    // };

    // url -> blob映射預(yù)覽
    const file = e.target.files[0];
    const url = URL.createObjectURL(file);
    setBase64(url);
  };
  return (
    <div className={styles.root}>
      <input type="file" onChange={change} />
      <img src={base64} />
    </div>
  );
};

服務(wù)器文件下載

// 設(shè)置responseType為blob
// 接口數(shù)據(jù)返回的是一個blob類型的數(shù)據(jù)带欢,我們可以通過URL.createObjectURL()方法將其轉(zhuǎn)換成一個url,然后通過a標(biāo)簽的download屬性來下載文件烤惊。
const App = () => {
  useEffect(() => {
    axios.get('http://localhost:1111/download',{
      responseType: 'blob'
    }).then(res => {
      const url = URL.createObjectURL(res.data);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'test.png';
      a.click();
      URL.revokeObjectURL(url);
    });
  }, []);
  return (
    <div className={styles.root}></div>
  );
};

切片上傳

// 設(shè)置每個片段的大小chunkSize
// 使用blob的slice分片乔煞,大小根據(jù)chunkSize以及上傳到的start決定。直到start > file.size
const App = () => {
  const change = e => {
    const file = e.target.files[0];
    const chunkSize = 4000;
    const url = "http://localhost:1111/upload";
    async function chunkedUpload() {
      for (let start = 0; start < file.size; start += chunkSize) {
        const chunk = file.slice(start, start + chunkSize + 1);
        const fd = new FormData();
        fd.append("file", chunk);
        await axios.post(url, fd);
      }
    }
    chunkedUpload();
  };
  return (
    <div className={styles.root}>
      <input type="file" onChange={change} />
    </div>
  );
};

斷點續(xù)傳

// 通過current記錄當(dāng)前上傳到的位置柒室,通過pauseRef.current來判斷是否暫停
// chunkCount = Math.ceil(fileSize / chunkSize) 計算出總共需要上傳的片段數(shù)
const App = () => {
  const [file, setFile] = useState({});
  const [current, setCurrent] = useState(0);

  const pauseRef = useRef(false);
  const [pause, setPause] = useState(false);

  const change = e => {
    const _file = e.target.files[0];
    setFile(_file);
  };

  const upload = async file => {
    const chunkSize = 4000;
    const url = "http://localhost:1111/upload";
    const fileSize = file.size;
    const chunkCount = Math.ceil(fileSize / chunkSize);
    for (let i = current; i < chunkCount; i+=1) {
      if (pauseRef.current) return;
      const start = i * chunkSize;
      const end = (i + 1) * chunkSize;
      const chunk = file.slice(start, end);
      const fd = new FormData();
      fd.append("file", chunk);
      await axios.post(url, fd);
      setCurrent(i + 1);
    }
  };

  const clickPause = () => {
    pauseRef.current = !pauseRef.current;
    setPause(pauseRef.current);
    if (!pauseRef.current) upload(file);
  };

  return (
    <div className={styles.root}>
      <input type="file" onChange={change} />
      <button onClick={() => upload(file)}>上傳</button>
      <button onClick={clickPause}>
        {pause ? '繼續(xù)' : '暫停'}
      </button>
    </div>
  );
};

上傳進(jìn)度

  • 大文件切片上傳渡贾,可以通過計算每個片段的大小,然后計算出總共需要上傳的片段數(shù)伦泥,然后通過current / chunkCount來計算出進(jìn)度剥啤。
const App = () => {
  const [file, setFile] = useState({});
  const [current, setCurrent] = useState(0);
  const [progress, setProgress] = useState(0);

  const pauseRef = useRef(false);
  const [pause, setPause] = useState(false);

  const change = e => {
    const _file = e.target.files[0];
    setFile(_file);
  };

  const upload = async file => {
    const chunkSize = 4000;
    const url = "http://localhost:1111/upload";
    const fileSize = file.size;
    const chunkCount = Math.ceil(fileSize / chunkSize);
    for (let i = current; i < chunkCount; i+=1) {
      if (pauseRef.current) return;
      const start = i * chunkSize;
      const end = (i + 1) * chunkSize;
      const chunk = file.slice(start, end);
      const fd = new FormData();
      fd.append("file", chunk);
      await axios.post(url, fd);
      setCurrent(i + 1);
      setProgress(Math.floor((i + 1) / chunkCount * 100));
    }
  };

  const clickPause = () => {
    pauseRef.current = !pauseRef.current;
    setPause(pauseRef.current);
    if (!pauseRef.current) upload(file);
  };

  return (
    <div className={styles.root}>
      <input type="file" onChange={change} />
      <button onClick={() => upload(file)}>上傳</button>
      <button onClick={clickPause}>
        {pause ? '繼續(xù)' : '暫停'}
      </button>
      <div className={styles.progress}>
        {progress}%
      </div>
    </div>
  );
};
  • 通過xhr的onprogress來計算進(jìn)度
const App = () => {
  const [file, setFile] = useState({});
  const [progress, setProgress] = useState(0);

  const change = e => {
    const _file = e.target.files[0];
    setFile(_file);
  };

  const upload = async file => {
    const url = "http://localhost:1111/upload";
    const fd = new FormData();
    fd.append("file", file);
    await axios.post(url, fd, {
      onUploadProgress: e => {
        const percent = Math.floor((e.loaded / e.total) * 100);
        setProgress(percent);
      }
    });
  };

  return (
    <div className={styles.root}>
      <input type="file" onChange={change} />
      <button onClick={() => upload(file)}>上傳</button>
      <div className={styles.progress}>
        {progress}%
      </div>
    </div>
  );
};

拖拽上傳

const App = () => {
  const dragover = e => {
    // 阻止默認(rèn)事件
    e.preventDefault();
    // 阻止冒泡
    e.stopPropagation();
  };

  const drop = e => {
    // 阻止默認(rèn)事件
    e.preventDefault();
    // 阻止冒泡
    e.stopPropagation();
    // DataTransfer表示拖放操作中的數(shù)據(jù)
    const file = e.dataTransfer.files[0];
    const formData = new FormData();
    formData.append('file', file);
    axios.post('http://localhost:1111/upload', formData).then(res => {
      console.log(res);
    }
    );
  };

  return (
    <div className={styles.root} onDragOver={dragover} onDrop={drop} >
      請拖到此處上傳
      <input type="file" />
    </div>
  );
};
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市不脯,隨后出現(xiàn)的幾起案子府怯,更是在濱河造成了極大的恐慌,老刑警劉巖防楷,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牺丙,死亡現(xiàn)場離奇詭異,居然都是意外死亡复局,警方通過查閱死者的電腦和手機(jī)冲簿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亿昏,“玉大人峦剔,你說我怎么就攤上這事〗枪常” “怎么了吝沫?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長递礼。 經(jīng)常有香客問我惨险,道長,這世上最難降的妖魔是什么脊髓? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任辫愉,我火速辦了婚禮,結(jié)果婚禮上将硝,老公的妹妹穿的比我還像新娘恭朗。我一直安慰自己屏镊,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布冀墨。 她就那樣靜靜地躺著闸衫,像睡著了一般涛贯。 火紅的嫁衣襯著肌膚如雪诽嘉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天弟翘,我揣著相機(jī)與錄音虫腋,去河邊找鬼。 笑死稀余,一個胖子當(dāng)著我的面吹牛悦冀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播睛琳,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼盒蟆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了师骗?” 一聲冷哼從身側(cè)響起历等,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辟癌,沒想到半個月后寒屯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡黍少,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年寡夹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厂置。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡菩掏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出昵济,到底是詐尸還是另有隱情智绸,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布砸紊,位于F島的核電站传于,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏醉顽。R本人自食惡果不足惜沼溜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望游添。 院中可真熱鬧系草,春花似錦通熄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至能耻,卻和暖如春赏枚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晓猛。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工饿幅, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人戒职。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓栗恩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親洪燥。 傳聞我的和親對象是個殘疾皇子磕秤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容