Node.js 設(shè)計(jì)模式筆記 —— 由 Promises 和 Async、Await 實(shí)現(xiàn)的異步模式

回調(diào)函數(shù)(Callbacks)是 Node.js 中異步編程的底層構(gòu)件泡垃,但它們遠(yuǎn)遠(yuǎn)達(dá)不到對(duì)用戶友好的程度旨怠。對(duì)于實(shí)現(xiàn)代碼中最常見的串行控制流渠驼,一個(gè)未經(jīng)訓(xùn)練的開發(fā)者很容易陷入到 callback hell 問(wèn)題中。即便實(shí)現(xiàn)是正確的鉴腻,該串行控制流也會(huì)顯得不必要的復(fù)雜和脆弱迷扇。

為了獲得更好的異步編程體驗(yàn),第一個(gè)出現(xiàn)的就是 promise爽哎,一種保存了異步操作的狀態(tài)和最終結(jié)果的對(duì)象蜓席。Promise 可以輕易地被串聯(lián)起來(lái),實(shí)現(xiàn)串行控制流倦青,可以像其他任何對(duì)象一樣自由地轉(zhuǎn)移瓮床。Pormise 大大簡(jiǎn)化了異步代碼,后來(lái)在此基礎(chǔ)上又有了 asyncawait产镐,能夠令異步代碼看起來(lái)就像是同步代碼一樣隘庄。

Promises

Promises 是 ECMAScript 2015 標(biāo)準(zhǔn)(ES6)的一部分,為傳遞異步結(jié)果提供了一種健壯的解決方案癣亚,替代原本的 CPS 樣式的回調(diào)函數(shù)丑掺。Promise 能夠令所有主要的異步控制流更加易讀、簡(jiǎn)潔和健壯述雾。

Promise 是一種用來(lái)代表異步操作的最終結(jié)果(或錯(cuò)誤)的對(duì)象街州。在專業(yè)術(shù)語(yǔ)中,當(dāng)異步操作未完成時(shí)玻孟,我們稱 Promise 是 pending 的唆缴;當(dāng)異步操作成功結(jié)束時(shí),Promise 是 fulfilled 的黍翎;當(dāng)異步操作因?yàn)殄e(cuò)誤終止時(shí)面徽,Promise 是 rejected 的;當(dāng) Promise 或者是 fulfilled 或者是 rejected,則將其認(rèn)定為 settled趟紊。

Promise 對(duì)象的 then() 方法可以獲取成功執(zhí)行后的結(jié)果或者終止時(shí)報(bào)出的錯(cuò)誤:

promise.then(onFulfilled, onRejected)

其中 onFulfilled 是一個(gè)回調(diào)函數(shù)氮双,最終會(huì)接收到 Promise 成功時(shí)的值;onRejected是另一個(gè)回調(diào)函數(shù)霎匈,最終會(huì)接收 Promise 異常終止時(shí)的值(如果有的話)戴差。

基于回調(diào)函數(shù)的如下代碼:

asyncOperation(arg, (err, result) => {
  if (err) {
    // handle the error
  }
  // do stuff with the result
})

Promise 實(shí)現(xiàn)上述同樣的功能,則更加優(yōu)雅铛嘱、結(jié)構(gòu)化:

asyncOperationPromise(arg)
  .then(result => {
    // do stuff with result
  }, err => {
    // handle the error
  })

asyncOperationPromise() 會(huì)返回一個(gè) Promise暖释,可以被用來(lái)獲取最終結(jié)果的值或者失敗的原因。但最為關(guān)鍵的屬性是墨吓,then() 方法會(huì)同步地返回另一個(gè) Promise饭入。
更進(jìn)一步地,如果 onFulfilled 或者 onRejected 函數(shù)返回一個(gè)值 x肛真,那么 then() 方法返回的 Promise 會(huì)有以下行為:

  • x 是一個(gè)值,則 then() 返回的 Promise 使用 x 作為自身完成時(shí)的值
  • x 是一個(gè) Promise 且成功完成爽航,則 x 完成時(shí)返回的值作為 then() 返回的 Promise 完成時(shí)的值
  • x 是一個(gè) Promise 且因?yàn)殄e(cuò)誤終止蚓让,則 x 終止的原因作為 then() 返回的 Promise 終止的原因

