5 分鐘擼一個(gè)前端性能監(jiān)控工具

簡(jiǎn)單而言推盛,有三點(diǎn)原因:

關(guān)注性能是工程師的本性 + 本分;

頁(yè)面性能對(duì)用戶體驗(yàn)而言十分關(guān)鍵谦铃。每次重構(gòu)對(duì)頁(yè)面性能的提升耘成,僅靠工程師開(kāi)發(fā)設(shè)備的測(cè)試數(shù)據(jù)是沒(méi)有說(shuō)服力的,需要有大量的真實(shí)數(shù)據(jù)用于驗(yàn)證驹闰;

資源掛了瘪菌、加載出現(xiàn)異常,不能總靠用戶投訴才后知后覺(jué)嘹朗,需要主動(dòng)報(bào)警控嗜。

一次性能重構(gòu),在千兆網(wǎng)速和萬(wàn)元設(shè)備的條件下骡显,頁(yè)面加載時(shí)間的提升可能只有 0.1%,但是這樣的數(shù)(土)據(jù)(豪)不具備代表性曾掂。網(wǎng)絡(luò)環(huán)境惫谤、硬件設(shè)備千差萬(wàn)別,對(duì)于中低端設(shè)備而言珠洗,性能提升的主觀體驗(yàn)更為明顯溜歪,對(duì)應(yīng)的數(shù)據(jù)變化更具備代表性。

不少項(xiàng)目都會(huì)把資源上傳到 CDN许蓖。而 CDN 部分節(jié)點(diǎn)出現(xiàn)問(wèn)題的時(shí)候蝴猪,一般不能精準(zhǔn)的告知“某某调衰,你的 xx 資源掛了”,因此需要我們主動(dòng)監(jiān)控自阱。

根據(jù)谷歌數(shù)據(jù)顯示嚎莉,當(dāng)頁(yè)面加載超過(guò) 10s 時(shí),用戶會(huì)感到絕望沛豌,通常會(huì)離開(kāi)當(dāng)前頁(yè)面趋箩,并且很可能不再回來(lái)。

用什么監(jiān)控

關(guān)于前端性能指標(biāo)加派,W3C 定義了強(qiáng)大的 Performance API叫确,其中又包括了 High Resolution Time 、 Frame Timing 芍锦、 Navigation Timing 竹勉、 Performance Timeline 、Resource Timing 娄琉、 User Timing 等諸多具體標(biāo)準(zhǔn)次乓。

本文主要涉及 Navigation Timing 以及 Resource Timing。截至到 2018 年中旬车胡,各大主流瀏覽器均已完成了基礎(chǔ)實(shí)現(xiàn)檬输。

Performance API 功能眾多,其中一項(xiàng)匈棘,就是將頁(yè)面自身以及頁(yè)面中各個(gè)資源的性能表現(xiàn)(時(shí)間細(xì)節(jié))記錄了下來(lái)丧慈。而我們要做的就是查詢和使用。

讀者可以直接在瀏覽器控制臺(tái)中輸入 performance 主卫,查看相關(guān) API逃默。

接下來(lái),我們將使用瀏覽器提供的 window.performance 對(duì)象(Performance API 的具體實(shí)現(xiàn))簇搅,來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)易的前端性能監(jiān)控工具完域。

5 分鐘擼一個(gè)前端性能監(jiān)控工具

第一行代碼

將工具命名為 pMonitor,含義是 performance monitor瘩将。

const pMonitor = {}

復(fù)制代碼

監(jiān)控哪些指標(biāo)

既然是“5 分鐘實(shí)現(xiàn)一個(gè) xxx”系列吟税,那么就要有取舍。因此姿现,本文只挑選了最為重要的兩個(gè)指標(biāo)進(jìn)行監(jiān)控:

頁(yè)面加載時(shí)間

資源請(qǐng)求時(shí)間

看了看時(shí)間肠仪,已經(jīng)過(guò)去了 4 分鐘,小編表示情緒穩(wěn)定备典,沒(méi)有一絲波動(dòng)异旧。

頁(yè)面加載

有關(guān)頁(yè)面加載的性能指標(biāo),可以在 Navigation Timing 中找到提佣。Navigation Timing 包括了從請(qǐng)求頁(yè)面起吮蛹,到頁(yè)面完成加載為止荤崇,各個(gè)環(huán)節(jié)的時(shí)間明細(xì)。

可以通過(guò)以下方式獲取 Navigation Timing 的具體內(nèi)容:

const navTimes = performance.getEntriesByType('navigation')

復(fù)制代碼

