Web应用程序(Web Applications)
从计算机纪元的黎明刚刚来临开始,不同平台间软件的互用性就一直是关注的焦点。为了尽可能实现用户的最大要求,软件发布者往往将流行软件从一个机器移植到另外一个机器上,这通常要花费数月的辛苦劳动,有时甚至是整个软件在新的硬件或者操作系统上的完全重写。随着计算机功能的不断强大,像C/C++这样的语言成为在多种平台上可用的标准,这是更加容易实现,一个程序只编写一次,就可以在所期望的众多系统上编译。只要系统中包含C编译器,软件就可以构建,并且可能会按照预期运行。
然而,当使用图形用户界面(GUI)的软件成为标准化时,就有了另外一个问题。Macs上的GUI和Windows或者Unix上的各种效果大相径庭。当然,有一些所谓的“widget toolkit”,像Tcl/Tk或者wxWidgets,可以绑定到很多流行的语言,如C++、Python和Perl。开发人员使用这些小工具可以创建平台无关的GUI,它们可以在Windows、Linux、Mac OS X和其他操作系统表现出(几乎)同样的动作。不过,在今天,几乎所有的机器上都有一个功能尤为强大的GUI解释器:web浏览器(the web browser)。
web浏览器 虽然最初只是为了以一种相对令人舒服的方式显示标记文本,但是今天已经发展到另外一个顶点。它们可以作为令人惊奇的复杂的软件界面,可以媲美那些使用C/C++和其他高级语言编写的软件。这些大部分要归功于JavaScript支持和Web2.0技术。几乎每一个平台上的每一个浏览器都支持该脚本语言的一个通用子集,这使得编写平台无关程序变得更加容易。
JavaScript工具箱(JavaScipt Toolkits)
最近,出现了很多来自于像Yahoo、Google和Dojo等地方的免费的JavaScript工具箱。非常像上面提到的widget toolkits,它们提供了各种各样使用JavaScript编写的小工具,如菜单、日历或者树,并且开发人员可以将它们轻而易举地继承到自己的网站或者web程序中。这里有一个不错的小总结。
如果需要为网页增加一些特效,这些工具是非常不错的,也有很多优点。它们已经在很多浏览器上进行过测试,可以在运行在所有标准的浏览器上。其中一些工具还包含了良好的文档,它们使得实现一个奇幻的网页变得轻而易举。然而,它们也可能是非常臃肿的,会为你的网页再增加数百KB,有时你又不得不等待对新浏览器支持功能的添加(例如,其中一些还不能在IE7上运行)。另外,很多公司不会或者不愿意使用开源软件,主要是因为软件责任的缺失(也就是,你必须自己承担使用它的风险),并且在调试方面也有很多困难。
本系列文章计划作为高级JavaScript技术指南,演示如何创建多浏览器兼容widget(包含完整的代码),类似前面提到的那些小工具,另外也会详细解释它们的工作原理。诸如实现如菜单、tab或树等之类的例子在网上随处可见,但是很多要么是代码编写得极其蹩脚,要么不具备跨浏览器兼容性。关于JavaScript、CSS、DOM和HTML有一个比较好的参考网站是www.w3school.com,如果在本文讨论的示例中遇到不熟悉的JavaScript函数或者HTML/CSS标签建议参考此网站。该网站上还有一篇关于HTML DOM的指南。将要讨论的内容有弹出式菜单、浮动消息、拖拽对象和XML HTTP(在不重载页面的情况下动态更新页面内容)。
应该指出的是,讨论的函数都已经在Windows XP下IE7和FIrefox2.0上测试通过,但是它们应该可以在当前流行的所有浏览器上运行。任何浏览器相关的代码都在讨论过程中特别指出。另外,请紧记,使用JavaScipt和DOM有很多方法可以实现同样的目的。下面的例子只是给出了一种这样的方法,在所有方式中并不是必要的最快的或者最好的。所有的例子都依据简单、实现功能并且依据阅读的原则编写。
JavaScript的魔力(The Magic of JavaScript)
我们将要看到的第一个例子是如何显示一个浮动的文本(或图片),并且在拖动浏览器窗口的滚动条时仍然会停留在同一个地方。这可以用来在web程序向用户显示一个消息,如“Please wait”,或者在网页增加一个水印。只需要几行JavaScript代码就可以实现这个效果:
使用下面的两个JavaScript函数可以完成上面的功能:
var moz = document.getElementById && !document.all;
var intr;
function Message_UpdatePos(msg, dy) {
var el = document.getElementById(msg);
if (ie) {
el.style.pixelTop = document.body.scrollTop + dy;
}
else if (moz) {
el.style.top = window.pageYOffset + dy + 'px';
}
}
function Message_Display(msg, vis, dx, dy) {
var el = document.getElementById(msg);
// Position Message
if (ie) {
el.style.pixelTop = document.body.scrollTop + dy;
el.style.pixelLeft = document.body.clientWidth - dx;
}
else if (moz) {
el.style.top = window.pageYOffset + dy + 'px';
el.style.left = window.innerWidth - dx + 'px';
}
if (vis) { // and display it
el.style.visibility = "visible";
intr = setInterval("Message_UpdatePos('" + msg + "', " + dy + ")", 1);
}
else { // or hide it
el.style.visibility = "hidden";
if (intr)
clearInterval(intr);
}
}
visibility: hidden; background: red;">This is a test…</span>
Message_Display的参数是:包含消息文本的span标签的id、一个表示显示或者隐藏消息的布尔值,以及消息的显示位置——相对于浏览器窗口右上角的x和y偏移量(在本例中是距离右端700px,顶端50px)。这里的关键所在span标签的style。通过使用绝对定位,我们是消息浮动在页面上,而不是附在页面随着页面滚动而滚动。我们还应该简要地说明一下为什么使用span标签。span和div标签本质都是“什么事情都不干(do nothing)”的标签,在文档中分别被用来描绘水平和垂直的块。正常情况下,它们在页面上都是不可见的,所以可以用来通过特定的样式来包围一块内容,而不用改变整个页面。因此在创建JavaScript小工具,它们是十分方便的。
这个函数的工作流程如下。首先,通过getElementById(获得标签指针的最简单方法)获得消息span标签的句柄。接下来,区分Internet Explorer(ie)或Firefox/Mozilla(moz),但是都要实现同样的目的——将消息定位到期望的位置(dx,dy)。使用visibility样式控制消息的显示和隐藏。最后一行需要更进一步的解释JavaScript的定时器(Timer)。
定时器是JavaScript中在某段时间后执行一个函数的有效方式,既可以使用setTimeout只执行一次,也可以使用setInterval循环执行。这些函数是非常有用的,因为在JavaScript中没有和sleep等价的命令,所有它们就提供了最好的替代方式。直到指定的时间过去,否则它们是消耗CPU时间的。开发人员必须注意不能滥用这些强大的工具,但是只要适当使用就会产生令人印象深刻的效果。在本例中,我们根据参数vis设置定时器执行Message_UpdatePos(msg, dy)。若vis非零,则定时器设置为每1毫秒执行一次。注意,我们必须定时器指针保存在全局变量intr中,因为在多次调用Message_Display时它要一直存在。
最后来看Message_UpdatePos,注意本质上它和Message_Display的开头部分完成同样的工作,获得消息句柄和更新到页面顶端的y偏移量。因此,当页面滚动时,消息的位置就会滚动事件完成1ms重新调整,看起来它好像一直漂浮在右上角的同样位置。注意,如果浏览器在水平方向调整大小(变宽或者变窄),消息并不会移动。作为,看你是否可以修改代码使其在该种情况也改变位置。
弹出式菜单(Pop_up Menus)
接下来,也是本文讨论的最后一个小工具,它比上一个例子更加复杂:弹出式菜单。
首先,看一下运行的结果:
For menu click here: X
这个例子更加复杂是因为它包含了一个JavaScript函数和一些更加复杂的事件处理。下面是用到的JavaScript代码和HTML:{
var dx = 0;
if (el.offsetParent) {
dx = el.offsetLeft + 8;
while (el = el.offsetParent) {
dx += el.offsetLeft;
}
}
return dx;
}
function getElementAbsPosY(el)
{
var dy = 0;
if (el.offsetParent) {
dy = el.offsetTop + el.offsetHeight / 2;
while (el = el.offsetParent) {
dy += el.offsetTop;
}
}
return dy;
}
function GetAbsWindowBottom()
{
// Compute the bottom of the popup window and the bottom of
// the browser window, in absolute co-ordinates - different
// on all browsers but the below should be accurate usually!
var abswindowbottom = 0;
if (typeof(window.innerHeight) == 'number')
abswindowbottom = window.innerHeight;
else if (document.documentElement && document.documentElement.clientHeight)
abswindowbottom = document.documentElement.clientHeight;
else if (document.body && document.body.clientHeight)
abswindowbottom = document.body.clientHeight;
if (typeof(window.pageYOffset) == 'number')
abswindowbottom = abswindowbottom + window.pageYOffset;
else if (document.body && document.body.scrollTop)
abswindowbottom = abswindowbottom + document.body.scrollTop;
else if (document.documentElement && document.documentElement.scrollTop)
abswindowbottom = abswindowbottom + document.documentElement.scrollTop;
return abswindowbottom;
}
function PopupMenu(name, vis)
{
var el = name + 'menu';
var tag = name + 'menuroot';
if (!document.getElementById(el)) // menu object not found
return;
if (vis == 0) { // hide the menu
document.getElementById(el).style.visibility = 'hidden';
return;
}
// Get menuroot position
var pos = document.getElementById(tag);
var dx = getElementAbsPosX(pos);
var dy = getElementAbsPosY(pos);
// Compare bottom of menu to bottom of window
var abspopupbottom = dy + document.getElementById(el).clientHeight + 10;
var abswindowbottom = GetAbsWindowBottom();
// If menu goes below bottom of window, move it up!
if (abspopupbottom > abswindowbottom)
dy = dy - (abspopupbottom - abswindowbottom);
// Set final menu position and make it appear
document.getElementById(el).style.left = dx + 'px';
document.getElementById(el).style.top = dy + 'px';
if (vis > 0)
document.getElementById(el).style.visibility = 'visible';
}
background-color: #afafaf; padding: 4px;" onmouseover="clearTimeout(tout);" onmouseout="tout=setTimeout('PopupMenu('test', 0);', 1500);">
<table>
<tr>
<td><span style="cursor:pointer; color:blue;" onclick="PopupMenu('test', 0);">Menu Item #1</span></td>
</tr>
<tr>
<td><span style="cursor:pointer; color:blue;" onclick="PopupMenu('test', 0);">Menu Item #2</span></td>
</tr>
<tr>
<td><span style="cursor:pointer; color:blue;" onclick="PopupMenu('test', 0);">Menu Item #3</span></td>
</tr>
</table>
</div>
onclick="PopupMenu('test', 1); tout=setTimeout('PopupMenu
('test', 0);', 1500);" >X</span>
首先,我们来看一下菜单。我们可以在div标签中放置任何我们喜欢的东西,所有的秘密都在div标签的属性中。特别需要注意的,和前面的例子一样,我们使用绝对定位,所以不管其他文本和图片出现的位置,我们都可以控制菜单的确切位置。我们还要确保菜单开始时visiblity:hidden,并且分配了ID testmenu,这样我们就可以在后面很容易获得一个句柄。
在div标签中,我们还会遇到setTimeout和clearTimeout函数的第一次使用。它们和上例中的setInterval函数的行为类似,只不过它们在某段时间后只执行一个命令一次。在本例中,它们用来在鼠标移出菜单区域1500毫秒后关闭菜单,或者鼠标重新移回到菜单区域时(如果仍然处于打开状态)取消退出效果。tout是在JavaScript文件中定义的一个全局变量,用来保存定时器对象的指针。当触发onmouseout事件时(鼠标移动由div标签指定的菜单区域之外)PopMenu('test', 0)被设定为在1500毫秒后调用(注意我们是如何转移引号的,因为它们已经在引号之内)。假如在1500毫秒耗尽之前动作没有被取消,这个函数就会被调用,菜单就会消失(就像我们在下面将要看到的那样)。然后,如果onmouseover时间被触发(鼠标移动到菜单上),clearTimeout(tout)就会执行,它会阻止取消定时器,阻止菜单消失,直到用户触发下一次鼠标事件。
在菜单自身内部,我们会包含一些执行其他任务或跳转其他页面的链接。当然,本例中只是一些虚假的链接。然而,注意每一个菜单项的onclick事件。在div标签上面,一点击菜单项菜单就会消失(试一下!)。注意,这里没有使用timeout,因为我们想在这种情况下菜单立即消失。
接下来是由id为testmenuroot的span标签包含的一段HTML代码,表示了菜单实际出现的点击区域。在本例总,我们只是在span标签中间放置了一个X字母,当然,你可以在这里放你喜欢的任何东西:按钮、图片等等。需要注意的另外一个事情是onclick时间,它使菜单显示,并且设置定时器以在1500ms后关闭菜单。然而,我们已经只要用户移动鼠标到菜单区域,定时器就会被取消,并且菜单会一直显示,直到下次鼠标移走。
最后,我们来看JavaScript函数本身,PopupMenu。它依赖的函数有:GetAbsWindowBottom,获得浏览器底部的绝对位置;getElementAbsPosX和getElementAbsPoxY,获得一个元素在绝对坐标系中的x和y位置。最后两个的计算方式是通过迭代加上父元素的偏移量,直到DocumentRoot。这些函数看起来有些过于复杂了,但是它们极有可能是在几乎每一个流行的浏览器上获得这些信息的唯一方式。不幸的是,每一个浏览器都使用一个非常不同的方式保存这些信息。
PopupMenu函数有两个输入参数:name和一个表示菜单应该显示或隐藏的标志。关于name,假设菜单被一个标签包围形成,其id由name参数加上“menu”组成(那么在我们的例子中就是“testmenu”,因为参数为“test”)。类似地,假设菜单的左上角定位在id为name加“menuroot”的标签(本例中为“testmenuroot”)上。如果vis参数是0,我们只需要把菜单标签的visibility的样式设置为hidden。否则,我们需要使菜单出现在右侧的位置上。首先,我们使用GetElementAbsPos函数获得“menuroot”标签的绝对位置。然后,我们可以把菜单的位置设置为这些值,这就可以让菜单的左上角出现在我们点击以使其弹出的位置。然而,我们还不希望菜单滚动出页面的底端,因为这是非常令人讨厌的。注意多数包含弹出式菜单的widget toolkits都只实现上面的功能,所以我们将尝试实现一个更好的。我们可以计算出绝对坐标系中窗口以及弹出菜单的底部位置,接下来的两行代码完成了这个任务。注意,菜单的高度由clientHeight属性给出,我们再加上10以使和页面底部留下一些空间。然后我们可以检查弹出菜单是否已经向下超出了浏览器窗口的底部,如果是这样,就会相应地调整它的坐标,如此它的底部就会刚刚接触窗口的底部。最后,我们接着向下看,就是定位弹出式菜单,并使其可见。
总结(Summary)
但愿,这次讨论和这两个例子可以让你领略到使用仅仅几行JavaScript代码就可以完成的事情,并且揭示了在JavaScript工具内部并没有很多很多的魔法。尽管有时候有些棘手,但是通常只需要额外的一点工作来确保函数可以在所有的主流浏览器上运行,并且这些小小的额外付出几乎永远是值得的。在后面的文章中,将会讨论更加详细的JavaScript widget的示例。
Howard Feldman是在位于Montreal, Quebec的Chemical Computing Group一名研究科学家。