router(原生 js 实现一个前端路由 router)

项目地址:https://gitee.com/cwq1211/router

效果体验地址:

[1. 滑动效果:https://gitee.com/cwq1211/router/route/index.html

[2. 淡入淡出效果: https://gitee.com/cwq1211/router/route/index2.html

1. 需求

因为我司的 H 5 的项目是用原生 js 写的,要用到路由,但是现在好用的路由都是和某些框架绑定在一起的,比如 vue-router ,framework7 的路由;但是又没必要为了一个路由功能而加入一套框架,现在自己写一个轻量级的路由。

2. 实现原理

现在前端的路由实现一般有两种,一种是 Hash 路由,另外一种是 History 路由。

2.1 History 路由

History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。

属性

  • History.length 是一个只读属性,返回当前 session 中的 history 个数,包含当前页面在内。举个例子,对于新开一个 tab 加载的页面当前属性返回值 1 。
  • History.state 返回一个表示历史堆栈顶部的状态的值。这是一种可以不必等待 popstate 事件而查看状态而的方式。

方法

  • History.back()

前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于 history.go(-1).

Note: 当浏览器会话历史记录处于第一页时调用此方法没有效果,而且也不会报错。

  • History.forward()

在浏览器历史记录里前往下一页,用户可点击浏览器左上角的前进按钮模拟此方法. 等价于 history.go(1).

Note: 当浏览器历史栈处于最顶端时( 当前页面处于最后一页时 )调用此方法没有效果也不报错。

  • History.go(n)

通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。比如:参数为 -1的时候为上一页,参数为 1 的时候为下一页. 当整数参数超出界限时 ( 译者注:原文为 When integerDelta is out of bounds ),例如: 如果当前页为第一页,前面已经没有页面了,我传参的值为 -1,那么这个方法没有任何效果也不会报错。调用没有参数的 go() 方法或者不是整数的参数时也没有效果。( 这点与支持字符串作为 url 参数的 IE 有点不同)。

  • history.pushState() 和 history.replaceState()

这两个 API 都接收三个参数,分别是

a. 状态对象(state object) — 一个JavaScript对象,与用 pushState() 方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate 事件都会被触发,并且事件对象的state 属性都包含历史记录条目的状态对象的拷贝。

b. 标题(title) — FireFox 浏览器目前会忽略该参数,虽然以后可能会用上。考虑到未来可能会对该方法进行修改,传一个空字符串会比较安全。或者,你也可以传入一个简短的标题,标明将要进入的状态。

c. 地址(URL) — 新的历史记录条目的地址。浏览器不会在调用 pushState() 方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的 URL 不一定是绝对路径;如果是相对路径,它将以当前 URL 为基准;传入的 URL 与当前 URL 应该是同源的,否则,pushState() 会抛出异常。该参数是可选的;不指定的话则为文档当前 URL。

相同之处: 是两个 API 都会操作浏览器的历史记录,而不会引起页面的刷新。

不同之处在于: pushState 会增加一条新的历史记录,而 replaceState 则会替换当前的历史记录。

例子:

本来的路由

 http://biaochenxuying.cn/

执行:

window.history.pushState(null, null, "http://biaochenxuying.cn/home");

路由变成了:

 http://biaochenxuying.cn/home

详情介绍请看:MDN

2.2 Hash 路由

我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,比如典型的回到顶部按钮原理、Github 上各个标题之间的跳转等,但是路由里的 # 不叫锚点,我们称之为 hash。

现在的前端主流框架的路由实现方式都会采用 Hash 路由,本项目采用的也是。

当 hash 值发生改变的时候,我们可以通过 hashchange 事件监听到,从而在回调函数里面触发某些方法。

3. 代码实现

3.1 简单版 - 单页面路由

先看个简单版的 原生 js 模拟 Vue 路由切换。

image

原理

  • 监听 hashchange ,hash 改变的时候,根据当前的 hash 匹配相应的 html 内容,然后用 innerHTML 把 html 内容放进 router-view 里面。

这个代码是网上的:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="author" content="">
    <title>原生模拟 Vue 路由切换</title>
    <style type="text/css">
        .router_box,
        #router-view {
            max-width: 1000px;
            margin: 50px auto;
            padding: 0 20px;
        }

        .router_box>a {
            padding: 0 10px;
            color: #42b983;
        }
    </style>
</head>

<body>
    <div class="router_box">
        <a href="/home" class="router">主页</a>
        <a href="/news" class="router">新闻</a>
        <a href="/team" class="router">团队</a>
        <a href="/about" class="router">关于</a>
    </div>
    <div id="router-view"></div>
    <script type="text/javascript">
        function Vue(parameters) {
            let vue = {};
            vue.routes = parameters.routes || [];
            vue.init = function() {
                document.querySelectorAll(".router").forEach((item, index) => {
                    item.addEventListener("click", function(e) {
                        let event = e || window.event;
                        event.preventDefault();
                        window.location.hash = this.getAttribute("href");
                    }, false);
                });

                window.addEventListener("hashchange", () => {
                    vue.routerChange();
                });

                vue.routerChange();
            };
            vue.routerChange = () => {
                let nowHash = window.location.hash;
                let index = vue.routes.findIndex((item, index) => {
                    return nowHash == ('#' + item.path);
                });
                if (index >= 0) {
                    document.querySelector("#router-view").innerHTML = vue.routes[index].component;
                } else {
                    let defaultIndex = vue.routes.findIndex((item, index) => {
                        return item.path == '*';
                    });
                    if (defaultIndex >= 0) {
                        window.location.hash = vue.routes[defaultIndex].redirect;
                    }
                }
            };

            vue.init();
        }

        new Vue({
            routes: [{
                path: '/home',
                component: "<h1>主页</h1><a href='https://github.com/biaochenxuying'>https://github.com/biaochenxuying</a>"
            }, {
                path: '/news',
                component: "<h1>新闻</h1><a href='http://biaochenxuying.cn/main.html'>http://biaochenxuying.cn/main.html</a>"
            }, {
                path: '/team',
                component: '<h1>团队</h1><h4>全栈修炼</h4>'
            }, {
                path: '/about',
                component: '<h1>关于</h1><h4>关注公众号:BiaoChenXuYing</h4><p>分享 WEB 全栈开发等相关的技术文章,热点资源,全栈程序员的成长之路。</p>'
            }, {
                path: '*',
                redirect: '/home'
            }]
        });
    </script>
</body>

</html>

3.2 复杂版 - 内联页面版,带缓存功能

首先前端用 js 实现路由的缓存功能是很难的,但像 vue-router 那种还好,因为有 vue 框架和虚拟 dom 的技术,可以保存当前页面的数据。

要做缓存功能,首先要知道浏览器的 前进、刷新、回退 这三个操作。

但是浏览器中主要有这几个限制:

  • 没有提供监听前进后退的事件
  • 不允许开发者读取浏览记录
  • 用户可以手动输入地址,或使用浏览器提供的前进后退来改变 url

所以要自定义路由,解决方案是自己维护一份路由历史的记录,存在一个数组里面,从而区分 前进、刷新、回退。

  • url 存在于浏览记录中即为后退,后退时,把当前路由后面的浏览记录删除。
  • url 不存在于浏览记录中即为前进,前进时,往数组里面 push 当前的路由。
  • url 在浏览记录的末端即为刷新,刷新时,不对路由数组做任何操作。

另外,应用的路由路径中可能允许相同的路由出现多次(例如 A -> B -> A),所以给每个路由添加一个 key 值来区分相同路由的不同实例。

这个浏览记录需要存储在 sessionStorage 中,这样用户刷新后浏览记录也可以恢复。

3.2.1 route.js

3.2.1.1 跳转方法 linkTo

像 vue-router 那样,提供了一个 router-link 组件来导航,而我这个框架也提供了一个 linkTo 的方法。

        // 生成不同的 key 
        function genKey() {
            var t = 'xxxxxxxx'
            return t.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16 | 0
                var v = c === 'x' ? r : (r & 0x3 | 0x8)
                return v.toString(16)
            })
        }

        // 初始化跳转方法
        window.linkTo = function(path) {
                if (path.indexOf("?") !== -1) {
                    window.location.hash = path + '&key=' + genKey()
                } else {
                    window.location.hash = path + '?key=' + genKey()
                }
        }

