Node.js

Node.js

  • Node.js 就是运行在服务端的 JavaScript。
  • Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。
  • Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8是Javascript代码解析器,V8引擎执行Javascript的速度非常快,性能非常好。

异步式I/O(非阻塞式(Asynchronous)I/O)
针对所有I/O操作不采用阻塞策略,当线程遇到I/O操作时,不会以阻塞的方式等待I/O操作的完成或数据的返回,而只是将IO请求发送给操作系统,继续执行下一条语句,当操作系统完成IO操作时,以事件的形式通知执行IO操作的线程,线程会在特定时候处理这个事件,为了 处理异步IO,线程必须有事件循环,不断的检查有没有未处理的事件,依次予以处理。

http://www.phperz.com/special/70.html node.js 资料

Node.js 应用是由哪几部分组成的:
1.引入 required 模块:我们可以使用 require 指令来载入 Node.js 模块。
2.创建服务器:服务器可以监听客户端的请求,类似于 Apache 、Nginx 等 HTTP 服务器。
3.接收请求与响应请求 服务器很容易创建,客户端可以使用浏览器或终端发送 HTTP 请求,服务器接收请求后返回响应数据。

Package.json 属性说明
name - 包名。
version - 包的版本号。
description - 包的描述。
homepage - 包的官网 url 。
author - 包的作者姓名。
contributors - 包的其他贡献者姓名。
dependencies - 依赖包列表。如果依赖包没有安装,npm 会自动将依赖包安装在 node_module 目录下。
repository - 包代码存放的地方的类型,可以是 git 或 svn,git 可在 Github 上。
main - main 字段指定了程序的主入口文件,require('moduleName') 就会加载这个文件。这个字段的默认值是模块根目录下面的 index.js。
keywords - 关键字

Node.js REPL(Read Eval Print Loop:交互式解释器) 表示一个电脑的环境,类似 Window 系统的终端或 Unix/Linux shell,我们可以在终端中输入命令,并接收系统的响应。

URL

url 模块提供了一些实用函数,用于 URL 处理与解析。 http://nodejs.cn/api/url.html
const url = require('url');
一个 URL 字符串是一个结构化的字符串,它包含多个有意义的组成部分。 当被解析时,会返回一个 URL 对象,它包含每个组成部分作为属性。
通过Node.js提供的API解析一个URL:

const url = require('url');
const myURL = url.parse('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash');

url.hash:获取及设置URL的分段(hash)部分。
const myURL = new URL('https://example.org/foo#bar');
console.log(myURL.hash);  // 输出 #bar

url.host:获取及设置URL的主机(host)部分。
const myURL = new URL('https://example.org:81/foo');
console.log(myURL.host);    // 输出 example.org:81
myURL.host = 'example.com:82';
console.log(myURL.href);   // 输出 https://example.com:82/foo
 
url.hostname:获取及设置URL的主机名(hostname)部分。 url.host和url.hostname之间的区别是url.hostname不 包含端口。
url.port:获取及设置URL的端口(port)部分。完全无效的端口字符串将被忽略。如果字符串以数字开头,那么开头部位的数字将会被赋值给port。数字范围0~65535(包括)。
url.href:获取及设置序列化的URL。获取href属性的值等同于调用url.toString()。
url.protocol:获取及设置URL的协议(protocol)部分。
url.origin:获取只读序列化的URL origin部分。

const myURL = new URL('https://example.org/foo/bar?baz');
console.log(myURL.origin);    // 输出 https://example.org

url.pathname:获取及设置URL的路径(path)部分。
const myURL = new URL('https://example.org/abc/xyz?123');
console.log(myURL.pathname);      // 输出 /abc/xyz

url.search:获取及设置URL的序列化查询(query)部分部分。
const myURL = new URL('https://example.org/abc?123');
console.log(myURL.search);      // 输出 ?123  问号后面的那部分

url.username:获取及设置URL的用户名(username)部分。
const myURL = new URL('https://abc:xyz@example.com');
console.log(myURL.username);      // 输出 abc
console.log(myURL.password);      // 输出 xyz

url.toString():在URL对象上调用toString()方法将返回序列化的URL。
url.toJSON():在URL对象上调用toJSON()方法将返回序列化的URL。

