前端网络请求

前端网络请求的方式主要有 Ajax ,jQuery封装的 Ajax,fetch,axios、request 等开源库等。
这里插一下http response status,指示一个 http 请求是否成功完成,具体分为五类:
(1) informational responses,1XX,临时回应,表示客户端请继续:
100 Continue: 代表目前一切正常,客户端应该继续等待请求完成,或者直接忽略如果请求已经完成。
101 Switching Protocol: 对客户端发送的 Upgrade请求头的回应,请求者已要求服务器切换协议,服务器已确认并准备切换。
102 Processing: 代表服务器已经收到请求并正在处理,不过此时仍没有响应返回。
103 Early Hints: 主要用于 Link 头以允许客户端开始预加载资源,而此时服务器仍在准备一个响应。

(2) successful responses:
200 OK: 请求成功,但具体含义依请求方法而异,具体参考MDN
201 Created: 请求成功,并且因该请求而生成了新的资源,通常是由于发送了 POST 或者 PUT 请求。
202 Accepted: 请求已被收到,但是没有据此进行进一步操作,这表示无法返回一个异步响应来代表请求处理的结果,这通常出现在另一个进程或者服务器处理请求或者批处理中。
203 Non-Authoritative Information: 非权威性信息,表示返回的元信息并非源于目标服务器,而来自本地或者第三方副本。
204 No Content: 对于请求没有可返回的内容,即没有新文档,浏览器应该继续显示原来的文档。不过返回的响应头可能有用,客户端可以据此更新对应资源的缓存头。
205 Reset Content:没有新的内容,但浏览器应该重置它所显示的内容。用来强制浏览器清除表单输入内容。
206 Partial Content: 因为客户端发送了一个带有Range头的请求,而Ranger头用于将下载分为多个流。
还有207、208、226自己查资料吧;

(3) redirects,3xx,表示请求的目标有变化,希望客户端进一步处理。
300 Multiple Choice: 请求可以有多个响应,客户端自己挑一个。即客户端请求的资源可以在多个位置找到,这些位置已经在返回的文档内列出。如果服务器要提出优先选择,则应该在Location应答头指明。
301 Moved Permanently: 这意味着被请求资源对应的URI已被永久改变了,旧地址A上的资源已被永久移除了而不可访问了。在可通过 Location 响应头给出新地址的URL。
302 Found:这意味着被请求资源对应的URI已被暂时改变了,旧地址A的资源仍可以访问,重定向只是临时地从地址A跳转到地址B(详细请看这篇文章).
(Note: 关于301、302状态码,参考这篇文章,它们都表示重定向,即浏览器在拿到服务器返回的这个状态码后,会自动跳转到一个新的URL(可从响应的Location头部中获取,用户看到的效果就是他输入的地址A变成了另一个地址B),尽量使用301跳转)

303 See Other: 服务器返回该状态码指示客户端通过GET方法使用另一个URI去获取被请求的资源。
304 Not Modified: 出于缓存目的,告诉客户端被请求的资源没被修改,可以继续使用对应的缓存。即客户端本地已经有缓存的版本,并且在 Request 中告诉了服务器,当服务器通过 if-modified-since 或 e-tag 发现对应资源没更新时,就会返回一个不包含响应体的304状态。 (这是在ajax请求中,唯一一个可以成功处理的重定向类状态码,其他3XX都不行)
307 Temporary Redirect: 语义上与 302一致,只不过限制了客户端不能改变使用的请求方法类型,例如第一次请求使用的是 POST 方法,那么后续请求只能使用 POST 方法。
308 Permanent Redirect: 语义上与 301 一致,不过限制了客户端不能改变使用的请求方法类型。

