47. HTML 的创建和修改

  • 打印

引言

在本文中我将会阐述一些基础知识,告诉你如何利用 JavaScript 来对我们的页面进行操作,这些操作包括显示或隐藏页面上部分内容,以及添加新的 HTML 元素或对其进行删除。学完本文之后你就会明白 JavaScript 的最基本的功能就是改动页面内容,此外你还会学到进行页面内容改动的最好的方法。.

 

本文目录如下:

隐藏与显示

我们先从最容易的 JavaScript HTML 操作入手:隐藏或显示页面上已有的元素。为了实现这个目的,你可以用 JavaScript 来改变某个元素的样式。CSS 样式本身就是一种描述元素外观的有效方式,而元素外观的一个重要特性就是是否被显示。CSS 的 display 属性就是元素的显示和隐藏的关键所在:将其设为 display:none 就可以将一个元素隐藏起来。试想一下如下的一个段落:

<p id="mypara" style="display: none">I am a paragraph</p>

该段落在页面上将会是不可见的。 JavaScript 允许我们动态地将 display: none 样式添加到某个元素上以隐藏此元素。

JavaScript 允许我们获取某个 HTML 元素的引用。举例来说,通过 var el = document.getElementById("nav"); 就会得到 id="nav" 的元素的引用。在得到某个元素的引用后,就可以通过 style 属性来修改此元素的 CSS 。比如说,前面的 “mypara” 段落目前是隐藏的(它设置了 display: none )。为了将它显示出来,只要将 display 样式属性设置为 block 就可以了:

var el = document.getElementById('mypara');
mypara.style.display = 'block';

瞧,这个段落出现了。通过 style 属性来设置某个元素的 CSS 跟在 HTML 中设置元素 style 属性是一样的,因此上面代码的 mypara.style.display = 'block' 的效果跟直接在 HTML 中放入 style="display: block" 是一样的。当用 JavaScript 的修改时动态的。隐藏任何元素都非常简单:

var el = document.getElementById('mypara');
mypara.style.display = 'none';

隐藏与显示示例

理论好是好,但此处要是有一个更实际的例子就更好了。假定有一系列的标签,在某个标签上点击鼠标就能显示该标签,同时隐藏其它所有标签。

下面就是一套标签的示例:

<ol class="tablinks">
  <li><a href="#tab1">Tab 1</a></li>
  <li><a href="#tab2">Tab 2</a></li>
  <li><a href="#tab3">Tab 3</a></li>
</ol>

<div class="tab" id="tab1">This is tab 1</div>
<div class="tab" id="tab2">This is tab 2</div>
<div class="tab" id="tab3">This is tab 3</div>

在示例页面的 head 部分(例子实际效果请参见 tabs.html),你会看到如下所示的 CSS 和 JavaScript (一般这些代码会放在外部文件中,并导入到 HTML 中去,但此处我把所有代码都放在一个文件中以便阅读)。有些代码看起来很吓人,不过别担心,我们在后面会解释。

<style type="text/css">
ol.tablinks {
  margin: 0; padding: 0;
}
ol.tablinks li {
  float: left;
  border: 2px solid red;
  border-width: 2px 2px 0 2px;
  background: #eee;
  list-style: none;
  padding: 5px;
  margin: 0;
}
ol.tablinks li a {
  text-decoration: none;
  color: black;
}
div.tab {
  clear: left;
  border: 2px solid red;
  border-width: 1px 2px 2px 2px;
}
</style>