当URL对象使用JSON.stringify()序列化时将自动调用该方法。
const myURLs = [
  new URL('https://www.example.com'),
  new URL('https://test.example.org')
];
console.log(JSON.stringify(myURLs));
  // 输出 ["https://www.example.com/","https://test.example.org/"]

url.format(URL[, options])  方法允许输出的基本自定义。
const myURL = new URL('https://a:b@你好你好?abc#foo');
console.log(myURL.toString());
  // 输出 https://a:b@xn--6qqa088eba/?abc#foo
console.log(url.format(myURL, { fragment: false, unicode: true, auth: false }));
  // 输出 'https://你好你好/?abc'

auth:如果序列化的URL字符串应该包含用户名和密码为true,否则为false。默认为true
fragment:如果序列化的URL字符串应该包含分段为true,否则为false。默认true
search:如果序列化的URL字符串应该包含搜索查询为true,否则为false。默认true
unicode:如果出现在URL字符串主机元素里的Unicode字符应该被直接编码为true

url.format(urlObject)  urlObject一个 URL 对象(就像 url.parse() 返回的)。 如果是一个字符串,则通过 url.parse() 转换为一个对象。
url.format() 方法返回一个从 urlObject 格式化后的 URL 字符串。

url.parse(urlString[, parseQueryString[, slashesDenoteHost]])
urlString  为要解析的 URL 字符串。
url.parse() 方法会解析一个 URL 字符串并返回一个 URL 对象。

HTTP

Node.js 中的 HTTP 接口被设计成支持协议的许多特性。这些接口不缓冲完整的请求或响应,用户能够以流的形式处理数据。
HTTP 消息头由一个对象表示,键名是小写的,键值不能修改。例如:

{ 'content-length': '123',
  'content-type': 'text/plain',
  'connection': 'keep-alive',
  'host': 'mysite.com',
  'accept': '*/*' }

HTTP API 是非常底层的。 它只涉及流处理与消息解析。它把一个消息解析成消息头和消息主体,但不解析具体的消息头或消息主体。

http.Agent 类

Agent 负责为 HTTP 客户端管理连接的持续与复用。 它为一个给定的主机与端口维护着一个等待请求的队列,且为每个请求重复使用一个单一的 socket 连接直到队列为空,此时 socket 会被销毁或被放入一个连接池中,在连接池中等待被有着相同主机与端口的请求再次使用。 是否被销毁或被放入连接池取决于KeepAlive选项。

var http=require("http");
http.createServer(function(req,res){
    res.writeHead(200,{
        "content-type":"text/plain"
    });
    res.write("hello world");
    res.end();
}).listen(3000);

我们首先用http.createServer函数创建了一个服务器对象,然后调用了response.writeHead方法:该方法的第一个参数表示HTTP的响应状态(200)表示一切正常;第二个参数是“Content-Type”,表示我响应给客户端的内容类型。然再后我们调用了response.write方法,写入我们需要传递给客户端的内容。最后一步我们调用了response.end,表示此次请求已处理完成。

httpService (http服务器)

开篇的实例代码,也可以通过如下的代码进行改写一番:

var http=require("http");
var server=new http.Server();
server.on("request",function(req,res){
    res.writeHead(200,{
        "content-type":"text/plain"
    });
    res.write("hello nodejs");
    res.end();
});
server.listen(3000);