(4) client errors:
400 Bad Request: 由于语法错误,服务器无法理解请求。
401 Unauthorized: 语义上应该是"unauthenticated",未经验证的,也有“未授权”的叫法,要求身份验证。对于需要登录的网页,服务器可能返回该响应(服务器:你要我提供服务,先告诉我你是谁)。
403 Forbidden: 客户端无权限获取相应内容,即未得到授权因此服务器拒绝返回相应内容。与401不同,服务器知道客户端的身份,但不好意思,人家就是不想给你提供服务。
404 Not Found: 这个太常见了,服务器找不到被请求的资源(亲,我这边没有你要的东西哦)。不过服务器有时候也会用404代替403返回,以此来向无权限的客户端隐藏对应资源的存在(我有,但骗你说没有,哈哈)。
405 Method Not Allowed: 服务器知道请求使用的方法,但不允许。例如禁止 Delete 方法防止删除资源。不过 GET 和 HEAD 这两个方法不可以被禁止,即使用这两个方法去请求资源,不应该被返回 405.
406 Not Acceptable: 无法根据客户端给定的规则找到内容。
407 Proxy Authentication Required: 与401类似,但是需要通过代理来完成验证。
408 Request Timeout: 某些服务器会对空闲的连接(即没有传输任务在进行)发送该响应,即使客户端之前并未发送任何请求(即该响应可以是服务器主动发送的),意味着服务器想要关闭该连接。
还有还多4XX,这里就不介绍了。

(5) servers eerors:
500 Internal Server Error: 服务器内部错误。
501 Not Implemented: 请求使用的方法类型不被服务器所支持而无法处理。不过服务器一般都支持 get 、head 方法。
502 Bad Gateway: 服务器作为网关或代理,从上游服务器收到无效响应。
503 Service Unavailable: 服务器目前无法使用(由于超载或停机维护),通常只是暂时状态。
504 Gateway Timeout: 网关超时,服务器作为网关或者代理,没有及时从上游服务器收到请求。
505 HTTP Version Not Supported: 请求使用的HTTP版本不被服务器所支持。

1.原生 Ajax
Ajax: asynchoronous javascript and xml,这种技术能够向服务器请求数据而无须重新加载整个页面,带来更好的用户体验,关于ajax的超详细介绍请移步这里
本人画了下面这张示意图

ajax请求.jpg

说明:
(1)发出请求(xhr.send())之前的语句都是同步执行,从 send 方法内部开始, 浏览器为将要发生的网络请求创建新的http请求线程(这个线程独立于js引擎线程)。网络请求异步被发送出去后,js 引擎并不会等待 ajax 发起的http请求收到结果, 而是直接顺序往下执行。
(2)当ajax请求被服务器响应并且收到 response 后,浏览器事件触发线程捕获到了ajax的相应事件,并将 onreadystatechange (也可能是 onload 或者 onerror 等等)对应的事件处理程序添加到 任务队列 的末尾。
(3)一次ajax请求, 并非所有的部分都是异步进行的,例如 "readyState==1"的 onreadystatechange 回调以及 onloadstart 回调就是同步执行的代码。
xhr.send()刚开始执行,onloadstart 回调方法就被触发。
使用原生 ajax 的代码如下:

 let xhr=new XMLHttpRequest();
    //状态监听
    xhr.onreadystatechange=function () {
        console.log(xhr.readyState);
        if(xhr.readyState===4){
            if(xhr.status>=200 && xhr.status<300 || xhr.status==304){
                console.log(xhr.responseText);
            }
        }
    }
    //异常处理
    xhr.onerror=function(){
        console.log("Ajax request failed.");
    }

    //设置相关事件回调方法
    // onloadstart 在开始执行 xhr.send()就会被触发,属于同步执行。
    xhr.onloadstart=function(){
        console.log('loadstart');
    }
    //超时处理
    xhr.ontimeout=function(){
        console.log("请求超时!")
    }

    //处理请求参数
    postData={"name1":"value1","name2":"value2"};
    postData=(function (value) {
        let dataString="";
        for(let key in value){
            dataString+=key+"="+value[key]+"&";
        }
        return dataString;
    })(postData);

    xhr.open('get','https://www.qq.com',true);

    //设置请求头,要在 open 与 send 之间
    // 推测是设置请求头的时候,xhr需要已经完成初始化
    xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');
    //跨域携带cookie
    xhr.withCredentials=true;

    // 发出请求,之前的语句都是同步执行
    // 从send方法内部开始, 浏览器为将要发生的网络请求创建了新的http请求线程,
    xhr.send(postData);

2.jQuery对 Ajax 的封装

$.ajax({
    dataType: 'json', // 设置返回值类型
    contentType: 'application/json', // 设置参数类型
    headers: {'Content-Type','application/json'},// 设置请求头
    xhrFields: { withCredentials: true }, // 跨域携带 cookie
    data: JSON.stringify({a: [{b:1, a:1}]}), // 传递参数
    error:function(xhr,status){  // 错误处理
       console.log(xhr,status);
    },
    success: function (data,status) {  // 获取结果
       console.log(data,status);
    }
})

