第 11 章: 网页编程:速成课程

您很可能在网页浏览器中阅读本文,因此您可能对万维网至少有一些了解。本章将对构成万维网运作的各个元素以及它们与 JavaScript 之间的关系进行简要的、浅显的介绍。接下来的三章更具实用性,将展示 JavaScript 可以用于检查和更改网页的一些方法。


互联网本质上只是一个遍布全球的计算机网络。计算机网络使得计算机可以互相发送消息。网络背后的技术是一个有趣的话题,但不是本书的主题。您只需要知道,通常,一台计算机(我们将称之为 服务器)在等待其他计算机开始与它通信。一旦另一台计算机(客户端)与该服务器建立通信,它们将使用某种特定语言(协议)交换需要交换的信息。

互联网用于传输许多不同协议的消息。有用于聊天的协议,用于文件共享的协议,恶意软件用来控制安装了它的可怜虫的计算机的协议,等等。我们感兴趣的协议是万维网使用的协议。它被称为 HTTP,即超文本传输协议,用于检索网页及其相关文件。

在 HTTP 通信中,服务器是存储网页的计算机。客户端是向服务器请求页面的计算机(例如您的计算机),以便它可以显示该页面。这种请求页面被称为“HTTP 请求”。


可以通过互联网访问的网页和其他文件由 URL 标识,它是通用资源定位器的缩写。URL 看起来像这样

http://acc6.its.brooklyn.cuny.edu/~phalsall/texts/taote-v3.html

它由三个部分组成。开头部分,http://,表示该 URL 使用 HTTP 协议。还有一些其他协议,例如 FTP(文件传输协议),也使用 URL。下一部分,acc6.its.brooklyn.cuny.edu,命名了可以找到该页面的服务器。URL 的结尾,/~phalsal/texts/taote-v3.html,命名了该服务器上的一个特定文件。

大多数时候,万维网是通过浏览器访问的。在输入 URL 或单击链接后,浏览器将向相应的服务器发出相应的 HTTP 请求。如果一切顺利,服务器将通过将文件发送回浏览器来响应,浏览器将以某种方式将其显示给用户。

当检索到的文件(如示例中)是 HTML 文档时,它将被显示为网页。我们在 第 6 章 中简要讨论了 HTML,我们看到了它可以引用图像文件。在 第 9 章 中,我们发现 HTML 页面还可以包含 <script> 标签来加载 JavaScript 代码文件。当显示 HTML 文档时,浏览器将从其服务器获取所有这些额外的文件,以便将其添加到文档中。


虽然 URL 应该指向一个文件,但网页服务器可以执行比仅仅查找文件并将其发送给客户端更复杂的操作。——它可以先以某种方式处理该文件,或者可能根本没有文件,只有程序可以根据给定的 URL 以某种方式生成相关文档。

在服务器上转换或生成文档的程序是使网页不那么静态的常用方法。当文件只是一个文件时,它总是相同的,但是当有一个程序每次在请求时构建它时,它可以被设置为对每个人看起来不同,这取决于一些因素,例如这个人是否已登录或指定了一些特定偏好。这也使管理网页内容变得容易得多——不用每次在网站上添加新内容时都添加一个新的 HTML 文件,而是将新文档添加到某个中心存储中,程序知道在哪里可以找到它以及如何将其显示给客户端。

这种网页编程被称为 服务器端编程。它在文档发送给用户之前影响文档。在某些情况下,在页面发送后、用户正在查看页面时运行程序也是很实用的。这被称为 客户端编程,因为程序在客户端计算机上运行。客户端网页编程正是 JavaScript 的发明目的。


在客户端运行程序存在一个固有的问题。您永远无法真正提前知道您正在访问的页面将要运行哪些类型的程序。如果它可以从您的计算机发送信息到其他计算机、损坏某些东西或侵入您的系统,那么上网将是一项相当危险的活动。

为了解决这个难题,浏览器严格限制了 JavaScript 程序可以执行的操作。它不允许查看您的文件,也不允许修改与它附带的网页无关的任何内容。像这样隔离编程环境被称为 沙盒。允许程序有足够的空间发挥作用,同时限制它们以防止它们造成伤害,这不是一件容易的事。每隔几个月,一些 JavaScript 程序员就会想出一种新的方法来规避限制并做一些有害或侵犯隐私的事情。负责浏览器的开发人员会通过修改他们的程序来使这种技巧失效,一切又恢复正常——直到下一个问题被发现。


