Express

Express框架

  1. 课程介绍

Ø Express介绍(了解)

Ø Express安装及使用(掌握)

Ø Express路由(掌握)

Ø response响应对象(掌握)

Ø request请求对象(掌握)

Ø 中间件(了解)

  1. Express介绍

Express 是一个基于 Node.js 平台的极简、灵活的 web 应用开发框架,它提供一系列强大的特性,帮助你创建各种 Web 和移动设备应用。

Express 框架核心特性:

可以设置中间件来响应 HTTP 请求。

定义了路由表用于执行不同的 HTTP 请求动作。

(url = 资源) 映射

可以通过向模板传递参数来动态渲染 HTML 页面。 模板引擎

  1. Express使用

3.1. 简单使用(了解)

1、新建一个NodeJs项目文件夹

2、npm init 初始化项目配置文件(package.json 包描述文件)

package.json中

"scripts": {
   "start":"node ./index" //命令
}

npm start 执行”start”后面的代码。

3、安装express

npm install express --save

4、编写一个app.js使用express

//导入express模块
    var express = require("express");
    //创建一个express应用
    var app = express();

    //处理"/"请求
    app.get("/",function(req,res){
         //响应输出“hello world”
         res.send("Hello world");
    });

    //启动服务器监听3000端口
    app.listen(3000,function(){
          console.log("express app 启动成功。。。");
    });

5、启动服务器

node app.js

6、浏览器访问

image.png

3.2. Express-generator(重点)

为了快速的创建express项目,express团队为使用者提供了项目快速生成工具,express-generator

为了快速的创建express项目,express团队为使用者提供了项目快速生成工具,express-generator。

1、安装express-generator

npm i express-generator -g  //全局安装

2、新建一个目录(或者找一个空目录)

F:\webproject (目录可以新建在任何位置,但最好不要中文路径)

3、通过命令创建express项目

express -e projecname  (express代表在当前目录下面建立express项目 -e代表使用ejs模版引擎)

express webapp (代表在当前目录下面,新建一个webapp文件夹,然后在建立express项目)

image.png

项目结构:

bin : 执行文件,也是express项目启动文件。

public:公共的资源,浏览器可以直接访问的资源。(图片,js,css)

views:服务器端模块文件。

routes:路由处理器,处理浏览器发出不同url的处理程序。

/login   function(){

//登录处理程序

}

app.js express应用的主文件,该文件主要用于整合其他第三方模块和配置express的系统参数。

4、安装依赖包

通过package.json

"dependencies": {
  "body-parser": "~1.15.1",
  "cookie-parser": "~1.4.3",
  "debug": "~2.2.0",
  "ejs": "~2.4.1",
  "express": "~4.13.4",
  "morgan": "~1.7.0",
  "serve-favicon": "~2.3.0"
}

npm i

image.png

5、启动express

node app 需要设置监听端口

npm start

node ./bin/www

image.png

补充:nodemon:https://stackoverflow.com/questions/49001369/nodemonexpressjs-app-not-starting-clean-exit-waiting-for-changes-before-resta
image.png

6、浏览器访问

image.png

3.3. Express服务器项目结构说明

     bin: 执行文件,也是express项目启动文件。

    public: 公共的资源(nodejs不做处理),浏览器可以直接访问的资源,相当于静态网页的根目录,访问时不需加路径。(图片,js,css)
    http://localhost:3000/test.html
    http://localhost:3000/images/img.jpg

    views: 服务器端模块或模板文件。

    routes: 路由处理器,处理浏览器发出不同url的处理程序。动态网页的目录

    ----------------------------------------------------------------------------------
    app.js 主模块文件,是总路由,分支路由写在routes目录下
    package.json 包管理,依赖包

    //引入系统和第三方模块
    var express = require('express');
    var path = require('path');
    var favicon = require('serve-favicon');
    var logger = require('morgan');
    var cookieParser = require('cookie-parser');
    var bodyParser = require('body-parser');

    //引入路由
    var index = require('./routes/index');
    var users = require('./routes/users');
    var vipCenter=require("./routes/vip");

    //实例化express框架
    var app = express();

    // view engine setup
    //设置模板的默认目录
    app.set('views', path.join(__dirname, 'views'));
    //设置ejs为模板引擎
    app.set('view engine', 'ejs');

    // uncomment after placing your favicon in /public
    //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
    //中间件
    app.use(logger('dev'));
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(cookieParser());
    //设置静态目录为public
    app.use(express.static(path.join(__dirname, 'public')));

    //使用分路由
    app.use('/', index);
    app.use('/users', users);
    app.use("/vip",vipCenter);

    // catch 404 and forward to error handler
    //404找不到
    app.use(function(req, res, next) {
      var err = new Error('Not Found');
      err.status = 404;
      next(err);
    });

    // error handler
    //错误
    app.use(function(err, req, res, next) {
      // set locals, only providing error in development
      res.locals.message = err.message;
      res.locals.error = req.app.get('env') === 'development' ? err : {};

      // render the error page
      res.status(err.status || 500);
      res.render('error');
    });

    //导出模块
