前端重新学习(7)DOM与DOM2、DOM3区别以及DOM2新特性

------------摘抄自他人笔记(http://itbilu.com/javascript/js/Vyxodm_1g.html、https://blog.csdn.net/pxy_lele/article/details/49755071)

感谢分享----------------------

目录

DOM0、DOM1、DOM2、DOM3的区别

DOM0

DOM0与DHTML

DOM1的出现

DOM2与DOM3

DOM2总结

DOM2新增模块-样式

 1. DOM样式属性和方法

2. 计算的样式

操作样式表 

 

DOM2新增模块-遍历

NodeIterator 

TreeWalker

DOM2新增模块-范围

1. 用 DOM范围实现简单选择 

2. 用 DOM范围实现复杂选择 

3. 操作 DOM范围中的内容 

4. 插入 DOM范围中的内容 

5. 折叠 DOM范围 

6. 比较 DOM范围 

7. 复制 DOM范围 

8. 清理 DOM范围 

  IE8及更早版本中的范围 

1. 用 IE范围实现简单的选择 

2. 使用 IE范围实现复杂的选择 

3. 操作 IE范围中的内容 

4. 折叠 IE范围 

5. 比较 IE范围 

6. 复制 IE范围 


DOM0、DOM1、DOM2、DOM3的区别

DOM0

JavaScript在早期版本中提供了查询和操作Web文档的内容API(如:图像和表单),在JavaScript中定义了定义了'images''forms'等,因此我们可以像下这样访问第一张图片或名为“user”的表单:

document.images[0]
document.forms['user']

这实际上是未形成标准的试验性质的初级阶段的DOM,现在习惯上被称为DOM0,即:第0级DOM。由于DOM0W3C进行标准备化之前出现,还处于未形成标准的初期阶段,这时NetscapeMicrosoft各自推出自己的第四代浏览器,自此DOM开始出各种问题。

DOM0与DHTML

Netscape Navigator 4IE4分别发布于1997年的6月和10月,这两种浏览器都大幅扩展了DOM,使JavaScript的功能大大增加,而此时也开始出现一个新名词:DHTML

DHTMLDynamic HTML(动态HTML)的简称。DHTML并不是一项新技术,而是将HTML、CSS、JavaScript技术组合的一种描述。即:

  • 利用HTML把网页标记为各种元素
  • 利用CSS设置元素样式及其显示位置
  • 利用JavaScript操控页面元素和样式

利用DHTML,看起来可以很容易的控制页面元素,并实现一此原本很复杂的效果(如:通过改变元素位置实现动画)。但事实并非如此,因为没有规范和标准,两种浏览器对相同功能的实现确完全不一样。为了保持程序的兼容性,程序员必须写一些探查代码以检测JavaScript是运行于哪种浏览器之下,并提供与之对应的脚本。JavaScript陷入了前所未有的混乱,DHTML也因此在人们心中留下了很差的印象。

DOM1的出现

在浏览器厂商进行浏览器大站的同时,W3C结合大家的优点推出了一个标准化的DOM,并于1998年10月完成了第一级 DOM,即:DOM1W3CDOM定义为一个与平台和编程语言无关的接口,通过这个接口程序和脚本可以动态的访问和修改文档的内容、结构和样式。

DOM1级主要定义了HTML和XML文档的底层结构。在DOM1中,DOM由两个模块组成:DOM Core(DOM核心)和DOM HTML。其中,DOM Core规定了基于XML的文档结构标准,通过这个标准简化了对文档中任意部分的访问和操作。DOM HTML则在DOM核心的基础上加以扩展,添加了针对HTML的对象和方法,如:JavaScript中的Document对象

DOM2与DOM3

DOM1的基础上DOM2DOM3引入了更多的交互能力,也支持了更高级的XML特性。DOM2DOM3将DOM分为更多具有联系的模块。DOM2级在原来DOM的基础上又扩充了鼠标、用户界面事件、范围、遍历等细分模块,而且通过对象接口增加了对CSS的支持。DOM1级中的DOM核心模块也经过扩展开始支持XML命名空间。DOM2中引入了下列模块,在模块包含了众多新类型和新接口:

  • DOM视图(DOM Views):定义了跟踪不同文档视图的接口
  • DOM事件(DOM Events):定义了事件和事件处理的接口
  • DOM样式(DOM Style):定义了基于CSS为元素应用样式的接口
  • DOM遍历和范围(DOM Traversal and Range):定义了遍历和操作文档树的接口

DOM3进一步扩展了DOM,DOM3中引入了以下模块:

  • DOM加载和保存模块(DOM Load and Save):引入了以统一方式加载和保存文档的方法
  • DOM验证模块(DOM Validation):定义了验证文档的方法
  • DOM核心的扩展(DOM Style):支持XML 1.0规范,涉及XML Infoset、XPath和XML Base

DOM2总结

DOM2级规范定义了一些模块,用于增强 DOM1级。“DOM2级核心”为不同的 DOM类型引入了 一些与 XML命名空间有关的方法。这些变化只在使用 XML或 XHTML文档时才有用;对于 HTML文 档没有实际意义。除了与 XML 命名空间有关的方法外, “DOM2 级核心”还定义了以编程方式创建 Document 实例的方法,也支持了创建 DocumentType 对象。 

“DOM2级样式”模块主要针对操作元素的样式信息而开发,其特性简要总结如下。 

  • 每个元素都有一个关联的 style 对象,可以用来确定和修改行内的样式。 
  • 要确定某个元素的计算样式(包括应用给它的所有 CSS规则),可以使用 getComputedStyle() 方法。 
  • IE不支持getComputedStyle()方法,但为所有元素都提供了能够返回相同信息 currentStyle 属性。 
  • 可以通过 document.styleSheets 集合访问样式表。 
  • 除 IE之外的所有浏览器都支持针对样式表的这个接口,IE也为几乎所有相应的 DOM功能提供 了自己的一套属性和方法。

“DOM2级遍历和范围”模块提供了与 DOM结构交互的不同方式,简要总结如下。 

  • 遍历即使用 NodeIterator 或 TreeWalker 对 DOM执行深度优先的遍历。 
  • NodeIterator 是一个简单的接口,只允许以一个节点的步幅前后移动。而 TreeWalker 在提 供相同功能的同时,还支持在 DOM结构的各个方向上移动,包括父节点、同辈节点和子节点等 方向。 
  • 范围是选择 DOM结构中特定部分,然后再执行相应操作的一种手段。 
  • 使用范围选区可以在删除文档中某些部分的同时,保持文档结构的格式良好,或者复制文档中 的相应部分。 
  • IE8及更早版本不支持“DOM2级遍历和范围”模块,但它提供了一个专有的文本范围对象,可 以用来完成简单的基于文本的范围操作。IE9完全支持 DOM遍历。 

DOM2新增模块-样式

在 HTML中定义样式的方式有 3种:通过<link/>元素包含外部样式表文件、使用<style/>元素 定义嵌入式样式,以及使用 style 特性定义针对特定元素的样式。“DOM2级样式”模块围绕这 3种应用 样式的机制提供了一套 API。要确定浏览器是否支持 DOM2级定义的 CSS能力,可以使用下列代码。 

var supportsDOM2CSS = document.implementation.hasFeature("CSS", "2.0"); 
var supportsDOM2CSS2 = document.implementation.hasFeature("CSS2", "2.0"); 

 在 style 特性中指定的任何 CSS属性都将表现为这个 style 对象的相应属性。对于使用短划线(分隔不同的词汇,例如 background-image)的 CSS属性 名,必须将其转换成驼峰大小写形式,才能通过 JavaScript来访问。下表列出了几个常见的 CSS属性及 其在 style 对象中对应的属性名。 
 


注:float 是 JavaScript中的保留字,因此不能用作属性名

“DOM2级样式”规范规定 样式对象上相应的属性名应该是 cssFloat;Firefox、Safari、Opera和 Chrome都支持这个属性,而 IE 支持的则是 styleFloat。

只要取得一个有效的DOM元素的引用,就可以随时使用JavaScript为其设置样式。

var myDiv = document.getElementById("myDiv"); 
 
//设置背景颜色
 myDiv.style.backgroundColor = "red"; 
 
//改变大小 
myDiv.style.width = "100px"; myDiv.style.height = "200px"; 
 
//指定边框 
myDiv.style.border = "1px solid black";
// 在以这种方式改变样式时,元素的外观会自动被更新。 

在标准模式下,所有度量值都必须指定一个度量单位。在混杂模式下,可以将 style.width 设置为"20",浏览器会假设它是"20px";但在标准模式下,将 style.width 设置为"20"会导致被忽略——因为没有度量单位。在实践中,最好始 终都指定度量单位 

 1. DOM样式属性和方法

“DOM2级样式”规范还为style对象定义了一些属性和方法。这些属性和方法在提供元素的style 特性值的同时,也可以修改样式。下面列出了这些属性和方法。 

  • cssText:如前所述,通过它能够访问到 style 特性中的 CSS代码。 
  • length:应用给元素的 CSS属性的数量。 
  • parentRule:表示 CSS信息的 CSSRule 对象。本节后面将讨论 CSSRule 类型。 
  • getPropertyCSSValue(propertyName):返回包含给定属性值的 CSSValue 对象。 
  • getPropertyPriority(propertyName):如果给定的属性使用了!important 设置,则返回 "important";否则,返回空字符串。  getPropertyValue(propertyName):返回给定属性的字符串值。 
  • item(index):返回给定位置的 CSS属性的名称。 
  • removeProperty(propertyName):从样式中删除给定属性。 
  • setProperty(propertyName,value,priority):将给定属性设置为相应的值,并加上优先 权标志("important"或者一个空字符串)

通过cssText属性可以访问style特性中的CSS代码。

在读取模式下,cssText返回浏览器对style 特性中 CSS代码的内部表示。

在写入模式下,赋给 cssText 的值会重写整个 style 特性的值;也就是 说,以前通过 style 特性指定的样式信息都将丢失。例如,如果通过 style 特性为元素设置了边框, 然后再以不包含边框的规则重写 cssText,那么就会抹去元素上的边框。js的优先级高于css。

2. 计算的样式

虽然 style 对象能够提供支持 style 特性的任何元素的样式信息,但它不包含那些从其他样式表 层叠而来并影响到当前元素的样式信息。“DOM2 级样式”增强了 document.defaultView,提供了 getComputedStyle()方法。这个方法接受两个参数:要取得计算样式的元素和一个伪元素字符串(例 如":after")。

如果不需要伪元素信息,第二个参数可以是 null。getComputedStyle()方法返回一 个 CSSStyleDeclaration 对象(与 style 属性的类型相同),其中包含当前元素的所有计算的样式。

<!--下面这个 HTML页面为例。-->
 <!DOCTYPE html> 
<html> 
    <head>     
        <title>Computed Styles Example</title>     
    <style type="text/css">        
     #myDiv {             
                background-color: blue;             
                width: 100px;             
                height: 200px;         
            }    
     </style> 
    </head> 