3.fetch
Fetch API是一个用用于访问和操纵 HTTP 管道的强大的原生 API。
这种功能以前是使用 XMLHttpRequest 实现的。Fetch 提供了一个更好的替代方法,可以很容易地被其他技术使用,例如 Service Workers。Fetch 还提供了单个逻辑位置来定义其他 HTTP 相关概念,例如 CORS 和 HTTP 的扩展。
fetch是作为XMLHttpRequest的替代品出现的。
优点:
(1)符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里;
(2)更好更方便的写法;
(3)基于标准 Promise 实现,支持 async/await;
(4)更加底层,提供的API丰富(request, response);
(5)脱离了XHR,是ES规范里新的实现方式。
使用fetch,你不需要再额外加载一个外部资源。但它还没有被浏览器完全支持,所以仍然需要一个polyfill。
一个基本的fetch请求如下:

const options = {
    method: "POST", // 请求参数
    headers: { "Content-Type": "application/json"}, // 设置请求头
    body: JSON.stringify({name:'123'}), // 请求参数
    credentials: "same-origin", // cookie 设置
    mode: "cors", // 跨域
}
// fetch方法返回的是一个 promise
// fetch(input, init)
fetch('http://www.xxx.com', options)
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson); // 响应数据
  })
  .catch(function(err){
    console.log(err); // 异常处理
  })

fetch polyfill 源码
polyfill 主要对 fetch API 的 Headers, Request, Response, fetch 进行了封装。

fetch polyfill代码结构

(1) fetch
(a)构造一个Promise对象并返回;
(b)创建一个Request对象;
(c)创建一个XMLHttpRequest对象(注意 原生 fetch 并未使用 XHR 对象);
(d)取出Request对象中的请求url,请求方发,open一个xhr请求,并将Request对象中存储的headers取出赋给 xhr;
(e)xhr onload后取出response的status、headers、body封装Response对象,调用resolve。

// fetch 方法的封装
    function fetch(input, init) {
        return new Promise(function (resolve, reject) {
            let request=new Request(input,init);
            let xhr=new XMLHttpRequest();
            xhr.open(request.method,request.url,true);

            //响应获取完毕
            xhr.onload=function () {
                let options={
                    status:xhr.status,
                    statusText:xhr.statusText,
                    headers:parseHeaders(xhr.getAllResponseHeaders() || '')
                }
                options.url='responseURL' in xhr? xhr.responseURL:options.headers.get('X-Request-URL');
                let body='response' in xhr? xhr.response: xhr.responseText;
                resolve(new Response(body,options));
            }

            //异常处理
            function abortXhr(){
                xhr.abort();
            }
            // 1.请求失败,网络故障或请求被阻止才触发,
            //而收到服务器异常状态码如404,500不会触发
            xhr.onerror=function(){
                reject(new TypeError('Network request failed.'));
            }
            //2.请求超时
            xhr.ontimeout=function(){
                reject(new TypeError('请求超时'));
            }
            xhr.onabort=function(){
                reject(new DOMException('Aborted','AbortError'));
            }
            //3.手动终止
            if(request.signal){
                request.signal.addEventListener('abort',abortXhr);
                xhr.onreadystatechange=function () {
                    if(xhr.readyState===4){
                        request.signal.removeEventListener('abort',abortXhr);
                    }
                }
            }
            // forEach 的具体实现在 Headers.prototype 中
            request.headers.forEach(function (value, name) {
                xhr.setRequestHeader(name,value);
            });

            xhr.send();
        })
    }

(2) Request
Request对象接收的两个参数即fetch函数接收的两个参数,第一个参数可以直接传递url,也可以传递一个构造好的request对象。第二个参数即控制不同配置的option对象。

 // 2.Request对象封装
    function Request(input, options) {
        options=options || {}
        let body=options.body

        if(input instanceof Request){
            this.url=input.url
            this.method=input.method
            //...
        }
        else{
            this.url=String(input)
        }
        this.credentials=options.credentials || this.credentials || 'same-origin'
        if(options.headers || !this.headers){
            this.headers=new Headers(options.headers)
        }
        this.method=normalizeMethod(options.method || this.method || 'GET')
        this.mode=options.mode || this.mode || null
        this.signal=options.signal || this.signal
        this.referrer=null
        //...
        this._initBody(body)
    }

