Service Workers 和離線緩存

系列文章:

  1. Service Workers 和離線緩存 (本文)
  2. Notification with Service Workers push events
  3. PWA:添加應(yīng)用至桌面及分享

第一次聽到 Service Workers 這個(gè)詞還是在去年 Google 來安利 Angular 2 的時(shí)候把将,那時(shí)就覺得很驚艷王财,想搞一搞,但是因?yàn)闆]把網(wǎng)站升級成 https 一直拖到現(xiàn)在绒净。不久前,把網(wǎng)站升級成了 https改览,終于可以搞一發(fā)了缤言。

本篇主要包含以下內(nèi)容:

當(dāng)然,還是先來看看 Service Workers 究竟是什么庆揩?

<a name="Whats-service-workers"></a>

What's Service Workers?

Service Workers 是谷歌 chrome 團(tuán)隊(duì)提出并大力推廣的一項(xiàng) web 技術(shù)跌穗。在 2015 年,它加入到 W3C 標(biāo)準(zhǔn)蚌吸,進(jìn)入草案階段。W3C 標(biāo)準(zhǔn)中對 Service Workers 的解釋太細(xì)致奕枢,相對而言肉迫,我更喜歡 MDN 上的解釋,更簡練跌造,更易于理解。

Service workers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs. - MDN

簡單翻譯一下:Service workers 基本上充當(dāng)應(yīng)用同服務(wù)器之間的代理服務(wù)器陵珍,可以用于攔截請求,也就意味著可以在離線環(huán)境下響應(yīng)請求互纯,從而提供更好的離線體驗(yàn)磕蒲。同時(shí),它還可以接收服務(wù)器推送和后臺同步 API辣往。

那么,這項(xiàng)技術(shù)的瀏覽器支持情況是什么樣坊萝,還是來看一眼 Can I use?

可以從看到许起,Chrome 和 Firefox, Opera 都已經(jīng)支持 Service Workers,底下的備注也寫到 Edge 在開發(fā)中园细,Safari 也考慮支持惦积。至于 IE荣刑,船長都跳船了伦乔×液停看了 PC 端,再來看看移動端招刹。移動端的支持率并不盡如人意窝趣,不過在安卓 4.4 之后,安卓原生瀏覽器妇拯,以及安卓版的 Chrome 都已經(jīng)開始支持 Service Workers。

說句題外話越锈,突然發(fā)現(xiàn)在 Can I use 中選擇導(dǎo)入我國數(shù)據(jù)時(shí),竟出現(xiàn)了 UC 和 QQ 瀏覽器的支持情況稀拐,口以口以??????...