module.exports = app;
  1. Express路由(重点)

路由是指如何定义应用的端点(URIs)以及如何响应客户端的请求。

路由是由一个 URI、HTTP 请求(GET、POST等)和若干个句柄(函数)组成,它的结构如下: app.METHOD(path, [callback...], callback), app 是 express 对象的一个实例, METHOD 是一个 HTTP 请求方法, path 是服务器上的路径, callback 是当路由匹配时要执行的函数。

4.1. 基础用法

var express = require('express');
var app = express();

//设置请求路径“/”对应的处理器
app.get('/', function(req, res) {
  res.send('hello world');
});

4.2. 路由方法

路由与HTTP 请求方法(GET、POST)相关联。

为应用“/”路径定义的 GET 和 POST 请求:
// 处理get请求方式,超链接、浏览器地址栏直接访问
app.get('/', function (req, res) {
  res.send('处理get请求');
});

// 处理post请求方式,表单提交
app.post('/', function (req, res) {
  res.send('处理post请求');
});

app.all() 是一个特殊的路由方法,没有任何 HTTP 方法与其对应,它的作用是对于一个路径上的所有请求加载中间件。 all相当于既可以处理GET,也可以处理POST。

app.all('/, function (req, res, next) {
  res.send('任意方式的请求');
});

4.3. Router(重点中的重点)

express.Router 类可以创建模块化(独立的)、可挂载的路由对象。Router 对象是一个完整的中间件和路由系统,因此常称其为一个 “mini-app”。

1、新建一个模块vip.js (express项目要求我们放到routes)

var express = require('express');
var router = express.Router();

// 定义模块的主页的路由
router.get('/', function(req, res) {
  res.send('vip首页');
});
// 定义模块“/getScore”路径的路由
router.get('/getScore', function(req, res) {
  res.send('vip积分');
});
module.exports = router;

2、app.js 使用

var vip = require('./vip);

...

app.use('/vip', vip);  // 路径“/vip”使用vip路由模块,这个行为就是把“vip”模块挂载到“/vip”路径下面。

访问

[http://localhost:3000/vip](http://localhost:3000/vip)/           //vip首页

[http://localhost:3000/vip](http://localhost:3000/vip)/getScore    //vip积分
  1. 响应对象(重点)

响应对象(res)的方法向客户端返回响应,终结请求响应的循环。如果在路由函数中一个方法也不调用,来自客户端的请求会一直挂起。

5.1. send方法(重点中的重点)

send(data) 可以返回任意类型数据。

res.send(new Buffer('whoop'));//流
res.send({ some: 'json' });// json数据
res.send('<p>some html</p>');//普通文本

//设置状态码,并且返回内容
res.status(404).send('Sorry, we cannot find that!');
res.status(500).send({ error: 'something blew up' });

5.2.json方法
json(data) 返回json对象,一般针对ajax应用。

res.json(null);
res.json({ user: 'tobi' });

//设置状态码,并返回json数据
res.status(500).json({ error: 'message' });

5.3. jsonp方法

jsonp(data) 返回json对象,一般针对ajax的跨域访问。

res.jsonp(null);
res.jsonp({ user: 'tobi' });

//设置状态码,并返回json数据
res.status(500).jsonp({ error: 'message' });

5.4. render视图模板

ejs模板的使用

index.ejs模板

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <ul id="mainNav">
       <li><a href="/">首页</a></li>
       <li><a href="/news">新闻中心</a></li>
       <li><a href="/vip">vip会员中心</a></li>
       <li><a href="/users">用户注册</a></li>
    </ul>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>
  </body>
</html>

index.js 路由

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });  //将视图和数据合并后发送给客户端

});

