「前端進(jìn)階」徹底弄懂前端路由(轉(zhuǎn))

什么是路由

路由這個概念最先是后端出現(xiàn)的,其響應(yīng)過程是這樣的
1.瀏覽器發(fā)出請求
2.服務(wù)器監(jiān)聽到80端口(或443)有請求過來,并解析url路徑
3.根據(jù)服務(wù)器的路由配置,返回相應(yīng)信息(可以是 html 字串拳昌,也可以是 json 數(shù)據(jù)竞滓,圖片等)
4.瀏覽器根據(jù)數(shù)據(jù)包的Content-Type來決定如何解析數(shù)據(jù)

簡單來說路由就是用來跟后端服務(wù)器進(jìn)行交互的一種方式纤壁,通過不同的路徑叁丧,來請求不同的資源稠鼻,請求不同的頁面是路由的其中一種功能冈止。

前段路由的誕生

最開始的網(wǎng)頁是多頁面的,直到 Ajax 的出現(xiàn)候齿,才慢慢有了 單頁面web應(yīng)用(SPA-(single-page application))熙暴。

SPA 的出現(xiàn)大大提高了 WEB 應(yīng)用的交互體驗。在與用戶的交互過程中慌盯,不再需要重新刷新頁面周霉,獲取數(shù)據(jù)也是通過 Ajax 異步獲取,頁面顯示變的更加流暢润匙。

但由于 SPA 中用戶的交互是通過 JS 改變 HTML 內(nèi)容來實現(xiàn)的诗眨,頁面本身的 url 并沒有變化,這導(dǎo)致了兩個問題:

  1. SPA 無法記住用戶的操作記錄孕讳,無論是刷新、前進(jìn)還是后退巍膘,都無法展示用戶真實的期望內(nèi)容厂财。
  2. SPA 中雖然由于業(yè)務(wù)的不同會有多種頁面展示形式,但只有一個 url峡懈,對 SEO 不友好璃饱,不方便搜索引擎進(jìn)行收錄。

前端路由就是為了解決上述問題而出現(xiàn)的肪康。

前端路由的兩種實現(xiàn)原理

1. Hash模式

這里的 hash 就是指 url 后的 # 號以及后面的字符荚恶。比如說 "www.baidu.com/#hashhash" ,其中 "#hashhash" 就是我們期望的 hash 值磷支。

由于 hash 值的變化不會導(dǎo)致瀏覽器像服務(wù)器發(fā)送請求谒撼,而且 hash 的改變會觸發(fā) hashchange 事件饱普,瀏覽器的前進(jìn)后退也能對其進(jìn)行控制凸舵,所以在 H5 的 history 模式出現(xiàn)之前,基本都是使用 hash 模式來實現(xiàn)前端路由余耽。

使用到的api

window.location.hash = 'hash字符串'; // 用于設(shè)置 hash 值

let hash = window.location.hash; // 獲取當(dāng)前 hash 值

// 監(jiān)聽hash變化善榛,點擊瀏覽器的前進(jìn)后退會觸發(fā)
window.addEventListener('hashchange', function(event){ 
    let newURL = event.newURL; // hash 改變后的新 url
    let oldURL = event.oldURL; // hash 改變前的舊 url
},false)

注:react-router中的hashHistory也是基于此實現(xiàn)
接下來我們來實現(xiàn)一個路由對象
創(chuàng)建一個路由對象, 實現(xiàn) register 方法用于注冊每個 hash 值對應(yīng)的回調(diào)函數(shù)

class HashRouter{
    constructor(){
        //用于存儲不同hash值對應(yīng)的回調(diào)函數(shù)
        this.routers = {};
    }
    //用于注冊每個視圖
    register(hash,callback = function(){}){
        this.routers[hash] = callback;
    }
}

不存在hash值時辩蛋,認(rèn)為是首頁,所以實現(xiàn) registerIndex 方法用于注冊首頁時的回調(diào)函數(shù)

