造一個(gè) copy-to-clipboard 輪子

項(xiàng)目代碼:https://github.com/Haixiang6123/my-copy-to-clipboard
預(yù)覽地址:http://yanhaixiang.com/my-copy-to-clipboard/
參考輪子:https://www.npmjs.com/package/copy-to-clipboard

用 JS 來復(fù)制文本在網(wǎng)頁(yè)應(yīng)用里十分常見岂却,比如 github 里復(fù)制 remote 地址的功能:


今天就來帶大家一起寫一個(gè) JS 復(fù)制文本的輪子吧~

從零開始

關(guān)于 JS 做復(fù)制功能的文章還挺多的瞒御,這里列舉一篇 阮一峰的《剪貼板操作 Clipboard API 教程》 作為例子。

大部分文章的做法是這樣:創(chuàng)建一個(gè)輸入框(input 或者 textarea)碘橘,將復(fù)制文本賦值到元素的 value 值弊攘,JS 選中文本內(nèi)容笆载,最后使用 document.exec('copy') 完成復(fù)制蜈漓。

這里的問題是蛹锰,在某些環(huán)境下文本輸入框會(huì)存在一些怪異的行為,比如:

  • 如果不是文本輸入標(biāo)簽熔脂,需要主動(dòng)創(chuàng)建一個(gè)可輸入文本的標(biāo)簽(input和textarea)然后將待復(fù)制的文本賦值給這個(gè)標(biāo)簽承匣,再調(diào)用.select()方法選中這個(gè)標(biāo)簽才能繼續(xù)執(zhí)行 document.execCommand('copy') 去復(fù)制。
  • 如果是文本輸入標(biāo)簽锤悄,標(biāo)簽不可以賦予 disable 或者 readonly,這會(huì)影響 select() 方法嘉抒。
  • 移動(dòng)端 iOS 在選中輸入框的時(shí)候會(huì)有自動(dòng)調(diào)整頁(yè)面縮放的問題零聚,如果沒有對(duì)這個(gè)進(jìn)行處理,調(diào)用 select() 方法時(shí)(其實(shí)就是讓標(biāo)簽處于focus狀態(tài))會(huì)出現(xiàn)同樣的問題些侍。

聽起來就很麻煩隶症。為了去掉這些兼容問題,可以使用 <span> 元素作為復(fù)制文本的容器岗宣,那先按上面的思路蚂会,造一個(gè)最簡(jiǎn)單的輪子吧。

const copy = (text: string) => {
  const range = document.createRange()
  const selection = document.getSelection()

  const mark = document.createElement('span')
  mark.textContent = text

  // 插入 body 中
  document.body.appendChild(mark)

  // 選中
  range.selectNodeContents(mark)
  selection.addRange(range)

  const success = document.execCommand('copy')

  if (success) {
    alert('復(fù)制成功')
  } else {
    alert('復(fù)制失敗')
  }

  if (mark) {
    document.body.removeChild(mark)
  }
}

這里用到 Selection 和 Range 兩個(gè)對(duì)象耗式。關(guān)于 Selection 表示用戶選擇的文本范圍或插入符號(hào)的當(dāng)前位置胁住。它代表頁(yè)面中的文本選區(qū)趁猴,可能橫跨多個(gè)元素;而 Range 表示一個(gè)包含節(jié)點(diǎn)與文本節(jié)點(diǎn)的一部分的文檔片段彪见。一個(gè) Selection 可以有多個(gè) Range 對(duì)象儡司。

上面邏輯很簡(jiǎn)單,創(chuàng)建 span 元素余指,從 textContent 加入復(fù)制文本捕犬。這里有人就問了:為啥不用 innerText 呢?他們有什么區(qū)別呢酵镜?區(qū)別詳見 Stackoverflow: Difference between textContent vs innerText碉碉。