上述行為能夠令我們將多個(gè) promise 連接成鏈,輕松地將異步操作聚合在一起讥珍。如果我們沒(méi)有指定一個(gè) onFulfilled 或者 onRejected handler历极,Promise 完成時(shí)的值或者終止時(shí)的原因都會(huì)自動(dòng)地傳遞給鏈條中的下一個(gè) Promise。通過(guò) Promise 鏈衷佃,任務(wù)的執(zhí)行順序突然變得很簡(jiǎn)單趟卸。

asyncOperationPromise(arg)
  .then(result1 => {
    // return another promise
    return asyncOperationPromise(arg2)
  })
  .then(result2 => {
    // return a value
    return 'done'
  })
  .then(undefined, err => {
    // any error in the chain is caught here
  })

promise API

Promise 構(gòu)造函數(shù)(new Promise((resolve, reject) => {}))會(huì)創(chuàng)建一個(gè)新的 Promise 實(shí)例,其完成還是終止取決于作為參數(shù)傳入的函數(shù)的行為氏义。
作為參數(shù)傳入的函數(shù)接收如下兩個(gè)參數(shù):

  • resolve(obj):resolve 是一個(gè)函數(shù)锄列,在調(diào)用時(shí)為 Promise 提供完成時(shí)的值。當(dāng) obj 是值時(shí)惯悠,則 obj 本身作為 Promise 完成時(shí)的值邻邮;當(dāng) obj 是另一個(gè) Promise 時(shí),則 obj 完成時(shí)的值作為當(dāng)前 Promise 完成時(shí)的值
  • reject(err):Promise 因?yàn)?err 終止
function delay(milliseconds) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date())
    }, milliseconds)
  })
}

console.log(`${new Date().getSeconds()}s\nDelaying...`)
delay(1000)
  .then(newDate => {
    console.log(`${newDate.getSeconds()}s`)
  })

Promise 最重要的靜態(tài)方法:

  • Promise.resolve(obj):從另一個(gè) Promise克婶、thenable 對(duì)象或者值創(chuàng)建一個(gè)新的 Promise
  • Promise.reject(err):創(chuàng)建一個(gè) Promise筒严,該 Promise 會(huì)因?yàn)?err 終止
  • Promise.all(iterable):從一個(gè)可迭代對(duì)象創(chuàng)建 Promise,若該 iterable 中的每一項(xiàng)都提供了一個(gè) fulfill 值情萤,則 Promise 最終以包含這些值的列表作為 fulfill 值鸭蛙;若其中有任意一項(xiàng) reject,則 Promise.all() 返回的 Promise 以第一個(gè) reject 的 err 終止
  • Promise.allSettled(iterable):此方法會(huì)等待所有輸入的 Promise 或者 fulfill 或者 reject筋岛,之后返回一個(gè)包含所有 fulfill 值和 reject 原因的列表
  • Promise.race(iterable):返回可迭代對(duì)象中第一個(gè) fulfill 或 reject 的 Promise

Promise 關(guān)鍵的實(shí)例方法:

  • promise.catch(onRejected):實(shí)際上就是 promise.then(undefined, onRejected) 的語(yǔ)法糖
  • promise.finally(onFinally):允許我們?cè)O(shè)置一個(gè) onFinally 回調(diào)函數(shù)娶视,在 promise fulfill 或者 reject 時(shí)調(diào)用

順序執(zhí)行

順序執(zhí)行意味著,每次只執(zhí)行一系列任務(wù)中的一個(gè)泉蝌,完成后再依次執(zhí)行后面的任務(wù)歇万。這一系列任務(wù)的先后順序必須是預(yù)先定義好的揩晴,因?yàn)橐粋€(gè)任務(wù)的結(jié)果有可能影響后續(xù)任務(wù)的執(zhí)行。

An example of sequential execution flow with three tasks

上述執(zhí)行流程有著不同形式的變種:

  • 順序執(zhí)行一系列已知的任務(wù)贪磺,不需要在它們之間傳遞數(shù)據(jù)
  • 前一個(gè)任務(wù)的輸出作為后一個(gè)任務(wù)的輸入(chain硫兰、pipelinewaterfall
  • 迭代任務(wù)集合寒锚,同時(shí)在每個(gè)元素上一個(gè)接一個(gè)地運(yùn)行異步任務(wù)

package.json

{
  "name": "03-promises-web-spider-v2",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "cheerio": "^1.0.0-rc.3",
    "mkdirp": "^0.5.1",
    "superagent": "^5.2.2",
    "slug": "^1.1.0"
  },
  "engines": {
    "node": ">=14"
  },
  "engineStrict": true
}

spider.js

