JS 與 二進(jìn)制(1)

轉(zhuǎn)載自 那些年,被blob虐過(guò)的程序猿覺(jué)醒了

前言

本文介紹前端如何進(jìn)行圖片處理,然后穿插介紹二進(jìn)制还惠、Blob厕诡、Blob URL蝗锥、Base64、Data URL此迅、ArrayBuffer铁孵、TypedArray锭硼、DataView 和圖片壓縮相關(guān)的知識(shí)點(diǎn)。



Blob


Blob表示二進(jìn)制類(lèi)型的大對(duì)象蜕劝,通常是影像檀头、聲音或多媒體文件轰异,在 javaScript中Blob表示一個(gè)不可變、原始數(shù)據(jù)的類(lèi)文件對(duì)象鳖擒。

其構(gòu)造函數(shù)如下:

new Blob(blobParts, options);
  • lobParts:數(shù)組類(lèi)型溉浙,可以存放任意多個(gè)ArrayBuffer, ArrayBufferView, Blob_或者_(dá)DOMString(會(huì)編碼為UTF-8)烫止,將它們連接起來(lái)構(gòu)成Blob對(duì)象的數(shù)據(jù)蒋荚。
  • options:可選項(xiàng),用于設(shè)置blob對(duì)象的屬性馆蠕,可以指定如下兩個(gè)屬性:
  • type:存放到blob中數(shù)組內(nèi)容的MIME類(lèi)型(默認(rèn)為"")期升。
  • endings:用于指定包含行結(jié)束符\n的字符串如何被寫(xiě)入。值為native表示行結(jié)束符會(huì)被更改為適合宿主操作系統(tǒng)文件系統(tǒng)的換行符(默認(rèn)值為transparent表示會(huì)保持blob中保存的結(jié)束符不變)

DOMString 是一個(gè)UTF-16字符串互躬。由于JavaScript已經(jīng)使用了這樣的字符串播赁,所以DOMString直接映射到 一個(gè)String。
ArrayBuffer(二進(jìn)制數(shù)據(jù)緩沖區(qū))吼渡、ArrayBufferView(二進(jìn)制數(shù)據(jù)緩沖區(qū)的array-like視圖)

屬性

Blob對(duì)象有兩個(gè)屬性容为,參見(jiàn)下表??:

屬性名 描述
size Blob對(duì)象中所包含數(shù)據(jù)的大小。字節(jié)為單位寺酪。只讀坎背。
type 一個(gè)字符串,表明該Blob對(duì)象所包含數(shù)據(jù)的MIME類(lèi)型寄雀。如果類(lèi)型未知得滤,則該值為空字符串。只讀盒犹。

方法

  • slice(start:number, end:number, contentType:DOMString)
    類(lèi)似于數(shù)組的slice方法懂更,將原始Blob對(duì)象按照指定范圍分割成新的blob對(duì)象并返回,可以用作切片上傳
    • start:開(kāi)始索引,默認(rèn)為0
    • end:結(jié)束索引急膀,默認(rèn)為最后一個(gè)索引
    • contentType:新Blob的MIME類(lèi)型沮协,默認(rèn)情況下為空字符串
  • stream():返回一個(gè)能讀取blob內(nèi)容的ReadableStream
  • text():返回一個(gè)Promise對(duì)象且包含blob所有內(nèi)容的UTF-8格式的 USVString卓嫂。
  • arrayBuffer():返回一個(gè)Promise 對(duì)象且包含blob所有內(nèi)容的二進(jìn)制格式的ArrayBuffer慷暂。

將blob(或者file)二進(jìn)制文件保存到 formData 進(jìn)行網(wǎng)絡(luò)請(qǐng)求(之后可以獲取到圖片的imageUrl可以用作圖片展示或者后續(xù)的通過(guò)websocket發(fā)送圖片地址)

示例如下??

  1. 創(chuàng)建一個(gè)包含domstring對(duì)象的blob對(duì)象
const blob = new Blob(['<div>john</div>'], { type: 'text/xml' });
console.log(blob); // Blob {size: 15, type: "text/xml"}
  1. 創(chuàng)建一個(gè)包含arraybuffer對(duì)象的blob對(duì)象
var abf = new ArrayBuffer(8);
const blob = new Blob([abf], { type: 'text/plain' });
console.log(blob); // Blob {size: 8, type: "text/plain"}
  1. 創(chuàng)建一個(gè)包含arraybufferview對(duì)象的blob對(duì)象
var abf = new ArrayBuffer(8);
var abv = new Int16Array(abf);
const blob = new Blob(abv, { type: 'text/plain' });
console.log(blob); // Blob {size: 4, type: "text/plain"}


File


File 對(duì)象是一種特殊的Blob對(duì)象,繼承了所有Blob的屬性和方法,當(dāng)然同樣也可以用作 formData 二進(jìn)制文件上傳

屬性

File對(duì)象屬性命黔,參見(jiàn)下表??:

屬性名 描述
lastModified 引用文件最后修改日期
name 文件名或文件路徑
size 以字節(jié)為單位返回文件的大小
type 文件的 MIME 類(lèi)型

方法

File 對(duì)象沒(méi)有自己的實(shí)例方法呜呐,由于繼承了 Blob 對(duì)象,因此可以使用 Blob 的實(shí)例方法 slice()悍募。

File的獲饶⒓:


下面我們分別使用input和拖放方式選擇多張圖片操作??:

  • input獲取本地文件
  <input type="file" multiple id="f" />
  <script>
    var elem = document.getElementById('f');
    elem.onchange = function (event) {
      var files = event.target.files;
      console.log(files); // [{{name: "1.jpg",lastModified: 1594369580771...},{name:'2.jpg',lastModified: 1596012406708...}]
      var file = files[0];
      console.log(file); // {name: "1.jpg",lastModified: 1594369580771,size: 22344,type: "image/jpeg"...}
      console.log(file instanceof File); //true
      console.log(files instanceof FileList); // true
      
      /* File繼承Blob */
      console.log(file.__proto__.__proto__); // Blob {size: 22344, type: ""}
    };
  </script>