getEntriesByType 是我們獲取性能數(shù)據(jù)的一種方式潮针。performance 還提供了 getEntries 以及 getEntriesByName 等其他方式术荤,由于“時(shí)間限制”,

返回結(jié)果是一個(gè)數(shù)組然低,其中的元素結(jié)構(gòu)如下所示:

{

"connectEnd": 64.15495765894057,

"connectStart": 64.15495765894057,

"domainLookupEnd": 64.15495765894057,

"domainLookupStart": 64.15495765894057,

"domComplete": 2002.5385066728431,

"domContentLoadedEventEnd": 2001.7384263440083,

"domContentLoadedEventStart": 2001.2386167400286,

"domInteractive": 1988.638474368076,

"domLoading": 271.75174283737226,

"duration": 2002.9385468372606,

"entryType": "navigation",

"fetchStart": 64.15495765894057,

"loadEventEnd": 2002.9385468372606,

"loadEventStart": 2002.7383663540235,

"name": "document",

"navigationStart": 0,

"redirectCount": 0,

"redirectEnd": 0,

"redirectStart": 0,

"requestStart": 65.28225608537441,

"responseEnd": 1988.283025689508,

"responseStart": 271.75174283737226,

"startTime": 0,

"type": "navigate",

"unloadEventEnd": 0,

"unloadEventStart": 0,

"workerStart": 0.9636893776343863

}

復(fù)制代碼

關(guān)于各個(gè)字段的時(shí)間含義喜每,Navigation Timing Level 2 給出了詳細(xì)說(shuō)明:

不難看出,細(xì)節(jié)滿滿雳攘。因此带兜,能夠計(jì)算的內(nèi)容十分豐富,例如 DNS 查詢時(shí)間吨灭,TLS 握手時(shí)間等等刚照。可以說(shuō)喧兄,只有想不到无畔,沒(méi)有做不到~

既然我們關(guān)注的是頁(yè)面加載,那自然要讀取 domComplete:

const [{ domComplete }] = performance.getEntriesByType('navigation')

復(fù)制代碼

定義個(gè)方法吠冤,獲取 domComplete:

pMonitor.getLoadTime = () => {

const [{ domComplete }] = performance.getEntriesByType('navigation')

return domComplete

}

復(fù)制代碼

到此浑彰,我們獲得了準(zhǔn)確的頁(yè)面加載時(shí)間。

資源加載

既然頁(yè)面有對(duì)應(yīng)的 Navigation Timing拯辙,那靜態(tài)資源是不是也有對(duì)應(yīng)的 Timing 呢郭变?

答案是肯定的,其名為 Resource Timing涯保。它包含了頁(yè)面中各個(gè)資源從發(fā)送請(qǐng)求起诉濒,到完成加載為止,各個(gè)環(huán)節(jié)的時(shí)間細(xì)節(jié)夕春,和 Navigation Timing 十分類(lèi)似未荒。

獲取資源加載時(shí)間的關(guān)鍵字為 'resource', 具體方式如下:

performance.getEntriesByType('resource')

復(fù)制代碼

不難聯(lián)想,返回結(jié)果通常是一個(gè)很長(zhǎng)的數(shù)組及志,因?yàn)榘隧?yè)面上所有資源的加載信息片排。

每條信息的具體結(jié)構(gòu)為:

{

"connectEnd": 462.95008929525244,

"connectStart": 462.95008929525244,

"domainLookupEnd": 462.95008929525244,

"domainLookupStart": 462.95008929525244,

"duration": 0.9620853673520173,

"entryType": "resource",

"fetchStart": 462.95008929525244,

"initiatorType": "img",

"name": "https://cn.bing.com/sa/simg/SharedSpriteDesktopRewards_022118.png",

"nextHopProtocol": "",

"redirectEnd": 0,

"redirectStart": 0,

"requestStart": 463.91217466260445,

"responseEnd": 463.91217466260445,

"responseStart": 463.91217466260445,

"startTime": 462.95008929525244,

"workerStart": 0

}

復(fù)制代碼

以上為 2018 年 7 月 7 日,在 cn.bing.com 下搜索 test 時(shí)速侈,performance.getEntriesByType("resource") 返回的第二條結(jié)果划纽。

我們關(guān)注的是資源加載的耗時(shí)情況,可以通過(guò)如下形式獲得:

const [{ startTime, responseEnd }] = performance.getEntriesByType('resource')

const loadTime = responseEnd - startTime

復(fù)制代碼