以上代码是通过直接创建一个http.Server对象,然后为其添加request事件监听,其实也就说createServer方法其实本质上也是为http.Server对象添加了一个request事件监听,这似乎更好理解了,那让我们看看http的重要属性:
createServer方法中的参数函数中的两个参数req和res则是分别代表了请求对象和响应对象。其中req是http.IncomingMessage的实例,res是http.ServerResponse的实例。

  • http.IncomingMessage
    http.IncomingMessage是HTTP请求的信息,是后端开发者最关注的内容,一般由http.Server的request事件发送,并作为第一个参数传递,包含三个事件
    • data:当请求体数据到来时,该事件被触发,该事件提供一个参数chunk,表示接受的数据,如果该事件没有被监听,则请求体会被抛弃,该事件可能会被调用多次(这与nodejs是异步的有关系)
    • end:当请求体数据传输完毕时,该事件会被触发,此后不会再有数据
    • close:用户当前请求结束时,该事件被触发,不同于end,如果用户强制终止了传输,也是用close
      可以参考另一篇文章《nodejs + cheerio 爬取极客学院的nodejs课程数据》来了解http模块在爬虫中的简单应用。
  • http.ServerResponse
    http.ServerResponse是返回给客户端的信息,决定了用户最终看到的内容,一般也由http.Server的request事件发送,并作为第二个参数传递,它有三个重要的成员函数,用于返回响应头、响应内容以及结束请求
    • res.writeHead(statusCode,[heasers]):向请求的客户端发送响应头,该函数在一个请求中最多调用一次,如果不调用,则会自动生成一个响应头
    • res.write(data,[encoding]):想请求的客户端发送相应内容,data是一个buffer或者字符串,如果data是字符串,则需要制定编码方式,默认为utf-8,在res.end调用之前可以多次调用
    • res.end([data],[encoding]):结束响应,告知客户端所有发送已经结束,当所有要返回的内容发送完毕时,该函数必需被调用一次,两个可选参数与res.write()相同。如果不调用这个函数,客户端将用于处于等待状态。

http client

http模块提供了两个函数 http.requesthttp.get,功能是作为客户端向http服务器发起请求。

  • http.request(options,callback)
    options是一个类似关联数组的对象,表示请求的参数,callback作为回调函数,需要传递一个参数,为http.ClientResponse的实例,http.request返回一个http.ClientRequest的实例。
    options常用的参数有host、port(默认为80)、method(默认为GET)、path(请求的相对于根的路径,默认是“/”,其中querystring应该包含在其中,例如/search?query=byvoid)、headers(请求头内容)

      var http=require("http");
      var options={
          hostname:"cn.bing.com",
          port:8080
      }
      var req=http.request(options,function(res){
          res.setEncoding("utf-8");
          res.on("data",function(chunk){
              console.log(chunk.toString())
          });
          console.log(res.statusCode);
      });
      req.on("error",function(err){
          console.log(err.message);
      });
      req.end();
    

    发送POST请求(模拟了向慕课网发起评论的功能,headers请使用开发者工具从请求中获取,基本上是参考scott老师的代码)

      var http=require("http");
      var querystring=require("querystring");
      var postData=querystring.stringify({
          "content":"just a test",
          "mid":8837
      });
      var options={
          hostname:"www.imooc.com",
          port:80,
          path:"/course/document",
          method:"POST",
          headers:{
              "Accept":"application/json, text/javascript, */*; q=0.01",
              "Accept-Encoding":"gzip, deflate",
              "Accept-Language":"zh-CN,zh;q=0.8",
              "Connection":"keep-alive",
              "Content-Length":postData.length,
              "Content-Type":"application/x-www-form-urlencoded; charset=UTF-8",
              "Cookie":"imooc_uuid=6cc9e8d5-424a-4861-9f7d-9cbcfbe4c6ae; imooc_isnew_ct=1460873157; loginstate=1; apsid=IzZDJiMGU0OTMyNTE0ZGFhZDAzZDNhZTAyZDg2ZmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjkyOTk0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGNmNmFhMmVhMTYwNzRmMjczNjdmZWUyNDg1ZTZkMGM1BwhXVwcIV1c%3DMD; PHPSESSID=thh4bfrl1t7qre9tr56m32tbv0; Hm_lvt_f0cfcccd7b1393990c78efdeebff3968=1467635471,1467653719,1467654690,1467654957; Hm_lpvt_f0cfcccd7b1393990c78efdeebff3968=1467655022; imooc_isnew=2; cvde=577a9e57ce250-34",
              "Host":"www.imooc.com",
              "Origin":"http://www.imooc.com",
              "Referer":"http://www.imooc.com/video/8837",
              "User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2763.0 Safari/537.36",
              "X-Requested-With":"XMLHttpRequest",
          }
      }
      var req=http.request(options,function(res){
          res.on("data",function(chunk){
              console.log(chunk);
          });
          res.on("end",function(){
              console.log("### end ##");
          });
          console.log(res.statusCode);
      });
      req.on("error",function(err){
          console.log(err.message);
      })
      req.write(postData);
      req.end();
    
  • http.get(options,callback)
    这个方法是http.request方法的简化版,唯一的区别是http.get自动将请求方法设为了GET请求,同时不需要手动调用req.end(),但是需要记住的是,如果我们使用http.request方法时没有调用end方法,服务器将不会收到信息。