<body>   
  
<div id="myDiv" style="background-color: red; border: 1px solid black"></div> 

</body> 
</html> 

应用给这个例子中<div>元素的样式一方面来自嵌入式样式表(<style>元素中的样式),另一方 面来自其 style 特性。但是,style 特性中设置了 backgroundColor 和 border,没有设置 width 和 height,后者是通过样式表规则应用的。以下代码可以取得这个元素计算后的样式。 

var myDiv = document.getElementById("myDiv"); 
var computedStyle = document.defaultView.getComputedStyle(myDiv, null); 
alert(computedStyle.backgroundColor);  // "red" 
alert(computedStyle.width);    // "100px" 
alert(computedStyle.height);    // "200px" 
alert(computedStyle.border);    // 在某些浏览器中是"1px solid black" 

 不能指望某个 CSS 属性的默认值在不同浏览器中是相同 的。如果你需要元素具有某个特定的默认值,应该手工在样式表中指定该值。 

操作样式表 

使用下面的代码可以确定浏览器是否支持 DOM2级样式表。 

var supportsDOM2StyleSheets =  
            document.implementation.hasFeature("StyleSheets", "2.0"); 

 CSSStyleSheet 继承自 StyleSheet,后者可以作为一个基础接口来定义非 CSS 样式表。从 StyleSheet 接口继承而来的属性如下。  disabled:表示样式表是否被禁用的布尔值。这个属性是可读/写的,将这个值设置为 true 可 以禁用样式表。 

 

 应用于文档的所有样式表是通过 document.styleSheets 集合来表示的。通过这个集合的 length 属性可以获知文档中样式表的数量,而通过方括号语法或 item()方法可以访问每一个样式表。来看一个 例子。 

  • href:如果样式表是通过<link>包含的,则是样式表的 URL;否则,是 null。 
  • media:当前样式表支持的所有媒体类型的集合。与所有 DOM 集合一样,这个集合也有一个 length 属性和一个 item()方法。也可以使用方括号语法取得集合中特定的项。如果集合是空 列表,表示样式表适用于所有媒体。在 IE中,media 是一个反映<link>和<style>元素 media 特性值的字符串。 
  • ownerNode:指向拥有当前样式表的节点的指针,样式表可能是在 HTML 中通过<link>或 <style/>引入的(在 XML中可能是通过处理指令引入的)。如果当前样式表是其他样式表通过 @import 导入的,则这个属性值为 null。IE不支持这个属性。 
  • parentStyleSheet:在当前样式表是通过@import 导入的情况下,这个属性是一个指向导入 它的样式表的指针。 
  • title:ownerNode 中 title 属性的值。  type:表示样式表类型的字符串。对 CSS样式表而言,这个字符串是"type/css"。 除了 disabled 属性之外,其他属性都是只读的。在支持以上所有这些属性的基础上, CSSStyleSheet 类型还支持下列属性和方法: 
  • cssRules:样式表中包含的样式规则的集合。IE不支持这个属性,但有一个类似的 rules 属性。
  • ownerRule:如果样式表是通过@import 导入的,这个属性就是一个指针,指向表示导入的规 则;否则,值为 null。IE不支持这个属性。 deleteRule(index):删除 cssRules 集合中指定位置的规则。IE 不支持这个方法,但支持 一个类似的 removeRule()方法。 
  • insertRule(rule,index):向 cssRules 集合中指定的位置插入 rule 字符串。IE不支持这 个方法,但支持一个类似的 addRule()方法。 