用法:

//1\. 直接用 a 标签
<a href='#/list' >列表1</a>

//2\. 标签加 js 调用方法
<div onclick='linkTo(\"#/home\")'>首页</div>

// 3\. js 调用触发
linkTo("#/list")

3.2.1.2 构造函数 Router

定义好要用到的变量

function Router() {
        this.routes = {}; //保存注册的所有路由
        this.beforeFun = null; //切换前
        this.afterFun = null; // 切换后
        this.routerViewId = "#routerView"; // 路由挂载点 
        this.redirectRoute = null; // 路由重定向的 hash
        this.stackPages = true; // 多级页面缓存
        this.routerMap = []; // 路由遍历
        this.historyFlag = '' // 路由状态,前进,回退,刷新
        this.history = []; // 路由历史
        this.animationName = "slide" // 页面切换时的动画
    }

3.2.1.3 实现路由功能

包括:初始化、注册路由、历史记录、切换页面、切换页面的动画、切换之前的钩子、切换之后的钩子、滚动位置的处理,缓存。

Router.prototype = {
        init: function(config) {
            var self = this;
            this.routerMap = config ? config.routes : this.routerMap
            this.routerViewId = config ? config.routerViewId : this.routerViewId
            this.stackPages = config ? config.stackPages : this.stackPages
            var name = document.querySelector('#routerView').getAttribute('data-animationName')
            if (name) {
                this.animationName = name
            }
            this.animationName = config ? config.animationName : this.animationName

            if (!this.routerMap.length) {
                var selector = this.routerViewId + " .page"
                var pages = document.querySelectorAll(selector)
                for (var i = 0; i < pages.length; i++) {
                    var page = pages[i];
                    var hash = page.getAttribute('data-hash')
                    var name = hash.substr(1)
                    var item = {
                        path: hash,
                        name: name,
                        callback: util.closure(name)
                    }
                    this.routerMap.push(item)
                }
            }

            this.map()

            // 初始化跳转方法
            window.linkTo = function(path) {
                console.log('path :', path)
                if (path.indexOf("?") !== -1) {
                    window.location.hash = path + '&key=' + util.genKey()
                } else {
                    window.location.hash = path + '?key=' + util.genKey()
                }
            }

            //页面首次加载 匹配路由
            window.addEventListener('load', function(event) {
                // console.log('load', event);
                self.historyChange(event)
            }, false)

            //路由切换
            window.addEventListener('hashchange', function(event) {
                // console.log('hashchange', event);
                self.historyChange(event)
            }, false)

        },
        // 路由历史纪录变化
        historyChange: function(event) {
            var currentHash = util.getParamsUrl();
            var nameStr = "router-" + (this.routerViewId) + "-history"
            this.history = window.sessionStorage[nameStr] ? JSON.parse(window.sessionStorage[nameStr]) : []

            var back = false,
                refresh = false,
                forward = false,
                index = 0,
                len = this.history.length;

            for (var i = 0; i < len; i++) {
                var h = this.history[i];
                if (h.hash === currentHash.path && h.key === currentHash.query.key) {
                    index = i
                    if (i === len - 1) {
                        refresh = true
                    } else {
                        back = true
                    }
                    break;
                } else {
                    forward = true
                }
            }
            if (back) {
                this.historyFlag = 'back'
                this.history.length = index + 1
            } else if (refresh) {
                this.historyFlag = 'refresh'
            } else {
                this.historyFlag = 'forward'
                var item = {
                    key: currentHash.query.key,
                    hash: currentHash.path,
                    query: currentHash.query
                }
                this.history.push(item)
            }
            console.log('historyFlag :', this.historyFlag)
                // console.log('history :', this.history)
            if (!this.stackPages) {
                this.historyFlag = 'forward'
            }
            window.sessionStorage[nameStr] = JSON.stringify(this.history)
            this.urlChange()
        },
        // 切换页面
        changeView: function(currentHash) {
            var pages = document.getElementsByClassName('page')
            var previousPage = document.getElementsByClassName('current')[0]
            var currentPage = null
            var currHash = null
            for (var i = 0; i < pages.length; i++) {
                var page = pages[i];
                var hash = page.getAttribute('data-hash')
                page.setAttribute('class', "page")
                if (hash === currentHash.path) {
                    currHash = hash
                    currentPage = page
                }
            }
            var enterName = 'enter-' + this.animationName
            var leaveName = 'leave-' + this.animationName
            if (this.historyFlag === 'back') {
                util.addClass(currentPage, 'current')
                if (previousPage) {
                    util.addClass(previousPage, leaveName)
                }
                setTimeout(function() {
                    if (previousPage) {
                        util.removeClass(previousPage, leaveName)
                    }
                }, 250);
            } else if (this.historyFlag === 'forward' || this.historyFlag === 'refresh') {
                if (previousPage) {
                    util.addClass(previousPage, "current")
                }
                util.addClass(currentPage, enterName)
                setTimeout(function() {
                    if (previousPage) {
                        util.removeClass(previousPage, "current")
                    }
                    util.removeClass(currentPage, enterName)
                    util.addClass(currentPage, 'current')
                }, 350);
                // 前进和刷新都执行回调 与 初始滚动位置为 0
                currentPage.scrollTop = 0
                this.routes[currHash].callback ? this.routes[currHash].callback(currentHash) : null
            }
            this.afterFun ? this.afterFun(currentHash) : null
        },
        //路由处理
        urlChange: function() {
            var currentHash = util.getParamsUrl();
            if (this.routes[currentHash.path]) {
                var self = this;
                if (this.beforeFun) {
                    this.beforeFun({
                        to: {
                            path: currentHash.path,
                            query: currentHash.query
                        },
                        next: function() {
                            self.changeView(currentHash)
                        }
                    })
                } else {
                    this.changeView(currentHash)
                }
            } else {
                //不存在的地址,重定向到默认页面
                location.hash = this.redirectRoute
            }
        },
        //路由注册
        map: function() {
            for (var i = 0; i < this.routerMap.length; i++) {
                var route = this.routerMap[i]
                if (route.name === "redirect") {
                    this.redirectRoute = route.path
                } else {
                    this.redirectRoute = this.routerMap[0].path
                }
                var newPath = route.path
                var path = newPath.replace(/\s*/g, ""); //过滤空格
                this.routes[path] = {
                    callback: route.callback, //回调
                }
            }
        },
        //切换之前的钩子
        beforeEach: function(callback) {
            if (Object.prototype.toString.call(callback) === '[object Function]') {
                this.beforeFun = callback;
            } else {
                console.trace('路由切换前钩子函数不正确')
            }
        },
        //切换成功之后的钩子
        afterEach: function(callback) {
            if (Object.prototype.toString.call(callback) === '[object Function]') {
                this.afterFun = callback;
            } else {
                console.trace('路由切换后回调函数不正确')
            }
        }
    }