同 Navigation Timing 相似锌畸,關(guān)于 startTime 、 fetchStart靖避、connectStart 和 requestStart 的區(qū)別潭枣, Resource Timing Level 2 給出了詳細(xì)說(shuō)明:

并非所有的資源加載時(shí)間都需要關(guān)注比默,重點(diǎn)還是加載過(guò)慢的部分。

出于簡(jiǎn)化考慮盆犁,定義 10s 為超時(shí)界限命咐,那么獲取超時(shí)資源的方法如下:

const SEC = 1000

const TIMEOUT = 10 * SEC

const setTime = (limit = TIMEOUT) => time => time >= limit

const isTimeout = setTime()

const getLoadTime = ({ startTime, responseEnd }) => responseEnd - startTime

const getName = ({ name }) => name

const resourceTimes = performance.getEntriesByType('resource')

const getTimeoutRes = resourceTimes

.filter(item => isTimeout(getLoadTime(item)))

.map(getName)

復(fù)制代碼

這樣一來(lái),我們獲取了所有超時(shí)的資源列表谐岁。

簡(jiǎn)單封裝一下:

const SEC = 1000

const TIMEOUT = 10 * SEC

const setTime = (limit = TIMEOUT) => time => time >= limit

const getLoadTime = ({ requestStart, responseEnd }) =>

responseEnd - requestStart

const getName = ({ name }) => name

pMonitor.getTimeoutRes = (limit = TIMEOUT) => {

const isTimeout = setTime(limit)

const resourceTimes = performance.getEntriesByType('resource')

return resourceTimes.filter(item => isTimeout(getLoadTime(item))).map(getName)

}

復(fù)制代碼

上報(bào)數(shù)據(jù)

獲取數(shù)據(jù)之后醋奠,需要向服務(wù)端上報(bào):

// 生成表單數(shù)據(jù)

const convert2FormData = (data = {}) =>

Object.entries(data).reduce((last, [key, value]) => {

if (Array.isArray(value)) {

return value.reduce((lastResult, item) => {

lastResult.append(`${key}[]`, item)

return lastResult

}, last)

}

last.append(key, value)

return last

}, new FormData())

// 拼接 GET 時(shí)的url

const makeItStr = (data = {}) =>

Object.entries(data)

.map(([k, v]) => `${k}=${v}`)

.join('&')

// 上報(bào)數(shù)據(jù)

pMonitor.log = (url, data = {}, type = 'POST') => {

const method = type.toLowerCase()

const urlToUse = method === 'get' ? `${url}?${makeItStr(data)}` : url

const body = method === 'get' ? {} : { body: convert2FormData(data) }

const option = {

method,

...body

}

fetch(urlToUse, option).catch(e => console.log(e))

}

復(fù)制代碼

回過(guò)頭來(lái)初始化

數(shù)據(jù)上傳的 url、超時(shí)時(shí)間等細(xì)節(jié)伊佃,因項(xiàng)目而異窜司,所以需要提供一個(gè)初始化的方法:

// 緩存配置

let config = {}

/**

* @param {object} option

* @param {string} option.url 頁(yè)面加載數(shù)據(jù)的上報(bào)地址

* @param {string} option.timeoutUrl 頁(yè)面資源超時(shí)的上報(bào)地址

* @param {string=} [option.method='POST'] 請(qǐng)求方式

* @param {number=} [option.timeout=10000]

*/

pMonitor.init = option => {

const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option

config = {

url,

timeoutUrl,

method,

timeout

}

// 綁定事件 用于觸發(fā)上報(bào)數(shù)據(jù)

pMonitor.bindEvent()

}

復(fù)制代碼

何時(shí)觸發(fā)

性能監(jiān)控只是輔助功能,不應(yīng)阻塞頁(yè)面加載航揉,因此只有當(dāng)頁(yè)面完成加載后塞祈,我們才進(jìn)行數(shù)據(jù)獲取和上報(bào)(實(shí)際上,頁(yè)面加載完成前也獲取不到必要信息):

// 封裝一個(gè)上報(bào)兩項(xiàng)核心數(shù)據(jù)的方法

pMonitor.logPackage = () => {

const { url, timeoutUrl, method } = config

const domComplete = pMonitor.getLoadTime()

const timeoutRes = pMonitor.getTimeoutRes(config.timeout)

// 上報(bào)頁(yè)面加載時(shí)間

pMonitor.log(url, { domeComplete }, method)

if (timeoutRes.length) {

pMonitor.log(

timeoutUrl,

{

timeoutRes

},

method

)

}

}

// 事件綁定