為input元素添加multiple屬性,允許用戶(hù)選擇多個(gè)文件坠宴,用戶(hù)選擇的每一個(gè)文件都是一個(gè)file對(duì)象洋魂,而FileList對(duì)象則是這些file對(duì)象的列表,代表用戶(hù)選擇的所有文件,是file對(duì)象的集合副砍。

  • 拖放獲取
<div id="content" ondrop="drop(event)" ondragover="allowDrop(event);" />
<script>
function allowDrop(ev) {
  ev.preventDefault();
}
function drop(ev) {
  ev.preventDefault();
  const files = ev.dataTransfer.files;
  console.log(files); // [{{name: "1.jpg",lastModified: 1594369580771...},{name:'2.jpg',lastModified: 1596012406708...}]
  console.log(files instanceof FileList); // true
}
</script>
<style type="text/css">
#content {
  width: 500px;
  height: 500px;
  border: 1px solid brown;
}
</style>




數(shù)據(jù)緩沖區(qū)

XHR衔肢、File APICanvas 等等各種地方豁翎,讀取了一大串字節(jié)流角骤,如果用JS里的Array去存,又浪費(fèi)心剥,又低效邦尊。在編程中,數(shù)據(jù)緩沖區(qū)(或簡(jiǎn)稱(chēng)為緩沖區(qū))是物理內(nèi)存中中操作二進(jìn)制數(shù)據(jù)的存儲(chǔ)區(qū)(比硬盤(pán)驅(qū)動(dòng)器訪問(wèn)快)优烧,用于在數(shù)據(jù)從一個(gè)位置移動(dòng)到另一位置時(shí)存儲(chǔ)臨時(shí)數(shù)據(jù)蝉揍,解釋器借助存儲(chǔ)二進(jìn)制數(shù)據(jù)的內(nèi)存緩沖區(qū)讀取行。主內(nèi)存中有一個(gè)正在運(yùn)行的文件,如果解釋器必須返回文件以讀取每個(gè)位畦娄,則執(zhí)行過(guò)程將耗費(fèi)大量時(shí)間又沾。為了防止這種情況,JavaScript使用數(shù)據(jù)緩沖區(qū)熙卡,該緩沖區(qū)將一些位存儲(chǔ)在一起杖刷,然后將所有位一起發(fā)送給解釋器。這樣再膳,JavaScript解釋器就不必?fù)?dān)心從文件數(shù)據(jù)中檢索文件挺勿。這種方法節(jié)省了執(zhí)行時(shí)間并加快了應(yīng)用程序的速度。各種緩沖區(qū)類(lèi)對(duì)數(shù)據(jù)執(zhí)行有效的二進(jìn)制操作喂柒,包括File不瓶,BlobArrayBufferArray灾杰。選擇的方法決定了內(nèi)存中緩沖區(qū)的內(nèi)部結(jié)構(gòu)蚊丐。


Buffer


BufferNode.js提供的對(duì)象,前端沒(méi)有艳吠。它一般應(yīng)用于IO操作麦备,例如接收前端請(qǐng)求數(shù)據(jù)時(shí)候,可以通過(guò)Buffer相關(guān)的API創(chuàng)建一個(gè)專(zhuān)門(mén)存放二進(jìn)制數(shù)據(jù)的緩存區(qū)對(duì)接收到的前端數(shù)據(jù)進(jìn)行整合昭娩,一個(gè)Buffer類(lèi)似于一個(gè)整數(shù)數(shù)組凛篙,但它對(duì)應(yīng)于V8堆內(nèi)存之外的一塊原始內(nèi)存。


ArrayBuffer栏渺、ArrayBufferView


ArrayBuffer

ArrayBuffer表示固定長(zhǎng)度的二進(jìn)制數(shù)據(jù)的原始緩沖區(qū)呛梆,它的作用是分配一段可以存放數(shù)據(jù)的連續(xù)內(nèi)存區(qū)域,因此對(duì)于高密度的訪問(wèn)(如音頻數(shù)據(jù))操作而言它比JS中的Array速度會(huì)快很多磕诊,ArrayBuffer存在的意義就是作為數(shù)據(jù)源提前寫(xiě)入在內(nèi)存中填物,因此其長(zhǎng)度固定

先大致看下ArrayBuffer的功能:

圖片

ArrayBuffer對(duì)象的構(gòu)造函數(shù)如下(length表示ArrayBuffer的長(zhǎng)度)??:

ArrayBuffer(length)

Array和ArrayBuffer的區(qū)別??:

Array ArrayBuffer
可以放數(shù)字纹腌、字符串、布爾值以及對(duì)象和數(shù)組等 只能存放0和1組成的二進(jìn)制數(shù)據(jù)
數(shù)據(jù)放在堆中 數(shù)據(jù)放在棧中滞磺,取數(shù)據(jù)時(shí)更快
可以自由增減 只讀升薯,初始化后固定大小,無(wú)論緩沖區(qū)是否為空击困,只能借助TypedArrays涎劈、Dataview寫(xiě)入

屬性

ArrayBuffer對(duì)象屬性,參見(jiàn)下表??:

屬性名 描述
byteLength 表示ArrayBuffer的大小

方法

  • slice:有兩個(gè)參數(shù)??begin表示起始沛励,end表示結(jié)束點(diǎn)责语。方法返回一個(gè)新的 ArrayBuffer ,它的內(nèi)容是這個(gè)ArrayBuffer的字節(jié)副本目派,從begin(包括),到end(不包括)胁赢。

ArrayBuffer不能直接操作企蹭,而是要通過(guò) TypedArrayDataView 對(duì)象來(lái)操作,它們會(huì)將緩沖區(qū)中的數(shù)據(jù)轉(zhuǎn)換為各種數(shù)據(jù)類(lèi)型的數(shù)組智末,并通過(guò)這些格式來(lái)讀寫(xiě)緩沖區(qū)的內(nèi)容谅摄。

ArrayBufferView