第一个被广泛使用的 JavaScript 技巧之一是 window 对象的 open 方法。它以 URL 为参数,将打开一个新窗口显示该 URL。

var perry = window.open("http://www.pbfcomics.com");

除非您在 第 6 章 中关闭了弹出窗口拦截器,否则这个新窗口可能会被拦截。弹出窗口拦截器存在是有充分理由的。网页程序员,尤其是那些试图让人们关注广告的人,已经过度使用了可怜的 window.open 方法,以至于现在大多数用户都痛恨它。不过,它还是有其用处的,在这本书中,我们将使用它来显示一些示例页面。作为一个一般原则,您的脚本不应打开任何新窗口,除非用户请求它们。

请注意,由于 open(就像 setTimeout 及其同类)是 window 对象上的一个方法,因此可以省略 window. 部分。当以“正常”方式调用函数时,它作为顶级对象的方法被调用,而 window 就是顶级对象。就我个人而言,我认为 open 有点泛化,所以我通常会键入 window.open,这样可以清楚地表明是正在打开一个窗口。

window.open 返回的值是一个新窗口。这是在该窗口中运行的脚本的全局对象,包含所有标准内容,例如 Object 构造函数和 Math 对象。但是,如果您尝试查看它们,大多数浏览器(可能)不会让您查看……

show(perry.Math);

这是我之前提到的沙盒的一部分。由您的浏览器打开的页面可能会显示仅供您查看的信息,例如在您登录的网站上,因此如果任何随机脚本都可以访问和读取这些信息,那就很糟糕了。对此规则的例外是同一域上打开的页面:当在 eloquentjavascript.net 上的页面上运行的脚本打开同一域上的另一个页面时,它可以对该页面执行任何操作。

打开的窗口可以使用其 close 方法关闭。如果您还没有自己关闭它……

perry.close();

其他类型的子文档,例如框架(文档中的文档),从 JavaScript 程序的角度来看也是窗口,并且拥有自己的 JavaScript 环境。事实上,您在控制台中可以访问的环境属于隐藏在这个页面某个地方的一个小而不可见的框架——这样,您意外搞砸整个页面的可能性会稍微低一些。


每个窗口对象都有一个 document 属性,它包含一个表示该窗口中显示的文档的对象。该对象包含例如一个属性 location,它包含有关文档 URL 的信息。

show(document.location.href);

document.location.href 设置为新的 URL 可以用来让浏览器加载另一个文档。document 对象的另一个应用是其 write 方法。该方法在给定字符串参数时,将一些 HTML 写入文档。当它在完全加载的文档上使用时,它将用给定的 HTML 替换整个文档,这通常不是您想要的。其目的是让脚本在文档正在加载时调用它,在这种情况下,写入的 HTML 将插入到触发它的 script 标签的位置。这是一种向页面添加一些动态元素的简单方法。例如,以下是一个简单的文档,显示当前时间。

print(timeWriter);
var time = viewHTML(timeWriter);
time.close();

通常,第 12 章 中展示的技术提供了一种更干净、更通用的方法来修改文档,但偶尔,document.write 也是做一些事情最简单、最方便的方法。


网页中 JavaScript 的另一个常见应用围绕 表单。如果您不太确定“表单”的作用,让我简要总结一下。

基本的 HTTP 请求是对文件的简单请求。当该文件不是真正被动文件,而是服务器端程序时,在请求中包含除文件名以外的信息可能会变得有用。为此,HTTP 请求允许包含额外的“参数”。以下是一个示例

http://www.google.com/search?q=aztec%20empire

在文件名(/search)之后,URL 以问号继续,问号之后是参数。该请求包含一个参数,名为 q(代表“查询”,大概是),其值为 aztec empire%20 部分对应于空格。这些值中不能包含许多字符,例如空格、& 符号或问号。这些字符通过用 % 后跟其数值1 来“转义”,这与字符串和正则表达式中使用的反斜杠的作用相同,但更难读。

JavaScript 提供了 encodeURIComponentdecodeURIComponent 函数,用于在字符串中添加这些代码并再次移除它们。

var encoded = encodeURIComponent("aztec empire");
show(encoded);
show(decodeURIComponent(encoded));