好的我知道你不會(huì)看的,這里就簡(jiǎn)單列一下吧:

  1. 首先 innerText 是非標(biāo)準(zhǔn)的淮韭,textContent 是標(biāo)準(zhǔn)的
  2. innerText 非常容易受 CSS 的影響垢粮,textContent 則不會(huì):innerText 只返回可見的文本,而 textContent 返回全文本缸濒。比如 "Hello Wold" 文本足丢,用 display: none 把 "Hello" 變成看不見了,那么 innerText 會(huì)返回 "World"庇配,而 textContent 返回 "Hello World"斩跌。
  3. innerText 性能差一點(diǎn),因?yàn)樾枰鹊戒秩就炅酥笸ㄟ^頁(yè)面布局信息來獲取文本
  4. innerText 通過 HTMLElement 拿到捞慌,而 textContent 可以通過所有 Node 拿到耀鸦,獲取范圍更廣一些

回到代碼,把創(chuàng)建好的 span 放入 document.body 里啸澡,并選中元素袖订,把 range 加入 selection 中,document.exec 執(zhí)行復(fù)制操作嗅虏,最后一步把 mark 元素移除洛姑,收工了。

復(fù)制時(shí)好時(shí)壞

如果你弄了個(gè)按鈕并綁定 copy('Hello')皮服,點(diǎn)擊后會(huì)發(fā)現(xiàn):咦楞艾?怎么時(shí)好時(shí)壞的?一會(huì)可以復(fù)制一會(huì)又不行了龄广。

剛剛提到 Selection 有可能是插入符號(hào)的當(dāng)前位置硫眯,啥意思?想一想鼠標(biāo)點(diǎn)一下算不算選區(qū)呢择同?算的两入,只是長(zhǎng)度為 0 你看不見而已。

這時(shí)它被標(biāo)記為 Collapsed敲才,這表示選區(qū)被壓縮至一點(diǎn)裹纳,即光標(biāo)位置择葡。—— Selection

長(zhǎng)度為 0 好像也沒什么問題嘛痊夭,剛剛代碼不是 addRange 了么刁岸?然而 addRange 并不會(huì)添加新 Range 到 Selection 中!

Currently only Firefox supports multiple selection ranges, other browsers will not add new ranges to the selection if it already contains one. —— Selection.addRange()

總結(jié)一下復(fù)制不成功的問題:

  1. 當(dāng)鼠標(biāo)無(wú)意地點(diǎn)擊到頁(yè)面時(shí)(比如按鈕)她我,Selection 會(huì)加入一個(gè)看不見的 Range(變成光標(biāo)的位置虹曙,而不是一個(gè)選中的區(qū)域了)
  2. 在我們代碼中 selection.addRange 后并不會(huì)把 span 里的選中文本作為新的 Range 加入 Selection
  3. 執(zhí)行 document.exec('copy') 的時(shí)候,由于選區(qū)是個(gè)光標(biāo)位置番舆,復(fù)制了個(gè)寂寞酝碳,粘貼板還是原來的復(fù)制內(nèi)容,不會(huì)改變恨狈,如果原來是空疏哗,那粘貼出來的還是空
  4. 既然執(zhí)行了個(gè)寂寞,為啥 success 不為 false 呢禾怠?因?yàn)?MDN 說了執(zhí)行成功或者失敗和返回值毛關(guān)系沒有返奉,只有 document.exec 不被瀏覽器支持或未被啟用才會(huì)返回 false

Note: document.execCommand() only returns true if it is invoked as part of a user interaction. You can't use it to verify browser support before calling a command. From Firefox 82, nested document.execCommand() calls will always return false. —— Document.execCommand()

解決方法是:使用 selection.removeAllRanges吗氏,在 selection.addRange 之前把原有的 Range 清干凈就可以了芽偏。

const copy = (text: string) => {
  const range = document.createRange()
  const selection = document.getSelection()

  const mark = document.createElement('span')
  mark.textContent = text

  document.body.appendChild(mark)

  range.selectNodeContents(mark)
  selection.removeAllRanges() // 移除調(diào)用前已經(jīng)存在 Range
  selection.addRange(range)

  const success = document.execCommand('copy')

  if (success) {
    console.log('復(fù)制成功')
  } else {
    console.log('復(fù)制失敗')
  }

  if (mark) {
    document.body.removeChild(mark)
  }
}

上面使用 selection.removeAllRanges 移除當(dāng)前的 Range,這樣就可以把要復(fù)制的 Range 加入到 Selection 中了弦讽。

toggle-selection