由于ArrayBuffer對(duì)象不提供任何直接讀寫(xiě)內(nèi)存的方法,而ArrayBufferView對(duì)象實(shí)際上是建立在ArrayBuffer對(duì)象基礎(chǔ)上的視圖系馆,它指定了原始二進(jìn)制數(shù)據(jù)的基本處理單元送漠,通過(guò)ArrayBufferView對(duì)象來(lái)讀取ArrayBuffer對(duì)象的內(nèi)容。類(lèi)型化數(shù)組(TypedArrays)和DataView是ArrayBufferView的實(shí)例由蘑。

TypedArrays

類(lèi)型化數(shù)組(TypedArrays)是JavaScript中新出現(xiàn)的一個(gè)概念闽寡,專(zhuān)為訪問(wèn)原始的二進(jìn)制數(shù)據(jù)而生。
本質(zhì)上尼酿,類(lèi)型化數(shù)組和ArrayBuffer是一樣的爷狈,只不過(guò)是他具備讀寫(xiě)功能。

類(lèi)型數(shù)組的類(lèi)型有:??:

名稱(chēng) 大小 (以字節(jié)為單位) 說(shuō)明
Int8Array 1 8位有符號(hào)整數(shù)
Uint8Array 1 8位無(wú)符號(hào)整數(shù)
Int16Array 2 16位有符號(hào)整數(shù)
Uint16Array 2 16位無(wú)符號(hào)整數(shù)
Int32Array 4 32位有符號(hào)整數(shù)
Uint32Array 4 32位無(wú)符號(hào)整數(shù)
Float32Array 4 32位浮點(diǎn)數(shù)
Float64Array 8 64位浮點(diǎn)數(shù)

類(lèi)型轉(zhuǎn)換如圖??:


舉一些代碼例子展示如何轉(zhuǎn)換??:

// 創(chuàng)建一個(gè)指向b的視圖v1裳擎,采用Int32類(lèi)型涎永,開(kāi)始于默認(rèn)的字節(jié)索引0,直到緩沖區(qū)的末尾  
var v1 = new Int32Array(b);  // Int32Array(2) [0, 0]
v1[0] = 1
console.log(v1); // Int32Array(2) [1, 0]

// 創(chuàng)建一個(gè)指向b的視圖v2鹿响,采用Uint8類(lèi)型羡微,開(kāi)始于字節(jié)索引2,直到緩沖區(qū)的末尾  
var v2 = new Uint8Array(b, 2);  // Uint8Array(6) [0, 0, 0, 0, 0, 0]

// 創(chuàng)建一個(gè)指向b的視圖v3惶我,采用Int16類(lèi)型聊记,開(kāi)始于字節(jié)索引2,長(zhǎng)度為2  
var v3 = new Int16Array(b, 2, 2);  // Int16Array(2) [0, 0]` 

因?yàn)槠胀↗avascript數(shù)組使用的是Hash查找方式誊抛,而類(lèi)型化數(shù)組直接訪問(wèn)固定內(nèi)存猴抹,因此贬堵,速度很贊,比傳統(tǒng)數(shù)組要快结洼!同時(shí)黎做,類(lèi)型化數(shù)組天生處理二進(jìn)制數(shù)據(jù),這對(duì)于XMLHttpRequest松忍、canvas蒸殿、webGL等技術(shù)有著先天的優(yōu)勢(shì)。

TypedArray的應(yīng)用如何拼接兩個(gè)音頻文件?
fetch請(qǐng)求音頻資源 -> ArrayBuffer -> TypedArray -> 拼接成一個(gè) TypedArray -> ArrayBuffer -> Blob -> Object URL

DataView

DataView對(duì)象可以在ArrayBuffer中的任意位置讀取和存儲(chǔ)不同類(lèi)型的二進(jìn)制數(shù)據(jù)鸣峭。

創(chuàng)建DataView的語(yǔ)法如下:

var dataView = new DataView(DataView(buffer, byteOffset[可選], byteLength[可選])
屬性

DataView對(duì)象有三個(gè)屬性宏所,參見(jiàn)下表??:

屬性名 描述
buffer 表示ArrayBuffer
byteOffset 指緩沖區(qū)開(kāi)始處的偏移量
byteLength 指緩沖區(qū)部分的長(zhǎng)度
方法
  • setint8():從DataView起始位置以byte為計(jì)數(shù)的指定偏移量(byteOffset)處存儲(chǔ)一個(gè)8-bit數(shù)(一個(gè)字節(jié))
  • getint8():從DataView起始位置以byte為計(jì)數(shù)的指定偏移量(byteOffset)處獲取一個(gè)8-bit數(shù)(一個(gè)字節(jié))

除此之外還有g(shù)etInt16, getUint16, getInt32, getUint32... 使用方法一致,這里就不一一例舉

用法如下??:

let buffer = new ArrayBuffer(32);
let dataView = new DataView(buffer,0);
dataView.setInt16(1,56);
dataView.getInt16(1); // 56


FileReader


我們無(wú)法直接訪問(wèn)Blob或者文件對(duì)象的內(nèi)容摊溶,如果想要讀取它們并轉(zhuǎn)化為其他格式的數(shù)據(jù)爬骤,可以借助FileReader對(duì)象的API進(jìn)行操作

  • readAsText(Blob):將Blob轉(zhuǎn)化為文本字符串
  • readAsArrayBuffer(Blob):將Blob轉(zhuǎn)為ArrayBuffer格式數(shù)據(jù)
  • readAsDataURL(): 將Blob轉(zhuǎn)化為Base64格式的DataURL

使用分別如下??:

const blob = new Blob(['<xml>foo</xml>'], { type: 'text/xml' });
console.log(blob); // Blob(14) {size: 14, type: "text/xml"}

const reader = new FileReader();
reader.onload = () => {
    console.log(reader.result);
};
reader.readAsText(blob); // <xml>foo</xml>
reader.readAsArrayBuffer(blob); // ArrayBuffer(14) {}
reader.readAsDataURL(blob); // data:text/xml;base64,PHhtbD5mb288L3htbD4` </pre>

下面我們嘗試把一個(gè)文件的內(nèi)容通過(guò)字符串的方式讀取出來(lái):

<input type="file" id='f' />
<script> document.getElementById('f').addEventListener('change', function (e) {
    var file = this.files[0];
    // 首先,需要?jiǎng)?chuàng)建一個(gè)FileReader的實(shí)例莫换。
    const reader = new FileReader();
    reader.onload = function () {
        // 在加載完成時(shí)回調(diào)
        const content = reader.result;
        console.log(content);
    }
    reader.readAsText(file); // 將blob轉(zhuǎn)化為文本字符串讀取
  }, false); </script>

讀取結(jié)果如下??:



BlobURL


BlobURL(ObjectURL)是一種偽協(xié)議霞玄,只能由瀏覽器在內(nèi)部生成,我們知道script/img/video/iframe等標(biāo)簽的src屬性和background的url可以通過(guò)url和base64來(lái)顯示拉岁,我們同樣可以把blob或者file轉(zhuǎn)換為url生成BlobURL來(lái)展示圖像坷剧,BlobURL允許Blob和File對(duì)象用作圖像,下載二進(jìn)制數(shù)據(jù)鏈接等的URL源喊暖。

圖像展示??:

<div id="content">
    <input type="file" multiple id="f" />
  </div>
  <script>
    const elem = document.getElementById('f');
    const content = document.getElementById('content');

    // 根據(jù)不同瀏覽器封裝一個(gè)轉(zhuǎn)換BlobUrl的方法:file可以是File對(duì)象也可以是Blob對(duì)象
    const getObjectURL = (file) => {
      let url;
      if (window.createObjectURL) {
        url = window.createObjectURL(file);
      } else if (window.URL) {
        url = window.URL.createObjectURL(file);
      } else if (window.webkitURL) {
        url = window.webkitURL.createObjectURL(file);
      }
      return url;
    };

    elem.onchange = function (event) {
      const files = event.target.files;
      const file = files[0];
      const img = document.createElement('img');
      img.src = getObjectURL(file);
      content.appendChild(img);
    };
  </script>

我們查看demo頁(yè)面這個(gè)mm圖片元素惫企,會(huì)發(fā)現(xiàn)其URL地址既不是傳統(tǒng)HTTP,也不是Base64 URL陵叽,而是blob:開(kāi)頭的字符串,可以通過(guò)將其放在地址欄中進(jìn)行檢查狞尔。

文件下載??:

<body>
 <button onclick="download()">download.txt</button>

 <script> const getObjectURL = (file) => {
        let url;
        if (window.createObjectURL) {
          url = window.createObjectURL(file);
        } else if (window.URL) {
          url = window.URL.createObjectURL(file);
        } else if (window.webkitURL) {
          url = window.webkitURL.createObjectURL(file);
        }
        return url;
      };
      function download() {
        const fileName = 'download.txt';
        const myBlob = new Blob(['johnYu'], { type: 'text/plain' });
        downloadFun(fileName, myBlob);
      }
      function downloadFun(fileName, blob) {
        const link = document.createElement('a');
        link.href = getObjectURL(blob);
        link.download = fileName;
        link.click();
        link.remove();
        URL.revokeObjectURL(link.href);
      }</script>
  </body>

點(diǎn)擊按鈕下載文檔,文檔內(nèi)容為:johnYu

這里不調(diào)用revokeObjectURL時(shí)訪問(wèn)chrome://blob-internals/可以看到當(dāng)前內(nèi)部的blob文件列表:

不再使用的BlobUrl后續(xù)會(huì)自動(dòng)清除(關(guān)閉瀏覽器也會(huì)自動(dòng)清除)咨跌,但是最好使用URL.revokeObjectURL(url)手動(dòng)清除它們:

URL.revokeObjectURL('blob:http://127.0.0.1:5500/d2a9a812-0dbf-41c5-a96b-b6384d33f281')

執(zhí)行后再次訪問(wèn)chrome://blob-internals/可以看到文件已經(jīng)被清除


dataURL


dataURL允許內(nèi)容的創(chuàng)建者將較小的文件嵌入到文檔中沪么。與常規(guī)的URL使用場(chǎng)合類(lèi)似

其語(yǔ)法格式格式如下??:

data:[<mediatype>][;base64],data
  • data:前綴
  • mediatype表明數(shù)據(jù)類(lèi)型,是一個(gè)MIME類(lèi)型字符串,如image/jpeg表示一個(gè)JPEG圖片文件锌半。如果省略禽车,默認(rèn)值為text/plain;charset=US-ASCII
  • base64:標(biāo)志位(如果是文本刊殉,則可選)
  • data:數(shù)據(jù)本身

如何獲取DataUrl

圖片
  1. 上面示例中使用的方法readAsDataURL()就是將Blob轉(zhuǎn)化為Base64格式的DataUrl;
  2. 使用原生Web API編碼/解碼
    Javascript中有兩個(gè)函數(shù)負(fù)責(zé)編碼和解碼base64字符串殉摔,分別是atob和btoa。
    兩者都只針對(duì)Data URL中的data進(jìn)行處理记焊。
  • atob(): 負(fù)責(zé)解碼已經(jīng)使用base64編碼了的字符串逸月。 btoa('hello base64') // PHhtbD5mb288L3htbD4=
  • btoa(): 將二進(jìn)制字符串轉(zhuǎn)為base64編碼的ASCII字符串。atob('PHhtbD5mb288L3htbD4=') // <xml>foo</xml>
  1. Canvas的toDataURL方法:
    Canvas提供了toDataURL方法遍膜,用于獲取canvas繪制內(nèi)容碗硬,將其轉(zhuǎn)為base64格式
<body>
    <canvas id="canvas" width="200" height="50"></canvas>
    <textarea id="content" style="width: 200px; height: 200px"></textarea>

    <script> var canvas = document.getElementById('canvas');
      if (canvas.getContext) {
        var ctx = canvas.getContext('2d');
        // canvas的繪制
        ctx.font = 'Bold 20px Arial';
        ctx.textAlign = 'left';
        ctx.fillStyle = 'purple';
        ctx.fillText('johnYu', 10, 30);
        // 獲取 Data URL
        document.getElementById('content').value = canvas.toDataURL();
      }</script>
  </body>

如下圖所示瓤湘,文本框中的內(nèi)容即為canvas中繪制內(nèi)容的base64格式。

如果我們將前面的返回結(jié)果data:text/xml;base64,PHhtbD5mb288L3htbD4=放在瀏覽器的地址欄中恩尾,則可以看到顯示的內(nèi)容:

DataUrl的使用

  1. 由于可以將其用作URL的替代弛说,因此DataURL和BlobUrl一樣可以在script/img/video/iframe等標(biāo)簽的src屬性和background的url中使用,用法與BlobUrl基本一致翰意,只需要將前面的elem.onchange做如下改造
<body>
    <div id="content">
      <input type="file" multiple id="f" />
    </div>
    <script>
      const elem = document.getElementById('f');
      const content = document.getElementById('content');

      elem.onchange = function (event) {
        const files = event.target.files;
        const file = files[0];
        const img = document.createElement('img');
-        img.src = getObjectURL(file);
+        const reader = new FileReader();
+        reader.onload = function () {
+          img.src = reader.result;
+        };
+        reader.readAsDataURL(file);
        content.appendChild(img);
      };
    </script>
  </body>
  1. 由于數(shù)據(jù)本身由URL表示木人,因此可以將其保存在Cookie中傳遞給服務(wù)器。

  2. 當(dāng)圖片的體積太小冀偶,占用一個(gè)HTTP會(huì)話(huà)不是很值得時(shí)醒第。

  3. 當(dāng)訪問(wèn)外部資源很麻煩或受限時(shí)。

  4. DataUrl不會(huì)被瀏覽器緩存进鸠,但是小部分會(huì)通過(guò)css緩存稠曼,在下面例子中,DataUrl的使用是完全符合場(chǎng)景的堤如。它避免了讓這個(gè)小小的背景圖片獨(dú)自產(chǎn)生一次HTTP請(qǐng)求蒲列,而且,這個(gè)小圖片還能同CSS文件一起被瀏覽器緩存起來(lái)搀罢,重復(fù)使 用,不會(huì)每次使用時(shí)都加載一次侥猩。只要這個(gè)圖片不是很大榔至,而且不是在CSS文件里反復(fù)使用,就可以DataUrl方法呈現(xiàn)圖片降低頁(yè)面的加載時(shí)間欺劳,改善用戶(hù)的瀏覽體驗(yàn)唧取。

background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7")
  1. 作為下載連接使用。
    點(diǎn)擊a標(biāo)簽后后下載文本內(nèi)容為johnYu的txt文件,在下面的BlobURL同樣可以實(shí)現(xiàn)??
<script>
    const createDownload = (fileName, content) => {
      const blob = new Blob([content]);
      const reader = new FileReader();
      const link = document.createElement('a');
      link.innerHTML = fileName;
      link.download = fileName;
      reader.onload = () => {
        link.href = reader.result;
        document.getElementsByTagName('body')[0].appendChild(link);
      };
      reader.readAsDataURL(blob);
    };

    createDownload('download.txt', 'johnYu');
  </script>

區(qū)別

BlobURL 基本用法與 DataUrl 相同划提,都可以通過(guò)將其放在地址欄中進(jìn)行檢查也可以用作普通URL使用枫弟。
但也存在以下差異:

  1. BlobUrl始終是唯一字符串,即時(shí)你每次傳遞相同的Blob鹏往,每次也會(huì)生成不同的BlobUrl淡诗;DataUrl值跟隨blob變化;

  2. 就BlobUrl而言伊履,它并不代表數(shù)據(jù)本身韩容,數(shù)據(jù)存儲(chǔ)在瀏覽器中,BlobUrl只是訪問(wèn)它的key唐瀑。數(shù)據(jù)會(huì)一直有效群凶,直到關(guān)閉瀏覽器或者手動(dòng)清除。而DataUrl是直接編碼的數(shù)據(jù)本身哄辣。因此即使將BlobUrl傳遞給服務(wù)器等也無(wú)法訪問(wèn)數(shù)據(jù)请梢。關(guān)閉瀏覽器后仍然可以在地址欄訪問(wèn)后DataUrl赠尾,但是訪問(wèn)不到BlobUrl

  3. BlobUrl的長(zhǎng)度一般比較短,但DataUrl因?yàn)橹苯哟鎯?chǔ)圖片base64編碼后的數(shù)據(jù)毅弧,往往很長(zhǎng)(Base64編碼的數(shù)據(jù)體積通常會(huì)比二進(jìn)制格式的圖片體積大1/3气嫁。),因此當(dāng)顯式大圖片時(shí)形真,使用BlobUrl能獲取更好的可能性杉编,速度和內(nèi)存比DataUrl更有效

  4. BlobUrl可以方便的使用XMLHttpRequest獲取源數(shù)據(jù)(xhr.responseType = 'blob')。對(duì)于DataUrl咆霜,并不是所有瀏覽器都支持通過(guò)XMLHttpRequest獲取源數(shù)據(jù)的