module.exports = router;

5.5. download下载

//下载当前目录下面的xxx.doc文件,并且重命名为yyy.doc。
router.get('/down', function(req, res, next) {
  res.download("./downTest.doc","express使用说明.doc")
});

5.6. redirect重定向

重定向到从指定的URL路径(浏览器地址变为设置的地址)

 router.get('/it', function(req, res, next) {
   res.redirect("http://www.baidu.cn");
});

5.7. 404报错页面制作

image.png
router.get('/err', function(req, res, next) {
  //res.status(404).send("出错了:文件没有找到!");
  res.status(404).render("error",{message:"很抱歉,您查看的宝贝不存在,可能已下架或者被转移。"});
});

error.ejs
<h1><%= message %></h1>
<p><img src="./images/err.png" /></p>

5.8. 完整api

  1. res.app:同req.app一样

  2. res.append():追加指定HTTP头

  3. res.set()在res.append()后将重置之前设置的头

  4. res.cookie(name,value [,option]):设置Cookie

opition: domain / expires / httpOnly / maxAge / path / secure / signed

  1. res.clearCookie():清除Cookie
npm install cookie
npm install cookie
var cookie=require("cookie");
res.cookie("username",username);  // 设置cookie
req.cookies.名称                // 取值
res.clearCookie(‘名称’);       // 清除指定名称的Cookie
手动清除cookie,设置》高级》清除浏览数据
router.get("/checkLogin",function(req,res,next){
    var username=req.cookies.username;
    if(username) {
        res.send(true);
    }
    else{
        res.send(false);
    }
});
image.png
  1. res.download():传送指定路径的文件

  2. res.get():返回指定的HTTP头

  3. res.json():传送JSON响应

  4. res.jsonp():传送JSONP响应

  5. res.location():只设置响应的Location HTTP头,不设置状态码或者close response

  6. res.redirect():设置响应的Location HTTP头,并且设置状态码302

  7. res.send():传送HTTP响应

  8. res.sendFile(path [,options] [,fn]):传送指定路径的文件 -会自动根据文件extension设定Content-Type

  9. res.set():设置HTTP头,传入object可以一次设置多个头

  10. res.status():设置HTTP状态码

  11. res.type():设置Content-Type的MIME类型

  12. 请求对象(重点)

req(request)对象包含了数一次请求中的所有据(http头信息、请求参数...)

6.1. 获取浏览器地址栏中的参数(重点中的重点)

语法: req.query.参数名;

比如:http://localhost:3000/user?name=007

      req.query.name;

搜索功能

search.html?keywords=笔记本电脑&catetype=it数码

router.get('/search.html', function(req, res, next) {
  var keywords=req.query.keywords;
  var catetype=req.query.catetype;
  res.json({"关键词":keywords,"类别":catetype});
});

6.2. 获取表单提交的值(重点中的重点)

Post提交 req.body.参数名

Get提交 req.query.参数名;

login.html   public静态文件
    <form action="/loginPost" method="post">
        <p>用户账号 <input type="text" name="username" /></p>
        <p>登录密码 <input type="password" name="pwd" /></p>
        <p><input type="submit" value="Post登录" /> </p>
    </form>
    <hr/>
    <form action="/loginGet" method="get">
        <p>用户账号 <input type="text" name="username" /></p>
        <p>登录密码 <input type="password" name="pwd" /></p>
        <p><input type="submit" value="Get登录" /> </p>
    </form>
   路由文件
router.get('/loginGet', function(req, res, next) {
  var username=req.query.username;
  var pwd=req.query.pwd;
  res.json({"账号":username,"密码":pwd});
});

router.post('/loginPost', function(req, res, next) {
  var username=req.body.username;
  var pwd=req.body.pwd;
  res.json({"账号":username,"密码":pwd});
});

6.3. 获取路由中的参数parameters

京东的产品地址:https://item.jd.com/5268701.html

  /product/9999

  router.get("/product/:id",function(req,res){
    var productID=req.params.id;
    res.send("产品的编号是:"+productID);
});

parameter [pəˈræmɪtɚ] params [pəˈræms]

