7.2.4 客户端脚本错误
到目前为止,我们已了解了来自ASP的错误。然而ASP也经常用于创建包含客户端脚本的网页。如果包含客户端代码的<SCRIPT>元素没有被设置成RUNAT="SERVER"属性,ASP将不考虑服务器,而把网页信息不加改变地传送到客户端。
因此,如果打开了一个ASP网页,并且显示的是一个浏览器错误对话框,就不应该在服务器端寻找ASP程序代码的错误。浏览器看不到ASP程序代码,所以不能识别任何错误,如果有一个对话框出现在客户端,那么在客户端代码中必定有一个错误。
1. 语法错误
如果在网页中的客户端程序代码有语法错误的话,当脚本下载到客户端,浏览器便会出现相应的错误。尽管网页中内容仍可正常载入(除非由这些客户端脚本代码动态装入),但网页停止执行。用户将看到一个包含错误细节的对话框,或者是一个指示网页包含错误的状态条消息。
现代浏览器趋向于隐藏网页脚本错误的细节,而仅在状态条上显示一个小的错误图标。在IE 4.0和IE 5.0中,正常的错误对话框可以通过Internet Options对话框的Advanced页进行设置来激活,如图7-14所示:
图7-14 Advanced页面设置屏幕
处理脚本程序代码中的客户端错误和在服务器端相似,并且通常会更容易些,因为经常可以直接从服务器目录中通过双击来下载网页。一般不需要通过Web服务器和HTTP获得网页来观察浏览器中的结果,其中的唯一不同是一些服务器交互由客户端脚本来完成,如使用RDS的数据绑定或者动态装入。
2. 运行期或语义错误
在客户端脚本中,通常可能会遇到语法错误,也会经常遇到运行期或语义错误。事实上,在客户端,这种现象是很普遍的。因为在客户端不能像服务器端那样对脚本的环境进行控制,不能肯定用户在他们的机器上正运行什么,实际上在服务器上仅能从一些组件如Browser Capabilities中得到大概情况。
所以,使用客户端对象或特殊版本的脚本语言和属性的脚本程序很可能不能正常工作。尽管如此,处理客户端错误和处理服务器端错误是差不多的。
3. 在服务器上创建的客户端程序代码
在错误发生时,作为“客户端对话框对应于ASP错误页面”规则(关于出错的地方)的一个特别的例外是,使用ASP程序代码在服务器上动态地创建客户端程序代码。例如,可能想在ASP中进行求值运算,然后把数据传给运行在客户端的脚本代码,可能最容易的方法是把数据作为一个变量插入脚本代码中:
<%
' get the name of our server from the ServerVariables collection
strServerNameInASP = Request.ServerVariables("SERVER_NAME")
%>
<SCRIPT LANGUAGE="JScript" RUNAT="CLIENT">
<!-- hide code from older browsers
var strServerName = "<% = strServerNameInASP %>";
…
alert('Server name is: ' + strServerName);
…
// stop hiding code
-->
</SCRIPT>
在客户端,在ASP处理这个页面之后,将得到的是:
<SCRIPT LANGUAGE="JScript" RUNAT="CLIENT">
<!-- hide code from older browsers
var strServerName = "WROXBOX";
…
alert('Server name is: ' + strServerName);
…
// stop hiding code
-->
</SCRIPT>
可以忽略RUNAT="CLIENT"属性,但是加上这一项可以使得在查看运行代码的ASP网页时更加清楚。
这样,如果在某个位置想把服务器端数据库中的数据加入到一个客户端数组中,可以采用下面的程序实现:
<SCRIPT LANGUAGE="JScript" RUNAT="CLIENT">
<!-- hide code from older browsers
var arrBooks = new Array(10) //highest available index will be
<% ' start of ASP processing
intIndex = 0
Do While { not at the end of some recordset }
strTitle = { get title from database record }
Response.Write "arrBooks[" & CInt(intIndex) & "] = '" _
& strTitle & "'; " & vbCrlf
intIndex = intIndex +1
{ move to next record in database }
Loop
…
do something here on the client with the array of book titles
…
// stop hiding code
-->
</SCRIPT>
这段服务器端ASP程序代码产生的客户端代码,在客户端运行时创建书名标题数组。同时产生的客户端脚本错误出现在浏览器的错误对话框中。错误的原因是以arrBooks命名的数组是由JavaScript代码运行在客户端时创建的,仅能接受9个书名;而服务器端代码能很可能产生多于9个的书名,具体多少由源数据库中的记录数来决定。这相当于如下客户端代码:
<SCRIPT LANGUAGE="JScript" RUNAT="CLIENT">
<!-- hide code from older browsers
var arrBooks = new Array(10) //highest available index will be
arrBooks[0] = 'Instant JavaScript';
arrBooks[1] = 'Professional ASP 3.0 Programming';
arrBooks[2] = 'ADO 2.5 Programmers Reference';
…
etc
…
arrBooks[9] = 'ASP Techniques for Webmasters';
arrBooks[10] = 'ASP Programmers Reference'; // <- client-side error occurs here
arrBooks[11] = 'ADSI CDO Programming';
arrBooks[12] = 'Professional MTS and MSMQ Programming';
…
do something here on the client with the array of book titles
…
// stop hiding code
-->
</SCRIPT>
这个页面只有经过修正之后才能正常工作,可以通过增加数组大小,也可以通过控制来自数据库的记录数使其正常工作。
7.3 防止错误
上面已经看到了能够出现的一些不同类型的错误,并且有了一些查找错误的感觉。下面将考虑如何避免把错误引入程序中,尽管不能保证所编写的程序没有错误,但是这里概括的许多技术有助于减少错误数目。
良好的编程习惯
在编程中避免出现错误是和良好的编程习惯相关的,这里有许多工作我们要做,以减少把错误带进网页的可能性。可能有些人因采用某个技术而走向极端,甚至一定程度上在某个特殊问题上因书生气十足而引入了更多的错误。当然编程人员也不可能采用了这里列出的所有技术。
要考虑的主要内容是:
· 代码的格式化和缩进编排。
· 变量显式表明。
· 变量转换为合适的数据类型。
· 使用有意义的变量命名约定。
· 封装脚本。
· 注意潜在的错误情况。
1. 代码的格式化和缩进编排
许多VBScript编程员懒于格式化编排其书写的程序。尽管这并不阻碍程序运行,但这使得查找何处产生了错误变得困难。例如,在前面我们看到的程序中,丢失了一个End If,由于嵌套结构的缩进,错误在哪里是相当明显的:
objCounters.Remove strCounterName
Response.Write "Removed counter " & strCounterName
<--- missing 'End If' should be here
End If
End If
%>
如果程序看起来像下面所示的那样,寻找错误将不是一件易事:
<% if Len(Request.Form("cmdSet")) then
strCounterName=Request.Form("lstSet")
strNewValue=Request.Form("txtSet")
if isnumeric (strnewvalue) then
intNewValue =cint(strNewValue)
objCounters.Set strCounterName, intNewValue
Response.write "Set counter" & strCounterName &" to " & strNewValue
else
Response.write strNewValue &" is not a valid number"
If Len ( Request.Form ("cmdRemove")) then
StrCounterName = Request.Form("lstRemove")
objCounters.Remove strCounterName
Response.write "Removed counter "& strCounterName
end if
End IF
%>
2. 显式表明变量
VBScript支持Option Explicit语句。在一个脚本页面的开头插入Option Explicit语句时,可以避免使用没有用Dim命令(或用于动态数组的ReDim)定义的变量。似乎不需要这么做,因为脚本语言允许通过给一个变量赋值来创建一个需要的变量。然而用Option Explicit进行定义有助于避免错误,特别是那些难以发现的引起脚本产生不正确结果的逻辑错误。
例如,编写如下程序:
<%
' get value for calculation
strSalesTotal = Request.Form("SalesTotal")
curSalesTotal = CCur(strSalesTotal)
sngCommissionPercent = 2.5
' calculate commission payment
sngCommission = curSalesTotal * (sngComissionPercent /100)
%>
运行这段程序不会产生错误(当然,除非用户给销售合计值赋了非法的值)。然而这段程序总是会产生0的结果,因为在程序的最后一行中sngCommissionPercent变量名拼写错了。脚本解释器将产生一个新的变量名(叫作sngComissionPercent),由于没有赋值,在数学计算时返回值总为0。
为了防止这种错误,仅需在程序开头增加Option Explicit语句。
<%
Option Explicit
Dim strSalesTotal
Dim curSalesTotal
Dim sngCommissionPercent
' get value for calculation
strSalesTotal = Request.Form("SalesTotal")
curSalesTotal = CCur(strSalesTotal)
sngCommissionPercent = 2.5
' calculate commission payment
sngCommission = curSalesTotal * (sngComissionPercent /100)
%>
这时,当脚本引擎试图解释程序时将识别出一个语法错误,并且能够指出此变量没有声明,如图7-15所示:
图7-15 显示的错误信息
在JScript中引用一个没有声明的变量将返回一个“Undefined”信息,并且在试图使用变量之前,能够检测到这种情况。
3. 变量转换为合适的数据类型
回头看看前面的程序,可能发现用CCur函数把用户提供的数据转换成了货币型数据类型。在VBScript中,有一系列类似这样的数据变形变换函数,在第3章中有详细的描述。
blnBoolean = Cbool(varVariant) ' converts to a Variant of subtype Boolean
bytByte = Cbyte(varVariant) ' converts to a Variant of subtype Byte
curCurrency = CCur(varVariant) ' converts to a Variant of subtype Currency
datDate = CDate(varVariant) ' converts to a Variant of subtype Date
dblDouble = CDbl(varVariant) ' converts to a Variant of subtype Double
intInteger = CInt(varVariant) ' converts to a Variant of subtype Integer
lngLong = CLng(varVariant) ' converts to a Variant of subtype Long
sngSingle = CSng(varVariant) ' converts to a Variant of subtype Single
strString = CStr(varVariant) ' converts to a Variant of subtype String
如果不能完成变换,也就是说变量内容对新数据类型来说是无效的,便会出现一个运行期错误。然而,如果对数值类型进行变换,我们希望这个数值是有效的,并且能在程序中使用。因此能够检测一个均衡的值对于防止错误的出现是一件“幸事”。
如果想把输入空格作0对待,并且把任何其他无效的输入作为用户错误对待,前面程序变为:
strSalesTotal = Request.Form("SalesTotal")
If Len(strSalesTotal) = 0 Then
' no value entered, so assume zero
curSalesTotal = 0
ElseIf Not IsNumeric(strSalesTotal) Then
' not a valid number, so report an error and stop
Response.Write "The value you entered is not a valid number. "
Response.Flush
Resonse.End
Else
' OK to conver the string value and use it
curSalesTotal = CCur(strSalesTotal)
End If
在JScript中,所有的变量都是对象,并且有typeOf()方法。可以使用typeOf()来确定存在变量中的数据是什么类型,见第3章中的详细论述。
也可以对“null”(VBScript中为Null)进行测试保证在程序使用各种变量之前它们已经赋了值。一个特例是从数据库中获得数据时,数据库中的字段内容经常是Null,表示没有数据。
4. 变量命名和编码约定
阅读过本章和前面几章后,读者可以看出我们对变量名使用三个字母的前缀,用以指明它所代表的数据类型。尽管在这两种脚本语言中用ASP提供的所有变量都是Variant(或JScript中的等价物)类型的,但用变量名来区分出存储在其中的数据的类型仍是非常有用的,有助于防止编写程序时出错。
有许多不同的变量命约定,经常使用的见表7-2:
表7-2 变量类型及前缀
变量类型
前 缀
布尔型(Boolean)
bln
字节型(Byte)
byt
日期/时间型(Date/Time)
dat或dtm
集合型(Collection)
col
双精度型(Double)
dbl
整型(Integer)
int
长整型(Long)
lng
对象型(Object)
obj
单精度型(Single)
sng
字符型(string)
str
对于一个包含函数和子程序的网页,指出某个变量是否已经声明或存在于任何函数和子程序之外是非常有用的。若已经声明,则该变量对网页来说是全局变量。对全局变量加上“g”前缀,所以一全局字符串变量可能被命名为gstrMystring;类似的,以“a”为前缀的变量是数组或数组元素。
程序注释
许多编程人员感觉到对程序增加注释不仅增加了不必要的开发时间,而且也减缓了网页的运行速度,因为脚本解释器每次必须先读整个程序,然后再跳过这些注释。尽管这种观点有一定的道理,但是一个月后再回过头来想读懂没有注释的程序,是非常困难的。
至少应该对常用函数和子程序进行注释以便你和其他人能重新使用这些程序。特别是,使用新的Server.Execute方法更加容易(详细情况参阅第4章)。下面是微软提供的一个例程的注释格式。
'*****************************************************
'Purpose: what the routine is designed to achieve
'Inputs: a list of all the parameters to the routine
' parameter1: description, data-type, etc.
' parameter2: description, data-type, etc.
'Returns: what data type is returned, and what it contains
'Comments: other comments about the routine, update history, etc.
'*****************************************************
5. 封装脚本语言以便代码重用
刚刚看到了如何注释子程序和函数以便易于重新使用。面向对象编程的原理是建立在程序代码重用的基础上的,并且SSI的#include和新的Server.Execute方法使调用存储在程序库中的函数更容易。
例如,如果有一系列函数用于计算税收和商品的应付费用。可把包含这段程序的页面插入其他页面中:
<!-- #inculde VIRTUAL="/library/code/online_sales/tax_and_delivery.inc" -->
包含文件必须含有脚本定界符,或者用<SCRIPT RUNAT="SERVER">...</SCRIPT>或者用<%...%>,每一个子程序和函数应该采用其要求的数值做参数,并且用函数值或更新的参数返回结果。不能使用全局变量,况且不同网页之间的全局变量也是不可用的。但在主网页中的程序能安全地调用所需的函数和子程序。
另外可使用Server.Execute(或者Server.Transfer)把执行转到另一个网页。如果有一段ASP代码用来为客户创建在线采购一览表,这种方法是非常有用的。它包含HTML用来创建标题、表格,用代码进行计算并用Request集合的内容填写相应值(记住不能使用Server.Execute或Server.Transfer把脚本变量传到另一个网页)。另外,运行的网页能够支持函数、类定义(在VBScript中),或者其他设计为可重新使用的内容。
6. 注意潜在的错误情况
编程时不管如何仔细,比如在使用和对变量类型转换之前对变量值进行测试,但总还是有一些情况不能避免错误的出现。明显的例子是:当使用FileSystemObject对象的方法设法访问一个用户指定的文件时,不能确定这个文件是否已移动、删除或者标记成只读型,所有这些操作都可能使程序不能工作。
其他类似的情况可能是,当访问数据库或其他数据存储时,对用户帐户而言,有时要求某一层权限。在这种情况下,可能因为需要的访问不能实现,使程序不能工作。
可以通过采取预防性措施编写程序,来测试类似的潜在错误。例如,可以使用Tools组件或者FileSystemObject对象的FileExists方法来查看,是否一个文件在访问之前就已存在了;或者使用Permission Checker组件来查看当前用户帐号是否有访问需要的文件或资源的权限;也可以通过使用FileSystemObject获得一个文件的属性设置,以便在删除或重写之前查看文件是否是只读的。
如果不考虑可能发生的错误并防止错误发生的话,这些情况和许多类似的情况都可能是潜在的运行期错误源。
7. 最后的测试
很明显,测试是对错误的最好防范方法。错误能通过应用程序影响到其他操作,如果不及时发现能引起不可估量的损失。用各种数值(如用户提交的,或者访问一个数据库得到的数据)对网页进行测试。同时,更重要的是,如果采用我们不希望的值会发生什么。程序能避免这些情况产生其他错误或者避免扰乱正在测试的子程序吗?
好的测试技术应该包括一系列值,如期望的值、边界条件值和超出边界的值。用期望传送到网页的值进行测试是应该经常做的工作,同样超出边界的值通常比较容易阻止。例如,可以限制可接受的数值范围为-100~+100,程序如下:
If (intValue < -100) or (intValue > 100) Then
' not a valid value, so report an error and stop
Response.Write "Value must be between –100 and +100 inclusive. "
Response.Flush
Response.End
End If
然而要记住查看边界条件,上面的程序能处理-100和+100吗?想把数据限制在-100~+100中吗?取0时会发生什么?上面的程序会显示“Divide By Zero”错误而最终停止执行吗?
7.4 处理错误
即使采用了防御性编程技术之后,错误仍能进入到网页,这可能是因为测试并不充分,或者是因为所依靠的一些其他资源或服务没有正确工作。为了防止页面出现问题,在程序中要能够进行定制错误处理。
7.4.1 ASP缺省错误处理器
前面已经看到过,ASP和IIS能找出网页中的大多数错误,并且能自动生成错误信息页,这些错误几乎总是500.100类型的,并且IIS用Server.Transfer方法装载以500-100.asp命名的缺省错误页,然后传送给客户。第4章介绍了这一工作过程,以及如何与定制错误网页接口。
然而,运行期脚本错误不总是由IIS发现的,当一个运行期错误发生时,脚本引擎会查看一下目前执行点或语句的环境。如果正在执行一个子程序或函数,缺省的脚本引擎错误处理器通过终止子程序的运行并返回调用子程序的地方来指出错误。
在这里,程序会查看是否实现了其他的错误处理器,如果没有的话,又会重复这个过程,然后返回到调用子程序的地方。当子程序返回到网页的主程序(在任何其他子程序或函数外面)时,程序又查看是否实现了任何其他的错误处理器。在这个过程中,只有确实没有发现其他的错误处理器,程序才给ASP提示错误,指示IIS把执行转到缺省的错误页面。