class HashRouter{
    constructor(){
        //用于存儲不同hash值對應(yīng)的回調(diào)函數(shù)
        this.routers = {};
    }
    //用于注冊每個視圖
    register(hash,callback = function(){}){
        this.routers[hash] = callback;
    }
    //用于注冊首頁
    registerIndex(callback = function(){}){
        this.routers['index'] = callback;
    }
}

通過 hashchange 監(jiān)聽 hash 變化移盆,并定義 hash 變化時的回調(diào)函數(shù)

class HashRouter{
    constructor(){
        //用于存儲不同hash值對應(yīng)的回調(diào)函數(shù)
        this.routers = {};
        window.addEventListener('hashchange',this.load.bind(this),false)
    }
    //用于注冊每個視圖
    register(hash,callback = function(){}){
        this.routers[hash] = callback;
    }
    //用于注冊首頁
    registerIndex(callback = function(){}){
        this.routers['index'] = callback;
    }
    //用于調(diào)用不同視圖的回調(diào)函數(shù)
    load(){
        let hash = location.hash.slice(1),
            handler;
        //沒有hash 默認(rèn)為首頁
        if(!hash){
            handler = this.routers.index;
        }else{
            handler = this.routers[hash];
        }
        //執(zhí)行注冊的回調(diào)函數(shù)
        handler.call(this);
    }
}

我們做一個例子來演示一下我們剛剛完成的 HashRouter

<body>
    <div id="nav">
        <a href="#/page1">page1</a>
        <a href="#/page2">page2</a>
        <a href="#/page3">page3</a>
    </div>
    <div id="container"></div>
</body>
let router = new HashRouter();
let container = document.getElementById('container');

//注冊首頁回調(diào)函數(shù)
router.registerIndex(()=> container.innerHTML = '我是首頁');

//注冊其他視圖回到函數(shù)
router.register('/page1',()=> container.innerHTML = '我是page1');
router.register('/page2',()=> container.innerHTML = '我是page2');
router.register('/page3',()=> container.innerHTML = '我是page3');

//加載視圖
router.load();

來看一下效果:
[圖片上傳失敗...(image-8d3aa5-1602561145767)]
基本的路由功能我們已經(jīng)實現(xiàn)了悼院,但依然有點小問題

缺少對未在路由中注冊的 hash 值的處理
hash 值對應(yīng)的回調(diào)函數(shù)在執(zhí)行過程中拋出異常

對應(yīng)的解決辦法如下:

我們追加 registerNotFound 方法,用于注冊 hash 值未找到時的默認(rèn)回調(diào)函數(shù)咒循;
修改 load 方法据途,追加 try/catch 用于捕獲異常绞愚,追加 registerError 方法,用于處理異常

代碼修改后:

class HashRouter{
    constructor(){
        //用于存儲不同hash值對應(yīng)的回調(diào)函數(shù)
        this.routers = {};
        window.addEventListener('hashchange',this.load.bind(this),false)
    }
    //用于注冊每個視圖
    register(hash,callback = function(){}){
        this.routers[hash] = callback;
    }
    //用于注冊首頁
    registerIndex(callback = function(){}){
        this.routers['index'] = callback;
    }
    //用于處理視圖未找到的情況
    registerNotFound(callback = function(){}){
        this.routers['404'] = callback;
    }
    //用于處理異常情況
    registerError(callback = function(){}){
        this.routers['error'] = callback;
    }
    //用于調(diào)用不同視圖的回調(diào)函數(shù)
    load(){
        let hash = location.hash.slice(1),
            handler;
        //沒有hash 默認(rèn)為首頁
        if(!hash){
            handler = this.routers.index;
        }
        //未找到對應(yīng)hash值
        else if(!this.routers.hasOwnProperty(hash)){
            handler = this.routers['404'] || function(){};
        }
        else{
            handler = this.routers[hash]
        }
        //執(zhí)行注冊的回調(diào)函數(shù)
        try{
            handler.apply(this);
        }catch(e){
            console.error(e);
            (this.routers['error'] || function(){}).call(this,e);
        }
    }
}

