☆页面加载多图优化

0 前言

项目中某个页面因业务要求会有很多图片,加载速度不给力,用户体验很差。在处理这个问题的过程中,在网上参考了很多大佬的博客,收获很多,在这里总结一下,分享给大家。


1 业务场景

首先是页面的截图,页面中可能会有上百张的“小图片”平铺展示在页面中,点击每张图片都可查看大图。

页面截图

点击查看大图

但实际查看页面代码时,发现无论“小图片”和大图片,图片地址均为文件服务器的图片地址,页面中显示的“小图片”,仅仅是将原图的尺寸缩小。也就是说页面即使不点击查看大图,加载页面时也需要从文件服务器加载上百张大图。

<li v-for="item in imgs">
    <img v-bind:src="item.qryUrl" v-bind:alt="item.name" width="80" height="50" />
</li>

于是有了以下的优化:


2 图片压缩

页面是由“小图”平铺来的,却需要加载大量原图,得不偿失。于是很自然的会想到,将“小图”变为真正的小图,当实际点击大图时再去请求原图,这样便会大大减少页面加载时间。

a. 图片异源加载
HTML代码img标签中将真实图片地址写在 data-original 属性中,而 src 属性中的图片换成占位符的图片(压缩图)

<!--
添加 width 和 height 属性有助于在图片未加载时占满所需要的空间
-->
<img class="lazy" src="grey.gif" data-original="example.jpg" width="640" heigh="480">

b. Java后台图片压缩

  • 利用Java原生的imageIO类进行裁剪
/**
     * 缩放图像(按比例缩放)
     *
     * @param src    源图像
     * @param output 输出流
     * @param scale  缩放比例
     * @param flag   缩放选择:true 放大; false 缩小;
     */
    public static final void zoomScale(BufferedImage src, OutputStream output, String type, double scale, boolean flag) {
        try {
            // 得到源图宽
            int width = src.getWidth();
            // 得到源图长
            int height = src.getHeight();
            if (flag) {
                // 放大
                width = Long.valueOf(Math.round(width * scale)).intValue();
                height = Long.valueOf(Math.round(height * scale)).intValue();
            } else {
                // 缩小
                width = Long.valueOf(Math.round(width / scale)).intValue();
                height = Long.valueOf(Math.round(height / scale)).intValue();
            }
            Image image = src.getScaledInstance(width, height, Image.SCALE_DEFAULT);
            BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics g = tag.getGraphics();
            // 绘制缩小后的图
            g.drawImage(image, 0, 0, null);
            g.dispose();
            // 输出为文件
            ImageIO.write(tag, type, output);
        } catch (IOException e) {
            throw new KitException(e);
        }
    }

使用原生imageIO类进行压缩图片,速度较快,但仅能对图片尺寸进行压缩,但不能压缩图片质量。


  • 借助一些三方插件,如使用google开源工具Thumbnailator实现图片压缩

Thumbnailator是一个用java生成高质量缩略图的第三方库,可以用来:

1.生成缩率图;2.添加水印;3.图片旋转;4.图片大小缩放;5.图片压缩;

Thumbnailator库只有一个jar,不依赖第三方库,maven依赖

<dependency>
  <groupId>net.coobird</groupId>
  <artifactId>thumbnailator</artifactId>
  <version>0.4.8</version>
</dependency>

Thumbnailator接口链式风格写法,使用简单

Thumbnails.of("原图文件的路径").scale(1f).outputQuality(0.5f).toFile("压缩后文件的路径");

  • 前端JS实现图片压缩
    HTML5 file API加canvas实现图片JS压缩