当一个请求包含多个参数时,它们之间用与符号隔开,例如...

http://www.google.com/search?q=aztec%20empire&lang=nl

表单本质上是一种简化浏览器用户创建参数化 URL 的方法。它包含许多字段,例如文本输入框、可以“选中”和“取消选中”的复选框,或允许您从给定值集中选择内容的选项框。它通常还包含一个“提交”按钮,以及对用户不可见的“操作”URL,表单将发送到该 URL。当单击提交按钮或按下 Enter 键时,在字段中输入的信息将作为参数添加到此操作 URL 中,浏览器将请求该 URL。

这是一个简单表单的 HTML 代码

<form name="userinfo" method="get" action="info.html">
  <p>Please give us your information, so that we can send
  you spam.</p>
  <p>Name: <input type="text" name="name"/></p>
  <p>E-Mail: <input type="text" name="email"/></p>
  <p>Sex: <select name="sex">
            <option>Male</option>
            <option>Female</option>
            <option>Other</option>
          </select></p>
  <p><input name="send" type="submit" value="Send!"/></p>
</form>

表单的名称可以用于使用 JavaScript 访问它,我们将在稍后看到。字段的名称决定了用于存储其值的 HTTP 参数的名称。发送此表单可能会产生类似这样的 URL

http://planetspam.com/info.html?name=Ted&[email protected]&sex=Male

表单中可以使用许多其他标签和属性,但在这本书中,我们将坚持使用简单的标签,以便我们可以专注于 JavaScript。


上面显示的示例表单的 method="get" 属性表示此表单应将接收到的值编码为 URL 参数,如之前所示。有一种发送参数的替代方法,称为 post。使用 post 方法的 HTTP 请求除了 URL 之外,还包含一个数据块。使用 post 方法的表单会将参数的值放入此数据块中,而不是放在 URL 中。

在发送大量数据时,get 方法会导致 URL 变得非常长,因此 post 通常更方便。但这两种方法之间的区别不仅仅是方便性问题。传统上,get 请求用于只向服务器请求文档的请求,而 post 请求用于执行更改服务器上内容的操作。例如,获取互联网论坛上最近消息的列表将是一个 get 请求,而添加新消息将是一个 post 请求。大多数页面遵循这种区分是有充分理由的——自动浏览网络的程序,例如搜索引擎使用的程序,通常只执行 get 请求。如果可以通过 get 请求更改网站,这些“爬虫”可能会造成各种损害。


当浏览器显示包含表单的页面时,JavaScript 程序可以检查和修改表单字段中输入的值。这为各种技巧提供了可能性,例如在将值发送到服务器之前检查它们,或自动填写某些字段。

上面显示的表单可以在 example_getinfo.html 文件中找到。打开它。

var form = window.open("example_getinfo.html");

当 URL 不包含服务器名称时,它被称为 相对 URL。浏览器将相对 URL 解释为引用与当前文档位于同一服务器上的文件。除非它们以斜杠开头,否则将保留当前文档的路径(或目录),并将给定的路径附加到它。

我们将向表单添加一个有效性检查,使其只有在名称字段未留空且电子邮件字段包含类似有效电子邮件地址的内容时才提交。由于我们不再希望表单在按下“发送!”按钮时立即提交,因此它的 type 属性已从 "submit" 更改为 "button",这将其变成了一个没有任何效果的普通按钮。——第 13 章将展示一种好的方法,但现在,我们使用简单的方法。


为了能够使用新打开的窗口(如果您已关闭它,请先重新打开它),我们将控制台“附加”到它,如下所示

attach(form);

执行完此操作后,从控制台中运行的代码将在给定的窗口中运行。为了验证我们确实正在使用正确的窗口,我们可以查看文档的 locationtitle 属性。

print(document.location.href);
print(document.title);

由于我们进入了一个新的环境,之前定义的变量,例如 form,不再存在。

show(form);

要返回到我们的初始环境,我们可以使用 detach 函数(不带参数)。但首先,我们必须将该验证系统添加到表单中。


文档中显示的每个 HTML 标签都与一个 JavaScript 对象相关联。这些对象可用于检查和操作文档的几乎所有方面。在本章中,我们将使用表单和表单字段的对象,第 12 章将详细介绍这些对象。

document 对象有一个名为 forms 的属性,它包含指向文档中所有表单的链接(按名称)。我们的表单有一个属性 name="userinfo",因此它可以在 userinfo 属性下找到。

