JS高程:读书摘要(八)DOM扩展

虽然DOM为与XMLHTML文档交互制定了一系列核心API,但仍然有几个规范对标准的DOM进行了扩展。这些扩展中有很多原来是浏览器专有的,但后来成为了事实标准,于是其他浏览器也都提供了相同的实现。

一、选择符API

众多JavaScript库中最常用的一项功能,就是根据CSS选择符选择与某个模式匹配的DOM元素。jQuery(www.jquery.com的核心就是通过CSS选择符查询DOM文档取得元素的引用,从而抛开了getElementById()getElementsByTagName()

所有实现这一功能的JavaScript库都会写一个基础的CSS解析器,然后再使用已有的DOM方法查询文档并找到匹配的节点。而把这个功能变成原生API之后,解析和树查询操作可以在浏览器内部通过编译后的代码来完成,极大地改善了性能。

querySelector()方法

querySelector()方法接收一个CSS选择符(字符串),返回与该模式匹配的第一个元素,如果没有找到匹配的元素,返回null

//取得body 元素
var body = document.querySelector("body");
//取得ID 为"myDiv"的元素
var myDiv = document.querySelector("#myDiv");
//取得类为"selected"的第一个元素
var selected = document.querySelector(".selected");
//取得类为"button"的第一个图像元素
var img = document.body.querySelector("img.button");

通过Document类型调用querySelector()方法时,会在文档元素的范围内查找匹配的元素。而通过Element类型调用querySelector()方法时,只会在该元素后代元素的范围内查找匹配的元素。如果传入了不被支持的选择符,querySelector()会抛出错误。

querySelectorAll()方法

querySelectorAll()方法接收的参数与querySelector()方法一样,都是一个CSS选择符,但返回的是所有匹配的元素而不仅仅是一个元素。返回的值实际上是带有所有属性和方法的NodeList,而其底层实现则类似于一组元素的快照,而非不断对文档进行搜索的动态查询,不是动态的。这样实现可以避免使用NodeList对象通常会引起的大多数性能问题。如果没有找到匹配的元素,NodeList就是空的。如果传入了不被支持的选择符,querySelectorAll()会抛出错误。

要取得返回的NodeList中的每一个元素,可以使用item()方法,也可以使用方括号语法。NodeList[i]或者NodeList.item(i)

二、元素遍历

对于元素间的空格,IE9 及之前版本不会返回文本节点,而其他所有浏览器都会返回文本节点。这样,就导致了在使用childNodesfirstChild等属性时的行为不一致。为了弥补这一差异,而同时又保持DOM规范不变,因此新定义了一组属性,从而可以更方便地查找DOM元素了。

  • childElementCount:返回子元素(不包括文本节点和注释)的个数。
  • firstElementChild:指向第一个子元素;firstChild 的元素版。
  • lastElementChild:指向最后一个子元素;lastChild 的元素版。
  • previousElementSibling:指向前一个同辈元素;previousSibling 的元素版。
  • nextElementSibling:指向后一个同辈元素;nextSibling 的元素版。

三、HTML5与DOM 节点相关部分

与类相关的扩充

1、getElementsByClassName()方法

该方法接收一个参数,即一个包含一或多个类名的字符串,返回带有指定类的所有元素的NodeList。传入多个类名时,类名的先后顺序不重要。

//取得所有类中包含"username"和"current"的元素,类名的先后顺序无所谓
var allCurrentUsernames = document.getElementsByClassName("username current");
//取得ID 为"myDiv"的元素中带有类名"selected"的所有元素
var selected = document.getElementById("myDiv").getElementsByClassName("selected");

document对象上调用getElementsByClassName()始终会返回与类名匹配的所有元素,在元素上调用该方法就只会返回后代元素中匹配的元素。因为返回的对象是NodeList,所以使用这个方法与使用getElementsByTagName()以及其他返回NodeListDOM方法都具有同样的性能问题。

2、classList 属性

在操作类名时,需要通过className属性添加、删除和替换类名。因为className中是一个字符串,所以即使只修改字符串一部分,也必须每次都设置整个字符串的值。

因此,HTML5为所有元素添加了classList属性。这个classList属性是新集合类型DOMTokenList的实例。与其他DOM集合类似,DOMTokenList有一个表示己包含多少元素的length属性,而要取得每个元素可以使用item()方法,也可以使用方括号语法。此外,这个新类型还定义如下方法。

  • add(value):将给定的字符串值添加到列表中。如果值已经存在,就不添加了。
  • contains(value):表示列表中是否存在给定的值,如果存在则返回true,否则返回false
  • remove(value):从列表中删除给定的字符串。
  • toggle(value):如果列表中已经存在给定的值,删除它;如果列表中没有给定的值,添加它。
//删除"disabled"类
oDiv.classList.remove("disabled");
//添加"current"类
oDiv.classList.add("current");
//切换"user"类
oDiv.classList.toggle("user");
//确定元素中是否包含既定的类名
if (oDiv.classList.contains("bd") && !oDiv.classList.contains("disabled")){
    //执行操作
)
//迭代类名
for (var i=0, len=div.classList.length; i < len; i++){
    doSomething(div.classList[i]);
}
焦点管理

HTML5也添加了辅助管理DOM焦点的功能。首先就是document.activeElement属性,这个属性始终会引用DOM中当前获得了焦点的元素。元素获得焦点的方式有页面加载、用户输入(通常是通过按Tab键)和在代码中调用focus()方法。

var button = document.getElementById("myButton");
button.focus();
if(document.activeElement === button){
  // 当前获取的焦点是button时 需要做的操作
}

默认情况下,文档刚刚加载完成时,document.activeElement中保存的是document.body元素的引用。文档加载期间,document.activeElement的值为null

另外就是新增了document.hasFocus()方法,这个方法用于确定文档(页面)是否获得了焦点。

var button = document.getElementById("myButton");
button.focus();
alert(document.hasFocus()); //true

通过检测文档是否获得了焦点,可以知道用户是不是正在与页面交互。

HTMLDocument的变化

1、readyState属性

DocumentreadyState属性有两个可能的值:

  • "loading",正在加载文档;
  • "complete",已经加载完文档。

使用document.readyState的最恰当方式,就是通过它来实现一个指示文档已经加载完成的指示器。在这个属性得到广泛支持之前,要实现这样一个指示器,必须借助onload事件处理程序设置一个标签,表明文档已经加载完毕。document.readyState 属性的基本用法如下。

if (document.readyState == "complete"){
    //执行操作
}

2、兼容模式

自从IE6 开始区分渲染页面的模式是标准的还是混杂的,检测页面的兼容模式就成为浏览器的必要功能。IE 为此给document添加了一个名为compatMode的属性,这个属性就是为了告诉开发人员浏览器采用了哪种渲染模式。在标准模式下,document.compatMode 的值等于"CSS1Compat",而在混杂模式下,document.compatMode的值等于"BackCompat"

3、head 属性
作为对document.body引用文档的<body>元素的补充,HTML5新增了document.head属性,引用文档的<head>元素。要引用文档的<head>元素,可以结合使用这个属性和另一种后备方法。

var head = document.head || document.getElementsByTagName("head")[0];
// 实现document.head 属性的浏览器包括Chrome 和Safari 5。
字符集属性

HTML5 新增了几个与文档字符集有关的属性。其中,charset属性表示文档中实际使用的字符集,也可以用来指定新字符集。默认情况下,这个属性的值为"UTF-16",但可以通过<meta>元素、响应头部或直接设置charset属性修改这个值。

alert(document.charset); //"UTF-16"
document.charset = "UTF-8";
自定义数据属性

HTML5规定可以为元素添加非标准的属性,但要添加前缀data-,目的是为元素提供与渲染无关的信息,或者提供语义信息。这些属性可以任意添加、随便命名,只要以data-开头即可。

添加了自定义属性之后,可以通过元素的dataset属性来访问自定义属性的值。dataset属性的值是DOMStringMap的一个实例,也就是一个名值对儿的映射。在这个映射中,每个data-name形式的属性都会有一个对应的属性,只不过属性名没有data-前缀(比如,自定义属性是data-myname,那映射中对应的属性就是myname)。

HTML
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>

JS
var div = document.getElementById("myDiv");
//取得自定义属性的值
var appId = div.dataset.appId;
var myName = div.dataset.myname;
//设置值
div.dataset.appId = 23456;
div.dataset.myname = "Michael";
//有没有"myname"值呢?
if (div.dataset.myname){
    alert("Hello, " + div.dataset.myname);
}
插入标记

1.、innerHTML 属性

在读模式下,innerHTML 属性返回与调用元素的所有子节点(包括元素、注释和文本节点)对应的HTML 标记。在写模式下,innerHTML 会根据指定的值创建新的DOM树,然后用这个DOM树完全替换调用元素原先的所有子节点。。如果设置的值仅是文本而没有HTML标签,那么结果就是设置纯文本,设置innerHTML 之后,可以像访问文档中的其他节点一样访问新创建的节点。

使用innerHTML属性也有一些限制。比如,在大多数浏览器中,通过innerHTML插入<script>元素并不会执行其中的脚本。IE8及更早版本是唯一能在这种情况下执行脚本的浏览器,但必须满足一些条件。一是必须为<script>元素指定defer属性,二是<script>元素必须位于(微软所谓的)“有作用域的元素”(scoped element)之后。<script>元素被认为是“无作用域的元素”(NoScope element),也就是在页面中看不到的元素,与<style>元素或注释类似。如果通过innerHTML插入的字符串开头就是一个“无作用域的元素”,那么IE 会在解析这个字符串前先删除该元素。换句话说,以下代码达不到目的:

div.innerHTML = "<script defer>alert('hi');<\/script>"; //无效

div.innerHTML = "_<script defer>alert('hi');<\/script>";
div.innerHTML = "<div>&nbsp;</div><script defer>alert('hi');<\/script>";
div.innerHTML = "<input type=\"hidden\"><script defer>alert('hi');<\/script>";

第一行代码会在<script>元素前插入一个文本节点。事后,为了不影响页面显示,你可能需要移除这个文本节点div.removeChild(div.firstChild);

第二行代码采用的方法类似,只不过使用的是一个包含非换行空格的<div>元素。如果仅仅插入一个空的<div>元素,还是不行;必须要包含一点儿内容,浏览器才会创建文本节点。同样,为了不影响页面布局,恐怕还得移除这个节点。

第三行代码使用的是一个隐藏的<input>域,也能达到相同的效果。不过,由于隐藏的<input>域不影响页面布局,因此这种方式在大多数情况下都是首选。

但在IE8 及更早版本中,<style>也是一个“没有作用域的元素”,因此必须像下面这样给它前置一个“有作用域的元素”:

div.innerHTML = "_<style type=\"text/css\">body {background-color: red; }</style>";
div.removeChild(div.firstChild);

2、outerHTML属性

在读模式下,outerHTML返回调用它的元素(包含调用元素本身)以及所有子节点的HTML标签。在写模式下,outerHTML会根据指定的HTML字符串创建新的DOM子树,然后用这个DOM子树完全替换调用元素。

div.outerHTML = "<p>This is a paragraph.</p>";
// p元素会完全替换div元素

等同于

var p = document.createElement("p");
p.appendChild(document.createTextNode("This is a paragraph."));
div.parentNode.replaceChild(p, div);

3、insertAdjacentHTML()方法

将指定的文本解析为HTMLXML,并将结果节点插入到DOM树中的指定位置。它不会重新解析它正在使用的元素,因此它不会破坏元素内的现有元素。这避免了额外的序列化步骤,使其比直接innerHTML操作更快。

它接收两个参数:插入位置和要插入的HTML文本。第一个参数必须是下列值之一:

  • "beforebegin",在当前元素之前插入一个紧邻的同辈元素;
  • "afterbegin",在当前元素之下插入一个新的子元素或在第一个子元素之前再插入新的子元素;
  • "beforeend",在当前元素之下插入一个新的子元素或在最后一个子元素之后再插入新的子元素;
  • "afterend",在当前元素之后插入一个紧邻的同辈元素。
//作为前一个同辈元素插入
element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>");
//作为第一个子元素插入
element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>");
//作为最后一个子元素插入
element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>");
//作为后一个同辈元素插入
element.insertAdjacentHTML("afterend", "<p>Hello world!</p>");

4、内存与性能问题

在删除带有事件处理程序或引用了其他JavaScript对象子树时,就有可能导致内存占用问题。假设某个元素有一个事件处理程序(或者引用了一个JavaScript对象作为属性),在使用前述innerHTMLouterHTML属性将该元素从文档树中删除后,元素与事件处理程序(或JavaScript对象)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现,页面占用的内存数量就会明显增加。因此,在使用innerHTMLouterHTML 属性和insertAdjacentHTML()方法时,最好先手工删除要被替换的元素的所有事件处理程序和JavaScript对象属性

不可避免地,创建和销毁HTML解析器也会带来性能损失,所以最好能够将设置innerHTMLouterHTML的次数控制在合理的范围内,所以要尽量避免下面这种操作。

for (var i=0, len=values.length; i < len; i++){
    ul.innerHTML += "<li>" + values[i] + "</li>"; //要避免这种频繁操作!!
}

可以先将单独构建字符串,之后一次把结果字符串赋值给innerHTML

var itemsHtml = "";   
for (var i=0, len=values.length; i < len; i++){
    itemsHtml += "<li>" + values[i] + "</li>";
}
ul.innerHTML = itemsHtml;

// 这个例子的效率要高得多,因为它只对innerHTML 执行了一次赋值操作。
scrollIntoView()方法

scrollIntoView()可以在所有HTML元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。如果给这个方法传入true作为参数,或者不传入任何参数,那么窗口滚动之后会让调用元素的顶部与视口顶部尽可能平齐。如果传入false作为参数,调用元素会尽可能全部出现在视口中,(可能的话,调用元素的底部会与视口顶部平齐。)不过顶部不一定平齐。

//让元素可见
document.forms[0].scrollIntoView();

四、专有扩展

虽然所有浏览器开发商都知晓坚持标准的重要性,但在发现某项功能缺失时,这些开发商都会一如既往地向DOM中添加专有扩展,以弥补功能上的不足。表面上看,这种各行其事的做法似乎不太好,但实际上专有扩展为Web开发领域提供了很多重要的功能,这些功能最终都在HTML5规范中得到了标准化。即便如此,仍然还有大量专有的DOM扩展没有成为标准,(并不是说明它们将来不会被写进标准)

1、文档模式
IE8引入了一个新的概念叫“文档模式”(document mode)。页面的文档模式决定了可以使用什么功能。换句话说,文档模式决定了你可以使用哪个级别的CSS,可以在JavaScript中使用哪些API,以及如何对待文档类型(doctype)。到了IE9,总共有以下4 种文档模式。

  • IE5:以混杂模式渲染页面(IE5 的默认模式就是混杂模式)。IE8 及更高版本中的新功能都无法使用。
  • IE7:以IE7 标准模式渲染页面。IE8 及更高版本中的新功能都无法使用。
  • IE8:以IE8 标准模式渲染页面。IE8 中的新功能都可以使用,因此可以使用Selectors API、更多CSS2 级选择符和某些CSS3 功能,还有一些HTML5 的功能。不过IE9 中的新功能无法使用。
  • IE9:以IE9 标准模式渲染页面。IE9 中的新功能都可以使用,比如ECMAScript 5、完整的CSS3以及更多HTML5 功能。这个文档模式是最高级的模式。

要强制浏览器以某种模式渲染页面,可以使用HTTP头部信息X-UA-Compatible,或通过等价的<meta>标签来设置:

比如,要想让文档模式像在IE7 中一样,可以使用下面这行代码:
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">

如果不打算考虑文档类型声明,而直接使用IE7 标准模式,那么可以使用下面这行代码:
<meta http-equiv="X-UA-Compatible" content="IE=7">

这里IE的版本(IEVersion)有以下一些不同的值。

  • Edge:始终以最新的文档模式来渲染页面。忽略文档类型声明。对于IE8,始终保持以IE8 标准模式渲染页面。对于IE9,则以IE9 标准模式渲染页面。
  • EmulateIE9:如果有文档类型声明,则以IE9 标准模式渲染页面,否则将文档模式设置为IE5。
  • EmulateIE8:如果有文档类型声明,则以IE8 标准模式渲染页面,否则将文档模式设置为IE5。
  • EmulateIE7:如果有文档类型声明,则以IE7 标准模式渲染页面,否则将文档模式设置为IE5。
  • 9:强制以IE9 标准模式渲染页面,忽略文档类型声明。
  • 8:强制以IE8 标准模式渲染页面,忽略文档类型声明。
  • 7:强制以IE7 标准模式渲染页面,忽略文档类型声明。
  • 5:强制将文档模式设置为IE5,忽略文档类型声明。

通过document.documentMode属性可以知道给定页面使用的是什么文档模式。这个属性是IE8中新增的,它会返回使用的文档模式的版本号(在IE9 中,可能返回的版本号为5、7、8、9):

var mode = document.documentMode;

知道页面采用的是什么文档模式,有助于理解页面的行为方式。无论在什么文档模式下,都可以访问这个属性。

2、contains()方法
在实际开发中,经常需要知道某个节点是不是另一个节点的后代。IE为此率先引入了contains()方法,以便不通过在DOM文档树中查找即可获得这个信息。调用contains()方法的应该是祖先节点,也就是搜索开始的节点,这个方法接收一个参数,即要检测的后代节点。如果被检测的节点是后代节点,该方法返回true;否则,返回false

alert(document.documentElement.contains(document.body)); //true

这个例子测试了<body>元素是不是<html>元素的后代,在格式正确的HTML页面中,以上代码返回true

3、插入文本

前面介绍过,IE 原来专有的插入标记的属性innerHTMLouterHTML已经被HTML5纳入规范。但另外两个插入文本的专有属性则没有这么好的运气。这两个没有被HTML5看中的属性是innerTextouterText

  • innerText属性

通过innertText属性可以操作元素中包含的所有文本内容,包括子文档树中的文本。在通过innerText读取值时,它会按照由浅入深的顺序,将子文档树中的所有文本拼接起来。在通过innerText写入值时,结果会删除元素的所有子节点,插入包含相应文本值的文本节点。

设置innerText属性移除了先前存在的所有子节点,完全改变了DOM子树。此外,设置innerText属性的同时,也对文本中存在的HTML语法字符(小于号、大于号、引号及和号)进行了编码。再看一个例子。

div.innerText = "Hello & welcome, <b>\"reader\"!</b>";

会得到以下dom树

<div id="content">Hello &amp; welcome, &lt;b&gt;&quot;reader&quot;!&lt;/b&gt;</div>

设置innerText永远只会生成当前节点的一个子文本节点,而为了确保只生成一个子文本节点,就必须要对文本进行HTML编码(因为可能字符串会包含HTML的标签,这样可能会生成子元素,所以要进行HTML编码)。利用这一点,可以通过innerText属性过滤掉HTML 标签。方法是将innerText设置为等于innerText,这样就可以去掉所有HTML标签(因为innerText属性读的时候会拿到元素中所有的文档内容并拼接起来,这样即使有HTML标签被插入了,在读的时候也会被过滤掉,然后在赋值。)

// HTML

<div class="oDiv">
    "Hello & welcome, <b>\"reader\"!</b>"
</div>

// JS

var oDiv = document.getElementsByClassName('oDiv')[0];
console.log(oDiv.innerText); // "Hello & welcome, \"reader\"!";
oDiv.innerText = oDiv.innerText; ;
  • outerText属性

在读取文本值时,outerTextinnerText的结果完全一样。但在写模式下,outerText就完全不同了:outerText不只是替换调用它的元素的子节点,而是会替换整个元素(包括子节点)。

div.outerText = "Hello world!";

等同于

var text = document.createTextNode("Hello world!");
div.parentNode.replaceChild(text, div);

由于这个属性会导致调用它的元素不存在,因此并不常用。也建议尽可能不要使用这个属性。

4、滚动

如前所述,HTML5之前的规范并没有就与页面滚动相关的API做出任何规定。但HTML5在将scrollIntoView()纳入规范之后,仍然还有其他几个专有方法可以在不同的浏览器中使用。下面列出的几个方法都是对HTMLElement类型的扩展,因此在所有元素中都可以调用。

  • scrollIntoViewIfNeeded(alignCenter):只在当前元素在视口中不可见的情况下,才滚动浏览器窗口或容器元素,最终让它可见。如果当前元素在视口中可见,这个方法什么也不做。如果将可选的alignCenter参数设置为true,则表示尽量将元素显示在视口中部(垂直方向)。Safari 和Chrome 实现了这个方法。

  • scrollByLines(lineCount):将元素的内容滚动指定的行高,lineCount值可以是正值,也可以是负值。Safari 和Chrome 实现了这个方法。

  • scrollByPages(pageCount):将元素的内容滚动指定的页面高度,具体高度由元素的高度决定。Safari 和Chrome 实现了这个方法。

希望大家要注意的是,scrollIntoView()scrollIntoViewIfNeeded()的作用对象是元素的容器,而scrollByLines()scrollByPages()影响的则是元素自身

//在当前元素不可见的时候,让它进入浏览器的视口
document.images[0].scrollIntoViewIfNeeded();

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

推荐阅读更多精彩内容