言歸正傳丹弱,在真正開始使用 Service Workers 之前,還有幾點(diǎn)要注意:

  1. Service Workers 基于 Https蜓洪,這是硬性條件(如何升級 https 可以參考上一篇文章
  2. 每個(gè) Service Worker 都有自己的作用域,它只會處理自己作用域下的請求蝠咆,而 Service Worker 的存放位置就是它的最大作用域
  3. Service Workder 是 Web Worker 的一種北滥,它不能夠直接操作 DOM

Github 上有一個(gè)非常棒的資源,它用圖片的方式展示了 Servic Workers 的一些核心要點(diǎn)菊霜。

搞定這些基礎(chǔ)就可以正式開搞了...

<a name="try-service-workers"></a>

小試 Service Workers

和其他 worker 一樣济赎,service worker 有一個(gè)獨(dú)自的文件。由于之前所提到的 service worker 只能作用在自己存放位置之下的文件司训,所以,一般在應(yīng)用根目錄下存放 service worker 文件勾徽。

首先统扳,先寫一個(gè)最簡單的來看看瀏覽器是不是支持,以及能否正確地安裝并運(yùn)行 service worker吹由。

// service-worker.js
const _self = this;

console.log('In service worker.');

_self.addEventListener('install', function () {
    console.log('Install success');
});

_self.addEventListener('activate', function () {
    console.log('Activated');
});

雖然朱嘴,service worker 是 web worker 其中的一種,但它有些不同,它有自己的注冊方式舌劳。

// ServiceWorkerService.js
const SERVICE_WORKER_API = 'serviceWorker';
const SERVICE_WORKER_FILE_PATH = 'service-worker.js';

const isSupportServiceWorker = () => SERVICE_WORKER_API in navigator;

if (isSupportServiceWorker()) {
    navigator
        .serviceWorker
        .register(SERVICE_WORKER_FILE_PATH)
        .then(() => console.log('Load service worker Success.'))
        .catch(() => console.error('Load service worker fail'));
} else {
    console.info('Browser not support Service Worker.');
}

重啟程序之后甚淡,你應(yīng)該就能在控制臺中看到 Load service worker Success.。然而贯卦,卻沒有另兩句的輸出焙贷,難道加載失敗了?但是啡彬,控制臺不是顯示加載成功了么故硅?不要擔(dān)心,程序沒有出錯(cuò)往踢,只是 service worker 中的日志信息有它自己的輸出位置,而并非輸出在主日志之中峻呕。

接下去趣效,先來看看如何調(diào)試 service worker。

<a name="debug-service-workers"></a>

調(diào)試 Service Workers

在 Chrome 中佩憾,service worker 的信息顯示在 Application -> Service Workers 中干花,就像這樣

里面會顯示注冊的 service worker池凄,以及它當(dāng)前的狀態(tài)鬼廓。還能通過切換最上面的選項(xiàng)來模擬不同的網(wǎng)絡(luò)環(huán)境,測試在不同環(huán)境下 service worker 的響應(yīng),它們分別是:

  • Offline: 離線
  • Update on reload: 加載時(shí)更新
  • Bypass for network: 使用網(wǎng)絡(luò)內(nèi)容

回到之前的問題馏锡,如何查看 service worker 之中的日志哪伟端?只需點(diǎn)擊圖中的 inspect 鏈接,它會彈出另一個(gè)開發(fā)者窗口责蝠,在里面可以查看 service worker 的日志。是不是覺得需要那么多步有點(diǎn)麻煩齿拂,別擔(dān)心肴敛,Chrome 已經(jīng)替我們解決了這個(gè)煩惱。重新刷新頁面后砸狞,Chrome 的開發(fā)者工具中已經(jīng)能夠查看 service workers 的信息了,比如:在 console 選項(xiàng)卡勾選 Show all messages 就能顯示 service workers 中控制臺的信息趾代;在 source 選項(xiàng)卡也能看到 service workers 的代碼丰辣,當(dāng)然也可以打斷點(diǎn)啦~

在 firefox 中笙什,默認(rèn)會將 service worker 中的日志輸出到主控制臺中,但要打開 service worker 的調(diào)試器就有點(diǎn)麻煩了琐凭。有兩種方法查看,一個(gè)是在地址欄中輸入 about:debugging#workers胚吁,另一種就是通過菜單欄中選擇 Tools -> Web Developer -> Service Workers愁憔。

更多關(guān)于在 firefox 中調(diào)試 service workers 的信息可以點(diǎn)此查看吨掌。

雖然脓恕,已經(jīng)將日志輸出到主控制臺了窿侈,可這里就有個(gè)疑問了,主頁能不能獲取 service workers 中的信息哪乃秀?答案是肯定的乘瓤,那就是通過 postMessage

<a name="postmessage"></a>

通過 postMessage 與主窗口通信

和 web worker 一樣抬吟,service worker 與主窗口通訊也需要通過 postMessage,但它的語法又有些許不同火本。

首先聪建,是主頁面給 service worker 發(fā)消息。

// ServiceWorkerService.js
const sendMessageToSW = msg => navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg);

if (isSupportServiceWorker()) {
    const sw = navigator.serviceWorker;

    sw.register(SERVICE_WORKER_FILE_PATH)
        .then(() => console.log('Load service worker Success.'))
        .catch(() => console.error('Load service worker fail'))
        .then(() => sendMessageToSW('Hello, service worker.'))
        .catch(() => console.error('Send message error.'));
} else {
    console.info('Browser not support Service Worker.');
}

可以看到擎析,postMessage 方法并不在 worker 實(shí)例下挥下,而是在 serviceWorker 下的 controller 對象下。這里需要注意一下现斋,當(dāng) service worker 還沒有注冊成功時(shí),navigator.serviceWorker.controller 對象的值是 null偎蘸,所以,在調(diào)用 postMessage 之前需要確保 controller 對象已經(jīng)存在限书。在 service worker 這邊就沒有什么區(qū)別了

// service-worker.js
_self.addEventListener('message', function(event) {
    console.log(event.data);
});