import {promises as fsPromises} from 'fs'
import {dirname} from 'path'
import superagent from 'superagent'
import mkdirp from 'mkdirp'
import {urlToFilename, getPageLinks} from './utils.js'
import {promisify} from 'util'

const mkdirpPromises = promisify(mkdirp)

function download(url, filename) {
  console.log(`Downloading ${url}`)
  let content
  return superagent.get(url)
    .then((res) => {
      content = res.text
      return mkdirpPromises(dirname(filename))
    })
    .then(() => fsPromises.writeFile(filename, content))
    .then(() => {
      console.log(`Downloaded and saved: ${url}`)
      return content
    })
}

function spiderLinks(currentUrl, content, nesting) {
  let promise = Promise.resolve()
  if (nesting === 0) {
    return promise
  }
  const links = getPageLinks(currentUrl, content)
  for (const link of links) {
    promise = promise.then(() => spider(link, nesting - 1))
  }

  return promise
}

export function spider(url, nesting) {
  const filename = urlToFilename(url)
  return fsPromises.readFile(filename, 'utf8')
    .catch((err) => {
      if (err.code !== 'ENOENT') {
        throw err
      }

      // The file doesn't exist, so let’s download it
      return download(url, filename)
    })
    .then(content => spiderLinks(url, content, nesting))
}

spider-cli.js

import {spider} from './spider.js'

const url = process.argv[2]
const nesting = Number.parseInt(process.argv[3], 10) || 1

spider(url, nesting)
  .then(() => console.log('Download complete'))
  .catch(err => console.error(err))

utils.js

import {join, extname} from 'path'
import {URL} from 'url'
import slug from 'slug'
import cheerio from 'cheerio'

function getLinkUrl(currentUrl, element) {
  const parsedLink = new URL(element.attribs.href || '', currentUrl)
  const currentParsedUrl = new URL(currentUrl)
  if (parsedLink.hostname !== currentParsedUrl.hostname ||
    !parsedLink.pathname) {
    return null
  }
  return parsedLink.toString()
}

export function urlToFilename(url) {
  const parsedUrl = new URL(url)
  const urlPath = parsedUrl.pathname.split('/')
    .filter(function (component) {
      return component !== ''
    })
    .map(function (component) {
      return slug(component, {remove: null})
    })
    .join('/')
  let filename = join(parsedUrl.hostname, urlPath)
  if (!extname(filename).match(/htm/)) {
    filename += '.html'
  }

  return filename
}

export function getPageLinks(currentUrl, body) {
  return Array.from(cheerio.load(body)('a'))
    .map(function (element) {
      return getLinkUrl(currentUrl, element)
    })
    .filter(Boolean)
}

node spider-cli.js http://www.baidu.com 2

其中的 spiderLinks() 函數(shù)通過(guò)循環(huán)動(dòng)態(tài)地構(gòu)建了一條 Promise 鏈:

  • 先定義一個(gè)“空的” Promise 對(duì)象(resovle 到 undefined)劫映,這個(gè)空 Promise 只是作為鏈條的起點(diǎn)
  • 在循環(huán)中,不斷將 promise 變量更新為新的 Promise 對(duì)象(通過(guò)調(diào)用上一個(gè) Promise 的 then() 方法得到)刹前。這就是 Promise 的異步遍歷模式

for 循環(huán)的最后泳赋,promise 變量會(huì)是最后一個(gè) then() 方法返回的 Promise,因而只有當(dāng)鏈條中的所有 Promise 都 resolve 時(shí)喇喉,promise 才會(huì) resolve祖今。

縱觀所有代碼,我們可以不需要像使用 callback 那樣拣技,強(qiáng)制地包含眾多錯(cuò)誤傳遞邏輯千诬。因而大大減少了代碼量和出錯(cuò)的機(jī)會(huì)。

并行執(zhí)行

在某些情況下膏斤,一系列異步任務(wù)的執(zhí)行順序并不重要徐绑,我們需要的只是當(dāng)所有的任務(wù)都完成后能收到通知。

An example of parallel execution with three tasks

雖然 Node.js 是單線程的莫辨,但得益于其 non-blocking nature傲茄,我們?nèi)钥梢詫?shí)現(xiàn)并發(fā)行為。

An example of how asynchronous tasks run in parallel