(3) Headers

 // 3.Headers 封装
    function Headers(headers) {
        this.map={}
        // (1)传入的参数headers本身就是 Headers 的实例
        if(headers instanceof Headers){
            headers.forEach(function (value, name) {
                this.append(name,value)
            },this)
        }
        //  (2)传入的参数headers是数组类型(二维)
        else if(Array.isArray(headers)){
            headers.forEach(function (header) {
                this.append(header[0],header[1])
            },this)
        }
        // (3)传入的参数headers是普通对象
        else if(headers){
            Object.getOwnPropertyNames(headers).forEach(function (name) {
                this.append(name,headers[name])
            })
        }
    }

在 Headers 中维护了一个map对象,构造函数中可以传入Headers对象、数组、普通对象类型的header,并将所有的值维护到map中。
fetch及上段代码中都有 headers.forEach,这个 forEach 方法的具体实现如下:

    Headers.prototype.forEach=function (callback, thisArg) {
        for(let name in this.map){
            if(this.map.hasOwnProperty(name)){
                callback.call(thisArg,this.map[name],name,this)
            }
        }
    }

可见header的遍历即其内部map的遍历。
另外Header还提供了append、delete、get、set等方法,都是对其内部的map对象进行操作。

(4) Response
在 fetch中有对 Response 的操作:

  xhr.onload=function(){
    ···
    resolve(new Response(body,options))
  }
 function Response(bodyInit,options) {
        options=options || {}
        this.type='default'
        this.status=options.status===undefined? 200 : options.status
        this.ok=this.status>=200 && this.status<300
        this.statusText='statusText' in options?options.statusText:'OK'
        this.headers=new Headers(options.headers)
        this.url=options.url || ''
        // _initBody 是在 Body 函数中挂载到 Response.prototype 对象上的
        this._initBody(bodyInit)
    }
    Body.call(Response.prototype)

可见在构造函数中主要对options中的status、statusText、headers、url等分别做了处理并挂载到Response对象上。
构造函数里面并没有对responseText的明确处理,最后交给了_initBody函数处理,而Response并没有主动声明_initBody属性,代码最后使用Response调用了Body函数,实际上_initBody函数是通过Body函数挂载到Response身上的,先来看看Body函数:

function Body() {
        this.bodyUsed=false;
        // 这就是 Response 中的 _initBody
        this._initBody=function (body) {
            //具体代码见后面
        }

        this.text=function () {
            // _bodyText...
        }
        
        this.json=function () {
            this.text().then(JSON.parse)
        }
        
        if(support.blob){
            this.blob=function () {
                //_bodyBlob...
            }
        }
        if(support.formData){
            this.formData=function () {
                //_bodyFormData...
            }
        }
        return this
    }

Body函数中还为Response对象挂载了四个函数,text、json、blob、formData,这些函数中的操作就是将 _initBody 中得到的不同类型的返回值返回。
这也说明了,在fetch执行完毕后,不能直接在response中获取到返回值而必须调用text()、json()等函数才能获取到返回值。
再来看看_initBody函数

 function Body() {
        this.bodyUsed=false;
        // 这就是 Response 中的 _initBody
        this._initBody=function (body) {
            if(!body){
                this._bodyText=''
            }
            else if(typeof body==='string'){
                this._bodyText=body
            }
            else if(support.blob && Blob.prototype.isPrototypeOf(body)){
                this._bodyBlob=body
            }
            else if(support.formData && FormData.prototype.isPrototypeOf(body)){
                this._bodyFormData=body
            }
            // else if ...
            else{
                this._bodyText=body=Object.prototype.toString.call(body)
            }
        }
       // ...
}

可见,_initBody函数根据xhr.response的类型(Blob、FormData、String...),为不同的参数进行赋值,这些参数在Body方法中得到不同的应用.
这里还有一点需要说明:Response相关的几个函数中都有类似下面的逻辑:

    var rejected = consumed(this)
    if (rejected) {
      return rejected
    }

   function consumed(body) {
      if (body.bodyUsed) {
          return Promise.reject(new TypeError('Already read'))
       }
       body.bodyUsed = true
   }