上面雖然解決了不能復(fù)制的問題污尉,但是會(huì)把原來選中的區(qū)域也整沒了。比如用戶選了一段文字往产,執(zhí)行了 copy 導(dǎo)致原來的文字沒有選中了被碗。copy 函數(shù)就會(huì)有 side-effect 了,對(duì)應(yīng)用不友好仿村。

解決方法也很簡(jiǎn)單:執(zhí)行 copy 前移除當(dāng)前選區(qū)锐朴,執(zhí)行過后再恢復(fù)原來選區(qū)。

export const deselectCurrent = () => {
  const selection = document.getSelection()

  // 當(dāng)前沒有選中
  if (selection.rangeCount === 0) {
    return () => {}
  }

  let $active = document.activeElement

  // 獲取當(dāng)前選中的 ranges
  const ranges: Range[] = []
  for (let i = 0; i < selection.rangeCount; i++) {
    ranges.push(selection.getRangeAt(i))
  }

  // deselect
  selection.removeAllRanges();

  return () => {
    // 如果是插入符則移除 ranges
    if (selection.type === 'Caret') {
      selection.removeAllRanges()
    }

    // 沒有選中蔼囊,就把之前的 ranges 加回來
    if (selection.rangeCount === 0) {
      ranges.forEach(range => {
        selection.addRange(range)
      })
    }
  }
}

deselectCurrent 函數(shù)將當(dāng)前選區(qū)存在 ranges 里包颁,最后返回一個(gè)函數(shù),該函數(shù)可用于恢復(fù)當(dāng)前選區(qū)压真。

另外,我們還要考慮到如果 activeElement 為 input 或 textarea 的情況蘑险,deselect 時(shí)要 blur滴肿,reselect 時(shí)則要 focus 回來。

export const deselectCurrent = () => {
  const selection = document.getSelection()

  if (selection.rangeCount === 0) {
    return () => {}
  }

  let $active = document.activeElement

  const ranges: Range[] = []
  for (let i = 0; i < selection.rangeCount; i++) {
    ranges.push(selection.getRangeAt(i))
  }

  // 如果為輸入元素先 blur 再 focus
  switch ($active.tagName.toUpperCase()) {
    case 'INPUT':
    case 'TEXTAREA':
      ($active as HTMLInputElement | HTMLTextAreaElement).blur()
      break
    default:
      $active = null
  }

  selection.removeAllRanges();

  return () => {
    if (selection.type === 'Caret') {
      selection.removeAllRanges()
    }
    if (selection.rangeCount === 0) {
      ranges.forEach(range => {
        selection.addRange(range)
      })
    }

    // input 或 textarea 要再 focus 回來
    if ($active) {
      ($active as HTMLInputElement | HTMLTextAreaElement).focus()
    }
  }
}

copy 里就可以愉快 deselect 和 reselect 了:

const copy = (text: string) => {
  const reselectPrevious = deselectCurrent() // 去掉當(dāng)前選區(qū)

  ...

  const success = document.execCommand('copy')

  if (mark) {
    document.body.removeChild(mark)
  }

  reselectPrevious() // 恢復(fù)以前的選區(qū)

  return success
}

onCopy

復(fù)制的時(shí)候?qū)⒂|發(fā) copy 事件佃迄,因此這里還可以給調(diào)用方提供 onCopy 的回調(diào)泼差,自定義 listener贵少。

interface Options {
  onCopy?: (copiedText: DataTransfer | null) => unknown
}

const copy = (text: string, options: Options = {}) => {
  const {onCopy} = options

  const reselectPrevious = deselectCurrent()

  const range = document.createRange()
  const selection = document.getSelection()

  const mark = document.createElement('span')
  mark.textContent = text

  // 自定義 onCopy
  mark.addEventListener('copy', (e) => {
    if (onCopy) {
      e.stopPropagation()
      e.preventDefault()
      onCopy(e.clipboardData)
    }
  })

  document.body.appendChild(mark)

  range.selectNodeContents(mark)
  selection.addRange(range)

  const success = document.execCommand('copy')

  if (mark) {
    document.body.removeChild(mark)
  }

  reselectPrevious()

  return success
}