再來一個例子昨凡,演示一下:

<body>
    <div id="nav">
        <a href="#/page1">page1</a>
        <a href="#/page2">page2</a>
        <a href="#/page3">page3</a>
        <a href="#/page4">page4</a>
        <a href="#/page5">page5</a>
    </div>
    <div id="container"></div>
</body>
let router = new HashRouter();
let container = document.getElementById('container');

//注冊首頁回調(diào)函數(shù)
router.registerIndex(()=> container.innerHTML = '我是首頁');

//注冊其他視圖回到函數(shù)
router.register('/page1',()=> container.innerHTML = '我是page1');
router.register('/page2',()=> container.innerHTML = '我是page2');
router.register('/page3',()=> container.innerHTML = '我是page3');
router.register('/page4',()=> {throw new Error('拋出一個異常')});

//加載視圖
router.load();
//注冊未找到對應(yīng)hash值時的回調(diào)
router.registerNotFound(()=>container.innerHTML = '頁面未找到');
//注冊出現(xiàn)異常時的回調(diào)
router.registerError((e)=>container.innerHTML = '頁面異常爽醋,錯誤消息:<br>' + e.message);

來看一下效果:


image

至此,基于 hash 方式實現(xiàn)的前端路由便脊,我們已經(jīng)將基本雛形實現(xiàn)完成了蚂四。
接下來我們來介紹前端路由的另一種模式:history 模式。

2.history 模式

在 HTML5 之前哪痰,瀏覽器就已經(jīng)有了 history 對象遂赠。但在早期的 history 中只能用于多頁面的跳轉(zhuǎn):

history.go(-1);       // 后退一頁
history.go(2);        // 前進(jìn)兩頁
history.forward();     // 前進(jìn)一頁
history.back();      // 后退一頁

在 HTML5 的規(guī)范中,history 新增了以下幾個 API:

history.pushState();         // 添加新的狀態(tài)到歷史狀態(tài)棧
history.replaceState();      // 用新的狀態(tài)代替當(dāng)前狀態(tài)
history.state                // 返回當(dāng)前狀態(tài)對象

HTML5引入了 history.pushState() 和 history.replaceState() 方法晌杰,它們分別可以添加和修改歷史記錄條目跷睦。這些方法通常與window.onpopstate 配合使用。
history.pushState() 和 history.replaceState() 均接收三個參數(shù)(state, title, url)

參數(shù)說明如下:

state:合法的 Javascript 對象肋演,可以用在 popstate 事件中
title:現(xiàn)在大多瀏覽器忽略這個參數(shù)抑诸,可以直接用 null 代替
url:任意有效的 URL,用于更新瀏覽器的地址欄

history.pushState() 和 history.replaceState() 的區(qū)別在于:

history.pushState() 在保留現(xiàn)有歷史記錄的同時爹殊,將 url 加入到歷史記錄中蜕乡。
history.replaceState() 會將歷史記錄中的當(dāng)前頁面歷史替換為 url。

由于 history.pushState() 和 history.replaceState() 可以改變 url 同時梗夸,不會刷新頁面层玲,所以在 HTML5 中的 histroy 具備了實現(xiàn)前端路由的能力。
回想我們之前完成的 hash 模式反症,當(dāng) hash 變化時辛块,可以通過 hashchange 進(jìn)行監(jiān)聽。
而 history 的改變并不會觸發(fā)任何事件铅碍,所以我們無法直接監(jiān)聽 history 的改變而做出相應(yīng)的改變润绵。
所以,我們需要換個思路该酗,我們可以羅列出所有可能觸發(fā) history 改變的情況授药,并且將這些方式一一進(jìn)行攔截,變相地監(jiān)聽 history 的改變呜魄。
對于單頁應(yīng)用的 history 模式而言悔叽,url 的改變只能由下面四種方式引起:

點擊瀏覽器的前進(jìn)或后退按鈕
點擊 a 標(biāo)簽
在 JS 代碼中觸發(fā) history.pushState 函數(shù)
在 JS 代碼中觸發(fā) history.replaceState 函數(shù)

思路已經(jīng)有了,接下來我們來實現(xiàn)一個路由對象

  1. 創(chuàng)建一個路由對象, 實現(xiàn) register 方法用于注冊每個 location.pathname 值對應(yīng)的回調(diào)函數(shù)
  2. 當(dāng) location.pathname === '/' 時爵嗅,認(rèn)為是首頁娇澎,所以實現(xiàn) registerIndex 方法用于注冊首頁時的回調(diào)函數(shù)
  3. 解決 location.path 沒有對應(yīng)的匹配,增加方法 registerNotFound 用于注冊默認(rèn)回調(diào)函數(shù)
  4. 解決注冊的回到函數(shù)執(zhí)行時出現(xiàn)異常睹晒,增加方法 registerError 用于處理異常情況
class HistoryRouter{
    constructor(){
        //用于存儲不同path值對應(yīng)的回調(diào)函數(shù)
        this.routers = {};
    }
    //用于注冊每個視圖
    register(path,callback = function(){}){
        this.routers[path] = callback;
    }
    //用于注冊首頁
    registerIndex(callback = function(){}){
        this.routers['/'] = callback;
    }
    //用于處視圖未找到的情況
    registerNotFound(callback = function(){}){
        this.routers['404'] = callback;
    }
    //用于處理異常情況
    registerError(callback = function(){}){
        this.routers['error'] = callback;
    }
}
  1. 定義 assign 方法趟庄,用于通過 JS 觸發(fā) history.pushState 函數(shù)
  2. 定義 replace 方法括细,用于通過 JS 觸發(fā) history.replaceState 函數(shù)
class HistoryRouter{
    constructor(){
        //用于存儲不同path值對應(yīng)的回調(diào)函數(shù)
        this.routers = {};
    }
    //用于注冊每個視圖
    register(path,callback = function(){}){
        this.routers[path] = callback;
    }
    //用于注冊首頁
    registerIndex(callback = function(){}){
        this.routers['/'] = callback;
    }
    //用于處理視圖未找到的情況
    registerNotFound(callback = function(){}){
        this.routers['404'] = callback;
    }
    //用于處理異常情況
    registerError(callback = function(){}){
        this.routers['error'] = callback;
    }
    //跳轉(zhuǎn)到path
    assign(path){
        history.pushState({path},null,path);
        this.dealPathHandler(path)
    }
    //替換為path
    replace(path){
        history.replaceState({path},null,path);
        this.dealPathHandler(path)
    }
    //通用處理 path 調(diào)用回調(diào)函數(shù)
    dealPathHandler(path){
        let handler;
        //沒有對應(yīng)path
        if(!this.routers.hasOwnProperty(path)){
            handler = this.routers['404'] || function(){};
        }
        //有對應(yīng)path
        else{
            handler = this.routers[path];
        }
        try{
            handler.call(this)
        }catch(e){
            console.error(e);
            (this.routers['error'] || function(){}).call(this,e);
        }
    }
}
  1. 監(jiān)聽 popstate 用于處理前進(jìn)后退時調(diào)用對應(yīng)的回調(diào)函數(shù)
  2. 全局阻止A鏈接的默認(rèn)事件,獲取A鏈接的href屬性戚啥,并調(diào)用 history.pushState 方法
  3. 定義 load 方法奋单,用于首次進(jìn)入頁面時 根據(jù) location.pathname 調(diào)用對應(yīng)的回調(diào)函數(shù)
    最終代碼如下:
class HistoryRouter{
    constructor(){
        //用于存儲不同path值對應(yīng)的回調(diào)函數(shù)
        this.routers = {};
        this.listenPopState();
        this.listenLink();
    }
    //監(jiān)聽popstate
    listenPopState(){
        window.addEventListener('popstate',(e)=>{
            let state = e.state || {},
                path = state.path || '';
            this.dealPathHandler(path)
        },false)
    }
    //全局監(jiān)聽A鏈接
    listenLink(){
        window.addEventListener('click',(e)=>{
            let dom = e.target;
            if(dom.tagName.toUpperCase() === 'A' && dom.getAttribute('href')){
                e.preventDefault()
                this.assign(dom.getAttribute('href'));
            }
        },false)
    }
    //用于首次進(jìn)入頁面時調(diào)用
    load(){
        let path = location.pathname;
        this.dealPathHandler(path)
    }
    //用于注冊每個視圖
    register(path,callback = function(){}){
        this.routers[path] = callback;
    }
    //用于注冊首頁
    registerIndex(callback = function(){}){
        this.routers['/'] = callback;
    }
    //用于處理視圖未找到的情況
    registerNotFound(callback = function(){}){
        this.routers['404'] = callback;
    }
    //用于處理異常情況
    registerError(callback = function(){}){
        this.routers['error'] = callback;
    }
    //跳轉(zhuǎn)到path
    assign(path){
        history.pushState({path},null,path);
        this.dealPathHandler(path)
    }
    //替換為path
    replace(path){
        history.replaceState({path},null,path);
        this.dealPathHandler(path)
    }
    //通用處理 path 調(diào)用回調(diào)函數(shù)
    dealPathHandler(path){
        let handler;
        //沒有對應(yīng)path
        if(!this.routers.hasOwnProperty(path)){
            handler = this.routers['404'] || function(){};
        }
        //有對應(yīng)path
        else{
            handler = this.routers[path];
        }
        try{
            handler.call(this)
        }catch(e){
            console.error(e);
            (this.routers['error'] || function(){}).call(this,e);
        }
    }
}

再做一個例子來演示一下我們剛剛完成的 HistoryRouter

<body>
    <div id="nav">
        <a href="/page1">page1</a>
        <a href="/page2">page2</a>
        <a href="/page3">page3</a>
        <a href="/page4">page4</a>
        <a href="/page5">page5</a>
        <button id="btn">page2</button>
    </div>
    <div id="container">

    </div>
</body>
let router = new HistoryRouter();
let container = document.getElementById('container');

//注冊首頁回調(diào)函數(shù)
router.registerIndex(() => container.innerHTML = '我是首頁');

//注冊其他視圖回到函數(shù)
router.register('/page1', () => container.innerHTML = '我是page1');
router.register('/page2', () => container.innerHTML = '我是page2');
router.register('/page3', () => container.innerHTML = '我是page3');
router.register('/page4', () => {
    throw new Error('拋出一個異常')
});

document.getElementById('btn').onclick = () => router.assign('/page2')


//注冊未找到對應(yīng)path值時的回調(diào)
router.registerNotFound(() => container.innerHTML = '頁面未找到');
//注冊出現(xiàn)異常時的回調(diào)
router.registerError((e) => container.innerHTML = '頁面異常,錯誤消息:<br>' + e.message);
//加載頁面
router.load();

來看一下效果:


3.gif

至此猫十,基于 history 方式實現(xiàn)的前端路由览濒,我們已經(jīng)將基本雛形實現(xiàn)完成了。
但需要注意的是拖云,history 在修改 url 后贷笛,雖然頁面并不會刷新,但我們在手動刷新宙项,或通過 url 直接進(jìn)入應(yīng)用的時候乏苦,
服務(wù)端是無法識別這個 url 的。因為我們是單頁應(yīng)用尤筐,只有一個 html 文件汇荐,服務(wù)端在處理其他路徑的 url 的時候,就會出現(xiàn)404的情況盆繁。
所以拢驾,如果要應(yīng)用 history 模式,需要在服務(wù)端增加一個覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態(tài)資源改基,則應(yīng)該返回單頁應(yīng)用的 html 文件。
接下來咖为,我們來探究一下秕狰,何時使用 hash 模式,何時使用 history 模式躁染。

拓展:一個問題:

react router為什么推薦使用browserHistory而不推薦hashHistory鸣哀?

首先 browserHistory 其實使用的是 HTML5 的 History API,瀏覽器提供相應(yīng)的接口來修改瀏覽器的歷史記錄吞彤;而 hashHistory 是通過改變地址后面的 hash 來改變?yōu)g覽器的歷史記錄我衬;

History API 提供了 pushState() 和 replaceState() 方法來增加或替換歷史記錄。而 hash 沒有相應(yīng)的方法饰恕,所以并沒有替換歷史記錄的功能挠羔。但 react-router 通過 polyfill 實現(xiàn)了此功能,具體實現(xiàn)沒有看埋嵌,好像是使用 sessionStorage破加。

另一個原因是 hash 部分并不會被瀏覽器發(fā)送到服務(wù)端,也就是說不管是請求 http://domain.com/index.html#foo 還是 http://domain.com/index.html#bar 雹嗦,服務(wù)只知道請求了 index.html 并不知道 hash 部分的細(xì)節(jié)范舀。而 History API 需要服務(wù)端支持合是,這樣服務(wù)端能獲取請求細(xì)節(jié)。

還有一個原因是因為有些應(yīng)該會忽略 URL 中的 hash 部分锭环,記得之前將 URL 使用微信分享時會丟失 hash 部分聪全。

注意:原文主要產(chǎn)出于下面兩篇文章,在此自己記錄下方便日后查看
https://juejin.im/post/6844903890278694919
https://juejin.im/post/6844903759458336776#heading-10

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辅辩,一起剝皮案震驚了整個濱河市难礼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌汽久,老刑警劉巖鹤竭,帶你破解...
    沈念sama閱讀 212,332評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異景醇,居然都是意外死亡臀稚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,508評論 3 385
  • 文/潘曉璐 我一進(jìn)店門三痰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吧寺,“玉大人,你說我怎么就攤上這事散劫≈苫” “怎么了?”我有些...
    開封第一講書人閱讀 157,812評論 0 348
  • 文/不壞的土叔 我叫張陵获搏,是天一觀的道長赖条。 經(jīng)常有香客問我,道長常熙,這世上最難降的妖魔是什么纬乍? 我笑而不...
    開封第一講書人閱讀 56,607評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮裸卫,結(jié)果婚禮上仿贬,老公的妹妹穿的比我還像新娘。我一直安慰自己墓贿,他們只是感情好茧泪,可當(dāng)我...
    茶點故事閱讀 65,728評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著聋袋,像睡著了一般队伟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舱馅,一...
    開封第一講書人閱讀 49,919評論 1 290
  • 那天缰泡,我揣著相機與錄音,去河邊找鬼。 笑死棘钞,一個胖子當(dāng)著我的面吹牛缠借,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宜猜,決...
    沈念sama閱讀 39,071評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼泼返,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了姨拥?” 一聲冷哼從身側(cè)響起绅喉,我...
    開封第一講書人閱讀 37,802評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎叫乌,沒想到半個月后柴罐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,256評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡憨奸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,576評論 2 327
  • 正文 我和宋清朗相戀三年革屠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片排宰。...
    茶點故事閱讀 38,712評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡似芝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出板甘,到底是詐尸還是另有隱情党瓮,我是刑警寧澤,帶...
    沈念sama閱讀 34,389評論 4 332
  • 正文 年R本政府宣布盐类,位于F島的核電站寞奸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏在跳。R本人自食惡果不足惜蝇闭,卻給世界環(huán)境...
    茶點故事閱讀 40,032評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望硬毕。 院中可真熱鬧,春花似錦礼仗、人聲如沸吐咳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽韭脊。三九已至,卻和暖如春单旁,著一層夾襖步出監(jiān)牢的瞬間沪羔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,026評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蔫饰,地道東北人琅豆。 一個月前我還...
    沈念sama閱讀 46,473評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像篓吁,于是被迫代替她去往敵國和親茫因。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,606評論 2 350