伪静态: 看起来是一个静态文件,但其实是动态的。好处可以方便搜索引擎收录

6.4. 获取ip地址

router.get('/home', function(req, res, next) {
  res.send("<h1>我是首页homepage!!!</h1><p>你的ip地址是:"+req.hostname+"_"+req.ip+"</p>");
});

6.5. 完整api

  1. req.app:当callback为外部文件时,用req.app访问express的实例

  2. req.baseUrl:获取路由当前安装的URL路径

  3. req.body / req.cookies:获得「请求主体」/ Cookies

  4. req.fresh / req.stale:判断请求是否还「新鲜」

  5. req.hostname / req.ip:获取主机名和IP地址

  6. req.originalUrl:获取原始请求URL

  7. req.params:获取路由的parameters

  8. req.path:获取请求路径

  9. req.protocol:获取协议类型

  10. req.query:获取URL的查询参数串

  11. req.route:获取当前匹配的路由

  12. req.subdomains:获取子域名

  13. req.accpets():检查请求的Accept头的请求类型

  14. req.acceptsCharsets / req.acceptsEncodings / req.acceptsLanguages

  15. req.get():获取指定的HTTP请求头

  16. req.is():判断请求头Content-Type的MIME类型

  1. 中间件(了解)

Express 是一个自身功能极简,完全是由路由中间件构成一个的 web 开发框架:从本质上来说,一个 Express 应用就是在调用各种中间件。

image.png

7.1. 中间件到底是什么

中间件(Middleware)本质就是一个函数,它可以访问请求对象(request object), 响应对象(response object), 和 web 应用中处于请求-响应循环流程中的中间件,一般被命名为 next 的变量。(next 尾函数,执行下一个任务)

中间件的功能包括:

  执行任何代码。

  修改请求和响应对象。

  终结请求-响应循环。

  调用堆栈中的下一个中间件。

如果当前中间件没有终结请求-响应循环,则必须调用 next() 方法将控制权交给下一个中间件,否则请求就会挂起。

7.2. 应用级中间件

应用级中间件绑定到 app 对象 使用 app.use() 和 app.METHOD(), 其中, METHOD 是需要处理的 HTTP 请求的方法,例如 GET, PUT, POST 等等,全部小写。

//最简单的中间件

app.js
var express = require('express');
var app = express();
…………………………. ………………………….

/*
 * 中间件:
 *  1. 中间件是一个函数
 *  2. 中间件可以访问请求对象和响应对象
 *  3. 可以阻止请求继续执行,如果不阻止,可以调用尾函数 next()
 *
 * 尾函数next:
 *  1. 在一个中间件中执行尾函数,就可以调用下一个中间件
 *  2. 如果不用调用尾函数,就阻止执行
 *  3. 在尾函数后面的代码会执行,并且是在尾函数调起的下一个中间件结束后才执行
 */

 app.use(function(req,res,next){
 console.log('111');
 next();
 console.log('222');
 });

 app.use(function(req,res,next){
 console.log('333');
 next();
 console.log('444');
 });

…………………………. ………………………….
module.exports = app;

7.3. 内置中间件

Express中只为我们提供了唯一一个中间件,其他的中间件需要安装。


image.png

image.png

下面的例子使用了 express.static中间件,其中的 options 对象经过了精心的设计。

var options = {
  dotfiles: 'ignore',
  etag: false,
  extensions: ['htm', 'html'],
  index: false,
  maxAge: '1d',
  redirect: false,
  setHeaders: function (res, path, stat) {
    res.set('x-timestamp', Date.now());
  }}

app.use(express.static('public', options));
每个应用可有多个静态目录。
app.use(express.static('public'));
app.use(express.static('uploads'));
app.use(express.static('files'));

app.use(logger('dev'));  //控制台日志显示的中间件
app.use(express.static(path.join(__dirname, 'public')));   //静态资源目录的中间件

7.4. 第三方中间件

通过使用第三方中间件从而为 Express 应用增加更多功能。

安装所需功能的 node 模块,并在应用中加载,可以在应用级加载,也可以在路由级加载。

Multer 翻译文档https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md

文件上传中间件的使用

fileUpload.html 静态页面

fileUpload.html 静态页面
    <form action="/upload" method="post" enctype="multipart/form-data">
        <h2>图片上传</h2>
        <input type="file" name="imgUpload">
        <input type="submit" value="上传">
    </form>