var sheet = null; 
    for (var i=0, len=document.styleSheets.length; i < len; i++){ 
           sheet = document.styleSheets[i];     
            alert(sheet.href); 
    } 
 

 以上代码可以输出文档中使用的每一个样式表的 href 属性(<style>元素包含的样式表没有 href 属性) 。 不同浏览器的 document.styleSheets 返回的样式表也不同。所有浏览器都会包含<style>元素 和 rel 特性被设置为"stylesheet"的<link>元素引入的样式表。IE和 Opera也包含 rel 特性被设置 为"alternate stylesheet"的<link>元素引入的样式表。 

DOM2新增模块-遍历

“DOM2级遍历和范围”模块定义了两个用于辅助完成顺序遍历 DOM结构的类型:NodeIterator 和 TreeWalker。这两个类型能够基于给定的起点对 DOM结构执行深度优先(depth-first)的遍历操作。 在与 DOM兼容的浏览器中(Firefox 1及更高版本、Safari 1.3及更高版本、Opera 7.6及更高版本、Chrome 0.2及更高版本),都可以访问到这些类型的对象。IE不支持 DOM遍历。使用下列代码可以检测浏览器 对 DOM2级遍历能力的支持情况。 

var supportsTraversals = document.implementation.hasFeature("Traversal", "2.0");
 var supportsNodeIterator = (typeof document.createNodeIterator == "function"); 
var supportsTreeWalker = (typeof document.createTreeWalker == "function"); 

 DOM 遍历是深度优先的 DOM 结构遍历,也就是说,移动的方向至少有两个(取决 于使用的遍历类型)。遍历以给定节点为根,不可能向上超出 DOM树的根节点。

从 document 开始依序向前,访问的第一个节点是 document,访问的后一个节点是包含 "world!"的文本节点。从文档后的文本节点开始,遍历可以反向移动到 DOM 树的顶端。此时,访 问的第一个节点是包含"Hello"的文本节点,访问的后一个节点是 document 节点。NodeIterator 和 TreeWalker 都以这种方式执行遍历。

NodeIterator 

NodeIterator 类型是两者中比较简单的一个,可以使用 document.createNodeIterator()方 法创建它的新实例。

这个方法接受下列 4个参数。 

  • root:想要作为搜索起点的树中的节点。 
  • whatToShow:表示要访问哪些节点的数字代码。 
  • filter:是一个 NodeFilter 对象,或者一个表示应该接受还是拒绝某种特定节点的函数。 
  • entityReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在 HTML 页面 中没有用,因为其中的实体引用不能扩展。 

whatToShow 参数是一个位掩码,通过应用一或多个过滤器(filter)来确定要访问哪些节点。这个 参数的值以常量形式在 NodeFilter 类型中定义,如下所示。 

  • NodeFilter.SHOW_ALL:显示所有类型的节点。 
  • NodeFilter.SHOW_ELEMENT:显示元素节点。 
  • NodeFilter.SHOW_ATTRIBUTE:显示特性节点。由于DOM结构原因,实际上不能使用这个值。
  • NodeFilter.SHOW_TEXT:显示文本节点。 
  • NodeFilter.SHOW_CDATA_SECTION:显示 CDATA节点。对 HTML页面没有用。 
  • NodeFilter.SHOW_ENTITY_REFERENCE:显示实体引用节点。对 HTML页面没有用。 
  • NodeFilter.SHOW_ENTITYE:显示实体节点。对 HTML页面没有用。 
  • NodeFilter.SHOW_PROCESSING_INSTRUCTION:显示处理指令节点。对 HTML页面没有用。
  • NodeFilter.SHOW_COMMENT:显示注释节点。 
  • NodeFilter.SHOW_DOCUMENT:显示文档节点。 
  • NodeFilter.SHOW_DOCUMENT_TYPE:显示文档类型节点。 
  • NodeFilter.SHOW_DOCUMENT_FRAGMENT:显示文档片段节点。对 HTML页面没有用。 
  • NodeFilter.SHOW_NOTATION:显示符号节点。对 HTML页面没有用。 

除了 NodeFilter.SHOW_ALL 之外,可以使用按位或操作符来组合多个选项,如下面的例子所示:

var whatToShow = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT; 
 

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
   
  
</head>
<body>
    <div id="div1">
        <p>
            <b>Hello</b> world!</p>
        <ul>
            <li>List item 1</li>
            <li>List item 2</li>
            <li>List item 3</li>
        </ul>
    </div>
</body>
<script src="./static/main.js"></script>
</html>
/*main.js*/
var div = document.getElementById("div1"); 
//
var filter = function (node) {
     return node.tagName.toLowerCase() == "li" ? 
     NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; 
    };
//filter是过滤条件,下面iterator中filter改成null,则表示对输出没有输出限制
var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, 
    filter, false); 
var node = iterator.nextNode(); 
    while (node !== null) {     
        document.write (node.tagName+"   ");        //输出标签名     
        node = iterator.nextNode(); 
    }

 由于 nextNode()和 previousNode()方法都基于 NodeIterator 在 DOM结构中的内部指针工 作,所以 DOM结构的变化会反映在遍历的结果中。 

TreeWalker

TreeWalker 是 NodeIterator 的一个更高级的版本。除了包括 nextNode()和 previousNode() 在内的相同的功能之外,这个类型还提供了下列用于在不同方向上遍历 DOM结构的方法。 

  • parentNode():遍历到当前节点的父节点; 
  • firstChild():遍历到当前节点的第一个子节点; 
  • lastChild():遍历到当前节点的后一个子节点; 
  • nextSibling():遍历到当前节点的下一个同辈节点; 
  • previousSibling():遍历到当前节点的上一个同辈节点。 

创建 TreeWalker 对象要使用 document.createTreeWalker()方法,这个方法接受的 4个参数 与 document.createNodeIterator()方法相同作为遍历起点的根节点、要显示的节点类型、过滤 器和一个表示是否扩展实体引用的布尔值。由于这两个创建方法很相似,所以很容易用 TreeWalker 来代替 NodeIterator,如下面的例子所示。 
 

var div = document.getElementById("div1"); 
var filter = function(node){     /*条件建立*/
    return node.tagName.toLowerCase() == "li"?          
        NodeFilter.FILTER_ACCEPT :          
    NodeFilter.FILTER_SKIP; 
}; 
 /*,filter 可以返回的值有所不同。除了 NodeFilter.FILTER_ACCEPT 和 
NodeFilter. FILTER_SKIP 之外,还可以使用 NodeFilter.FILTER_REJECT。
在使用 NodeIterator 对象时, NodeFilter.FILTER_SKIP 与 NodeFilter.FILTER_REJECT 的作用相同:跳过指定的节点。
但在使 用 TreeWalker 对象时,NodeFilter.FILTER_SKIP 会跳过相应节点继续前进到子树中的下一个节
点, 而 NodeFilter.FILTER_REJECT 则会跳过相应节点及该节点的整个子树。例如,将前面例子中的 
NodeFilter.FILTER_SKIP 修改成 NodeFilter.FILTER_REJECT,结果就是不会访问任何节点。这是 因为
第一个返回的节点是<div>,它的标签名不是"li",于是就会返回 NodeFilter.FILTER_REJECT, 这意味着
遍历会跳过整个子树。在这个例子中,<div>元素是遍历的根节点,于是结果就会停止遍历。 */
var walker= document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT,                 filter, false); 
 
var node = iterator.nextNode(); 
while (node !== null) {     
    alert(node.tagName);        //输出标签名     
    node = iterator.nextNode(); 
}

 TreeWalker 真正强大的地方在于能够在 DOM结构中沿任何方向移动。使用 TreeWalker 遍历 DOM树,即使不定义过滤器,也可以取得所有<li>元素,如下面的代码所示

var div = document.getElementById("div1"); 
var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null, false); 
 
walker.firstChild();            
//转到<p> 
walker.nextSibling();         
// 转到<ul> 
var node = walker.firstChild();        
//转到第一个<li> 
while (node !== null) {     
    alert(node.tagName);     
    node = walker.nextSibling(); 
}  
 

 TreeWalker 类型还有一个属性,名叫 currentNode,表示任何遍历方法在上一次遍历中返回的 节点。通过设置这个属性也可以修改遍历继续进行的起点

var node = walker.nextNode(); 
alert(node === walker.currentNode);     //true 
walker.currentNode = document.body;     //修改起点 
/* 与 NodeIterator 相比,TreeWalker 类型在遍历 DOM时拥有更大的灵活性。
由于 IE中没有对 应的类型和方法,所以使用遍历的跨浏览器解决方案非常少见*/

DOM2新增模块-范围

“DOM2级遍历和范围”模块定义了“范围”(range)接口。

通过范围可以选择文档中的一个区域,而不必考虑节点的界限(选择在后台完成,对用户是不可见的) 。 在常规的 DOM操作不能更有效地修改文档时,使用范围往往可以达到目的。Firefox、Opera、Safari和 Chrome都支持 DOM范围。IE以专有方式实现了自己的范围特性。 

DOM2级在 Document 类型中定义了 createRange()方法。在兼容 DOM的浏览器中,这个方法 属于 document 对象。使用 hasFeature()或者直接检测该方法,都可以确定浏览器是否支持范围。 

var supportsRange = document.implementation.hasFeature("Range", "2.0"); 
var alsoSupportsRange = (typeof document.createRange == "function"); 
 

如果浏览器支持范围,那么就可以使用 createRange()来创建 DOM范围,如下所示: 

var range = document.createRange(); 

与节点类似,新创建的范围也直接与创建它的文档关联在一起,不能用于其他文档。创建了范围之 后,接下来就可以使用它在后台选择文档中的特定部分。而创建范围并设置了其位置之后,还可以针对 范围的内容执行很多种操作,从而实现对底层 DOM树的更精细的控制。

每个范围由一个 Range 类型的实例表示,这个实例拥有很多属性和方法。下列属性提供了当前范 围在文档中的位置信息。

  • startContainer:包含范围起点的节点(即选区中第一个节点的父节点)。 
  • startOffset:范围在 startContainer 中起点的偏移量。如果 startContainer 是文本节 点、注释节点或 CDATA节点,那么 startOffset 就是范围起点之前跳过的字符数量。否则, startOffset 就是范围中第一个子节点的索引。 
  • endContainer:包含范围终点的节点(即选区中后一个节点的父节点)。 
  • endOffset:范围在 endContainer 中终点的偏移量(与 startOffset 遵循相同的取值规则)。
  • commonAncestorContainer:startContainer 和 endContainer 共同的祖先节点在文档树 中位置深的那个。 