<body>
    <button onclick="download1()">XMLHttpRequest下載</button>
    <button onclick="download2()">fetch下載</button>
    <img id="img" />
    <script> var eleAppend = document.getElementById('forAppend');
      const url = 'https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/9ecb4e119c26e64b8b4ec5258f159b3b~300x300.image';
      const pingan = document.querySelector('#pingan');
      function download1() {
        const xhr = new XMLHttpRequest();
        xhr.open('get', url, true);
        xhr.responseType = 'blob';
        xhr.onload = function () {
          if (this.status == 200) {
            renderImage(this.response);
          }
        };
        xhr.send(null);
      }
      function download2() {
        fetch(url)
          .then((res) => {
            return res.blob();
          })
          .then((myBlob) => {
            renderImage(myBlob);
          });
      }

      function renderImage(blob) {
        window.URL = window.URL || window.webkitURL;
        var img = document.getElementById('img');
        img.onload = function (e) {
          window.URL.revokeObjectURL(img.src); // 清除釋放
        };
        img.src = window.URL.createObjectURL(blob);
      }</script>
  </body>
  1. BlobUrl除了可以用作圖片資源的網(wǎng)絡(luò)地址邓馒,BlobUrl也可以用作其他資源的網(wǎng)絡(luò)地址,例如html文件蛾坯、json文件等光酣,為了保證瀏覽器能正確的解析BlobUrl返回的文件類(lèi)型,需要在創(chuàng)建Blob對(duì)象時(shí)指定相應(yīng)的type