index.js 路由

/*
 * npm i multer --save
 *
 前端准备工作:
 1、需要一个表单,表单里面必须有一个文件域
 2、必须给form表单指定enctype="multipart/form-data" 属性。
 3、提交按钮类型为submit。

 后端:接收请求
 1:前端请求表单页面http://127.0.0.1/upload/
 2:渲染模板,不需加载额外的数据。

 教程:
 http://blog.csdn.net/CatieCarter/article/details/77841208
 https://github.com/expressjs/multer
 */

//引入文件模块
var fs = require("fs");

//引入上传中间件模块
var multer  = require('multer');

//初始化上传目录,自定义本地保存的路径
//var upload = multer({ dest: './files/' }); //使用storage时不需要单独制定目录,storage中有目录设置
var uploadFolder='./public/files/'; //放入静态资源目录才能正常显示

// 通过storage的 filename 属性定制上传文件名称
var storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, uploadFolder);    // 保存的路径,备注:需要自己创建如果不存在会报错
  },
  filename: function (req, file, cb) {
    //将保存文件名设置为 前缀+时间戳+文件扩展名
    var extName=file.originalname.substring(file.originalname.lastIndexOf(".")); //.jpg
    cb(null, file.fieldname + '_' + new Date().getTime() + extName);
  }
});

// 通过 storage 选项来对 上传行为 进行定制化
var upload = multer({ storage: storage });

//文件上传的路由,upload.single("imgUpload")指定单个文件上传,上传框的名称为imgUpload
router.post('/upload',upload.single("imgUpload"), function(req, res, next) {
  var fileInfo = req.file; //multer会将文件的信息写到 req.file上
  console.log('文件类型:', fileInfo.mimetype);
  console.log('原始文件名:', fileInfo.originalname);
  console.log('文件大小:', fileInfo.size);
  console.log('文件保存路径:', fileInfo.path);

  //渲染图片显示的模板,直接获取文件存放的地址,显示时不需要public目录
  var filepath=fileInfo.path.toString().replace("public","");
  res.render("imgFileList.ejs",{imgShow: filepath});

  //直接显示出来
  res.set({"Content-Type":"text/html"});
  res.send("<img src='"+fileInfo.path.toString().replace("public","")+"' />");
});

imgFileList.ejs  图片显示模板
  <body>
    <h1>图片上传展示</h1>
    <p><img src="<%= imgShow%>" /></p>
  </body>
image.png
  1. 课程总结

8.1. 重点

  1. 安装 express

  2. express的路由编写

  3. 请求对象

  4. 响应对象

8.2. 难点

  1. 路由编写

  2. 中间件

8.3. 如何掌握?

  1. 此技能通过使用升级。

  2. 将常见的用法截图保存到文件夹中,时常回顾。

8.4. 排错技巧(技巧)

  1. console.log()方法。

  2. 作业

作业难度:☆☆☆

1、在自己的电脑上安装exress框架

2、如何获取GET和POST方式传值,分别使用什么方法。

3、编写路由实现一个简易的企业网站基本结构,根据不同链接路径响应不同的内容。

   首页 | 新闻中心 | 产品展示 | 客户留言 | 关于我们 | 联系我们 | 官方商城
  1. 面试题
  1. 网络服务器的工作原理
  1. 扩展知识或课外阅读推荐(可选)

11.1. 扩展知识

使用 express-generator 创建的基本框架中各个目录的作用,以及静态目录

【补充】 cookie

由于HTTP是无状态协议,无法识别两次请求之间的关系,为了识别用户身份使用cookie技术。cookie技术是一向由服务器端设置数据,存储在客户端浏览器缓存中的一项技术。

只要服务器向浏览器设置了cookie,每一次浏览器发起请求时,都会自动携带这些cookie数据去访问服务器。服务器可以接收到数据并识别用户身份。

//设置新的cookie
    res.cookie('名称','值');
    
    //修改cookie
    res.cookie('名称','新的值');
    
    //删除cookie
    res.clearCookie('名称');
    
    //获取查询使用cookie
    req.cookies.名称;

req.cookies.名称;

npm i --save nodemon
nodemon appname

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

推荐阅读更多精彩内容