在把范围放到文档中特定的位置时,这些属性都会被赋值。 

1. 用 DOM范围实现简单选择 

要使用范围来选择文档中的一部分,简的方式就是使用 selectNode()或 selectNodeContents()。 这两个方法都接受一个参数,即一个 DOM 节点,然后使用该节点中的信息来填充范围。其中,selectNode()方法选择整个节点,包括其子节点;而 selectNodeContents()方法则只选择节点的 子节点。以下面的 HTML代码为例。 

<!DOCTYPE html>
 <html>     
  <body>           
      <p id="p1">
        <b>Hello</b> world!
     </p>     
</body>
 </html> 

我们可以使用下列代码来创建范围

var range1 = document.createRange();     
range2 = document.createRange();    
 p1 = document.getElementById("p1"); 
range1.selectNode(p1);
 range2.selectNodeContents(p1); 
 

这里创建的两个范围包含文档中不同的部分:rang1 包含<p/>元素及其所有子元素,而 rang2 包 含<b/>元素、文本节点"Hello"和文本节点"world!"(如图 所示) 。 

在调用 selectNode()时,startContainer、endContainer 和 commonAncestorContainer 都等于传入节点的父节点,也就是这个例子中的 document.body。而 startOffset 属性等于给定节 点在其父节点的 childNodes 集合中的索引(在这个例子中是 1——因为兼容 DOM的浏览器将空格算 作一个文本节点),endOffset 等于 startOffset 加 1(因为只选择了一个节点)。 

在调用 selectNodeContents()时,startContainer、endContainer 和 commonAncestorConta- iner 等于传入的节点,即这个例子中的<p>元素。而 startOffset 属性始终等于 0,因为范围从给定节 点的第一个子节点开始。后,endOffset 等于子节点的数量(node.childNodes.length),在这个例 子中是 2。 

此外,为了更精细地控制将哪些节点包含在范围中,还可以使用下列方法。 

  •  setStartBefore(refNode):将范围的起点设置在 refNode 之前,因此 refNode 也就是范围 选区中的第一个子节点。同时会将 startContainer 属性设置为 refNode.parentNode,将 startOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引。 
  • setStartAfter(refNode):将范围的起点设置在 refNode 之后,因此 refNode 也就不在范 围之内了,其下一个同辈节点才是范围选区中的第一个子节点。同时会将 startContainer 属 性设置为 refNode.parentNode,将 startOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引加 1。 
  • setEndBefore(refNode):将范围的终点设置在 refNode 之前,因此 refNode 也就不在范围 之内了,其上一个同辈节点才是范围选区中的后一个子节点。同时会将 endContainer 属性设置为refNode.parentNode,将endOffset 属性设置为refNode在其父节点的childNodes 集合中的索引。 
  • setEndAfter(refNode):将范围的终点设置在 refNode 之后,因此 refNode 也就是范围选区 中的后一个子节点。同时会将 endContainer 属性设置为 refNode.parentNode,将 endOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引加 1。 

在调用这些方法时,所有属性都会自动为你设置好。不过,要想创建复杂的范围选区,也可以直接 指定这些属性的值。 

2. 用 DOM范围实现复杂选择 

要创建复杂的范围就得使用 setStart()和 setEnd()方法。这两个方法都接受两个参数:一个参 照节点和一个偏移量值。对 setStart()来说,参照节点会变成 startContainer,而偏移量值会变成 startOffset。对于 setEnd()来说,参照节点会变成 endContainer,而偏移量值会变成 endOffset。

可以使用这两个方法来模仿 selectNode()和 selectNodeContents()。来看下面的例子: 
 

var range1 = document.createRange();     
range2 = document.createRange();     
p1 = document.getElementById("p1");     
p1Index = -1;     
i, len; 
for (i=0, len=p1.parentNode.childNodes.length; i < len; i++) {     
    if (p1.parentNode.childNodes[i] == p1) {         
        p1Index = i;         
        break;     
    } 
}          
range1.setStart(p1.parentNode, p1Index); 
range1.setEnd(p1.parentNode, p1Index + 1); 
range2.setStart(p1, 0); 
range2.setEnd(p1, p1.childNodes.length); 

显然,要选择这个节点(使用 range1),就必须确定当前节点(p1)在其父节点的 childNodes 集合中的索引。而要选择这个节点的内容(使用 range2),也不必计算什么;只要通过 setStart() 和 setEnd()设置默认值即可。模仿 selectNode()和 selectNodeContents()并不是 setStart() 和 setEnd()的主要用途,它们更胜一筹的地方在于能够选择节点的一部分。 

假设你只想选择前面 HTML 示例代码中从"Hello"的"llo"到"world!"的"o"——很容易做到。 第一步是取得所有节点的引用,如下面的例子所示: 

var p1 = document.getElementById("p1");     
helloNode = p1.firstChild.firstChild;    
 worldNode = p1.lastChild;  
 

实际上,"Hello"文本节点是<p>元素的孙子节点,因为它本身是<b>元素的一个子节点。因此, p1.firstChild取得的是<b>,而p1.firstChild.firstChild取得的才是这个文本节点。"world!" 文本节点是<p>元素的第二个子节点(也是后一个子节点),因此可以使用 p1.lastChild 取得该节点。然后,必须在创建范围时指定相应的起点和终点,如下面的例子所示。


 var range = document.createRange(); 
range.setStart(helloNode, 2); 
range.setEnd(worldNode, 3); 
 

因为这个范围的选区应该从"Hello"中"e"的后面开始,所以在 setStart()中传入 helloNode 的同时,传入了偏移量 2(即"e"的下一个位置;"H"的位置是 0)。设置选区的终点时,在 setEnd() 中传入 worldNode 的同时传入了偏移量 3,表示选区之外的第一个字符的位置,这个字符是"r",它的 位置是 3(位置 0上还有一个空格)。如图 12-7所示。 