const createDownload = (fileName, content) => {
      const blob = new Blob([content], { type: 'text/html' });
      const link = document.createElement('a');
      link.innerHTML = fileName;
      link.download = fileName;
      link.href = getObjectURL(blob);
      document.getElementsByTagName('body')[0].appendChild(link);
    };
    createDownload('download.html', '<button>foo</button>')
  1. DataUrl不會(huì)被瀏覽器緩存脉课,這意味著每次訪問(wèn)這樣頁(yè)面時(shí)都被下載一次救军。這是一個(gè)使用效率方面的問(wèn)題——尤其當(dāng)這個(gè)圖片被整個(gè)網(wǎng)站大量使用的時(shí)候。但是小部分可以通過(guò)css緩存



canvas


Canvas對(duì)象元素負(fù)責(zé)在頁(yè)面中設(shè)定一個(gè)區(qū)域倘零,然后就可以通過(guò) JavaScript 動(dòng)態(tài)地在這個(gè)區(qū)域中繪制圖形唱遭。

方法

  • toDataURL(type, encoderOptions)):以指定格式返回 DataUrl,該方法接收兩個(gè)可選參數(shù)

    • type:表示圖片格式,默認(rèn)為 image/png
    • encoderOptions:表示圖片的質(zhì)量呈驶,在指定圖片格式為 image/jpeg 或 image/webp 的情況下拷泽,可以從 0 到 1 的區(qū)間內(nèi)選擇圖片的質(zhì)量。如果超出取值范圍袖瞻,將會(huì)使用默認(rèn)值 0.92司致,其他參數(shù)會(huì)被忽略。
  • toBlob(callback, type, encoderOptions):創(chuàng)造Blob對(duì)象聋迎, 用于展示canvas的圖片脂矫,默認(rèn)圖片類(lèi)型是image/png,分辨率是96dpi

    • callback: 參數(shù)是blob對(duì)象的回調(diào)函數(shù)
  • getImageData(x,y,width,height):返回 ImageData 對(duì)象霉晕,該對(duì)象拷貝了畫(huà)布指定矩形的像素?cái)?shù)據(jù)庭再。

    • x: 開(kāi)始復(fù)制的左上角位置的 x 坐標(biāo)。
    • y: 開(kāi)始復(fù)制的左上角位置的 y 坐標(biāo)娄昆。
    • width: 將要復(fù)制的矩形區(qū)域的寬度佩微。
    • height: 將要復(fù)制的矩形區(qū)域的高度。
  • putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight):將圖像數(shù)據(jù)(從指定的 ImageData 對(duì)象)放回畫(huà)布上萌焰。

    • imgData: 規(guī)定要放回畫(huà)布的 ImageData 對(duì)象哺眯。
    • x: ImageData 對(duì)象左上角的 x 坐標(biāo),以像素計(jì)扒俯。
    • y: ImageData 對(duì)象左上角的 y 坐標(biāo)奶卓,以像素計(jì)一疯。
    • dirtyX: 可選。水平值(x)夺姑,以像素計(jì)墩邀,在畫(huà)布上放置圖像的位置。
    • dirtyY: 可選盏浙。水平值(y)眉睹,以像素計(jì),在畫(huà)布上放置圖像的位置废膘。
    • dirtyWidth: 可選竹海。在畫(huà)布上繪制圖像所使用的寬度。
    • dirtyHeight: 可選丐黄。在畫(huà)布上繪制圖像所使用的高度斋配。

應(yīng)用場(chǎng)景

當(dāng)我們需要獲取到canvas的內(nèi)容,可以用到toDataURLtoBlob屬性(可用于簽名灌闺,圖片剪裁艰争,圖片壓縮等場(chǎng)景),putImageData桂对、getImageData可以用于圖片灰度或者復(fù)制時(shí)使用(見(jiàn)后面的使用場(chǎng)景章節(jié)??)

獲取內(nèi)容:

<body>
    <div id="content">
      <button onclick="drawnImg()">繪制圖像</button>
      <button onclick="getImg()">獲取圖像</button>
      <canvas style="border: 1px solid black" id="drawing" width="200" height="200">A drawing of something.</canvas>
      <img src="./timg.jpg" alt="" />
    </div>
    <script> var drawing = document.getElementById('drawing');
      var quality = 0.3;
      const imgType = 'image/jpeg';

      var drawnImg = function () {
        if (drawing.getContext) {
          var context = drawing.getContext('2d');
          //取得圖像的數(shù)據(jù) URI
          var image = document.images[0];
          context.drawImage(image, 20, 20, 100, 100);
        }
      };
      var getImg = async function () {
        const content = getContent('base64');
        console.log(content);
        const content1 = await getContent('file');
        console.log(content1);
      };
      var getContent = function (type) {
        switch (type) {
          case 'base64':
            {
              const imgURL = drawing.toDataURL(imgType, quality);
              return imgURL;
            }
            break;
          case 'file':
            {
              // 轉(zhuǎn)為文件格式
              return new Promise((resolve) => {
                drawing.toBlob(
                  (blob) => {
                    resolve(blob);
                  },
                  imgType,
                  quality
                );
              });
            }
            break;
        }
      };</script>
  </body>


關(guān)系及轉(zhuǎn)換


關(guān)系及轉(zhuǎn)換