每次调用text()、json()等函数后会将bodyUsed变量变为true,用来标识返回值已经读取过了,下一次再读取直接抛出TypeError('Already read')。这也遵循了原生fetch的原则:
因为 Responses 对象被设置为了 stream 的方式,所以它们只能被读取一次。
fetch 的缺点
(1)不能直接传递JavaScript对象作为参数;
(2)需要自己判断返回值类型,并执行响应获取返回值的方法;
(3)获取返回值方法只能调用一次,不能多次调用;
(4)无法正常的捕获异常(服务器返回 400,500 错误码时并不会 reject,而是会将 resolve 的返回值的 ok 属性设置为 false;仅当网络故障时或请求被阻止时,才会标记为 reject。);
(5)老版浏览器不会默认携带cookie;
(6)不支持jsonp;
(7)fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费;
(8)fetch没有办法原生监测请求的进度,而XHR可以.

所以说半天, fetch并不是那么好用,偏底层,想要用的顺手还要自己封装,进行诸如 请求参数处理、cookie携带、异常处理、返回值处理等。

4.jsonp
fetch本身没有提供对jsonp的支持,jsonp本身也不属于一种非常好的解决跨域的方式。不过呢,多了解一种方式也不是坏事。jsonp 本身很简单,就是利用 script 标签的 src 属性不受同源策略约束,可进行跨域请求,不过服务端需要进行对应的设置才行。

// 服务器端
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
   response.setCharacterEncoding("UTF-8");
   response.setContentType("text/html;charset=UTF-8");
   //数据
   List<Student> studentList = getStudentList();
   JSONArray jsonArray = JSONArray.fromObject(studentList);
   String result = jsonArray.toString();
   //前端传过来的回调函数名称
   String callback = request.getParameter("callback");
   //用回调函数名称包裹返回数据,这样,返回数据就作为回调函数的参数传回去了
   result = callback + "(" + result + ")";
   response.getWriter().write(result);
 }

上段代码就是服务器端对 jsonp 请求的简单处理(摘自
下面就是对 jsonp 的封装。

  (function (window, document) {
        "use strict";
        let jsonp=function (url, data, callback) {
            // 1.将传入的data数据转化为url字符串形式
            // {id:1,name:'Jack'} => id=1&name=Jack
            let dataString=url.indexOf('?')==-1? '?': '&';
            for(let key in data){
                dataString+=key+'='+data[key]+'&';
            }

            // 2.处理url中的回调函数
            // cbFuncName 回调函数的名字 :my_json_cb_ 名字的前缀 + 随机数(把小数点去掉)
            let cbFuncName='my_json_cb_'+
                    Math.random().toString().replace('.','');
            dataString+='callback='+cbFuncName;

            // 3.创建一个 script 标签
            let scriptEle=document.createElement('script');
            scriptEle.src=url+dataString;

            // 4.挂载回调函数(cbFuncName是变量,不能直接作为回调函数名)
            window[cbFuncName]=function (data) {
                callback(data);
                // 处理完回调函数的数据之后,删除 jsonp 的 script 标签
                document.body.removeChild(scriptEle);
            }

            document.body.appendChild(scriptEle);
        }
        // 通过给全局window对象添加$jsonp属性,可在 此‘定义并立即调用匿名函数’执行后,
        // 在其他地方使用封装好的jsonp
        window.$jsonp=jsonp;
    })(window,document)

    //调用
    let url='https://www.xxx.com';
    let cb=function(data){console.log(data)}
    window.$jsonp(url,null,cb);

5.axios
Vue大佬尤雨溪推荐大家用axios替换JQuery封装的ajax.
axios 是一个基于Promise,可用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范,它本身具有以下特征:
1.从浏览器中创建 XMLHttpRequest
2.支持 Promise API
3.客户端支持防止CSRF
4.提供了一些并发请求的接口(重要,方便了很多的操作)
5.从 node.js 创建 http 请求
6.拦截请求和响应
7.转换请求和响应数据
8.取消请求
9.自动转换JSON数据
axios既提供了并发的封装,也没有fetch的各种问题,而且体积也较小,你值得拥有!
axios使用示例:

axios({
    method: 'post',
    url: '/user/12345',
    data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
    }
})
.then(function (response) {
    console.log(response);
})
.catch(function (error) {
    console.log(error);
});

Reference
1.全面分析前端的网络请求方式
2.ajax知识体系大梳理
3.ajax和axios、fetch的区别
4.jsonp原理学习笔记

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