這里添加了 "copy" 事件的監(jiān)聽。e.stopPropagation 阻止 copy 事件冒泡堆缘,e.prevenDefault 禁止默認(rèn)響應(yīng)滔灶,然后用 onCopy 函數(shù)接管復(fù)制事件的響應(yīng)。同時(shí)吼肥,onCopy 里傳入 e.clipbaordData录平,調(diào)用方可以隨意處理復(fù)制的數(shù)據(jù)。

比如:

$myCopy.onclick = () => {
  const myText = 'my text'

  copy('xxx', {
    onCopy: (clipboardData) => clipboardData.setData('text/plain', myText), // 復(fù)制 'my-text'
  })
}

有人就會(huì)問了:這個(gè) setData 好理解缀皱,不就設(shè)置復(fù)制文本嘛斗这,那要這個(gè) “text/plain" 干嘛用?

DataTransfer 里的 format

不知道大家有沒有關(guān)注過 clipboardData 類型呢啤斗?它其實(shí)是一個(gè) DataTransfer 的類型表箭,那 DataTransfer 又是干啥的?一般是拖拽時(shí)钮莲,用于存放拖拽內(nèi)容的免钻。復(fù)制也算是數(shù)據(jù)轉(zhuǎn)移的一種,所以 clipboardData 也為 DataTransfer 類型崔拥。

復(fù)制本質(zhì)上是復(fù)制內(nèi)容而非單一的文本极舔,也有格式的。我們可能學(xué)時(shí)一般就復(fù)制幾個(gè)文字握童,但是在一些情況下姆怪,比如復(fù)制一個(gè)鏈接、一個(gè) <h1> 標(biāo)簽的元素澡绩、甚至一張圖片后稽揭,當(dāng)粘貼到 docs 文件的時(shí)候,會(huì)發(fā)現(xiàn)這些元素的樣式和圖片全都帶過來了肥卡。

為什么發(fā)生這樣的事溪掀?因?yàn)樵趶?fù)制的時(shí)候系統(tǒng)會(huì)設(shè)定 format,而 World 正好可以識(shí)別這些 format步鉴,所以可以直接展示出帶樣式的復(fù)制內(nèi)容揪胃。

目前我們的函數(shù)僅支持純文本的復(fù)制,應(yīng)該再加一個(gè) format氛琢,讓調(diào)用方自定義復(fù)制的格式喊递。

interface Options {
  onCopy?: (copiedText: DataTransfer | null) => unknown
  format?: Format
}

const copy = (text: string, options: Options = {}) => {
  const {onCopy} = options

  const reselectPrevious = deselectCurrent()

  const range = document.createRange()
  const selection = document.getSelection()

  const mark = document.createElement('span')
  mark.textContent = text

  mark.addEventListener('copy', (e) => {
    e.stopPropagation();

    // 帶格式去復(fù)制內(nèi)容
    if (format) {
      e.preventDefault()
      e.clipboardData.clearData()
      e.clipboardData.setData(format, text)
    }

    if (onCopy) {
      e.preventDefault()
      onCopy(e.clipboardData)
    }
  })

  document.body.appendChild(mark)

  range.selectNodeContents(mark)
  selection.addRange(range)

  const success = document.execCommand('copy')

  if (mark) {
    document.body.removeChild(mark)
  }

  reselectPrevious()

  return success
}

在剛剛代碼基礎(chǔ)上,我們可以在 copy 事件里判斷是否有 format阳似,如果有則直接接管 copy listener骚勘,clearData 清除復(fù)制內(nèi)容,然后 setData(format, text) 來復(fù)制內(nèi)容。

兼容 IE

前端工程師們都會(huì)有一個(gè)共通的一生之?dāng)场狪E俏讹。目前查了文檔当宴,有以下兼容問題:

  • 在 IE 11 下,format 這里只有 TextUrl 兩種
  • 在 IE 下泽疆,copy 事件中 e.clipboardDataundefined户矢,但是會(huì)有 window.clipboardData
  • 在 IE 9 以下,document.execCommand 可能不被支持(有些貼子說可以殉疼,有些貼子說有問題)

針對(duì)上面的問題梯浪,我們要為 formate.clipboardDatadocument.execCommand 做好兜底兼容操作株依。

首先是 format驱证,提供一個(gè) format 的轉(zhuǎn)換 Mapper:

type Format = 'text/plain' | 'text/html' | 'default'
type IE11Format = 'Text' | 'Url'