`

字符串 → Uint8Array
var str = 'ab';
console.log(Uint8Array.from(str.split(''), (e) => e.charCodeAt(0))); // Uint8Array(2) [97, 98]
Uint8Array → 字符串
var u8 = Uint8Array.of(97, 98);
console.log(Array.from(u8, (e) => String.fromCharCode(e)).join('')); // ab
字符串 → DataUrl
var str = 'ab';
console.log('data:application/octet-stream;base64,' + btoa(str)); // data:application/octet-stream;base64,YWI=
DataUrl -> 字符串
var data = 'data:application/octet-stream;base64,YWI=';
console.log(atob(data.split(',')[1])); // ab
Uint8Array -> ArrayBuffer
var u8 = Uint8Array.of(1, 2);
console.log(u8.buffer); // ArrayBuffer(2) {}
ArrayBuffer -> Uint8Array
var buffer = new ArrayBuffer(2);
console.log(new Uint8Array(buffer)); // Uint8Array(2) [0, 0]
ArrayBuffer -> DataView
var buffer = new ArrayBuffer(2);
var dataView = new DataView(buffer, 0); // DataView(2) {}
DataView -> ArrayBuffer
console.log(dataView.buffer); // ArrayBuffer(2) {}
ArrayBuffer → Blob
var buffer = new ArrayBuffer(32);
var blob = new Blob([buffer]);  // Blob {size: 32, type: ""}
UintXXArray → Blob
var u8 = Uint8Array.of(97, 32, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33);
var blob = new Blob([u8])

##### 字符串 → Blob
var blob = new Blob(['Hello World!'], {type: 'text/plain'}); 
// Blob {size: 12, type: "text/plain"}`

以上都是用new Blob()轉(zhuǎn)blob

DataUrl -> blob
var data = 'data:application/octet-stream;base64,YWI=';
    function dataURLtoBlob(dataurl) {
      var arr = dataurl.split(','),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);

      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new Blob([u8arr], { type: mime });
    }
    console.log(dataURLtoBlob(data)); // Blob {size: 2, type: "application/octet-stream"}
Blob →

需要用到 FileReader 的 Api 轉(zhuǎn)換 readAsText(Blob)甩卓、readAsArrayBuffer(Blob)、readAsDataURL() 但是需要異步執(zhí)行

var blob = new Blob(['a Hello world!'], { type: 'text/plain' });
    var reader = new FileReader();
    reader.readAsText(blob, 'utf-8');
    reader.onload = function (e) {
      console.info(reader.result); // a Hello world!
    };
    reader.onerror = function (e) {
      console.error(reader.error);
    }

可以用promise做多次轉(zhuǎn)換

var blob = new Blob(['a Hello world!'], { type: 'text/plain' });
    function read(blob) {
      var fr = new FileReader();
      var pr = new Promise((resolve, reject) => {
        fr.onload = (eve) => {
          resolve(fr.result);
        };
        fr.onerror = (eve) => {
          reject(fr.error);
        };
      });

      return {
        arrayBuffer() {
          fr.readAsArrayBuffer(blob);
          return pr;
        },
        binaryString() {
          fr.readAsBinaryString(blob);
          return pr;
        },
        dataURL() {
          fr.readAsDataURL(blob);
          return pr;
        },
        text() {
          fr.readAsText(blob);
          return pr;
        },
      };
    }
    var pstr1 = read(blob).binaryString();
    var pstr2 = read(blob)
      .arrayBuffer()
      .then((e) => Array.from(new Uint8Array(e), (e) => String.fromCharCode(e)).join(''));
    Promise.all([pstr1, pstr2]).then((e) => {
      console.log(e[0]); // a Hello world!
      console.log(e[0] === e[1]); // true
    });


應(yīng)用場(chǎng)景


圖像灰度化

這里主要用到canvasimageData的轉(zhuǎn)換

<body>
    <button onclick="drawngray()">黑白圖片</button>
    <img src="./syz.jpg" alt="" />
    <canvas id="myCanvas">canvas</canvas>
    <script> var drawngray = function () {
        var myCanvas = document.getElementById('myCanvas');
        if (myCanvas.getContext) {
          var context = myCanvas.getContext('2d');
          var image = document.images[0];
          // 動(dòng)態(tài)設(shè)置canvas的大小
          myCanvas.width = image.width;
          myCanvas.height = image.height;
          var imageData, data, i, len, average, red, green, blue, alpha;
          //繪制原始圖像
          context.drawImage(image, 0, 0);
          //取得圖像數(shù)據(jù)
          imageData = context.getImageData(0, 0, image.width, image.height);
          data = imageData.data;
          for (i = 0, len = data.length; i < len; i += 4) {
            red = data[i];
            green = data[i + 1];
            blue = data[i + 2];
            // alpha = data[i + 3];
            //求得 rgb 平均值
            average = Math.floor((red + green + blue) / 3);
            //設(shè)置顏色值蕉斜,透明度不變
            data[i] = average;
            data[i + 1] = average;
            data[i + 2] = average;
          }

          //回寫(xiě)圖像數(shù)據(jù)并顯示結(jié)果
          imageData.data = data;
          context.putImageData(imageData, 0, 0);
        }
      };</script>
  </body>

除次之外 getImageDataputImageData 還可以用作 cavas 圖片復(fù)制: https://www.w3school.com.cn/tiy/t.asp?f=html5_canvas_getimagedata

圖片壓縮

在前端要實(shí)現(xiàn)圖片壓縮猛频,我們可以利用 Canvas 對(duì)象提供的 toDataURL() 方法

compress.js

const MAX_WIDTH = 800; // 圖片最大寬度

function compress(base64, quality, mimeType) {
  let canvas = document.createElement('canvas');
  let img = document.createElement('img');
  img.crossOrigin = 'anonymous';
  return new Promise((resolve, reject) => {
    img.src = base64;
    img.onload = () => {
      let targetWidth, targetHeight;
      if (img.width > MAX_WIDTH) {
        targetWidth = MAX_WIDTH;
        targetHeight = (img.height * MAX_WIDTH) / img.width;
      } else {
        targetWidth = img.width;
        targetHeight = img.height;
      }
      canvas.width = targetWidth;
      canvas.height = targetHeight;
      let ctx = canvas.getContext('2d');
      ctx.clearRect(0, 0, targetWidth, targetHeight); // 清除畫(huà)布
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      // 通過(guò)toDataURL壓縮后的base64
      let imageData = canvas.toDataURL(mimeType, quality / 100);
      resolve(imageData);
    };
  });
}