是不是很簡單章咧?不過,反過來 service worker 給主頁面發(fā)消息就要復(fù)雜一點(diǎn)了慧邮。在 service worker 里發(fā)送信息需要通過 Client 對象的 postMessage 方法。獲取 Client 的方法有很多耻矮,比如裆装,剛從主頁面發(fā)來的消息倡缠,事件的來源就是一個(gè) Client 對象哨免,即 event.source。不過昙沦,這只能向來源發(fā)消息琢唾,但如果你開了幾個(gè)網(wǎng)頁,或者不是通過主頁消息發(fā)來的該怎么辦哪盾饮?方法還是有的采桃,在 service workers 中可以通過 clients 來獲取所有的頁面對象或其他的 service workers。

// service-worker.js
_self.clients.matchAll().then(function(clients) {
    clients.forEach(function(client) {
        client.postMessage('Service worker attached.');
    })
});

不過丘损,如果你發(fā)出一個(gè)消息需要等到另一方的返回的消息做處理普办,上述的辦法就做不到了徘钥。這時(shí)就需要建立一個(gè)通道來處理了,修改一下之前的 sendMessageToSW 方法舆驶。

// ServiceWorkerService.js
const sendMessageToSW = msg => new Promise((resolve, reject) => {
    const messageChannel = new MessageChannel();
    messageChannel.port1.onmessage = event => {
        if (event.data.error) {
            reject(event.data.error);
        } else {
            resolve(event.data);
        }
    };

    navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg, [messageChannel.port2]);
});