const clipboardToIE11Formatting: Record<Format, IE11Format> = {
  "text/plain": "Text",
  "text/html": "Url",
  "default": "Text"
}

接下來是 e.clipboardData 做兼容,這里有個(gè)知識(shí)點(diǎn)是在 IE 下恋腕,window 會(huì)有一個(gè) clipboardData抹锄,我們可以把要復(fù)制的內(nèi)容存到 window.clipboardData注意:這個(gè)全局變量只有 IE 下才會(huì)有荠藤,普通情況下還是使 e.clipboardData伙单。

const copy = (text: string, options: Options = {}) => {
  ...

  mark.addEventListener('copy', (e) => {
    e.stopPropagation();
    if (format) {
      e.preventDefault()
      if (!e.clipboardData) {
        // 只有 IE 11 里 e.clipboardData 一直為 undefined
        // 這里 format 要轉(zhuǎn)為 IE 11 里指定的 format
        const IE11Format = clipboardToIE11Formatting[format || 'default']
        // @ts-ignore clearData 只有 IE 上有
        window.clipboardData.clearData()
        // @ts-ignore setData 只有 IE 上有
        window.clipboardData.setData(IE11Format, text);
      } else {
        e.clipboardData.clearData()
        e.clipboardData.setData(format, text)
      }
    }

    if (onCopy) {
      e.preventDefault()
      onCopy(e.clipboardData)
    }
  })

  ...
}

最后一步是對(duì) document.execCommand 做兼容。目前我自己搜到的是會(huì)出現(xiàn)不生效的問題哈肖,以及 execCommand 不支持的問題吻育,為了應(yīng)對(duì) IE 下絕大多的問題,我們可以祭出 try-catch 大法淤井,只要有 error布疼,通通走 IE 的老路子去做復(fù)制。

const copy = (text: string, options: Options = {}) => {
  ...…

  try {
    // execCommand 有些瀏覽器可能不支持币狠,這里要 try 一下
    success = document.execCommand('copy')

    if (!success) {
      throw new Error("Can't not copy")
    }
  } catch (e) {
    try {
      // @ts-ignore window.clipboardData 這鬼玩意只有 IE 上有
      window.clipboardData.setData(format || 'text', text)
      // @ts-ignore window.clipboardData 這鬼玩意只有 IE 上有
      onCopy && onCopy(window.clipboardData)
    } catch (e) {
      // 最后兜底方案游两,讓用戶在 window.prompt 的時(shí)候輸入
      window.prompt('輸入需要復(fù)制的內(nèi)容', text)
    }
  } finally {
    if (selection.removeRange) {
      selection.removeRange(range)
    } else {
      selection.removeAllRanges()
    }

    if (mark) {
      document.body.removeChild(mark)
    }
    reselectPrevious()
  }

  return success
}

上面加了好幾個(gè) try-catch,第一個(gè)兼容 document.execCommand漩绵,有問題走 window.clipboardData.setData 的方式來復(fù)制贱案。第二個(gè)為兜底方案,使用 window.prompt 作為兜底止吐。

最后 finally 里對(duì) selection.removeRange 做了兼容宝踪,優(yōu)先使用 removeRange,失敗再使用 removeAllRanges 清除所有 Range碍扔。

兼容樣式

在創(chuàng)建和添加 mark 時(shí)還要對(duì)其樣式進(jìn)行處理瘩燥,防止頁(yè)面出現(xiàn) side-effect,比如:

  • 添加和刪除 mark 不能造成頁(yè)面滾動(dòng)
  • span 元素的 space 和 line-break 要為 pre不同,復(fù)制時(shí)可以把換行等特殊符號(hào)也帶上
  • 外部有可能會(huì)被設(shè)置成 "none"厉膀,所以 user-select 一定要為 "text",不然連選都選不中
const updateMarkStyles = (mark: HTMLSpanElement) => {
  // 重置用戶樣式
  mark.style.all = "unset";
  // 放在 fixed,防止添加元素后觸發(fā)滾動(dòng)行為
  mark.style.position = "fixed";
  mark.style.top = '0';
  mark.style.clip = "rect(0, 0, 0, 0)";
  // 保留 space 和 line-break 特性
  mark.style.whiteSpace = "pre";
  // 外部有可能 user-select 為 'none'站蝠,因此這里設(shè)置為 text
  mark.style.userSelect = "text";
}