var userForm = document.forms.userinfo;
print(userForm.method);
print(userForm.action);

在这种情况下,赋予 HTML form 标签的 methodaction 属性也作为 JavaScript 对象的属性存在。这种情况很常见,但并非总是如此:某些 HTML 属性在 JavaScript 中的拼写方式不同,另一些则根本不存在。第 12 章将展示一种获取所有属性的方法。

form 标签的对象有一个 elements 属性,它引用一个包含表单字段的对象(按名称)。

var nameField = userForm.elements.name;
nameField.value = "Eugène";

文本输入对象有一个 value 属性,可用于读取和更改其内容。如果您在运行上述代码后查看表单窗口,您会发现名称已填写。


例 11.1

能够读取表单字段的值使得能够编写一个名为 validInfo 的函数,该函数以表单对象作为其参数并返回一个布尔值:当 name 字段不为空且 email 字段包含类似电子邮件地址的内容时,返回 true;否则返回 false。编写此函数。

function validInfo(form) {
  return form.elements.name.value != "" &&
    /^.+@.+\.\w{2,3}$/.test(form.elements.email.value);
}

show(validInfo(document.forms.userinfo));

您确实想用正则表达式进行电子邮件检查,对吧?


现在我们只需确定人们单击“发送!”按钮时会发生什么。目前,它什么也不做。可以通过设置它的 onclick 属性来解决此问题。

userForm.elements.send.onclick = function() {
  alert("Click.");
};

就像赋予 setIntervalsetTimeout 的操作一样(第 8 章),存储在 onclick(或类似属性)中的值可以是函数或 JavaScript 代码字符串。在本例中,我们赋予它一个打开警报窗口的函数。尝试单击它。


例 11.2

通过为按钮的 onclick 属性赋予一个新值——一个检查表单、在表单有效时提交或在表单无效时弹出警告消息的函数——来完成表单验证器。了解表单对象有一个 submit 方法,该方法不接受任何参数并提交表单,这一点将很有用。

userForm.elements.send.onclick = function() {
  if (validInfo(userForm))
    userForm.submit();
  else
    alert("Give us a name and a valid e-mail address!");
};

与表单输入以及其他可以“选择”的内容(例如按钮和链接)相关的另一个技巧是 focus 方法。当您确定用户将在进入页面时立即开始在某个文本字段中输入内容时,您可以让您的脚本从将光标放置在该字段中开始,这样他就不必单击它或以其他方式选择它。

userForm.elements.name.focus();

由于表单位于另一个窗口中,因此可能无法清楚地看到选择了什么内容,具体取决于您使用的浏览器。有些页面还会在您完成填写一个字段时自动将光标跳转到下一个字段——例如,当您输入邮政编码时。这应该适度——它会使页面以用户不希望的方式行为。如果他习惯于按 Tab 手动移动光标,或者误输入了最后一个字符并想删除它,那么这种神奇的光标跳转就非常令人讨厌。


detach();

测试验证器。当您输入有效信息并单击按钮时,表单应提交。如果控制台仍然附加到它,这会导致它分离,因为页面重新加载,并且 JavaScript 环境被一个新的环境替换。

如果您还没有关闭表单窗口,这会关闭它。

form.close();

以上看起来可能很简单,但我向你保证,客户端网页编程并非易事。它有时会是一段非常痛苦的经历。为什么?因为应该在客户端计算机上运行的程序通常必须适用于所有流行的浏览器。这些浏览器中的每一个都倾向于以稍微不同的方式工作。更糟糕的是,它们中的每一个都包含一组独特的问题。不要假设一个程序只是因为是由一家价值数十亿美元的公司制作的,就一定是没有错误的。因此,我们这些网页程序员有责任严格测试我们的程序,找出问题所在,并找到解决问题的方法。

有些人可能会想,“我会向浏览器制造商报告我发现的任何问题/错误,他们一定会立即解决这些错误”。这些人将面临重大失望。Internet Explorer 的最新版本(仍然被大约 70% 的网民使用,并且每个网页开发人员都喜欢嘲笑它)仍然存在一些已知超过五年的错误。而且是严重的错误。

但不要让这让你气馁。只要具备合适的强迫症思维方式,这些问题就能够提供绝妙的挑战。对于那些不喜欢浪费时间的人来说,谨慎行事,避免浏览器功能的隐蔽角落,通常可以防止你遇到太多麻烦。