比如我們有一個(gè) Main 函數(shù)需要執(zhí)行兩個(gè)異步任務(wù):

  • Main 函數(shù)首先觸發(fā)異步任務(wù) Task1 和 Task2 的執(zhí)行沮榜。異步任務(wù)觸發(fā)后盘榨,會(huì)將程序控制權(quán)立即交還給 Main 函數(shù),再轉(zhuǎn)交給 event loop
  • 當(dāng) Task1 中的異步任務(wù)結(jié)束時(shí)蟆融,event loop 調(diào)用 Task1 的回調(diào)函數(shù)较曼,將控制權(quán)交給 Task1。Task1 執(zhí)行完成自身內(nèi)部的同步指令振愿,通知 Main 函數(shù)并返還控制權(quán)
  • 當(dāng) Task2 中的異步任務(wù)結(jié)束時(shí)捷犹,event loop 調(diào)用 Task2 的回調(diào)函數(shù),將控制權(quán)交給 Task2冕末。在 Task2 的終點(diǎn)萍歉,Main 函數(shù)再次被通知。Main 函數(shù)得知 Task1 和 Task2 全部結(jié)束档桃,繼續(xù)執(zhí)行或者返回結(jié)果

簡(jiǎn)單來(lái)說(shuō)枪孩,在 Node.js 中,我們只能并發(fā)地執(zhí)行異步操作,因?yàn)樗鼈兊牟l(fā)行為是由內(nèi)部的非阻塞 API 控制的蔑舞。同步(阻塞)操作無(wú)法并發(fā)地執(zhí)行拒担,除非它們的執(zhí)行與異步操作交織在一起,或者由 setTimeout()攻询、setImmediate() 包裹从撼。

Promise 實(shí)現(xiàn)并發(fā)執(zhí)行流,可以借助內(nèi)置的 Promise.all() 方法钧栖。該方法會(huì)返回一個(gè)新的 Promise低零,只有當(dāng)所有傳入的 Promise 都 fulfill 時(shí),新 Promise 才會(huì) fulfill拯杠。如果傳入的 Promise 之間沒(méi)有因果關(guān)系掏婶,這些 Promise 就會(huì)并發(fā)地執(zhí)行。

對(duì)于前面的 spider 應(yīng)用潭陪,只需要將 spiderLinks() 函數(shù)改為如下形式:

function spiderLinks(currentUrl, content, nesting) {
  if (nesting === 0) {
    return Promise.resolve()
  }
  const links = getPageLinks(currentUrl, content)
  const promises = links.map(link => spider(link, nesting - 1))
  return Promise.all(promises)
}

Async/await

Promise 鏈相對(duì)于 callback hell 來(lái)說(shuō)肯定是要好太多的雄妥,但是我們?nèi)匀恍枰{(diào)用 then() 方法,以及為鏈條中的每一個(gè)任務(wù)創(chuàng)建新的函數(shù)依溯,對(duì)于日常編程中非常普遍的控制流來(lái)說(shuō)還是比較麻煩茎芭。而 Async/await 可以幫助我們寫出像同步代碼一樣可讀性強(qiáng)、容易理解的異步代碼誓沸。
Async 函數(shù)是一種特殊的函數(shù),在函數(shù)體里面可以使用 await 表達(dá)式“暫鸵妓冢”任意一個(gè) Promise 的執(zhí)行拜隧,將控制權(quán)交還給 async 函數(shù)的調(diào)用者,等該 Promise revolve 后再返回到暫停的地方繼續(xù)執(zhí)行趁仙。

function delay(milliseconds) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date())
    }, milliseconds)
  })
}

async function playingWithDelays() {
  console.log('Initial date: ', new Date())
  const dateAfterOneSecond = await delay(1000)
  console.log('Date after one second: ', dateAfterOneSecond)

  const dateAfterThreeSeconds = await delay(3000)
  console.log('Date after 3 secnods: ', dateAfterThreeSeconds)
  return 'done'
}

playingWithDelays()
  .then(result => {
    console.log(`After 4 seconds: ${result}`)
  })

錯(cuò)誤處理

Async/await 的另一個(gè)巨大的優(yōu)勢(shì)在于洪添,它能夠標(biāo)準(zhǔn)化 try...catch 代碼塊的行為,不管是針對(duì)同步代碼中的 throw雀费,抑或是異步代碼中的 Promise reject干奢。

function delayError(milliseconds) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error(`Error after ${milliseconds}ms`))
    })
  })
}

async function playingWithErrors(throwSyncError) {
  try {
    if (throwSyncError) {
      throw new Error('This is a synchronous error')
    }
    await delayError(1000)
  } catch (err) {
    console.log(`We have an error: ${err.message}`)
  } finally {
    console.log('Done')
  }
}