const copy = (text: string, options: Options = {}) => {
  ...

  const mark = document.createElement('span')
  mark.textContent = text

  updateMarkStyles(mark)

  mark.addEventListener('copy', (e) => {
    ...
  })
  ...
}

在創(chuàng)建 span 元素之后應(yīng)該馬上更新樣式,確保不會(huì)有頁(yè)面變化(副作用)卓鹿。

總結(jié)

目前已經(jīng)完成 copy-to-clipboard 這個(gè)庫(kù)的所有功能了菱魔,主要做了以下幾件事:

  1. 完成復(fù)制功能
  2. 復(fù)制后會(huì)恢復(fù)原來選區(qū)
  3. 提供 onCopy,調(diào)用方可自己定義復(fù)制 listener
  4. 提供 format吟孙,可多格式復(fù)制
  5. 兼容了 IE
  6. 對(duì)樣式做了兼容澜倦,在不對(duì)頁(yè)面產(chǎn)生副作用情況下完成復(fù)制功能

最后

JS 復(fù)制這個(gè)需求應(yīng)該不少人都會(huì)遇到過。然而真正研究起來杰妓,要考慮的東西還是很多的藻治。

如果僅僅只是掃一眼源碼可能只會(huì)做出”從零開始“這一版,后面的兼容巷挥、format桩卵、回調(diào)等功能真的特別難想到。

最后再來說一下 Clipboard API倍宾。Clipboard API 是下一代的剪貼板操作方法雏节,比傳統(tǒng)的 document.execCommand() 方法更強(qiáng)大、更合理高职。它的所有操作都是異步的钩乍,返回 Promise 對(duì)象,不會(huì)造成頁(yè)面卡頓怔锌。而且寥粹,它可以將任意內(nèi)容(比如圖片)放入剪貼板。

不過埃元,目前還是 document.execCommand 使用的比較廣泛涝涤。雖然上面也說了 IE 對(duì) document.execCommand 不好,但是 Clipboard API 的兼容性更差亚情,F(xiàn)ireFox 和 Chome 在某些版本可能都會(huì)有問題妄痪。另外還有一個(gè)問題,使用 clipboard API 需要從權(quán)限 Permissions API 獲取權(quán)限之后楞件,才能訪問剪貼板內(nèi)容衫生,這樣會(huì)嚴(yán)重影響用戶體驗(yàn)。用戶:你讓我開權(quán)限土浸,是不是又想偷我密碼罪针??黄伊?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末泪酱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌墓阀,老刑警劉巖毡惜,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異斯撮,居然都是意外死亡经伙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門勿锅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帕膜,“玉大人,你說我怎么就攤上這事溢十】迳玻” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵张弛,是天一觀的道長(zhǎng)荒典。 經(jīng)常有香客問我,道長(zhǎng)乌庶,這世上最難降的妖魔是什么种蝶? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮瞒大,結(jié)果婚禮上螃征,老公的妹妹穿的比我還像新娘。我一直安慰自己透敌,他們只是感情好盯滚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酗电,像睡著了一般魄藕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撵术,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天背率,我揣著相機(jī)與錄音,去河邊找鬼嫩与。 笑死寝姿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的划滋。 我是一名探鬼主播饵筑,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼处坪!你這毒婦竟也來了根资?” 一聲冷哼從身側(cè)響起架专,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎玄帕,沒想到半個(gè)月后部脚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡裤纹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年睛低,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片服傍。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖骂铁,靈堂內(nèi)的尸體忽然破棺而出吹零,到底是詐尸還是另有隱情,我是刑警寧澤拉庵,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布灿椅,位于F島的核電站,受9級(jí)特大地震影響钞支,放射性物質(zhì)發(fā)生泄漏茫蛹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一烁挟、第九天 我趴在偏房一處隱蔽的房頂上張望婴洼。 院中可真熱鬧,春花似錦撼嗓、人聲如沸柬采。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)粉捻。三九已至,卻和暖如春斑芜,著一層夾襖步出監(jiān)牢的瞬間肩刃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工杏头, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盈包,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓大州,卻偏偏與公主長(zhǎng)得像续语,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子厦画,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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