/**
 * 图片压缩,默认同比例压缩
 * @param {Object} path 
 *   图片路径
 * @param {Object} obj
 *   obj 对象 有 width, height, quality(0-1)   不传width和 height,图片大小不变只改变像素值
 * @param {Object} callback
 *   回调函数有一个参数,base64的字符串数据
*/
function dealImage(path, obj, callback){

        var img = new Image();
        img.src = path;
        img.onload = function(){
            var that = this;
            // 默认按比例压缩
            var w = that.width,
            h = that.height,
            scale = w / h;
            w = obj.width || w;
            h = obj.height || (w / scale);
            var quality = 0.3;  // 默认图片质量为0.7
            //生成canvas
            var canvas = document.createElement('canvas');
            var ctx = canvas.getContext('2d');
            // 创建属性节点
            var anw = document.createAttribute("width");
            anw.nodeValue = w;
            var anh = document.createAttribute("height");
            anh.nodeValue = h;
            canvas.setAttributeNode(anw);
            canvas.setAttributeNode(anh); 
            ctx.drawImage(that, 0, 0, w, h);
            // 图像质量
            if(obj.quality && obj.quality <= 1 && obj.quality > 0){
                quality = obj.quality;
            }
            // quality值越小,所绘制出的图像越模糊
            var base64 = canvas.toDataURL('image/jpeg', quality );
            // 回调函数返回base64的值
            callback(base64);
        }
    }       

TIPS:上述压缩图片的方式,存在资源争抢时效率会大大降低。压缩图片均需要读原图然后进行压缩生成图片流或文件,如果原图本身较大、数量较多且有多个线程同时进行压缩时,每张图片压缩的时长均会成倍的增长。


3 将图片转Base64格式来节约请求

当我们的一个页面中要传入很多图片时,特别是一些比较小的图片,十几K、几K,这些小图标都会增加HTTP请求。比如要下载一些一两K大的小图标,其实请求时带上的额外信息有可能比图标的大小还要大。所以,在请求越多时,在网络传输的数据自然就越多了,传输的数据自然也就变慢了。而这里,我们采用Base64的编码方式将图片直接嵌入到网页中,而不是从外部载入,这样就减少了HTTP请求。

  • Base64编码由来

为什么会有Base64编码呢?因为有些网络传送渠道并不支持所有的字节,例如传统的邮件只支持可见字符的传送,像ASCII码的控制字符就 不能通过邮件传送。这样用途就受到了很大的限制,比如图片二进制流的每个字节不可能全部是可见字符,所以就传送不了。最好的方法就是在不改变传统协议的情 况下,做一种扩展方案来支持二进制文件的传送。把不可打印的字符也能用可打印字符来表示,问题就解决了。Base64编码应运而生,Base64就是一种 基于64个可打印字符来表示二进制数据的表示方法。

  • Base64编码索引

Base64的索引表如下,字符选用了"A-Z、a-z、0-9、+、/" 64个可打印字符。数值代表字符的索引,这个是标准Base64协议规定的,不能更改。
Base64编码索引
  • Base64编码原理

Base64的码表只有64个字符, 如果要表达64个字符的话,使用6的bit即可完全表示(2的6次方为64)。因为Base64的编码只有6个bit即可表示,而正常的字符是使用8个bit表示, 8和6的最小公倍数是24,所以4个Base64字符可以表示3个标准的ascll字符;
对以某编码方式编码后的字节数组为对象,以3个字节为一组,按顺序排列24bit数据,然后以6bit一组分成4组;再在每组的最高位补2个0凑足一个字节。这时一组就有4个字节了。若字节数组不是3的倍数,那么最后一组就填充1到2个0字节。
然后按Base64编码方式(就是映射关系)对字节数组进行解码,就会得到平时看到的Base64编码文本。对于字节数组不是3的倍数,最后一组填充1到2个0字节的情况,填补的0字节对应的是=(等号)。以下为具体的解析过程案例:

把AB这两个字符转换为Base64的过程

 ①. 对AB进行ASCII编码:得到A(65)B(66)
 ②. 转成二进制形式:得到A(01000001)B(01000010)
 ③. 以3个字节为一组,非3的倍数补0字节:010000010100001000000000
 ④. 以6bit为一组后高位补两个0:(00 010000)(00 010100)(00 001000)(00 000000)
 ⑤. 转为十进制:(16)(20)(8)(0)
 ⑥. 根据映射关系解码:QUI=