由于helloNode 和worldNode都是文本节点,因此它们分别变成了新建范围的startContainer 和 endContainer。此时 startOffset 和 endOffset 分别用以确定两个节点所包含的文本中的位置, 而不是用以确定子节点的位置(就像传入的参数为元素节点时那样)。此时的 commonAncestor- Container 是<p>元素,也就是同时包含这两个节点的第一个祖先元素。 

当然,仅仅是选择了文档中的某一部分用处并不大。但重要的是,选择之后才可以对选区进行操作。

3. 操作 DOM范围中的内容 

在创建范围时 ,内部会为这个范围创建一个文档片段,范围所属的全部节点都被添加到了这个文档 片段中。为了创建这个文档片段,范围内容的格式必须正确有效。在前面的例子中,我们创建的选区分 别开始和结束于两个文本节点的内部,因此不能算是格式良好的 DOM结构,也就无法通过 DOM来表 示。

但是,范围知道自身缺少哪些开标签和闭标签,它能够重新构建有效的 DOM结构以便我们对其进 行操作。 对于前面的例子而言,范围经过计算知道选区中缺少一个开始的<b>标签,因此就会在后台动态加 入一个该标签,同时还会在前面加入一个表示结束的</b>标签以结束"He"。于是,修改后的 DOM 就 变成了如下所示。 

<p><b>He</b><b>llo</b> world!</p> 

另外,文本节点"world!"也被拆分为两个文本节点,一个包含"wo",另一个包含"rld!"。最终的 DOM树如图 12-8所示,右侧是表示范围的文档片段的内容。

像这样创建了范围之后,就可以使用各种方法对范围的内容进行操作了(注意,表示范围的内部文 档片段中的所有节点,都只是指向文档中相应节点的指针)。

第一个方法,也是容易理解的方法,就是 deleteContents()。这个方法能够从文档中删除范 围所包含的内容。例如: 
 

var p1 = document.getElementById("p1");    
 helloNode = p1.firstChild.firstChild;     
worldNode = p1.lastChild;         
range = document.createRange(); 

range.setStart(helloNode, 2); 
range.setEnd(worldNode, 3); 
 
range.deleteContents(); 

执行以上代码后,页面中会显示如下 HTML代码: 

<p><b>He</b>rld!</p>   

由于范围选区在修改底层 DOM 结构时能够保证格式良好,因此即使内容被删除了,终的 DOM 结构依旧是格式良好的。 与 deleteContents()方法相似,extractContents()也会从文档中移除范围选区。但这两个方 法的区别在于,extractContents()会返回范围的文档片段。利用这个返回的值,可以将范围的内容 插入到文档中的其他地方。如下面的例子所示: 

var p1 = document.getElementById("p1");     
helloNode = p1.firstChild.firstChild;    
 worldNode = p1.lastChild;     
range = document.createRange(); 
 
range.setStart(helloNode, 2);
 range.setEnd(worldNode, 3); 
 
var fragment = range.extractContents(); 
p1.parentNode.appendChild(fragment); 

在这个例子中,我们将提取出来的文档片段添加到了文档<body>元素的末尾。(记住,在将文档片 段传入 appendChild()方法中时,添加到文档中的只是片段的子节点,而非片段本身。)结果得到如下 HTML代码: 

<p><b>He</b>rld!</p> <b>llo</b> wo 

还一种做法,即使用 cloneContents()创建范围对象的一个副本,然后在文档的其他地方插入该 副本。如下面的例子所示: 


 

var p1 = document.getElementById("p1"),     

    helloNode = p1.firstChild.firstChild,     
    worldNode = p1.lastChild,     
    range = document.createRange(); 
 
range.setStart(helloNode, 2); 
range.setEnd(worldNode, 3); 
 
var fragment = range.cloneContents(); 
p1.parentNode.appendChild(fragment); 

这个方法与 extractContents()非常类似,因为它们都返回文档片段。它们的主要区别在于, cloneContents()返回的文档片段包含的是范围中节点的副本,而不是实际的节点。执行上面的操作 后,页面中的 HTML代码应该如下所示: 

<p><b>Hello</b> world!</p> <b>llo</b> wo 

有一点请读者注意,那就是在调用上面介绍的方法之前,拆分的节点并不会产生格式良好的文档片 段。换句话说,原始的 HTML在 DOM被修改之前会始终保持不变。 

4. 插入 DOM范围中的内容 

利用范围,可以删除或复制内容,还可以像前面介绍的那样操作范围中的内容。使用 insertNode() 方法可以向范围选区的开始处插入一个节点。假设我们想在前面例子中的 HTML 前面插入以下 HTML 代码: 

<span style="color: red">Inserted text</span> 

那么,就可以使用下列代码: 

var p1 = document.getElementById("p1");      
helloNode = p1.firstChild.firstChild;     
worldNode = p1.lastChild;     
range = document.createRange(); 
 
range.setStart(helloNode, 2); 
range.setEnd(worldNode, 3); 
 
var span = document.createElement("span");
 span.style.color = "red"; 
span.appendChild(document.createTextNode("Inserted text"));
 range.insertNode(span); 

运行以上 JavaScript代码,就会得到如下 HTML代码: 

<p id="p1"><b>He<span style="color: red">Inserted text</span>llo</b> world</p>  
 

注意,<span>正好被插入到了"Hello"中的"llo"前面,而该位置就是范围选区的开始位置。还要 注意的是,由于这里没有使用上一节介绍的方法,结果原始的 HTML并没有添加或删除<b>元素。使用 这种技术可以插入一些帮助提示信息,例如在打开新窗口的链接旁边插入一幅图像。 除了向范围内部插入内容之外,还可以环绕范围插入内容,此时就要使用 surroundContents() 方法。这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列 步骤。 

(1) 提取出范围中的内容(类似执行 extractContent());

(2) 将给定节点插入到文档中原来范围所在的位置上;

(3) 将文档片段的内容添加到给定节点中。 

可以使用这种技术来突出显示网页中的某些词句,例如下列代码: 

var p1 = document.getElementById("p1");    
 helloNode = p1.firstChild.firstChild;     
worldNode = p1.lastChild;     
range = document.createRange();
 range.selectNode(helloNode); 
 
var span = document.createElement("span"); 
span.style.backgroundColor = "yellow";
 range.surroundContents(span); 

会给范围选区加上一个黄色的背景。得到的 HTML代码如下所示:

<p><b><span style="background-color:yellow">Hello</span></b> world!</p> 