3.2.1.4 注册到 Router 到 window 全局

    window.Router = Router;
    window.router = new Router();

完整代码:https://github.com/biaochenxuying/route/blob/master/js/route.js

3.2.2 使用方法

3.2.2.1 js 定义法

  • callback 是切换页面后,执行的回调
<script type="text/javascript">
        var config = {
            routerViewId: 'routerView', // 路由切换的挂载点 id
            stackPages: true, // 多级页面缓存
            animationName: "slide", // 切换页面时的动画
            routes: [{
                path: "/home",
                name: "home",
                callback: function(route) {
                    console.log('home:', route)
                    var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>首页</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/list\")'>列表</a></div><div class='height'>内容占位</div>"
                    document.querySelector("#home").innerHTML = str
                }
            }, {
                path: "/list",
                name: "list",
                callback: function(route) {
                    console.log('list:', route)
                    var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>列表</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail\")'>详情</a></div>"
                    document.querySelector("#list").innerHTML = str
                }
            }, {
                path: "/detail",
                name: "detail",
                callback: function(route) {
                    console.log('detail:', route)
                    var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>详情</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail2\")'>详情 2</a></div><div class='height'>内容占位</div>"
                    document.querySelector("#detail").innerHTML = str
                }
            }, {
                path: "/detail2",
                name: "detail2",
                callback: function(route) {
                    console.log('detail2:', route)
                    var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>详情 2</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/home\")'>首页</a></div>"
                    document.querySelector("#detail2").innerHTML = str
                }
            }]
        }

        //初始化路由
        router.init(config)
        router.beforeEach(function(transition) {
            console.log('切换之 前 dosomething', transition)
            setTimeout(function() {
                //模拟切换之前延迟,比如说做个异步登录信息验证
                transition.next()
            }, 100)
        })
        router.afterEach(function(transition) {
            console.log("切换之 后 dosomething", transition)
        })
    </script>