pMonitor.bindEvent = () => {

const oldOnload = window.onload

window.onload = e => {

if (oldOnload && typeof oldOnload === 'function') {

oldOnload(e)

}

// 盡量不影響頁(yè)面主線程

if (window.requestIdleCallback) {

window.requestIdleCallback(pMonitor.logPackage)

} else {

setTimeout(pMonitor.logPackage)

}

}

}

復(fù)制代碼

匯總

到此為止帅涂,一個(gè)完整的前端性能監(jiān)控工具就完成了~全部代碼如下:

const base = {

log() {},

logPackage() {},

getLoadTime() {},

getTimeoutRes() {},

bindEvent() {},

init() {}

}

const pm = (function() {

// 向前兼容

if (!window.performance) return base

const pMonitor = { ...base }

let config = {}

const SEC = 1000

const TIMEOUT = 10 * SEC

const setTime = (limit = TIMEOUT) => time => time >= limit

const getLoadTime = ({ startTime, responseEnd }) => responseEnd - startTime

const getName = ({ name }) => name

// 生成表單數(shù)據(jù)

const convert2FormData = (data = {}) =>

Object.entries(data).reduce((last, [key, value]) => {

if (Array.isArray(value)) {

return value.reduce((lastResult, item) => {

lastResult.append(`${key}[]`, item)

return lastResult

}, last)

}

last.append(key, value)

return last

}, new FormData())

// 拼接 GET 時(shí)的url

const makeItStr = (data = {}) =>

Object.entries(data)

.map(([k, v]) => `${k}=${v}`)

.join('&')

pMonitor.getLoadTime = () => {

const [{ domComplete }] = performance.getEntriesByType('navigation')

return domComplete

}

pMonitor.getTimeoutRes = (limit = TIMEOUT) => {

const isTimeout = setTime(limit)

const resourceTimes = performance.getEntriesByType('resource')

return resourceTimes

.filter(item => isTimeout(getLoadTime(item)))

.map(getName)

}

// 上報(bào)數(shù)據(jù)

pMonitor.log = (url, data = {}, type = 'POST') => {

const method = type.toLowerCase()

const urlToUse = method === 'get' ? `${url}?${makeItStr(data)}` : url

const body = method === 'get' ? {} : { body: convert2FormData(data) }

const init = {

method,

...body

}

fetch(urlToUse, init).catch(e => console.log(e))

}

// 封裝一個(gè)上報(bào)兩項(xiàng)核心數(shù)據(jù)的方法

pMonitor.logPackage = () => {

const { url, timeoutUrl, method } = config

const domComplete = pMonitor.getLoadTime()

const timeoutRes = pMonitor.getTimeoutRes(config.timeout)

// 上報(bào)頁(yè)面加載時(shí)間

pMonitor.log(url, { domeComplete }, method)

if (timeoutRes.length) {

pMonitor.log(

timeoutUrl,

{

timeoutRes

},

method

)

}

}

// 事件綁定

pMonitor.bindEvent = () => {

const oldOnload = window.onload

window.onload = e => {

if (oldOnload && typeof oldOnload === 'function') {

oldOnload(e)

}

// 盡量不影響頁(yè)面主線程

if (window.requestIdleCallback) {

window.requestIdleCallback(pMonitor.logPackage)

} else {

setTimeout(pMonitor.logPackage)

}

}

}

/**

* @param {object} option

* @param {string} option.url 頁(yè)面加載數(shù)據(jù)的上報(bào)地址

* @param {string} option.timeoutUrl 頁(yè)面資源超時(shí)的上報(bào)地址

* @param {string=} [option.method='POST'] 請(qǐng)求方式

* @param {number=} [option.timeout=10000]

*/

pMonitor.init = option => {

const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option

config = {

url,

timeoutUrl,

method,

timeout

}

// 綁定事件 用于觸發(fā)上報(bào)數(shù)據(jù)

pMonitor.bindEvent()

}

return pMonitor

})()

export default pm

復(fù)制代碼

如何议薪?是不是不復(fù)雜?甚至有點(diǎn)簡(jiǎn)單~

再次看了看時(shí)間媳友,5 分鐘什么的斯议,還是不要在意這些細(xì)節(jié)了吧 orz

補(bǔ)充說(shuō)明

調(diào)用

如果想追(吹)求(毛)極(求)致(疵)的話,在頁(yè)面加載時(shí)醇锚,監(jiān)測(cè)工具不應(yīng)該占用主線程的 JavaScript 解析時(shí)間哼御。因此,最好在頁(yè)面觸發(fā) onload 事件后搂抒,采用異步加載的方式:

// 在項(xiàng)目的入口文件的底部

const log = async () => {

const pMonitor = await import('/path/to/pMonitor.js')

pMonitor.init({ url: 'xxx', timeoutUrl: 'xxxx' })

pMonitor.logPackage()

// 可以進(jìn)一步將 bindEvent 方法從源碼中刪除

}

const oldOnload = window.onload

window.onload = e => {

if (oldOnload && typeof oldOnload === 'string') {

oldOnload(e)

}

// 盡量不影響頁(yè)面主線程

if (window.requestIdleCallback) {

window.requestIdleCallback(log)

} else {

setTimeout(log)

}

}

復(fù)制代碼

跨域等請(qǐng)求問(wèn)題

工具在數(shù)據(jù)上報(bào)時(shí)艇搀,沒(méi)有考慮跨域問(wèn)題,也沒(méi)有處理 GET 和 POST 同時(shí)存在的情況求晶。

5 分鐘還要什么自行車(chē)焰雕!

如有需求,可以自行覆蓋 pMonitor.logPackage 方法芳杏,改為動(dòng)態(tài)創(chuàng)建 <form/> 和 <iframe/> 矩屁,或者使用更為常見(jiàn)的圖片打點(diǎn)方式~

說(shuō)好的報(bào)警呢?光有報(bào)沒(méi)有警爵赵?吝秕!

這個(gè)還是需要服務(wù)端配合的嘛[認(rèn)真臉.jpg]。

既可以是每個(gè)項(xiàng)目對(duì)應(yīng)不同的上報(bào) url空幻,也可以是統(tǒng)一的一套 url烁峭,項(xiàng)目分配唯一 id 作為區(qū)分。

當(dāng)超時(shí)次數(shù)在規(guī)定時(shí)間內(nèi)超過(guò)約定的閾值時(shí),郵件/短信通知開(kāi)發(fā)人員约郁。

細(xì)粒度

現(xiàn)在僅僅針對(duì)超時(shí)資源進(jìn)行了簡(jiǎn)單統(tǒng)計(jì)缩挑,但是沒(méi)有上報(bào)具體的超時(shí)原因(DNS?TCP鬓梅?request供置? response?)绽快,這就留給讀者去優(yōu)化了芥丧,動(dòng)手試試吧~

下一步

本文介紹了關(guān)于頁(yè)面加載方面的性能監(jiān)控, 此外坊罢,JavaScript 代碼的解析 + 執(zhí)行续担,也是制約頁(yè)面首屏渲染快慢的重要因素(特別是單頁(yè)面應(yīng)用)。下一話艘绍,小編將帶領(lǐng)大家 進(jìn)一步探索 Performance Timeline Level 2赤拒, 實(shí)現(xiàn)更多對(duì)于 JavaScript 運(yùn)行時(shí)的性能監(jiān)控,敬請(qǐng)期待~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末诱鞠,一起剝皮案震驚了整個(gè)濱河市挎挖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌航夺,老刑警劉巖蕉朵,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異阳掐,居然都是意外死亡始衅,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)缭保,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)汛闸,“玉大人,你說(shuō)我怎么就攤上這事艺骂≈罾希” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵钳恕,是天一觀的道長(zhǎng)别伏。 經(jīng)常有香客問(wèn)我,道長(zhǎng)忧额,這世上最難降的妖魔是什么厘肮? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮睦番,結(jié)果婚禮上类茂,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好大咱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布恬涧。 她就那樣靜靜地躺著,像睡著了一般碴巾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上丑搔,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天厦瓢,我揣著相機(jī)與錄音,去河邊找鬼啤月。 笑死煮仇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谎仲。 我是一名探鬼主播浙垫,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼郑诺!你這毒婦竟也來(lái)了夹姥?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤辙诞,失蹤者是張志新(化名)和其女友劉穎辙售,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體飞涂,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡旦部,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了较店。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片士八。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖梁呈,靈堂內(nèi)的尸體忽然破棺而出婚度,到底是詐尸還是另有隱情,我是刑警寧澤捧杉,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布陕见,位于F島的核電站,受9級(jí)特大地震影響味抖,放射性物質(zhì)發(fā)生泄漏评甜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一仔涩、第九天 我趴在偏房一處隱蔽的房頂上張望忍坷。 院中可真熱鬧,春花似錦、人聲如沸佩研。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)旬薯。三九已至晰骑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間绊序,已是汗流浹背硕舆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骤公,地道東北人抚官。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像阶捆,于是被迫代替她去往敵國(guó)和親凌节。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

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