当转换到最后, 如果不足三个字符的话,我们直接在最后添加=号即可。

  • 图像和base64转换

图片的本质就是每个像素点都是一个数字,该数字表示颜色,然后把很多很多像素点的数字拼到一起,就是图像。图像转Base64,就是把图像的直方图所有数字转成Base64编码,反之,Base64也能转换回图像。

  • Data URI Scheme

data URI scheme 允许我们使用内联(inline-code)的方式在网页中包含数据,目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。常用于将图片嵌入网页。

//传统的图片HTML是这样用的,
<img src="http://gpeng.win/test.png" />
//Data URI的图片内嵌式是这样用的,
<img src="data:image/gif;base64,R0lGODlhAwADAIABAL6+vv///yH5BAEAAAEALAAAAAADAAMAAAIDjA9WADs=" />
//目前,Data URI scheme支持的类型有:
data:,                            文本数据
data:text/plain,                    文本数据
data:text/html,                  HTML代码
data:text/html;base64,            base64编码的HTML代码
data:text/css,                    CSS代码
data:text/css;base64,              base64编码的CSS代码
data:text/javascript,              Javascript代码
data:text/javascript;base64,        base64编码的Javascript代码
data:image/gif;base64,            base64编码的gif图片数据
data:image/png;base64,            base64编码的png图片数据
data:image/jpeg;base64,          base64编码的jpeg图片数据
data:image/x-icon;base64,          base64编码的icon图片数据

Data URI Scheme优缺点:

优点:
1. 减少资源请求链接数。
2. 当访问外部资源很麻烦或受限时,可以很好的利用Data URI Scheme
3. 没有跨域问题,无需考虑缓存、文件头或者cookies问题

缺点:
1. Data URL形式的图片不会被浏览器缓存,这意味着每次访问这样页面时都被下载一次,
   但可通过在css文件的background-image样式规则使用Data URI Scheme,使其随css文件一同被浏览器缓存起来)。
2. Base64编码的数据体积通常是原数据的体积4/3,也就是Data URL形式的图片会比二进制格式的图片体积大1/3。
3. 仅适用于极小或者极简单图片,不适用于大图片。
4. 移动端性能比较低。

4 图片预加载

  • 图片预加载的主要思路就是把稍后需要用到的图片悄悄的提前加载到本地,因为浏览器有缓存的原因,如果稍后用到这个url的图片了,浏览器会优先从本地缓存找该url对应的图片,如果图片没过期的话,就使用这个图片其中图片预加载也分为三种,无序加载,有序加载,基于用户行为的预加载(点击某个按钮或者滚动的时候进行加载)。
    预加载的实现很简单,其核心说到底就两句话:
var img = new Image();  
img.src = "my_image.jpg";
  • 浏览器在同一时间对同一域名下的资源的并发请求数目是有限制的,一般在2-6之间,超过限制数目的请求就会被阻塞。一些主流浏览器对 HTTP1.1 和 HTTP 1.0 的最大并发连接数目,可以参考下图:


    图片来源http://www.stevesouders.com/blog/2008/03/20/roundup-on-parallel-connections/

    图片预加载时,chrome浏览器会并发去请求6张同域名的图片:


    图片请求
  • 浏览器缓存
    在请求头Headers里,status有几种状态 :
    Status Code:200 OK
    Status Code:304
    Status Code:200 (from memory cache)
    Status Code:200 OK (from disk cache)

a. memory cache

MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。
目前Webkit资源分成两类,一类是主资源,比如HTML页面,或者下载项,一类是派生资源,比如HTML页面中内嵌的图片或者脚本链接,分别对应代码中两个类:MainResourceLoader和SubresourceLoader。虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。

b. disk cache

diskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。它与memoryCache最大的区别在于,当退出进程时,内存中的数据会被清空,而磁盘的数据不会,所以,当下次再进入该进程时,该进程仍可以从diskCache中获得数据,而memoryCache则不行。
diskCache与memoryCache相似之处就是也只能存储一些派生类资源文件。它的存储形式为一个index.dat文件,记录存储数据的url,然后再分别存储该url的response信息和content内容。Response信息最大作用就是用于判断服务器上该url的content内容是否被修改。

5 其他常见优化

a. 将图片服务和应用服务分离
对于服务器来说,图片是比较消耗系统资源的,如果将图片服务和应用服务放在同一服务器的话,应用服务器很容易会因为图片的高I/O负载而崩溃,所以当网站存在大量的图片读写操作时,建议使用图片服务器。

b. 图片懒加载
页面加载后只让文档可视区内的图片显示,其它不显示,随着用户对页面的滚动,判断其区域位置,生成img标签,让到可视区的图片加载出来。jQuery的lazyload插件便是一个可以实现图片延迟加载的插件,在用户触发某个条件之后再加载对应的图片资源,这对网页的打开速度有很大提升。
引入lazyload.js,对我们想要延迟加载的图片添加lazy样式,用”data-original” 替换图片的引用路径

<!-- 对img标签使用 -->
<img class="lazy" data-original="img/example.jpg">
<!-- 延迟加载元素的背景图 -->
<div class="lazy" data-original="img/bg.jpg">
    ...
</div>

在JS文件中调用lazyload()方法

$().ready(function(){
    //可任意选择你想延迟加载的目标元素,例如直接使用样式名lazy作为选择条件
    $("img .lazy").lazyload({
         placeholder : "img/grey.gif", //占位图
         effect: "fadeIn", // 加载效果
         threshold: 200, // 提前加载
         event: 'click',  // trigger
         container: $("#container"),  //指定容器
         failurelimit : 5 // 发生混乱时的hack手段
    })
})

c. CSS Sprites
当网站或者APP有大量小icon,要加载所有这些小icon将增加大量请求,这无疑将增加很多成本.
CSS Sprites 技术就是将这些小icon合并成一张图片,只需要加载一次,每次通过background-position来控制显示icon,这样就可以节约大量请求,节约成本.

icon合集


6 浏览器是这样干活的

a. 浏览器内核
浏览器最重要或者说核心的部分是“Rendering Engine”,可大概译为“渲染引擎”,一般习惯将之称为“浏览器内核”。负责对网页语法的解释(如标准通用标记语言下的一个应用HTML、JavaScript)并渲染(显示)网页。 所以,通常所谓的浏览器内核也就是浏览器所采用的渲染引擎,渲染引擎决定了浏览器如何显示网页的内容以及页面的格式信息。该进程下面拥有着多个线程(谷歌浏览器每个tab页都会新起一个单独的进程),靠着这些现成共同完成渲染任务。
① 图形用户界面GUI渲染线程

  • 负责渲染浏览器界面,包括解析HTML、CSS、构建DOM树、Render树、布局与绘制等
  • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行

② JS引擎线程

  • JS内核,也称JS引擎,负责处理执行javascript脚本
  • 等待任务队列的任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS引擎在运行JS程序

③ 事件触发线程

  • 听起来像JS的执行,但是其实归属于浏览器,而不是JS引擎,用来控制时间循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
  • 当JS引擎执行代码块如setTimeout时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
  • 注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

④ 定时触发器线程

  • setInterval与setTimeout所在线程
  • 定时计时器并不是由JS引擎计时的,因为如果JS引擎是单线程的,如果JS引擎处于堵塞状态,那会影响到计时的准确
  • 当计时完成被触发,事件会被添加到事件队列,等待JS引擎空闲了执行