這樣信息發(fā)送出去后會返回一個(gè) promise贞远,然后就可以優(yōu)雅地鏈?zhǔn)秸{(diào)用了蓝仲。

// ServiceWorkerService.js
if (isSupportServiceWorker()) {
    const sw = navigator.serviceWorker;

    sw.register(SERVICE_WORKER_FILE_PATH)
        .then(() => console.log('Load service worker Success.'))
        .catch(() => console.error('Load service worker fail'))
        .then(() => sendMessageToSW('Hello, service worker.'))
        .then(console.log)
        .catch(() => console.error('Send message error.'));
} else {
    console.info('Browser not support Service Worker.');
}

了解了如何在瀏覽器中調(diào)試 service workers 和與主頁面通信這些基礎(chǔ)之后袱结,就可以搞一些正真功能性的東西垢夹,比如創(chuàng)造 service workers 最初的動機(jī)——提供更好的離線體驗(yàn)果元。

<a name="offline-cache"></a>

為應(yīng)用添加離線緩存

為應(yīng)用添加緩存的方式有很多,但能夠提供離線緩存的蝇狼,據(jù)我所知倡怎,那就只有 service workers 一家了监署。這就好比已經(jīng)安裝了的應(yīng)用,無論是否有網(wǎng)絡(luò)連接都可以隨時(shí)打開使用(google 所推的 PWA 最終目的就是這個(gè))栖秕。你可能會懷疑累魔,聽起來這么高大上實(shí)現(xiàn)起來會不會很復(fù)雜垦写?然而并沒有梯投,使用 service workers 為應(yīng)用添加離線緩存還是相當(dāng)簡單的况毅。

就如同文章開頭 MDN 中所提到的,service workers 可以充當(dāng)應(yīng)用與服務(wù)器之前的代理服務(wù)器么鹤,它通過監(jiān)聽 fetch 事件來捕捉自己作用域下發(fā)出的網(wǎng)絡(luò)請求蒸甜,并通過 event.respondWith 來返回請求結(jié)果柠新,過程中可以對返回結(jié)果做任何的修改(所以必須 https 昂拊鳌)憔恳。

// service-worker.js
const handleFetchRequest = function(request) {
    return fetch(request);
};

const onFetch = function(event) {
    event.respondWith(handleFetchRequest(event.request));
};

_self.addEventListener('fetch', onFetch);

上面這段代碼就是捕獲請求最基本的方式,然后直接將請求發(fā)送出去喇嘱,并將請求的結(jié)果返回者铜,沒有做其他額外的操作作烟。如果拿撩,你這時(shí)觀察控制臺的網(wǎng)絡(luò)請求压恒,會發(fā)現(xiàn)所有請求的 size 都不再是原先的文件大小或來自緩存错邦,而是 from ServiceWorker撬呢。

接下去,就來給應(yīng)用添加離線緩存毛仪。既然箱靴,所有的請求都是手動發(fā)出的衡怀,而且能夠拿到返回的結(jié)果路翻,那么茂契,緩存這些結(jié)果就變得輕而易舉了掉冶。

不過脐雪,這里要先講另一個(gè)知識點(diǎn)——Cache Storage战秋。它作為 service worker 的一部分寫在草案中脂信。通過它狰闪,我們可以方便地把請求埋泵,以及請求結(jié)果一同緩存起來丽声。了解了 Cache Storage觉义,那就把上面的代碼改一下谁撼,讓它能夠緩存請求厉碟。

// service-worker.js
const handleFetchRequest = function(request) {
    return caches.match(request)
        .then(function(response) {
            return response || fetch(request)
                    .then(function(response) {
                        const clonedResponse = response.clone();

                        caches.open(CACHE_NAME)
                            .then(function(cache) {
                                cache.put(request, clonedResponse);
                            });

                        return response;
                    });
        });
};

這里主要修改了如何處理請求的方法箍鼓,先判斷這個(gè)請求是否已經(jīng)被緩存過了,緩存過了就直接返回結(jié)果何暮,沒有的話就去請求铐殃,并把結(jié)果添加到緩存中富腊,以便下次請求來時(shí)可以直接返回。

離線緩存就這樣添加好了肖揣,來看看效果怎么樣。這就要用到之前調(diào)試時(shí)所提到的模擬不同環(huán)境龙优,不記得的童鞋可以往上翻一翻彤断。(提示關(guān)鍵詞:控制臺, Application, Service Workers, Offline)這里模擬離線環(huán)境瓦糟,設(shè)置好后再刷新頁面。

Awesome~????

雖然已實(shí)現(xiàn)了離線緩存句伶,但是陆淀,使用 Cache Storage 還需要注意以下幾點(diǎn):

  1. 它只能緩存 GET 請求考余;
  2. 每個(gè)站點(diǎn)只能緩存屬于自己域下的請求轧苫,同時(shí)也能緩存跨域的請求含懊,比如 CDN岔乔,不過無法對跨域請求的請求頭和內(nèi)容進(jìn)行修改
  3. 緩存的更新需要自行實(shí)現(xiàn)雏门;
  4. 緩存不會過期茁影,除非將緩存刪除募闲,而瀏覽器對每個(gè)網(wǎng)站 Cache Storage 的大小有硬性的限制,所以需要清理不必要的緩存呼盆。

上面的代碼并沒有做緩存的清除和更新访圃,所以,還要更新一下饭宾。同時(shí)徽鼎,通過給跨域請求添加 {mode: 'cors'} 屬性來使請求支持跨域否淤,從而拿到響應(yīng)頭信息石抡。

const HOST_NAME = location.host;
const VERSION_NAME = 'CACHE-v1';
const CACHE_NAME = HOST_NAME + '-' + VERSION_NAME;
const CACHE_HOST = [HOST_NAME, 'cdn.bootcss.com'];

const isNeedCache = function(url) {
    return CACHE_HOST.some(function(host) {
        return url.search(host) !== -1;
    });
};

const isCORSRequest = function(url, host) {
    return url.search(host) === -1;
};

const isValidResponse = function(response) {
    return response && response.status >= 200 && response.status < 400;
};

const handleFetchRequest = function(req) {
    if (isNeedCache(req.url)) {
        const request = isCORSRequest(req.url, HOST_NAME) ? new Request(req.url, {mode: 'cors'}) : req;
        return caches.match(request)
            .then(function(response) {
                // Cache hit - return response directly
                if (response) {
                    // Update Cache for next time enter
                    fetch(request)
                        .then(function(response) {

                            // Check a valid response
                            if(isValidResponse(response)) {
                                caches
                                    .open(CACHE_NAME)
                                    .then(function (cache) {
                                        cache.put(request, response);
                                    });
                            } else {
                                sentMessage('Update cache ' + request.url + ' fail: ' + response.message);
                            }
                        })
                        .catch(function(err) {
                            sentMessage('Update cache ' + request.url + ' fail: ' + err.message);
                        });
                    return response;
                }

                // Return fetch response
                return fetch(request)
                    .then(function(response) {
                        // Check if we received an unvalid response
                        if(!isValidResponse(response)) {
                            return response;
                        }

                        const clonedResponse = response.clone();

                        caches
                            .open(CACHE_NAME)
                            .then(function(cache) {
                                cache.put(request, clonedResponse);
                            });

                        return response;
                    });
            });
    } else {
        return fetch(req);
    }
};

升級之后嗡贺,還是有緩存先拿緩存,這樣比較快厢漩,但依舊會在后臺發(fā)出請求溜嗜,如果返回合法的請求架谎,就更新 cache 中的值谷扣,那么,下次訪問時(shí)就是這次訪問返回的結(jié)果了瑞凑。

service worker 的 installactivite 事件對象都包含一個(gè) waitUntil 方法籽御,方法接受一個(gè) promise惰匙,當(dāng) promise 被 resolve 后才會繼續(xù)執(zhí)行到下一個(gè)狀態(tài)。如果哑梳,想要強(qiáng)制更新緩存鸠真,就可以通過這個(gè)方法在 service worker 激活時(shí)除舊版本緩存弧哎。

// service-worker.js
const onActive = function(event) {
    event.waitUntil(
        caches
            .keys()
            .then(function(cacheNames) {
                return Promise.all(
                    cacheNames.map(function(cacheName) {
                        // Remove expired cache response
                        if (CACHE_NAME.indexOf(cacheName) === -1) {
                            return caches.delete(cacheName);
                        }
                    })
                );
            })
    );
};

_self.addEventListener('activate', onActive);

這樣請求的緩存就能隨時(shí)更新了,不過蠢终,你可能會和我有同樣的疑問——那 service workers 怎么更新呢寻拂?

<a name="lifecycle-and-update"></a>

Service workers 的生命周期與更新

事實(shí)上祭钉,service workers 的更新并不需要我們操心慌核,只要 service workers 文件有任何一點(diǎn)的修改垮卓,瀏覽器就會立即裝載它粟按。然而,它還是有需要注意的地方疼鸟,不然也就不值一提了空镜。

雖然姑裂,瀏覽器立即裝載它舶斧,但它并沒有立即生效茴厉,這和它的生命周期有關(guān)。下面這張圖來自 Service Workers 101嗜闻,非常形象地展示了 service workers 的生命周期琉雳。

先看圖的右邊翠肘,它展示了 service workers 的 3 種狀態(tài):Installing, WaitingActive辫秧;左邊是 service workers 的生命周期绪妹,兩者結(jié)合在一起,直觀地展現(xiàn)了在 service workers 不同的生命周期時(shí)廊移,service workers 所處的狀態(tài)《可以看到,installactivate 2 個(gè)時(shí)間中間离唐,service workers 是處于 Waiting 的狀態(tài)。

回到剛才提到的 service workers 更新嵌戈,瀏覽器雖然會立即裝載最新的 service workers听皿,但只是讓它 install尉姨,并進(jìn)入 Waiting 的狀態(tài)又厉,而并沒有立即 activate。只有當(dāng)用戶將瀏覽器關(guān)閉后,重新打開頁面時(shí)婆排,舊的 service workers 才會被新的 service workers 替換腮猖。不過澈缺,圖中也有提到莱预,可以在 install 事件中 self.skipWaiting 方法來跳過等待,直接進(jìn)入 activate 狀態(tài)危喉。同樣的,可以在 activate 事件中調(diào)用 self.clients.claim 方法來更新所有客戶端上的 service works。

為 service workers 添加上述兩個(gè)方法就能較好地處理更新問題岂座。代碼改動很小杭措,這里就不再重復(fù)貼了鸳址,所有的代碼都已上傳 Github

下次準(zhǔn)備搗鼓 service workers 相關(guān)的服務(wù)器推送,敬請關(guān)注...??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市矿筝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棚贾,老刑警劉巖窖维,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件榆综,死亡現(xiàn)場離奇詭異,居然都是意外死亡陈辱,警方通過查閱死者的電腦和手機(jī)奖年,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人中燥,你說我怎么就攤上這事咱扣。” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵橙依,是天一觀的道長。 經(jīng)常有香客問我昔榴,道長辛藻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任吱肌,我火速辦了婚禮氮墨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘栗菜。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布蝗拿。 她就那樣靜靜地躺著晾捏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哀托。 梳的紋絲不亂的頭發(fā)上惦辛,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音仓手,去河邊找鬼胖齐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛俗或,可吹牛的內(nèi)容都是我干的市怎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼辛慰,長吁一口氣:“原來是場噩夢啊……” “哼区匠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起帅腌,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤驰弄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后速客,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戚篙,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年溺职,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了岔擂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡浪耘,死狀恐怖乱灵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情七冲,我是刑警寧澤痛倚,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站澜躺,受9級特大地震影響蝉稳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜掘鄙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一耘戚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧操漠,春花似錦收津、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吧黄,卻和暖如春部服,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拗慨。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工廓八, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赵抢。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓剧蹂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親烦却。 傳聞我的和親對象是個(gè)殘疾皇子宠叼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

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