抛开 bug 不谈,浏览器之间界面上的设计差异仍然构成一项有趣的挑战。当前的情况大致如下:一方面,存在所有“小型”浏览器:Firefox、Safari 和 Opera 是其中最重要的,但还有更多。这些浏览器都尽力遵守由 W3C 制定的或正在制定的标准集,W3C 是一家组织,致力于通过定义诸如此类标准接口来使 Web 变得不那么令人困惑。另一方面,存在微软的浏览器 Internet Explorer,它在许多标准尚未真正存在的时候就占据了主导地位,并没有做出太多努力来适应其他人的做法。

在某些方面,例如从 JavaScript 访问 HTML 文档内容的方式 (第 12 章),标准是基于 Internet Explorer 发明的方法,并且所有浏览器上的工作方式或多或少都相同。在其他方面,例如事件(鼠标点击、按键以及类似操作)的处理方式 (第 13 章),Internet Explorer 的工作方式与其他浏览器有根本的不同。

长期以来,由于部分原因是由于普通 JavaScript 开发人员的无知,部分原因是由于浏览器不兼容性在 Internet Explorer 4 或 5 版本以及 Netscape 的旧版本仍然很常见时要糟糕得多,处理这种差异的通常方法是检测用户正在运行的浏览器,并在代码中添加每个浏览器的备用解决方案——如果是 Internet Explorer,则执行此操作,如果是 Netscape,则执行该操作,如果是我们没有考虑的其他浏览器,那就只能寄希望于一切顺利。你可以想象这样的程序有多么丑陋、令人困惑和冗长。

许多网站也会在使用“不支持”的浏览器打开时拒绝加载。这导致一些小型浏览器不得不忍气吞声,假装自己是 Internet Explorer,以便能够加载这些页面。navigator 对象的属性包含有关页面加载所用浏览器的信息,但由于这种欺骗行为,这些信息并不可靠。看看你的浏览器怎么说2

forEachIn(navigator, function(name, value) {
  print(name, " = ", value);
});

更好的方法是尝试将我们的程序与不同浏览器之间的差异“隔离”。例如,如果你需要了解有关事件的更多信息,例如我们通过设置发送按钮的 onclick 属性来处理的点击事件,你必须查看 Internet Explorer 上名为 event 的顶级对象,但你必须使用传递给其他浏览器事件处理函数的第一个参数。为了处理这种情况以及其他与事件相关的差异,可以编写一个用于将事件附加到事物的辅助函数,该函数负责所有管道工作,并允许事件处理函数本身在所有浏览器中保持一致。在 第 13 章 中,我们将编写这样一个函数。

(注意:以下各章中提到的浏览器怪癖指的是 2007 年初的情况,在某些方面可能不再准确。)


这些章节将只对浏览器接口主题进行一些肤浅的介绍。它们不是本书的主要主题,而且它们本身就足够复杂,可以写成一本厚厚的书。当你了解这些接口的基础知识(并了解一些 HTML)后,在网上查找特定信息就不太困难了。FirefoxInternet Explorer 浏览器的接口文档是一个不错的起点。

下一章中的信息将不会涉及“上一代”浏览器的怪癖。它们涉及 Internet Explorer 6、Firefox 1.5、Opera 9、Safari 3 或相同浏览器的任何更新版本。其中大部分内容可能也适用于 Konqueror 等现代但不为人知的浏览器,但尚未经过广泛验证。幸运的是,这些上一代浏览器已经基本消失了,几乎不再使用。

但是,有一组 web 用户仍然使用没有 JavaScript 的浏览器。这组用户中的很大一部分是使用常规图形浏览器,但出于安全原因禁用了 JavaScript 的用户。然后是使用文本浏览器或盲人浏览器的人。在开发“严肃”网站时,通常最好从一个有效的纯 HTML 系统开始,然后使用 JavaScript 添加非必要的技巧和便利功能。

  1. 字符获得的值由 ASCII 标准决定,该标准将数字 0 到 127 分配给拉丁字母使用的字母和符号集。该标准是 第 2 章 中提到的 Unicode 标准的前身。
  2. 一些浏览器似乎隐藏了 navigator 对象的属性,在这种情况下,它将不打印任何内容。