在过去的几年中,DHTML几乎仅用于邪恶目的,这已经不是什么秘密了。 用户将技术与侵入性广告和容易出错的页面相关联,而开发人员将其与浏览器检测和丑陋的黑客相关联。
这个假设是不幸的。 浏览器技术在过去几年中取得了长足的进步。 如果操作正确,DHTML可以改善许多网页的用户体验。 而且过去几乎不需要使所有工作都能正常工作的骇客。
当使用现代DHTML时,我常常会回想起Web开发的过去,那是我最初对该技术感兴趣的时候。 尽管有我最好的意图,但我的许多第一个脚本现在都代表了当今DHTML编码人员应避免的示例-它们过于依赖特定的浏览器功能,并且在不满足这些要求时会抛出错误或不当降级。 它们不能与同一页面上的其他脚本很好地配合使用,并且有时会创建对其他技术的依赖关系。
当我遇到这样的脚本时,我认为它们表现不佳。 他们有潜力成为好人。 所有工具都在那里。 他们根本没有做应做的事情。
随着我成为Web开发人员的成长,我开始欣赏表现良好的DHTML的价值。 我总是可以向客户推销该脚本在任何浏览器中都可以运行或无法正常运行的事实。 他们并不总是欣赏明显的概括,就像如今几乎每个人都启用了DHTML一样,在不支持它的情况下,代码会优雅地降级。
我还注意到,在开发这种DHTML时,我倾向于一遍又一遍地遵循相同的五步过程。 以下是使用此过程创建非常基本的DHTML脚本的示例。 一旦理解了这些原理,就可以将此过程应用于大多数DHTML效果,并获得令人印象深刻的结果。
此处提供的代码示例假定您对JavaScript,HTML和DOM有所了解。 但是,任何Web开发人员或设计人员都应该能够从熟悉此过程中获得一些价值。
标签脚本
Web上DHTML的常见用法是创建动态标签。 动态标签用于标记表单字段。 但是,标签的文本呈现在form字段内,而不是与其相邻(这是更常见的)。
当表单字段引起注意时,标签消失,以便用户键入。 如果用户未键入任何内容,则在用户单击或离开该字段的选项卡后,标签将立即恢复。
动态标签可节省空间,看起来清晰且手感光滑。 在正确的情况下,它们可以是对基本表单标签的不错的改进。
幼稚的开发人员可能会实现这样的动态标签脚本:
<input type="text" name="username" value="username"
onfocus="if (this.value == 'username') this.value = '';"
onblur="if (this.value == '') this.value = 'username';" />
示例A显示了这种类型的实现。
这是有效的第一步,仅此而已。 像这样的DHTML就是过去设计不良的脚本的一个示例,绝不应该将其纳入任何生产网站中。
让我们一一看一下这些问题:
- 依靠JavaScript
如果禁用JavaScript,则效果不起作用。 在这种情况下,标签实际上仍会显示,因为它已硬编码到字段的value属性中。 但是,当用户聚焦表单时,什么也不会发生。 用户体验被严重破坏–可能比在字段旁边仅存在普通文本标签的情况更糟。
- 耦合到处理表格的代码
耦合是编程界中的一个术语,用于指示何时将两个组件的实现紧密地捆绑在一起-通常很不好。 耦合意味着当一个组件的代码更改时,另一组件的代码也可能必须更改。
在我们的案例中,创建效果的JavaScript与处理表单的服务器代码紧密耦合。 服务器代码必须知道每个表单字段的标签,并能够将其过滤出表单提交。 这是因为我们已将标签的文本放在每个字段的value属性中。 如果用户未在这些字段(或任何一个)中键入任何内容,则实际上将提交标签。
要查看实际操作的示例,只需单击提交,而无需在示例A中输入任何内容。
- 独家绑定到事件处理程序
新手DHTML脚本中常见的障碍是它们直接设置元素的事件属性的值。 您可以通过元素的属性或在带有属性的JavaScript中进行操作。 直接设置JavaScript事件通常不是一个好主意,因为每个事件只有一个代码块可以使用。 如果您开始在一个页面上运行多个脚本,则各个脚本的事件处理程序可能会相互覆盖。 这种DHTML较难维护,并可能导致难以调试的错误。
在现代浏览器中,我们可以使用事件监听器将多个函数绑定到特定事件。 除非绝对需要,否则请避免使用旧的事件处理方式。
- 非模块化设计
该脚本不是模块化设计的。 如果我们决定实现另一个动态标签,则别无选择,只能将当前代码复制并粘贴到该框的事件处理程序中,并更改标签文本显示的各个位置。
如果我们在脚本中发现错误或要进行更改,则必须记住要对每个标签进行更改。 如果决定更改标签文本,则必须在三个地方进行更改。 非模块化设计的程序很难维护和开发,因为它们容易出错。 容易出错且难以调试。
现在,我们已经分析了第一个动态标签脚本中的问题,我们对脚本的下一个迭代中的目标有一个很好的了解。 简而言之,我们需要一个动态标签脚本:
- 不依赖JavaScript
- 不与任何其他组件耦合
- 不排他地绑定任何事件
- 模块化设计
编写行为良好的DHTML的5个步骤
我们生产动态标签脚本的目标与大多数DHTML网页增强的目标没有什么不同。 实际上,我编写的几乎所有脚本都具有相同的目标。
随着时间的流逝,我发现几乎所有DHTML效果都可以遵循一个简单的过程来确保达到以下目标:
- 确定效果的基本逻辑结构。
- 创建一个完整的效果示例。
- 确定所有用户代理要求。
- 满足代理要求时,编写代码以转换逻辑结构。
- 彻底测试每个目标平台。
步骤1:确定效果的基本逻辑结构
我们的主要目标之一是避免对JavaScript的依赖。 解决此问题的一种常见但最终有缺陷的方法是尝试检测服务器上的“受支持”浏览器。 如果支持浏览器,则会向其发送代码的动态版本。 否则,将发送一个更简单的版本。
问题在于,几乎不可能明确检测服务器上的浏览器类型和版本。 即使可以,您也将无法检测到是否确实为特定用户启用了JavaScript。 浏览器根本没有向服务器发送足够的信息以可靠地标识其自身或配置。
避免依赖JavaScript的最佳方法是在不需要DHTML效果的简单,逻辑文档结构的基础上构建DHTML效果。 如果支持,该效果将在客户端上动态启用。 否则,用户将看到基本文档。
由于
label
HTML元素的存在,我们动态标签的逻辑结构可以很好地工作。标签元素在结构上将表单元素链接到其文本标签。 在大多数可视浏览器中,使用label元素和任何其他元素(或根本没有元素)之间的唯一触觉差异是,单击标签会将表单聚焦在与该标签关联的字段上。
但是,在这一点上,我们有兴趣简单地为我们的效果构建最符合逻辑的底层结构,因此我们将使用label元素。 例B展示了我们的工作。
显然这里没有什么幻想,而这正是我们想要的。 此步骤中的代码是我们效果的最低公分母视图。 理想情况下,无论是在最新版本的Mozilla中还是在手机上查看,该文档都应该有意义。 用户可以通过该文档查看其浏览器是否不具有我们的效果所要求的功能,或者未启用它们。
步骤2:创建最佳情况下效果的完整工作示例
一旦有了适当的逻辑结构,接下来要做的就是对其进行修改以创建效果的完整示例。 不必担心脚本在这一点上将如何降级,只需假设您所需的每个功能都将可用并已启用就可以使其运行。
从第一步开始看我们的工作,很容易看到我们必须为每个动态标签完成的高级任务才能显示出我们的效果:
- 隐藏常规的HTML标签元素。
- 将JavaScript函数附加到关联字段的onfocus和onblur事件,这些事件在正确的时间显示和隐藏标签。
完成第一个任务的最简单方法是使用CSS规则,如下所示:
<style type="text/css">
label {
display:none;
}
</style>如果您不熟悉CSS,可以在SitePoint.com或W3C上获得快速入门。
像这样的简单CSS规则的问题在于它将关闭页面上每个标签的显示。 如果要在具有要以常规方式显示标签元素的页面上使用该规则,而没有任何效果,则必须修改该规则。 这根本不是一个模块化的设计。
当然,解决方案是为我们要动态表现的标签提供一个特殊的类:
<style type="text/css">
label.dynamic {
display:none;
}
</style>第二项任务本质上要求我们遍历页面上的所有标签元素,检查它们是否具有正确的类,如果有,则将事件处理程序添加到其关联的字段中。 我们还应该将标签文本的副本保存在字段的属性中,以方便访问,并在此处初始化标签显示。
这需要一些文档对象模型的知识。 如果您对细节不满意,或者从未花时间学习,可以在W3C上学习 。 浏览器供应商通常也有很好的资源(例如Microsoft和Mozilla ),尽管这些资源显然偏向于自己的实现。
在理想的情况下,一旦我们了解了DOM的工作原理,就可以使用以下代码执行任务。 它使用
getElementsByTagName
,getElementById
方法以及className
属性。 其中的每一个都在DOM级别1中定义。此代码还使用DOM Level 2 Events中的
addEventListener
方法。n setupLabels() {
// get all the labels on the entire page
var objLabels = document.getElementsByTagName("LABEL");
var objField;
for (var i = 0; i < objLabels.length; i++) {
// if the label is supposed to be dynamic...
if ("dynamicLabel" == objLabels[i].className) {
// get the field associated with it
objField = document.getElementById(objLabels[i].htmlFor);
// add event handlers to the onfocus and onblur events
objField.addEventListener("focus", focusDynamicLabel, false);
objField.addEventListener("blur", blurDynamicLabel, false);
// save a copy of the label text
objField._labelText = objLabels[i].firstChild.nodeValue;
// initialize the display of the label
objField.value = objField._labelText;
}
}
}但是,此代码不适用于IE / windows,因为它不完全符合DOM。 它不支持DOM 2级事件模块。 相反,它支持功能相同的专有接口 。 由于IE / windows具有如此庞大的用户群-我们希望看到我们的影响-我们在脚本中添加了一个小技巧,以适应其不同的对象模型(请注意,更改后的行为粗体):
function setupLabels() {
// get all the labels on the entire page
var objLabels = document.getElementsByTagName("LABEL");
var objField;
for (var i = 0; i < objLabels.length; i++) {
// if the label is supposed to be dynamic...
if ("dynamicLabel" == objLabels[i].className) {
// get the field associated with it
objField = document.getElementById(objLabels[i].htmlFor);
// add event handlers to the onfocus and onblur events
addEvent(objField, "focus", focusDynamicLabel);
addEvent(objField, "blur", blurDynamicLabel);
// save a copy of the label text
objField._labelText = objLabels[i].firstChild.nodeValue;
// initialize the display of the label
objField.value = objField._labelText;
}
}
}
function addEvent(objObject, strEventName, fnHandler) {
// DOM-compliant way to add an event listener
if (objObject.addEventListener)
objObject.addEventListener(strEventName, fnHandler, false);
// IE/windows way to add an event listener
else if (objObject.attachEvent)
objObject.attachEvent("on" + strEventName, fnHandler);
}通过使用相同的实用程序函数将附加到窗口的onload事件附加到页面加载后,我们可以使此脚本运行。
addEvent(window, "load", setupLabels);
现在,我们要做的就是实现
focusDynamicLabel
和blurDynamicLabel
。 这很容易-就像我们第一个动态标签脚本中的原始代码一样。 唯一的区别是应该对其进行通用化,以便同一功能可用于页面上的每个动态标签。在完全兼容DOM的浏览器中,我们可以使用事件对象的target属性(也在DOM Level 2 Events中定义)来获取对触发事件的元素的引用并进行操作:
function focusDynamicLabel(event) {
// get the form field that fired this event
var elm = event.target;
// if it is currently displaying the label...
if (elm._labelText == elm.value) {
// ... turn it off
elm.value = "";
}
}
function blurDynamicLabel(event) {
// get the form field that fired this event
var elm = event.target;
// if it's empty...
if ("" == elm.value) {
// ... display the label text
elm.value = elm._labelText;
}
}但是再一次,IE /窗口实现此功能略有不同 ,使用属性
srcElement
而不是标准化的target
,以及提供通过事件对象window.event
,而不是含蓄把它传递给事件处理函数的标准化方式。我们将需要另一个小技巧和帮助功能:
function focusDynamicLabel(event) {
// get the form field that fired this event
var elm = getEventSrc(event);
// if it is currently displaying the label...
if (elm._labelText == elm.value) {
// ... turn it off
elm.value = "";
}
}
function blurDynamicLabel(event) {
// get the form field that fired this event
var elm = getEventSrc(event);
// if it's empty...
if ("" == elm.value) {
// ... display the label text
elm.value = elm._labelText;
}
}
function getEventSrc(e) {
// get a reference to the IE/windows event object
if (!e) e = window.event;
// DOM-compliant name of event source property
if (e.target)
return e. target;
// IE/windows name of event source property
else if (e.srcElement)
return e.srcElement;
}例C展示了到目前为止的工作。
现在,我们已经实现了原始标签脚本的更专业的版本。 它并不专门绑定到事件处理程序,我们通过将脚本实现为一系列功能来使脚本更具模块化。 因此,该脚本将更灵活地使用并且更易于维护。
但是,DHTML与处理表单的代码之间的耦合又如何呢? 如果我们将表单字段保留为空并按下Submit按钮,则“ Username”将被提交到服务器端进程。 我们仍然需要解决这个问题。
每个表单都有一个
onsubmit
事件 ,该事件在将其值提交给服务器之前立即触发。 我们只需要遍历页面上的每个表单并将事件处理程序添加到此事件中。 一个好的地方是在我们的设置功能中:function setupLabels() {
// get all the labels on the entire page
var objLabels = document.getElementsByTagName("LABEL");
var objField;
for (var i = 0; i < objLabels.length; i++) {
// if the label is supposed to be dynamic...
if ("dynamicLabel" == objLabels[i].className) {
// get the field associated with it
objField = document.getElementById(objLabels[i].htmlFor);
// add event handlers to the onfocus and onblur events
addEvent(objField, "focus", focusDynamicLabel);
addEvent(objField, "blur", blurDynamicLabel);
// save a copy of the label text
objField._labelText = objLabels[i].firstChild.nodeValue;
// initialize the display of the label
objField.value = objField._labelText;
}
}
// for each form in the document, handle the onsubmit event with the
// resetLabels function
for (var i = 0; i < document.forms.length; i++) {
addEvent(document.forms[i], "submit", resetLabels);
}
}要实现
resetLabels
函数,我们采取与设置相反的操作:循环浏览表单中的每个标签,并检查它是否为动态标签。 如果是,并且正在显示标签文本,我们将其值重置为空字符串。function resetLabels(event) {
var elm = getEventSrc(event);
// get all label elements in this form
var objLabels = elm.getElementsByTagName("LABEL");
var objField;
for (var i = 0; i < objLabels.length; i++) {
// if the label is dynamic...
if ("dynamicLabel" == objLabels[i].className) {
// get its associated form field
objField = document.getElementById(objLabels[i].htmlFor);
// if the field is displaying the label, reset it to empty string
if (objField._labelText == objField.value) {
objField.value = "";
}
}
}
}示例D显示了步骤2末尾的工作。我们已成功地将原始结构化文档转换为所需的动态效果。 它不再与处理表单的代码耦合,可以与其他脚本很好地工作,并且是模块化的代码。
步骤3:确定所有用户代理要求
这一步很容易:我们只需浏览步骤2中的代码,并确定我们使用的所有对象,功能和其他浏览器要求。 我们将使用此信息来创建JavaScript函数,以淘汰所有不满足这些要求的浏览器。
在标签脚本中,我们使用了许多不同的DOM技术,但实际上只需要测试三种:
-
document.getElementById
-
window.attachEvent
或 -
window.addEventListener
我们可以使用以下简单功能来做到这一点:
function supportsDynamicLabels() {
// return true if the browser supports getElementById and a method to
// create event listeners
return document.getElementById &&
(window.attachEvent || window.addEventListener);
}我们不需要测试更多属性的原因是,我们正在使用的所有DOM函数都来自DOM Level 1 HTML或DOM Level 2 Events。 一旦我们看到当前浏览器支持每个推荐中的一种方法,就可以假定它实现了该推荐的其余部分(至少是表面上的)。
我们只使用每个建议的一小部分,因此我们不需要在测试中更详细地介绍。 随着脚本变得越来越复杂,您会发现某些浏览器仅部分支持某些建议,并且您需要测试越来越多的特定功能。
W3C建议实际上为浏览器提供了一种通过
hasFeature
方法指示其支持的DOM级别的方法 。 具有讽刺意味的是,这种方法没有得到很好的支持。DHTML的现实可能总是包含部分和错误实现的规范。 开发人员应确保他们正确测试了所需的功能。
步骤4:满足代理要求时,转换逻辑结构。
使用功能检查功能后,下一步是编写代码,该代码实际上会将结构从您在步骤1中编写的逻辑代码转换为步骤2中的动态代码。
在进行转换的每个位置,您都应首先检查是否支持当前的浏览器。 这样,效果要么完全实现,要么根本不实现。
我们更改文档逻辑结构的两个主要地方是添加了样式规则以关闭HTML标签的显示,以及在窗口的onload事件中运行的设置功能。 如果不支持浏览器,我们只需要阻止这两种转换就可以了。
对于样式规则,我们将更改代码,以便使用JavaScript将规则实际写出到文档中。 这是我经常使用的优雅解决方案,因为它是如此可靠。 确保仅当存在JavaScript时才更改文档结构的最佳方法是仅使用JavaScript更改文档结构。
我们删除在第2步中添加的样式表规则,然后将其替换为以下JavaScript:
if (supportsDynamicLabels()) {
document.writeln('<style type="text/css">');
document.writeln('label { display:none; }');
document.writeln('</style>');
}我们也将setup函数移到“ if”分支中,因为我们希望它仅在满足我们的要求时运行:
if (supportsDynamicLabels()) {
document.writeln('<style type="text/css">');
document.writeln('label { display:none; }');
document.writeln('</style>');
addEvent(window, "load", setupLabels);
}实施例E显示了完成的效果。
步骤5:在所有目标平台上进行全面测试
仔细测试DHTML效果的重要性不可低估。 一个简单的事实是,如果您要编写DHTML,则需要能够在打算运行DHTML的大多数平台上进行个人测试。
例如,一个简单的Google搜索将发现Windows IE 5 + , Gecko和Safari似乎都实现了我们所需的功能。
但是,如果要在Safari 1.0上运行Example E,您会注意到一个大问题:效果只运行一次! 第一次单击文本框时,标签会正确消失。 但是一旦模糊,什么也不会发生。 文本框保持空白,您再也无法找回标签。
事实证明Safari有一个错误-在焦点对准下一个文本框之前,它不会对文本框触发onblur。 在我们的情况下,这意味着如果用户只是在不关注另一个文本框的情况下在文本框附近进行制表或单击,我们的标签将不会重新出现。
Safari的onblur问题是一个实施错误的示例,该错误无法通过简单的功能检测进行测试。 我们将需要更新功能测试功能,以专门测试Safari Web浏览器。 以下更改可以解决问题:
function supportsDynamicLabels() {
return
document.getElementById &&
(window.attachEvent || window.addEventListener) &&
null == navigator.appVersion.match(/Safari/d+$/);
}添加的行使用正则表达式测试导航器对象的
appVersion
属性,并在当前浏览器不是Safari时返回true。测试特定的浏览器时,通常最好在该浏览器的对象模型中测试特定的专有属性。 例如,IE具有
window.clientInformation
属性,可用于将其与其他浏览器明确地区分开。Safari似乎不支持任何专有属性。 因此,我们必须诉诸测试该导航器对象的
appVersion
属性。 您也可以测试userAgent
属性,但是它不那么可靠,因为某些浏览器的用户可以修改它。例F显示了我们的最终工作。 我们已经成功地将第一个行为不佳的动态标签脚本转换为更好的东西。 我们的最终代码是完全模块化的,不依赖JavaScript,无法与其他脚本很好地配合,并且不会与任何其他组件耦合。
在测试过程中,我们发现Safari在处理文本框上的焦点和模糊事件方面存在一个模糊的错误,这使其无法支持我们的效果。 我们期待着能够解决此错误的Safari版本,届时我们可以轻松更新功能测试功能以仅测试错误版本。
最重要的是,我们到目前为止所使用的五步过程可以轻松地应用于现代网站的任何其他DHTML效果。
DHTML可用于补充许多网页的UI,并且可以做到这一点,从而不需要其支持。 这种DHTML编码风格不应与过去表现欠佳的脚本一视同仁,而应被视为专业Web开发人员手中的另一个有价值的工具。