为了插入<span>,范围必须包含整个 DOM选区(不能仅仅包含选中的 DOM节点)。 

5. 折叠 DOM范围 

所谓折叠范围,就是指范围中未选择文档的任何部分。可以用文本框来描述折叠范围的过程。假设 文本框中有一行文本,你用鼠标选择了其中一个完整的单词。然后,你单击鼠标左键,选区消失,而光 标则落在了其中两个字母之间。同样,在折叠范围时,其位置会落在文档中的两个部分之间,可能是范 围选区的开始位置,也可能是结束位置。

图 12-9展示了折叠范围时发生的情形。 使用 collapse()方法来折叠范围,这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪 一端。参数 true 表示折叠到范围的起点,参数 false 表示折叠到范围的终点。要确定范围已经折叠完 毕,可以检查 collapsed 属性,如下所示: 
 

range.collapse(true);       //折叠到起点
 alert(range.collapsed);      //输出 true 

检测某个范围是否处于折叠状态,可以帮我们确定范围中的两个节点是否紧密相邻。例如,对于下 面的 HTML代码: 

<p id="p1">Paragraph 1</p><p id="p2">Paragraph 2</p>

如果我们不知道其实际构成(比如说,这行代码是动态生成的) ,那么可以像下面这样创建一个范围。

var p1 = document.getElementById("p1"),     
p2 = document.getElementById("p2"),     range = document.createRange(); 
range.setStartAfter(p1); 
range.setStartBefore(p2); 
alert(range.collapsed);     //输出 true 

在这个例子中,新创建的范围是折叠的,因为 p1 的后面和 p2 的前面什么也没有。 

6. 比较 DOM范围 

在有多个范围的情况下,可以使用 compareBoundaryPoints()方法来确定这些范围是否有公共 的边界(起点或终点)。这个方法接受两个参数:表示比较方式的常量值和要比较的范围。表示比较方 式的常量值如下所示。 

  • Range.START_TO_START(0):比较第一个范围和第二个范围的起点; 
  • Range.START_TO_END(1):比较第一个范围的起点和第二个范围的终点; 
  • Range.END_TO_END(2):比较第一个范围和第二个范围的终点; 
  • Range.END_TO_START(3):比较第一个范围的终点和第一个范围的起点。 

compareBoundaryPoints()方法可能的返回值如下:如果第一个范围中的点位于第二个范围中的 点之前,返回-1;如果两个点相等,返回 0;如果第一个范围中的点位于第二个范围中的点之后,返回 1。来看下面的例子

var range1 = document.createRange(); 
var range2 = document.createRange(); 
var p1 = document.getElementById("p1"); 
 
range1.selectNodeContents(p1); 
range2.selectNodeContents(p1); 
range2.setEndBefore(p1.lastChild); 
 
alert(range1.compareBoundaryPoints(Range.START_TO_START, range2));     //0 
alert(range1.compareBoundaryPoints(Range.END_TO_END, range2));         //1 

在这个例子中,两个范围的起点实际上是相同的,因为它们的起点都是由 selectNodeContents() 方法设置的默认值来指定的。因此,第一次比较返回 0。但是,range2 的终点由于调用 setEndBefore() 已经改变了,结果是 range1 的终点位于 range2 的终点后面(见图 12-10),因此第二次比较返回 1。 

 

7. 复制 DOM范围 

可以使用 cloneRange()方法复制范围。这个方法会创建调用它的范围的一个副本。 

var newRange = range.cloneRange(); 

新创建的范围与原来的范围包含相同的属性,而修改它的端点不会影响原来的范围。 

8. 清理 DOM范围 

在使用完范围之后,好是调用 detach()方法,以便从创建范围的文档中分离出该范围。调用 detach()之后,就可以放心地解除对范围的引用,从而让垃圾回收机制回收其内存了。来看下面的 例子。

range.detach();      //从文档中分离
 range = null;         //解除引用 

在使用范围的后再执行这两个步骤是我们推荐的方式。一旦分离范围,就不能再恢复使用了。 

  IE8及更早版本中的范围 

虽然 IE9支持 DOM范围,但 IE8及之前版本不支持 DOM范围。不过,IE8及早期版本支持一种类 似的概念,即文本范围(text range)。文本范围是 IE专有的特性,其他浏览器都不支持。顾名思义,文 本范围处理的主要是文本(不一定是 DOM节点)。通过<body>、<button>、<input>和<textarea> 等这几个元素,可以调用 createTextRange()方法来创建文本范围。以下是一个例子: 
 

var range = document.body.createTextRange(); 

像这样通过 document 创建的范围可以在页面中的任何地方使用(通过其他元素创建的范围则只能 在相应的元素中使用)。与 DOM范围类似,使用 IE文本范围的方式也有很多种。 

1. 用 IE范围实现简单的选择 

选择页面中某一区域的简单方式,就是使用范围的 findText()方法。这个方法会找到第一次出 现的给定文本,并将范围移过来以环绕该文本。如果没有找到文本,这个方法返回 false;否则返回 true。同样,仍然以下面的 HTML代码为例。 

<p id="p1"><b>Hello</b> world!</p> 

要选择"Hello",可以使用下列代码。 
 

var range = document.body.createTextRange(); var found = range.findText("Hello");

在执行完第二行代码之后,文本"Hello"就被包围在范围之内了。为此,可以检查范围的 text 属 性来确认(这个属性返回范围中包含的文本),或者也可以检查 findText()的返回值——在找到了文 本的情况下返回值为 true。例如: 

alert(found);           //true 
alert(range.text);      //"Hello" 

还可以为 findText()传入另一个参数,即一个表示向哪个方向继续搜索的数值。负值表示应该从 当前位置向后搜索,而正值表示应该从当前位置向前搜索。因此,要查找文档中前两个"Hello"的实例, 应该使用下列代码。 

var found = range.findText("Hello"); 
var foundAgain = range.findText("Hello", 1); 

IE中与 DOM中的 selectNode()方法接近的方法是 moveToElementText(),这个方法接受一 个 DOM元素,并选择该元素的所有文本,包括 HTML标签。下面是一个例子。 

var range = document.body.createTextRange(); 
var p1 = document.getElementById("p1"); 
range.moveToElementText(p1); 