// playingWithErrors(true)
playingWithErrors(false)

串行執(zhí)行

借助 Async/await,可以對(duì)之前的 spider 應(yīng)用實(shí)現(xiàn)很多優(yōu)化盏袄。比如 download() 函數(shù):

async function download(url, filename) {
  console.log(`Downloading ${url}`)
  const {text: content} = await superagent.get(url)
  await mkdirpPromises(dirname(filename))
  await fsPromises.writeFile(filename, content)
  console.log(`Downloaded and saved: ${url}`)
  return content
}

整段代碼行數(shù)大大減少忿峻,看起來(lái)也很“平整”,沒(méi)有任何層級(jí)和縮進(jìn)辕羽。

接下來(lái)是 spiderLinks() 函數(shù)逛尚,使用 async/await 異步地遍歷一個(gè)列表:

async function spiderLinks(currentUrl, content, nesting) {
  if (nesting === 0) {
    return
  }
  const links = getPageLinks(currentUrl, content)
  for (const link of links) {
    await spider(link, nesting - 1)
  }
}

然后是 spider() 函數(shù),如何簡(jiǎn)單地通過(guò) try...catch 處理錯(cuò)誤刁愿,令異步代碼更加易讀:

export async function spider(url, nesting) {
  const filename = urlToFilename(url)
  let content
  try {
    content = await fsPromises.readFile(filename, 'utf8')
  } catch (err) {
    if (err.code !== 'ENOENT') {
      throw err
    }
    content = await download(url, filename)
  }
  return spiderLinks(url, content, nesting)
}

并行執(zhí)行

使用純 async/await 實(shí)現(xiàn)并行的異步執(zhí)行流程绰寞,可以參考如下代碼:

async function spiderLinks(currentUrl, content, nesting) {
  if (nesting === 0) {
    return
  }
  const links = getPageLinks(currentUrl, content)
  const promises = links.map(link => spider(link, nesting - 1))
  for (const promise of promises) {
    await promise
  }
}

然而上述代碼存在一定的問(wèn)題。如果列表中有一個(gè) Promise reject 了,我們不得不等待列表中其他所有的 Promise 都 resolve滤钱,spiderLinks() 函數(shù)返回的 Promise 才會(huì) reject觉壶。這種行為在多數(shù)情況下都是不理想的。
我們通常都會(huì)想要在操作發(fā)生錯(cuò)誤的第一時(shí)間捕獲錯(cuò)誤信息件缸。因而并行執(zhí)行異步操作铜靶,最后仍建議使用下面形式的代碼:

async function spiderLinks(currentUrl, content, nesting) {
  if (nesting === 0) {
    return
  }
  const links = getPageLinks(currentUrl, content)
  const promises = links.map(link => spider(link, nesting - 1))
  return Promise.all(promises)
}

參考資料

Node.js Design Patterns: Design and implement production-grade Node.js applications using proven patterns and techniques, 3rd Edition

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市停团,隨后出現(xiàn)的幾起案子旷坦,更是在濱河造成了極大的恐慌,老刑警劉巖佑稠,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秒梅,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡舌胶,警方通過(guò)查閱死者的電腦和手機(jī)捆蜀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)幔嫂,“玉大人辆它,你說(shuō)我怎么就攤上這事÷亩鳎” “怎么了锰茉?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)切心。 經(jīng)常有香客問(wèn)我飒筑,道長(zhǎng),這世上最難降的妖魔是什么绽昏? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任协屡,我火速辦了婚禮,結(jié)果婚禮上全谤,老公的妹妹穿的比我還像新娘肤晓。我一直安慰自己,他們只是感情好认然,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布补憾。 她就那樣靜靜地躺著,像睡著了一般卷员。 火紅的嫁衣襯著肌膚如雪余蟹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天子刮,我揣著相機(jī)與錄音威酒,去河邊找鬼窑睁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛葵孤,可吹牛的內(nèi)容都是我干的担钮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼尤仍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼箫津!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎供填,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體田炭,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年漓柑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了教硫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辆布,死狀恐怖瞬矩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锋玲,我是刑警寧澤景用,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站惭蹂,受9級(jí)特大地震影響伞插,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜剿干,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望穆刻。 院中可真熱鬧置尔,春花似錦、人聲如沸氢伟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)朵锣。三九已至谬盐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诚些,已是汗流浹背飞傀。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工皇型, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人砸烦。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓弃鸦,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親幢痘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子唬格,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

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