test.html

<body>
    <input type="file" accept="image/*" onchange="loadFile(event)" />
    <script src="./compress.js"></script>
    <script> function dataUrlToBlob(base64) {
        var arr = base64.split(','),
          mime = arr[0].match(/:(.*?);/)[1],
          bstr = atob(arr[1]),
          n = bstr.length,
          u8arr = new Uint8Array(n);

        while (n--) {
          u8arr[n] = bstr.charCodeAt(n);
        }
        return new Blob([u8arr], { type: mime });
      }

      function uploadFile(url, blob) {
        let formData = new FormData();
        let request = new XMLHttpRequest();
        // 封裝到FormData中進(jìn)行文件的上傳
        formData.append('image', blob);
        request.open('POST', url, true);
        request.send(formData);
      }

      const loadFile = function (event) {
        const reader = new FileReader();
        reader.onload = async function () {
          let compressedDataURL = await compress(reader.result, 90, 'image/jpeg');
          // 壓縮后將base64轉(zhuǎn)為Blob 對(duì)象減少傳輸數(shù)據(jù)量
          let compressedImageBlob = dataUrlToBlob(compressedDataURL);
          uploadFile('https://httpbin.org/post', compressedImageBlob);
        };
        // 獲取用戶(hù)選取的圖片文件,通過(guò)FileReader轉(zhuǎn)化成base64
        reader.readAsDataURL(event.target.files[0]);
      };</script>
  </body>

分片上傳

<body>
    <input type="file" name="file" onchange="selfile();" />

    <script> const url = 'https://httpbin.org/post';
      /**
       * @param file 原始文件
       * @param chunkSize 默認(rèn)每次上傳分片大小
       */
      async function chunkedUpload(file, chunkSize = 1024 * 1024 * 5) {
        // 將文件拆分成chunkSize大小的分塊,然后每次請(qǐng)求只需要上傳這一個(gè)部分的分塊即可
        for (let start = 0; start < file.size; start += chunkSize) {
          // File對(duì)象繼承自Blob對(duì)象蛛勉,因此可以使用slice方法對(duì)大文件進(jìn)行切
          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())
            .then((res) => console.log(res)); // 打印上傳結(jié)果
        }
      }

      function selfile() {
        let file = document.querySelector('[name=file]').files[0];

        // 自定義分片大小
        const LENGTH = 1024 * 1024 * 1;
        chunkedUpload(file, LENGTH);
      }</script>
  </body>

服務(wù)器接收到這些切片后,再將他們拼接起來(lái)就可以了睦柴,下面是PHP拼接切片的示例代碼:

$filename = './upload/' . $_POST['filename'];//確定上傳的文件名
//第一次上傳時(shí)沒(méi)有文件诽凌,就創(chuàng)建文件,此后上傳只需要把數(shù)據(jù)追加到此文件中
if(!file_exists($filename)){
    move_uploaded_file($_FILES['file']['tmp_name'],$filename);
}else{
    file_put_contents($filename,file_get_contents($_FILES['file']['tmp_name']),FILE_APPEND);
    echo $filename;
}

測(cè)試時(shí)記得修改 nginx 的 server 配置坦敌,否則大文件可能會(huì)提示 413 Request Entity Too Large 的錯(cuò)誤侣诵。

server {
 // ...
 client_max_body_size 50m;
}

文件下載

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Blob 文件下載示例</title>
  </head>

  <body>
    <button id="downloadBtn">文件下載</button>
  </body>
  <script>
  const download = (fileName, blob) => {
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = fileName;
  link.click();
  link.remove();
  URL.revokeObjectURL(link.href);
};

const downloadBtn = document.querySelector("#downloadBtn");
downloadBtn.addEventListener("click", (event) => {
  const fileName = "blob.txt";
  const myBlob = new Blob(["一文徹底掌握 Blob Web API"], { type: "text/plain" });
  download(fileName, myBlob);
});
  </script>
</html>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狱窘,隨后出現(xiàn)的幾起案子杜顺,更是在濱河造成了極大的恐慌,老刑警劉巖蘸炸,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躬络,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡搭儒,警方通過(guò)查閱死者的電腦和手機(jī)穷当,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)提茁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人馁菜,你說(shuō)我怎么就攤上這事茴扁。” “怎么了汪疮?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵峭火,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我智嚷,道長(zhǎng)卖丸,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任纤勒,我火速辦了婚禮坯苹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘摇天。我一直安慰自己粹湃,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布泉坐。 她就那樣靜靜地躺著为鳄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腕让。 梳的紋絲不亂的頭發(fā)上孤钦,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音纯丸,去河邊找鬼偏形。 笑死,一個(gè)胖子當(dāng)著我的面吹牛觉鼻,可吹牛的內(nèi)容都是我干的俊扭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼坠陈,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼萨惑!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起仇矾,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤庸蔼,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后贮匕,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體姐仅,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了萍嬉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乌昔。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖壤追,靈堂內(nèi)的尸體忽然破棺而出磕道,到底是詐尸還是另有隱情,我是刑警寧澤行冰,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布溺蕉,位于F島的核電站,受9級(jí)特大地震影響悼做,放射性物質(zhì)發(fā)生泄漏疯特。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一肛走、第九天 我趴在偏房一處隱蔽的房頂上張望漓雅。 院中可真熱鬧,春花似錦朽色、人聲如沸邻吞。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)抱冷。三九已至,卻和暖如春梢褐,著一層夾襖步出監(jiān)牢的瞬間旺遮,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工盈咳, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留耿眉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓鱼响,卻偏偏與公主長(zhǎng)得像跷敬,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子热押,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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