⑤ 异步HTTP请求线程

  • 在XMLHttpRequest在连接后新启动的一个线程
  • 线程如果检测到请求的状态变更,如果设置有回调函数,该线程会把回调函数添加到事件队列,同理,等待JS引擎空闲了执行

b. 为什么JS引擎是单线程的
JavaScript作为一门客户端的脚本语言,主要的任务是处理用户的交互,而用户的交互无非就是响应DOM的增删改,使用事件队列的形式,一次事件循环只处理一个事件响应,使得脚本执行相对连续。如果JS引擎被设计为多线程的,那么DOM之间必然会存在资源竞争,那么语言的实现会变得非常臃肿,在客户端跑起来,资源的消耗和性能将会是不太乐观的,故设计为单线程的形式,并附加一些其他的线程来实现异步的形式,这样运行成本相对于使用JS多线程来说降低了很多。

c. 浏览器内核中线程之间的关系

  • GUI渲染进程与JS引擎线程互斥
    因为JS引擎可以修改DOM树,那么如果JS引擎在执行修改了DOM结构的同时,GUI线程也在渲染页面,那么这样就会导致渲染线程获取的DOM的元素信息可能与JS引擎操作DOM后的结果不一致。为了防止这种现象,GUI线程与JS线程需要设计为互斥关系,当JS引擎执行的时候,GUI线程需要被冻结,但是GUI的渲染会被保存在一个队列当中,等待JS引擎空闲的时候执行渲染。由此也可以推出,如果JS引擎正在进行CPU密集型计算,那么JS引擎将会阻塞,长时间不空闲,导致渲染进程一直不能执行渲染,页面就会看起来卡顿卡顿的,渲染不连贯,所以,要尽量避免JS执行时间过长。

  • JS引擎线程与事件触发线程、定时触发器线程、异步HTTP请求线程
    事件触发线程、定时触发器线程、异步HTTP请求线程三个线程有一个共同点,那就是使用回调函数的形式,当满足了特定的条件,这些回调函数会被执行。这些回调函数被浏览器内核理解成事件,在浏览器内核中拥有一个事件队列,这三个线程当满足了内部特定的条件,会将这些回调函数添加到事件队列中,等待JS引擎空闲执行。例如异步HTTP请求线程,线程如果检测到请求的状态变更,如果设置有回调函数,回调函数会被添加事件队列中,等待JS引擎空闲了执行。但是,JS引擎对事件队列(宏任务)与JS引擎内的任务(微任务)执行存在着先后循序,当每执行完一个事件队列的时间,JS引擎会检测内部是否有未执行的任务,如果有,将会优先执行(微任务)。

d. 页面渲染流程

  1. 用户输入网址(假设是个html页面,并且是第一次访问),浏览器向服务器发出请求,
    服务器返回html文件;
  2. 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文件;
  3. 浏览器又发出CSS文件的请求,服务器返回这个CSS文件;
  4. 浏览器继续载入html中<body>部分的代码,并且CSS文件已经拿到手了,可以开始渲染页面了;
  5. 浏览器在代码中发现一个<img>标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码;
  6. 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;
  7. 浏览器发现了一个包含一行Javascript代码的<script>标签,赶快运行它;
  8. Javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个(style.display=”none”)。杯具啊,突然就少了这么一个元素,浏览器不得不重新渲染这部分代码;
  9. 终于等到了</html>的到来,浏览器泪流满面……


    渲染流程

概念:

  • DOM Tree: 浏览器将HTML解析成树形的数据结构。

  • CSS Rule Tree:浏览器将CSS解析成树形的数据结构。

  • Render Tree:DOM树和CSS规则树合并后生产Render树。

  • layout:有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系,从而去计算出每个节点在屏幕中的位置。

  • painting: 按照算出来的规则,通过显卡,把内容画到屏幕上。

  • reflow(回流):当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染,内行称这个回退的过程叫 reflow。reflow 会从 这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显 示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲 染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。

  • repaint(重绘):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。


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

推荐阅读更多精彩内容