3.2.2.2 html 加 script 定义法

  • id="routerView" :路由切换时,页面的视图窗口
  • data-animationName="slide":切换时的动画,目前有 slide 和 fade。
  • class="page": 切换的页面
  • data-hash="/home":home 是切换路由时执行的回调方法
  • window.home : 回调方法,名字要与 data-hash 的名字相同
<div id="routerView" data-animationName="slide">
        <div class="page" data-hash="/home">
            <div class="page-content">
                <div id="home"></div>
                <script type="text/javascript">
                    window.home = function(route) {
                        console.log('home:', route)
                            // var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>首页</h2> <input type='text'> <div><a href='#/list' >列表1</div></div><div class='height'>内容占位</div>"
                        var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>首页</h2> <input type='text'> <div><div href='javascript:void(0);' onclick='linkTo(\"#/list\")'>列表</div></div><div class='height'>内容占位</div>"
                        document.querySelector("#home").innerHTML = str
                    }
                </script>
            </div>
        </div>
        <div class="page" data-hash="/list">
            <div class="page-content">
                <div id="list"></div>
                <div style="height: 700px;border: solid 1px red;background-color: #eee;margin-top: 20px;">内容占位</div>

                <script type="text/javascript">
                    window.list = function(route) {
                        console.log('list:', route)
                        var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>列表</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail\")'>详情</a></div>"
                        document.querySelector("#list").innerHTML = str
                    }
                </script>
            </div>
        </div>
        <div class="page" data-hash="/detail">
            <div class="page-content">
                <div id="detail"></div>
                <script type="text/javascript">
                    window.detail = function(route) {
                        console.log('detail:', route)
                        var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>详情</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail2\")'>详情 2</a></div><div class='height'>内容占位</div>"
                        document.querySelector("#detail").innerHTML = str
                    }
                </script>
            </div>
        </div>
        <div class="page" data-hash="/detail2">
            <div class="page-content">
                <div id="detail2"></div>
                <div style="height: 700px;border: solid 1px red;background-color: pink;margin-top: 20px;">内容占位</div>

                <script type="text/javascript">
                    window.detail2 = function(route) {
                        console.log('detail2:', route)
                        var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>详情 2</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/home\")'>首页</a></div>"
                        document.querySelector("#detail2").innerHTML = str
                    }
                </script>
            </div>
        </div>
    </div>
    <script type="text/javascript" src="./js/route.js"></script>
    <script type="text/javascript">
        router.init()
        router.beforeEach(function(transition) {
            console.log('切换之 前 dosomething', transition)
            setTimeout(function() {
                //模拟切换之前延迟,比如说做个异步登录信息验证
                transition.next()
            }, 100)
        })
        router.afterEach(function(transition) {
            console.log("切换之 后 dosomething", transition)
        })
    </script>

参考项目:https://github.com/kliuj/spa-routers

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容