<script type="text/JavaScript">
var tabify = {
  hasClass: function(el,c) {
    return el.className.match(new RegExp('(\\s|^)'+c+'(\\s|$)'));        
  },
  addClass: function(el,c) {
    if (!tabify.hasClass(el,c)) el.className += " " + c;
  },
  removeClass: function(el,c) {
    if (tabify.hasClass(el,c)) {
      el.className=el.className.replace(new RegExp('(\\s|^)'+c+'(\\s|$)'),' ');
    }
  },
  hideAllTabs: function(ol) {
    var links = ol.getElementsByTagName("a");
    for (var i=0; i<links.length; i++) {
      tabify.setTabFromLink(links[i], "none");
   }
  },
  setTabFromLink: function(link, style) {
    var dest = link.href.match(/#(.*)$/)[1];
    document.getElementById(dest).style.display = style;
    if (style == "none") {
        tabify.removeClass(link, "active");
    } else {
        tabify.addClass(link, "active");
    }
  },
  addEvent: function(obj, type, fn) {
    if ( obj.attachEvent ) {
      obj['e'+type+fn] = fn;
      obj[type+fn] = function(){obj['e'+type+fn]( window.event );};
      obj.attachEvent('on'+type, obj[type+fn] );
    } else {
      obj.addEventListener( type, fn, false );
    }
  },  
  init: function() {
    var ols = document.getElementsByTagName("ol");
    for (var i=0; i<ols.length; i++) {
      var ol = ols[i];
      if (!/(^|\s)tablinks(\s|$)/.test(ol.className)) { continue; }
      tabify.addEvent(ol, "click", function(e) {
        var target = window.event ? window.event.srcElement : e.target;
        if (target.nodeName.toLowerCase() === "a") {
          tabify.hideAllTabs(e.target.parentNode.parentNode);
          tabify.setTabFromLink(e.target, "block");
          if (e) e.preventDefault();
          if (window.event) window.event.returnValue = false;
          return false;
        }
      }, true);
      tabify.hideAllTabs(ol);
      tabify.setTabFromLink(ol.getElementsByTagName("a")[0], "block");
    }
  }
};
tabify.addEvent(window, "load", tabify.init);
</script>

每个标签都是一个链接。每个链接上都有一个 onclick 事件处理器。该事件处理器所承担的工作有两件:它将所有的 div隐藏起来(通过上面的 display: none 方法),然后将要显示的标签的 div style 设为 display: block

你可能已经注意到了,此处的 HTML 的创建方式使得该页面在脚本失效的情况下依然能够运行;当我们点击一个不会显示或隐藏标签的链接时,该链接会跳转到适当的标签,所以该页面就算在不支持 JavaScript 的浏览器中也仍然能够正常运行。这一点很重要:这就是你可能听说过的叫做“渐进增强”的技术(或者是三年以前的术语“柔性衰减”,不过现在不再用这个名字了)。这不仅对那些使用最新的浏览器但关闭了 JavaScript 的人群来说有重要意义,而且对大量的其它用户类型来说也是如此。像专供盲人用户使用的屏幕阅读器这样的辅助技术就严重依赖于语义性而且有意义的 HTML 结构,并且这些辅助技术对 JavaScript 的页面功能增强效果的支持也不如正常用户所使用的。移动电话浏览器的脚本支持也同样有限。搜索引擎读取的也是你的 HTML,而不是你的脚本——Google 可以说是世界上最视而不见的浏览器用户了。这就是 “渐进增强”的意义之所在:从一个可用的页面开始,将其内容显示在最简单的环境中,比如纯文本的web浏览器,然后再逐渐添加额外的功能,并保证在添加每一步功能的同时,你的网站也仍然是可用的,而且功能也正常。

所有的 JavaScript 代码基本上都由两部分组成:实际执行任务的部分,以及将该代码片段与 HTML 挂钩的部分。在本例中实际执行任务的代码是很简单的:用来显示某个链接的对应标签只需要两行 JavaScript 代码:

var dest = link.href.match(/#(.*)$/)[1];
document.getElementById(dest).style.display = "block";

如果你还记得的话,链接的形式类似于<a href="#tab1">Tab 1</a> ;第一行代码使用了一个正则表达式(参见下面的注释)来提取该链接位于# 符号之后的部分:在本例中就是字符串tab1。链接的这一部分跟对应的div的ID是一样的(如曾经提到过的那样,该页面是按照语义意义创建的 ,因此 “tab”链接到对应的div的)。然后我们通过document.getElementById来获取该div的引用,并像前面所谈到的那样将 style.display 设为 block

正则表达式

正则表达式是一种微型程序设计语言,它被设计用来解决文本“解析”问题——比如说,用来回答像“在那个字符串的内部有出现过这个字符串吗?”和“在字符串‘abc123def’中,‘c’和‘d’之间的数字是什么?”这样的问题。正则表达式是非常强大的工具,但也十分复杂。后面我们会对正则表达式的用途这一问题进行描述;现在先不管它们的工作原理,待会儿再回来思考这个问题吧。

本例中的“regex”(正则表达式英文“regular expression”的缩写)是/#(.*)$/。正则表达式中的每个字符都是用来和目标字符串中的字符序列进行比较的;正则表达式就是逐段地表达出某个目标字符串是如何构成的。

  • / … / ——斜杠标出了一个正则表达式的开头和结尾,就像双引号或单引号表示一个“字符串”一样。
  • # — 井号(#)的实际意思是“与一个井号相匹配”。
  • ( … ) ——括号表示“此处是该字符串的一部分,我以后才会用到它”。
  • .* — 这个符号的意思是:“你想要的任意字符”;实际上它是一个点号(.),表示“任意一个字符”,后面跟着一个星号(*),代表了“重复任意多次”。
  • $ — 美元符号表示“字符串的结束”。

因此,我们的正则表达式所描述的“匹配模式”是“一个井号,后面跟着你想要的任何字符”。link.href 就是我们正在处理的链接的目标地址(比如说,#tab1,因为它是一个符合“后跟任意字符的井号”的描述的字符串),“匹配模式”就适用于该字符串了

,因此,link.href.match(our_regexp))将会返回 true 而不是 false;它真正的返回值是一个只有两项的数组,["#tab1", "tab1"]。第一项是与整个正则表达式相匹配的子串,第二项是与括号内部的表达式相匹配的文本;这就是为什么正则式中要有括号的原因——这是为了标记出该字符串的“这就是我们感兴趣的那几个字符”部分。tab1 就是我们想要的字符串,因此我们就可以将其从返回数组中抓取出来(因为它是数组的第二项,所以用[1]就可以将它提取出来了——数组下标是从零开始的。)

连接工作代码和页面

就如我们前面所提到的,所有的代码都有两部分:实际执行任务的部分,以及将该代码片段与HTML挂钩的部分。将工作代码与HTML挂钩就是事件存在的目的。在这个特别的例子中,我们关注的是两个事件:用于宣告“全面开始”的 window load 事件,以及当用户在某个标签上点击时触发的 click 事件。在页面加载的时候,我们需要运行该连接代码,而连接代码则应当将标签的 click 事件与前面那段代码连接起来,以显示适当的标签。

在某个事件发生时运行某段代码,这一点在大多数浏览器中都是由 addEventListener 函数来完成的,在IE中则是由 attachEvent 函数来完成的。此处我们创建的是“包装”函数,它会依据浏览器支持情况调用适当的函数:

addEvent: function(obj, type, fn) {
  if ( obj.attachEvent ) {
    obj['e'+type+fn] = fn;
    obj[type+fn] = function(){obj['e'+type+fn]( window.event );};
    obj.attachEvent('on'+type, obj[type+fn] );
  } else {
    obj.addEventListener( type, fn, false );
  }
}

(不必太担心上面代码的工作原理;现在只要不加考虑地接受就可以了——随着你的 JavaScript 经验越来越丰富,自然就会更好地理解了。)这个函数有三个参数,objtype fn,分别代表“触发该事件的对象”,“我们所关注的事件”,以及“当事件被触发时要运行的函数”。在页面加载时我们要运行的是叫做 tabify.init 的函数;然后 tabify.init 函数就会负责连接每个标签的 click 事件。

addEvent(window, "load", tabify.init);

在前面的HTML结构中可以看到,我们实际上是把一套标签表示成了一个 class="tablinks" 的有序列表。因此
tabify.init 函数就需要完成下面这些任务:

  1. 查找到页面上所有的class为 tablinks <ol>
  2. 在每个 <ol>click 事件上绑定如下代码:
    1. 准确地计算出用户所点击的是哪个标签链接
    2. 算出与该标签链接相对应的是哪个标签
    3. 显示该标签
    4. 隐藏其它所有标签

下面的 init 函数实现了全部这些功能:

init: function() {
  var ols = document.getElementsByTagName("ol");
  for (var i=0; i<ols.length; i++) {
    var ol = ols[i];
    if (!/(^|\s)tablinks(\s|$)/.test(ol.className)) { continue; }
    tabify.addEvent(ol, "click", function(e) {
      var target = window.event ? window.event.srcElement : e.target;
      if (target.nodeName.toLowerCase() === "a") {
        tabify.hideAllTabs(e.target.parentNode.parentNode);
        tabify.setTabFromLink(e.target, "block");
        if (e) e.preventDefault();
        if (window.event) window.event.returnValue = false;
        return false;
      }
    }, true);
    tabify.hideAllTabs(ol);
    tabify.setTabFromLink(ol.getElementsByTagName("a")[0], "block");
  }
}

我们一步一步地来分析这个函数,依次来看该函数每一部分代码的功能:

var ols = document.getElementsByTagName("ol");
  for (var i=0; i<ols.length; i++) {
    var ol = ols[i];
    if (!/(^|\s)tablinks(\s|$)/.test(ol.className)) { continue; }
  }

这一部分的作用是查找出页面上所有的 class 为 tablinks <ol> ——首先建立一张包含所有 <ol> 的列表,对其中每一个 <ol> 执行“如果该 <ol> 的 class 不是 ‘tablinks’ 的话,就跳过它”。(class 的核对工作是由一个正则表达式来完成的;continue 的意思是“跳过这一个,到下一个去”。)

tabify.addEvent(ol, "click", function(e) {
  ...
});

这段代码的作用是将一些代码绑定到每个 <ol> click 事件上去。

var target = window.event ? window.event.srcElement : e.target;

这段代码会准确地计算出用户所点击的是哪个标签链接…

tabify.setTabFromLink(e.target, "block");

…然后这段代码就会显示该标签…

tabify.hideAllTabs(e.target.parentNode.parentNode);

…最后,这行代码会隐藏其它所有标签。

代码中还有 setTabFromLink hideAllTabs 函数;这两个函数用上面说过的方法隐藏或显示某个标签。我们来看看它们是怎样工作的——它们是两个独立的函数,因为将一个在多处地方都会被调用的代码块分离出来,将其写为一个函数,这通常是很管用的。(这样就能使你的代码更易于理解,而且可以在多处地方重复使用。人们有时把这叫做将代码“卸开”或“重构”到某个函数中。)

此处还有一点“额外学分”的工作,上面的代码巧妙地演示了如何添加与删除属性。setTabFromLink 函数不仅显示了适当的标签,还将“被激活”的标签的 class 设置成了
class="active" 。通过三个函数,hasClassremoveClassaddClass 可以实现这个功能的。同样地,通过 removeClass 来从所有的 tablinks 的 className 中删掉"active",然后用 addClass 将 "active" 添加到实际处于激活状态的那个 tablink 。仅仅向某个链接添加一个 class 看起来可能没什么意义——毕竟,class 是不可见的——但可用通过 class 进行样式化。我们可以(而且已经这么做了)通过添加额外的 CSS ,来使带有
class="active" 的链接的外观与其它链接区分开来:

ol.tablinks li a {
  background-color: red;
}

ol.tablinks li a.active {
  background-color: white;
}

因此,现在"active"的那个标签就成了白色的,而其它的标签是红色的。通过 JavaScript 来添加和删除 class 是非常常见的一种技术,你应该尽可能地使用它;CSS 在 HTML 元素布局,控制 HTML 元素位置和外观方面非常出色,因此利用 JavaScript 来改变这些元素的 class 就意味着它们可以采用各种不同的 CSS 样式。这是一种理想的联合方式; JavaScript 可以使你的元素改变状态,但它本身并不会对元素做多大的改变。 JavaScript 做的只是给该元素添加一个class,然后把其它活儿留给 CSS 去做。

创建 HTML

DOM 脚本不止能直接修改已有的 HTML 的 CSS 属性。你也可以从动态地创建页面中的新 HTML 元素。比如说,在技术新闻网站 Slashdot 上,某个位于注释之中的链接会在方括号中显示自己的目标地址,因此像
<a href="http://opera.com">A web browser</a>这样的链接就会显示成这样
<a href="http://opera.com">A web browser</a> [opera.com]。(之所以这样做是为了防止人们感觉被捉弄了)我们可以很方便地用 JavaScript 来添加这段 HTML 代码,并显示页面上每个链接的目标域名。

创建新的HTML元素是通过
document.createElement 函数来实现的。在本例中,我们只需添加一样东西:在每个链接之后,我们会添加一个包含着文本的
span 元素,该元素内的文本列出了该链接的域名(实际效果请点击 linkify.html )。本例的 HTML 如下所示:

<p id="start">This is some text that has 
<a href="http://www.w3.org/TR/html4/struct/links.html">links</a> in it
to various places, including <a href="http://www.opera.com">Opera</a>,
<a href="http://www.bbc.co.uk/">the BBC</a> and an internal link to
<a href="#start">the beginning of this section</a>. All the external links
should have <span>[domain]</span> after them.</p>

本例的 JavaScript 如下所示:

<script type="text/JavaScript">
var linksuffix = {
  addEvent: function(obj, type, fn) {
    if ( obj.attachEvent ) {
      obj['e'+type+fn] = fn;
      obj[type+fn] = function(){obj['e'+type+fn]( window.event );};
      obj.attachEvent('on'+type, obj[type+fn] );
    } else {
      obj.addEventListener( type, fn, false );
    }
  },  
  init: function() {
    var links = document.getElementById("linksuffixtext").getElementsByTagName("a");
    for (var i=0; i<links.length; i++) {
      var matches = links[i].href.match(/^http:\/\/(.*?)\//);
      if (matches) {
        var linkdomain = matches[1];
        var span = document.createElement("span");
        var spantext = document.createTextNode(" ["+linkdomain+"]");
        span.appendChild(spantext);
        links[i].parentNode.insertBefore(span, links[i].nextSibling);
      }
    }
  }
};
linksuffix.addEvent(window, "load", linksuffix.init);
</script>

本例中实现具体功能的脚本如下:

var links = document.getElementsByTagName("a");
for (var i=0; i<links.length; i++) {
  var matches = links[i].href.match(/^http:\/\/(.*?)\//);
  if (matches) {
    var linkdomain = matches[1];
    var span = document.createElement("span");
    var spantext = document.createTextNode(" ["+linkdomain+"]");
    span.appendChild(spantext);
    links[i].parentNode.insertBefore(span, links[i].nextSibling);
  }
}

下面分步解释上面代码:

var links = document.getElementsByTagName("a");
for (var i=0; i<links.length; i++) {
  ...
}

首先,它会查找出文档中所有的链接(getElementsByTagName("a"))。

var matches = links[i].href.match(/^http:\/\/(.*?)\//);
if (matches) {
  ...
}

这一行代码在每个链接上都使用了一个正则表达式,来查看该链接的目标地址是否由 http://something/开始的。如果是的话…

var linkdomain = matches[1];
var span = document.createElement("span");
var spantext = document.createTextNode(" ["+linkdomain+"]");
span.appendChild(spantext);

…接下来的这部分代码会首先获取 “域名链接”,也就是该链接的 www.opera.com 部分。然后它通过
document.createElement 创建了一个 <span> 元素。接着,这段代码创建了一个“文本节点”。由于 HTML 元素本身是由 document.createElement 创建的,而 HTML 文档中的所有文本实际上都包含在“文本节点”中,我们就得分开创建这些文本节点。在编写具体的 HTML 时你不必为此担心(你甚至不必知道这一点),但在用DOM脚本创建元素的时候你就必须对这一点有所了解了。文本节点中的文本就是具体的“[域名]”,该文本是通过合并字符串(添加到一起)而创建的。最后通过 <span>appendChild 方法来将文本节点放入 span 之内。

links[i].parentNode.insertBefore(span, links[i].nextSibling);

上面这行代码的作用是将 span 添加到文档中。此处,span 是一个 HTML 元素的引用,此元素如下所示:

<span> [example.com]</span>

但这个元素并不是该文档的组成部分。它目前还不是任何文档的组成部分;它还在四处飘流,毫无着落。我们可以通过下面两种方式之一来将该元素添加到文档中:使用上面所说的 appendChild,或通过 insertBeforeappendChild 函数会将我们的新元素添加到某个已有元素的末端(所以它才叫做添加)。由于我们想将该元素添加到已有元素的之间,我们就需要用到 insertBefore。我们当前的 HTML 是:

<p>... text that has 
<a href="http://www.w3.org/TR/html4/struct/links.html">links</a> in it
to ...

这段代码等价于图1所示的 DOM 树:

DOM tree before the addition of the span element after the link

图1:尚未在链接之后添加 span 元素时的 DOM 树。

我们应该将新的 span 元素添加到 <a> 和 “in it to” 文本节点之间,这样该元素就会出现在 <a> 后面了。改动之后的 DOM 树如图2所示:

DOM tree after the addition of the span element

图2:添加 span 元素之后的 DOM 树

其 HTML 如下:

<p>... text which has 
<a href="http://www.w3.org/TR/html4/struct/links.html">links</a>
<span> [domain]</span> in it to ...

如果在此处能够直接“将我们的新 span 插入到链接之后”就更方便了。不幸的是,并没有 insertAfter 函数。所以,我们就得将其插入到紧跟在该链接之后的内容之前(这实在是有点混乱,但是仔细想想就能明白其含义)。到“标记为el的元素之后的内容”的最快的途径就是 el.nextSibling 。我们需要在自己要插入的元素上调用 insertBefore 函数,也就是该链接的父元素 <p> ,我们可以通过
link.parentNode 来快速访问其父元素。上述调用的完整形式如下:

links[i].parentNode.insertBefore(span, links[i].nextSibling);

这段代码的功能是找出我们当前正在处理的链接( links[i] )的父元素(<p>),并将我们创建好的 span 元素( span )插入到跟在该链接之后的内容( links[i].nextSibling )之前。将 HTML 作为 DOM 树来处理并使用上面方法向其插入新元素的,这可能会让刚接触的人感到困惑,但随着练习增多,你很快就会变得越来越清楚的。

总结

HTML 所提供的是页面的结构,而 CSS 提供的是对其外观的描述。 JavaScript 所带来的是灵活性;你的 HTML 结构和 CSS 样式都会因此成为动态的。你可以基于用户的操作动态改变页面的外观和功能。接下来我们讨论的是:从经过深思熟虑而且结构良好的信息到随用户需求而改变的数据。你可以显示和隐藏元素,改变样式和外观,还可以按照需要,添加新 HTML 或删除旧的 HTML 。

练习题

  • 如何设置某个元素的 CSS 的 display 属性,从而隐藏该元素?
  • 元素和文本节点之间的区别是什么?
  • 举出两个理由,说明为什么渐进增强很重要。
  • appendChild insertBefore 之间的差别是什么?
  • 为什么我们要用 addClass 函数,而不是直接将新的 class 名和已有元素的 className 属性连接在一起?
  • 在下面的 HTML 结构中,document.getElementById("marker").nextSibling 所指的元素是什么?

    <p>This is a <strong>block of HTML with 
      <span id="marker">various markup<span> in it</strong>.</p>