在文本范围中包含 HTML的情况下,可以使用 htmlText 属性取得范围的全部内容,包括 HTML 和文本,如下面的例子所示。 

alert(range.htmlText); 

IE的范围没有任何属性可以随着范围选区的变化而动态更新。不过,其 parentElement()方法倒 是与 DOM的 commonAncestorContainer 属性类似。

var ancestor = range.parentElement(); 

这样得到的父元素始终都可以反映文本选区的父节点。 

2. 使用 IE范围实现复杂的选择 

在 IE 中创建复杂范围的方法,就是以特定的增量向四周移动范围。为此,IE 提供了 4 个方法: move()、moveStart()、moveEnd()和 expand()。这些方法都接受两个参数:移动单位和移动单位 的数量。其中,移动单位是下列一种字符串值。 

  •  "character":逐个字符地移动。 
  • "word":逐个单词(一系列非空格字符)地移动。 
  • "sentence":逐个句子(一系列以句号、问号或叹号结尾的字符)地移动。 
  • "textedit":移动到当前范围选区的开始或结束位置。 

通过 moveStart()方法可以移动范围的起点,通过 moveEnd()方法可以移动范围的终点,移动的 幅度由单位数量指定,如下面的例子所示。 

range.moveStart("word", 2);       //起点移动 2 个单词 
range.moveEnd("character", 1);   //终点移动 1 个字符 

使用 expand()方法可以将范围规范化。换句话说,expand()方法的作用是将任何部分选择的文 本全部选中。例如,当前选择的是一个单词中间的两个字符,调用 expand("word")可以将整个单词都 包含在范围之内。 

而 move()方法则首先会折叠当前范围(让起点和终点相等),然后再将范围移动指定的单位数量, 如下面的例子所示。 

range.move("character", 5);     //移动 5 个字符 

调用 move()之后,范围的起点和终点相同,因此必须再使用 moveStart()或 moveEnd()创建新 的选区。 

3. 操作 IE范围中的内容 

在 IE中操作范围中的内容可以使用 text 属性或 pasteHTML()方法。如前所述,通过 text 属性 可以取得范围中的内容文本;但是,也可以通过这个属性设置范围中的内容文本。来看一个例子。 

var range = document.body.createTextRange(); 
range.findText("Hello"); 
range.text = "Howdy"; 

如果仍以前面的 Hello World代码为例,执行以上代码后的 HTML代码如下。 

<p id="p1"><b>Howdy</b> world!</p> 

注意,在设置 text 属性的情况下,HTML标签保持不变。 要向范围中插入 HTML代码,就得使用 pasteHTML()方法,如下面的例子所示。 

var range = document.body.createTextRange(); 
range.findText("Hello"); 
range.pasteHTML("<em>Howdy</em>"); 

执行这些代码后,会得到如下 HTML。 

<p id="p1"><b><em>Howdy</em></b> world!</p> 

 不过,在范围中包含 HTML代码时,不应该使用 pasteHTML(),因为这样很容易导致不可预料的 结果——很可能是格式不正确的 HTML。 

4. 折叠 IE范围 

IE为范围提供的 collapse()方法与相应的 DOM方法用法一样:传入 true 把范围折叠到起点, 传入 false 把范围折叠到终点。例如: 
 

range.collapse(true);     //折叠到起点 

可惜的是,没有对应的 collapsed 属性让我们知道范围是否已经折叠完毕。为此,必须使用 boundingWidth 属性,该属性返回范围的宽度(以像素为单位)。如果 boundingWidth 属性等于 0, 就说明范围已经折叠了: 

var isCollapsed = (range.boundingWidth == 0); 

 此外,还有 boundingHeight、boundingLeft 和 boundingTop 等属性,虽然它们都不像 boundingWidth 那么有用,但也可以提供一些有关范围位置的信息。 

5. 比较 IE范围 

IE中的 compareEndPoints()方法与 DOM范围的 compareBoundaryPoints()方法类似。这个 方法接受两个参数:比较的类型和要比较的范围。比较类型的取值范围是下列几个字符串值:"StartToStart"、"StartToEnd"、"EndToEnd"和"EndToStart"。这几种比较类型与比较 DOM范 围时使用的几个值是相同的。 

同样与 DOM类似的是,compareEndPoints()方法也会按照相同的规则返回值,即如果第一个范 围的边界位于第二个范围的边界前面,返回-1;如果二者边界相同,返回 0;如果第一个范围的边界位 于第二个范围的边界后面,返回 1。仍以前面的 Hello World代码为例,下列代码将创建两个范围,一个 选择"Hello world!"(包括<b>标签),另一个选择"Hello"。 

var range1 = document.body.createTextRange(); 
var range2 = document.body.createTextRange(); 
 
range1.findText("Hello world!"); range2.findText("Hello"); 
 

alert(range1.compareEndPoints("StartToStart", range2));      //0 
alert(range1.compareEndPoints("EndToEnd", range2));       //1 

由于这两个范围共享同一个起点,所以使用 compareEndPoints()比较起点返回 0。而 range1 的终点在 range2 的终点后面,所以 compareEndPoints()返回 1。 IE 中还有两个方法,也是用于比较范围的:isEqual()用于确定两个范围是否相等,inRange() 用于确定一个范围是否包含另一个范围。下面是相应的示例。 
 

var range1 = document.body.createTextRange(); 
var range2 = document.body.createTextRange(); 
range1.findText("Hello World");
 range2.findText("Hello"); 
alert("range1.isEqual(range2): " + range1.isEqual(range2));  //false 
alert("range1.inRange(range2):" + range1.inRange(range2));    //true 

这个例子使用了与前面相同的范围来示范这两个方法。由于这两个范围的终点不同,所以它们不相 等,调用 isEqual()返回 false。由于 range2 实际位于 range1 内部,它的终点位于后者的终点之 前、起点之后,所以 range2 被包含在 range1 内部,调用 inRange()返回 true。 

6. 复制 IE范围 

在 IE中使用 duplicate()方法可以复制文本范围,结果会创建原范围的一个副本,如下面的例子 所示。 

var newRange = range.duplicate(); 

新创建的范围会带有与原范围完全相同的属性。 

猜你喜欢

转载自blog.csdn.net/qq_16546829/article/details/81289055
今日推荐