request

可以将requset模块想象成一个简化版的第三方类http模块,同时支持https 和重定向,戳这里区官网。下文列出几个能够让你快速上手的知识点。

安装

npm install request --save
var request = require('request');

API

  • GET

      request(url,function(error,response,body){
          if(!error && response.statusCode == 200){
              //输出返回的内容
              console.log(body);
          }
      });
    
  • POST

      var options = { 
        uri: 'https://www.googleapis.com/urlshortener/v1/url', 
        method: 'POST', 
        json: { "longUrl": "http://www.google.com/" }
      };
      request({
          url: 'http://xxx.xxx.com',
          method: 'POST',
          body: formData
      }, function(error, response, body) {
          if (!error && response.statusCode == 200) {
              //输出返回的内容
              console.log(body);
          }
      });
    

任何响应都可以输出到文件流。

request('http://google.com/doodle.png').pipe(fs.createWriteStream('doodle.png'))

反过来,也可以将文件传给PUT或POST请求。未提供header的情况下,会检测文件后缀名,在PUT请求中设置相应的content-type。

fs.createReadStream('file.json').pipe(request.put('http://mysite.com/obj.json'))

表单

request支持 application/x-www-form-urlencodedmultipart/form-data 实现表单上传。

  • x-www-form-urlencoded:

      request.post('http://service.com/upload', {form:{key:'value'}})
      或者:
      request.post('http://service.com/upload').form({key:'value'})
    
  • multipart/form-data

      var r = request.post('http://service.com/upload')
      var form = r.form()
      form.append('my_field', 'my_value')
      form.append('my_buffer', new Buffer([1, 2, 3]))
      form.append('my_file', fs.createReadStream(path.join(__dirname, 'doodle.png'))
      form.append('remote_file', request('http://google.com/doodle.png'))
    

superagent

superagent它是一个强大并且可读性很好的轻量级ajax API,是适用于nodejs环境的一个关于HTTP方面的库。

安装

npm install superagent --save

简单使用

一个请求的初始化可以用请求对象里合适的方法来执行,然后调用end()来发送请求。

var superagent = require('superagent');
superagent
    .post('/api')
    .send({
        'key': 'value'
    })
    .set('header_key', 'header_value')
    .end(function(err, res) {
        if (err) {
            //do something
        } else {
            //do something
        }
    })

或
superagetn
    .get(''http://example.com/search'')
    .end(function(res){ });

API

请求方法的参数可以直接使用多个key/value,也可以分多次调用请求方法每次传递一对key/value或者key/value字符串

  • GET

        //接下来四种方法所形成的URL为/api?name=An&age=20&sex=male
        //第一种
        superagent
            .get(/api)
            .query({name:'liang'})
            .query({age:18})
            .query({sex:'female'})
            .end(function(res){ })
        //第二种
        superagent
            .get(/api)
            .query({name:'liang',age:18,sex:'female'})
            .end(function(res){ })
        //第三种 
        superagent
            .get(/api)
            .query('name=liang&age=18&sex=female')
            .end(function(res){ })
        //第四种
        superagent
            .get(/api)
            .query('name=liang')
            .query('age=18')
            .query('sex=female')
            .end(function(res){ })
    
  • POST

        superagent
            .post('/api')
            .set('Content-Type','application/json')
            .send('{"name":"An","age":20,"sex":"male"}')
            .end(cb)
        //等价于 下面的写法,因为json是默认的 Content-Type
        superagent
            .post('/api')
            .send({name:"An",age:20,sex:"male"})
            .end(cb)
        //等价于 ==>
        superagent
            .post('/api')
            .send({name:"An"})
            .send({age:20})
            .sex({sex:'male'})
            .end(cb)
    

    superagent的请求数据格式化是可以扩展的,不过默认支持form和json两种格式,想发送数据以application/x-www-form-urlencoded类型的话,则可以简单的调用.type()方法传递form参数就行,这里默认是json,下面的请求将会发送post name=a&age=18:

      request
        .post('/user') 
        .type('form') 
        .send({ name: 'tj' }) 
        .send({ pet: 'tobi' }) 
        .end(callback)
    

post && get

当用.send(obj)方法来发送一个post请求,并且希望传递一些查询字符串,可以调用.query()方法,比如向?format=json&dest=/login发送post请求:

request 
  .post('/') 
  .query({ format: 'json' })
  .query({ dest: '/login' }) 
  .send({ post: 'data', msg: 'hello' }) 
  .end(callback);

请求设置

  • 设置请求头:调用set()方法,参数传递一组键值对
      superagent
          .get('/api')
          .set({
              'Referer','https://www.google.com',
              'Accept','image/webp,image/*,*/*;q=0.8'
          })
           .end(function(req,res){
              //do something
          })
    
  • Response
    响应一般会提供很多有用的标识以及属性,都在response对象里,按照respone.text,解析后的response.body,头字段,一些标识的顺序来排列。
- res.text
    包含未被解析的响应数据
- res.body
    包含解析的数据,跟请求数据自动序列化一样,响应数据也会自动的解析,
    当为一个Content-Type。定义一个解析器后,就能自动解析,默认解析包
    含application/json和application/x-www-form-urlencoded,可以
    通过访问res.body来访问解析对象。
- res.header
    响应头,res.header包含解析之后的响应头数据,键值都是node处理成小
    写字母形式,比如res.header['content-length'].
- res.type & res.charset 类型和编码格式
    Content-Type响应头字段是一个特列,服务器提供res.type来访问它,
    默认res.charset是空的,如果有的话,则自动填充,例如Content-Type
    值为text/html; charset=utf8,则res.type为text/html,res.charst
    为utf8.
- res.status状态码

其他设置

  • req.abort() 终止请求

  • req.timeout(ms) 暂停请求 ms 表示毫秒为单位的时间

  • 管道数据
    nodejs客户端允许使用一个请求流来输送数据,比如请求一个文件作为输出流:

      var request = require('superagent') ,fs = require('fs');
      var stream = fs.createReadStream('path/to/my.json');
      var req = request.post('/somewhere');
      req.type('json');
      stream.pipe(req);
    

    或者输送一个响应流到文件中:

      var request = require('superagent') , fs = require('fs');
      var stream = fs.createWriteStream('path/to/my.json');
      var req = request.get('/some.json');
      req.pipe(stream);
    
  • 错误处理
    当发送错误时,superagent首先会检查回调函数的参数数量,当err参数提供的话,参数就是两个,如下:

      request 
        .post('/upload') 
        .attach('image', 'path/to/tobi.png') 
        .end(function(err, res){ });
      ``
      当省略了回调函数,或者回调只有一个参数的话,可以添加error事件的处理.
      ```js
      request 
        .post('/upload') 
        .attach('image', 'path/to/tobi.png') 
        .on('error', handle) 
        .end(function(res){ });
    

RESTful API
支持Web服务和动态Web应用程序的多层架构,实现可重用性、可扩展性、组件可响应性的清晰分离。开发人员可轻松使用AJAX和RESTful Web服务创建丰富网络应用。

单线程
Node.js单线程是指Node并没有创建线程的能力,所以代码都是单线程执行的。不过Node宿主环境并不是单线程的,它维护一个执行队列,循环检测并调度JS线程来执行,因此单线程执行和并发操作并不冲突。

事件轮询机制
Node.js 可在不断新增额外线程的情况下,依然对任务进行并发处理。它是通过事件轮询(event loop)来实现并行操作的。

非堵塞I/O
由于Node.js是事件驱动的,因此使用了事件循环来解决I/O操作带来的瓶颈。在Node.js中一个I/O操作通常会带有 一个回调函数,当I/O操作完毕并返回时,会调用此回调函数。与此同时,主线程则继续执行接下来的代码。

V8虚拟机
Node.js是一个基于Google Chrome V8 Javascript引擎之上的平台,可用于创建轻量级、快速、可扩展、事件驱动和非堵塞I/O的应用。

事件驱动
Node.js使用事件驱动模型,即当Web Server接收到请求时,将其关闭然后进行处理,然后去服务下一个Web请求。当请求完成后,被放回处理队列中。当到达队列开头时,结果被返回给客户端。

node中实现IPC通道的是管道技术(pipe)。在node中管道是个抽象层面的称呼,具体细节实现由libuv提供,在win下是命名管道(named pipe)实现,在*nix下,采用unix Domain Socket来实现。

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

推荐阅读更多精彩内容