深入Atlas系列:客户端代码编写规则分析与指南
2006-10-25 18:29 by 老赵, 4664 visits在RTM版本中,我们可以发现ASP.NET AJAX的客户端脚本引入了许多规则:有方法注释规则,有参数验证规则,而且对于Debug和Release模式下的脚本代码,甚至添加在程序集里的方式,也有相当严禁的规则。如果我们想要编写真正规范和严谨的代码或组件,了解这些规则是非常必要的。有了这些规则,用户在使用Debug模式进行开发和调试时可以得到更好的提示(比如Call Stack),下一版本的Visual Studio“Orcas”也会根据这些规则提供良好的IntelliSense功能。
另外,了解这些规则也有利于帮助开发人员阅读和理解客户端代码,这不也是我写“深入Atlas系列”的目的吗?在这片文章里,我将使用ASP.NET AJAX的脚本代码为范例,对开发规则进行一番描述。在某些时候也会对客户端和服务器端的部分代码进行简单的分析。
一、脚本方法命名规则
在这里,一些简单的规则我就不详细说明了,我在这里就简单地列举一下:
- 属性的get和set方法,使用“get_”和“set_”作为前缀。
- 事件的add和remove方法,使用“add_”和“remove_”作为前缀。
- 对于私有的方法,使用下划线“_”作为前缀。
1 Sys.Net._WebMethod = function Sys$Net$_WebMethod(proxy, methodName, fullName, useGet) {
2 ……
3 }
4
5 function Sys$Net$_WebMethod$addHeaders(headers) {
6 ……
7 }
8
9 function Sys$Net$_WebMethod$getUrl(params) {
10 ……
11 }
12
13 function Sys$Net$_WebMethod$getBody(params) {
14 ……
15 }
16
17
18 function Sys$Net$_WebMethod$_execute(params) {
19 ……
20 }
21
22 function Sys$Net$_WebMethod$_invokeInternal(params, onSuccess, onFailure, userContext) {
23 ……
24 }
25
26 Sys.Net._WebMethod.prototype = {
27
28 addHeaders: Sys$Net$_WebMethod$addHeaders,
29
30 getUrl: Sys$Net$_WebMethod$getUrl,
31
32 getBody: Sys$Net$_WebMethod$getBody,
33
34 _execute: Sys$Net$_WebMethod$_execute,
35
36 _invokeInternal: Sys$Net$_WebMethod$_invokeInternal
37 }
38
39 Sys.Net._WebMethod.registerClass('Sys.Net._WebMethod');
当然,也有使用例如“.prototype.xxx = function xxx$xxx(...)”这样的定义方法,不过很明显,它们是完全相同的。
很容易得出这种命名的方式的规则:将一个方法的名称包含namespace,类名和方法名写全(例如:Sys.Net._WebMethod._execute),再将所有的点“.”替换成“$”(Sys$Net$$_WebMethod$_execute)。这样的命名规则有什么好处呢?从官方Whitepaper和试验结果来看,它们的作用是在调试时提供详细的Call Stack信息。
我们来做一个试验,先来写一个不使用这种命名方式代码。如下:
1 Type.regeisterNamespace("Jeffz.TraditionalNaming");
2
3 Jeffz.TraditionalNaming.method1()
4 {
5 Jeffz.TraditionalNaming.method2();
6 }
7
8 Jeffz.TraditionalNaming.method2()
9 {
10 Jeffz.TraditionalNaming.method3();
11 }
12
13 Jeffz.TraditionalNaming.method3()
14 {
15 debugger;
16 }
下面是使用了命名规则的代码,如下:
1 Type.registerNamespace("Jeffz.AtlasNaming");
2
3 Jeffz.AtlasNaming.method1 = function Jeffz$AtlasNaming$method1()
4 {
5 Jeffz.AtlasNaming.method2();
6 }
7
8 Jeffz.AtlasNaming.method2 = function Jeffz$AtlasNaming$method2()
9 {
10 Jeffz.AtlasNaming.method3();
11 }
12
13 Jeffz.AtlasNaming.method3 = function Jeffz$AtlasNaming$method3()
14 {
15 debugger;
16 }
代码没有任何功能,仅仅是为了调出调试窗口。在调试普通命名的脚本时,Call Stack显示所有的方法都是“JScript anonymous function”。如下:
这样就不能直观地看出Call Stack中的方法了。如果使用了上述的命名规则,就会大大改善这种情况。
不过很明显,这样的命名方式只是对于调试有用,在功能上却没有帮助。因此在Release的脚本中不应该使用这种命名方式,以减小脚本的体积。
二、脚本方法参数验证和注释规则
在ASP.NET AJAX的Debug脚本中,可以看到几乎每个方法都会调用了Function._validateParams方法,作用就是对于参数的正确传递进行校验。由于Javascript的特点,在参数传递这个方面非常灵活。但是按照ASP.NET AJAX的官方说法,MS AJAX Library的目的是要建立一个良好的模型,因此为方法接收到参数进行严禁的检验可以说是重要的一步了。
进行参数检验的方法很简单,看一下脚本中的例子就可以得到简单地使用方式。在使用时会运用下面这个模式:
1 var e = Function._validateParams(
2 arguments,
3 [
4 {name: "param1", attribute1: value, attribute2: value...},
5 {name: "param2", attribute1: value, attribute2: value...},
6 ……
7 ]);
8
9 if (e) throw e;
传递给Function_validateParams方法的第一个参数是需要被验证的真实参数,而第二个参数是一个数组,依次存放了对于每个参数的描述信息,其中name的作用仅仅是为了在出错提示中显示更加具体的信息,这个描述性的关键在于之后所有的attribute信息。它们可以有哪些?分别有些什么作用?既然文档中没有提供,那么我们通过阅读代码来获得答案。
首先,自然是Function_validateParams方法的代码。如下:
1 Function._validateParams = function Function$_validateParams(params, expectedParams) {
2 var e;
3 // 检查参数数量
4 e = Function._validateParameterCount(params, expectedParams);
5 // 如果出错
6 if (e) {
7 // 则初始化错误对象的stack信息
8 e.popStackFrame();
9 return e;
10 }
11
12 // 枚举每一个真实参数
13 for (var i=0; i < params.length; i++) {
14 // 取到当前参数的描述,可能有parameterArray,所以最多使用最后一个参数描述信息
15 var expectedParam = expectedParams[Math.min(i, expectedParams.length - 1)];
16
17 // 得到参数名
18 var paramName = expectedParam.name;
19 // 如果当前参数应该属于一个parameter array
20 if (expectedParam.parameterArray) {
21 // 则参数后加上表示数组的后缀,这样组成的参数名会被使用到错误信息中
22 paramName += "[" + (i - expectedParams.length + 1) + "]";
23 }
24
25 // 检查参数
26 e = Function._validateParameter(params[i], expectedParam, paramName);
27 if (e) {
28 e.popStackFrame();
29 return e;
30 }
31 }
32
33 return null;
34 }
在上面的代码中,将错误对象e返回之前会调用它的popStackFrame方法。这个方法的目的是初始化错误对象的stack信息,它类似于.NET Framework中Exception的StackTrace。一个对象的popStackFrame方法被多次调用也不会出现问题,但是由于它会验证stack信息是否存在,因此只有第一次调用才是真正有效的。
在上面的代码的注释中,我提到了parameterArray这个信息。这是个很强大的东西,它的作用是在Javascript函数中产生类似C#中params关键字的效果:
{
……
}
这样,在产生代码的代码中也能够使用一一枚举的方式了。不过Javascript并没有提供自动组成一个Array对象的能力,因此只能自己从arguments对象中构造了。例如:
1 function method(var param1)
2 {
3 var paramArray = new Array();
4 for (var i = 1; i < arguments.length; i++)
5 {
6 paramArray.push(arguments[i]);
7 }
8
9 ……
10 }
接下面我们来分析一下Function._validateParameterCount方法的实现。如下:
1 Function._validateParameterCount = function Function$_validateParameterCount(params, expectedParams) {
2 // 最多个数为expectedParams的长度
3 var maxParams = expectedParams.length;
4 // 先将最小参数个数置零
5 var minParams = 0;
6 // 枚举每一个参数
7 for (var i=0; i < expectedParams.length; i++) {
8 // 如果某一个参数为prameterArray
9 if (expectedParams[i].parameterArray) {
10 // 则最多参数个数为无穷大
11 maxParams = Number.MAX_VALUE;
12 }
13 else if (!expectedParams[i].optional) { // 如果该参数不是可选参数
14 // 那么最小参数个数加1
15 minParams++;
16 }
17 }
18
19 // 如果参数个数不在正确范围内
20 if (params.length < minParams || params.length > maxParams) {
21 // 那么抛出parameterCount()异常
22 var e = Error.parameterCount();
23 // 调用popStackFrame方法保证e.stack的存在
24 e.popStackFrame();
25 return e;
26 }
27
28 return null;
29 }
不做多余的分析了,可以看出,如果有了parameterArray,那么在检测参数数量时就允许出现任意多个参数。
在Function._validateParams方法的后段里,会调用Function._validateParameter方法来检验单个参数。如下:
1 Function._validateParameter = function Function$_validateParameter(param, expectedParam, paramName) {
2 var e;
3
4 // 期望类型
5 var expectedType = expectedParam.type;
6 // 是个整型
7 var expectedInteger = !!expectedParam.integer;
8 // 允许为null
9 var mayBeNull = !!expectedParam.mayBeNull;
10
11 // 检验类型
12 e = Function._validateParameterType(param, expectedType, expectedInteger, mayBeNull, paramName);
13 if (e) {
14 e.popStackFrame();
15 return e;
16 }
17
18 // 获得数组参数的元素类型
19 var expectedElementType = expectedParam.elementType;
20 // 元素可否为空
21 var elementMayBeNull = !!expectedParam.elementMayBeNull;
22 if (expectedType === Array && typeof(param) !== "undefined" && param !== null &&
23 (expectedElementType || !elementMayBeNull)) {
24 // 元素是整型
25 var expectedElementInteger = !!expectedParam.elementInteger;
26 // 枚举数组的每一个元素
27 for (var i=0; i < param.length; i++) {
28 // 调用_validateParaeterType检查类型
29 var elem = param[i];
30 e = Function._validateParameterType(elem, expectedElementType, expectedElementInteger, elementMayBeNull,
31 paramName + "[" + i + "]");
32 if (e) {
33 e.popStackFrame();
34 return e;
35 }
36 }
37 }
38
39 return null;
40 }
在代码的前半段(第16行之前)的是用来判断单个参数,可以为参数指定类型,是否为整型,是否允许为null等限制。
在代码的后半段(第18行之后)就是用来判断一个Array参数的元素类型了,在这里会对数组内的每一个元素的类型进行检验。
Function._validateParameterType方法的作用很清晰。代码如下:
1 Function._validateParameterType = function Function$_validateParameterType(param, expectedType, expectedInteger, mayBeNull, paramName) {
2 var e;
3
4 // 如果参数未定义
5 if (typeof(param) === "undefined") {
6 // 如果允许为null
7 if (mayBeNull) {
8 // Pass
9 return null;
10 }
11 else {
12 // 否则返回argumentUndefined错误对象
13 e = Error.argumentUndefined(paramName);
14 e.popStackFrame();
15 return e;
16 }
17 }
18
19 // 如果传入的null参数,同上,只是最后错误对象不同
20 if (param === null) {
21 if (mayBeNull) {
22 return null;
23 }
24 else {
25 e = Error.argumentNull(paramName);
26 e.popStackFrame();
27 return e;
28 }
29 }
30
31 // 如果确定期望类型,并且期望类型是枚举类型
32 // (__enum在registerEnum时被设置为true)
33 if (expectedType && expectedType.__enum) {
34 // 则参数必须为Number类型
35 if (typeof(param) !== 'number') {
36 e = Error.argumentType(paramName, Object.getType(param), expectedType);
37 e.popStackFrame();
38 return e;
39 }
40 // 参数值必须是整数
41 if ((param % 1) === 0) {
42 // 取到枚举的数值集合
43 var values = expectedType.prototype;
44 // 当该类型不是标记(flag)类型,或者参数值是0,
45 // 则枚举查看参数值是否在数值集合中。
46 // (如果是flag的话参数的值不必等于枚举值,因此不必检验)
47 if (!expectedType.__flags || (param === 0)) {
48 for (var i in values) {
49 if (values[i] === param) return null;
50 }
51 }
52 else { // 参数类型为标记(flag),并且参数值不为0
53 var v = param;
54 // 枚举每一个值
55 for (var i in values) {
56 // 找到非0的枚举值
57 var vali = values[i];
58 if (vali === 0) continue;
59 // 如果参数值包含这个枚举
60 if ((vali & param) === vali) {
61 // 则从值中去除这个枚举值
62 v -= vali;
63 }
64
65 // 剩余值为0,则Pass
66 if (v === 0) return null;
67 }
68 }
69 }
70
71 e = Error.argumentOutOfRange(paramName, param, String.format(Sys.Res.enumInvalidValue, param, expectedType.getName()))
72 e.popStackFrame();
73 return e;
74 }
75
76 // 使用isInstanceOfType来判断类型
77 if (expectedType && !expectedType.isInstanceOfType(param)) {
78 e = Error.argumentType(paramName, Object.getType(param), expectedType);
79 e.popStackFrame();
80 return e;
81 }
82
83 // 如果类型为Number,并且必须为整数
84 if (expectedType === Number && expectedInteger) {
85 // 如果类型不是整数
86 if ((param % 1) !== 0) {
87 e = Error.argumentOutOfRange(paramName, param, Sys.RuntimeRes.argumentInteger);
88 e.popStackFrame();
89 return e;
90 }
91 }
92
93 return null;
94 }
个人认为这段代码里对于枚举和标记的判断比较有特色,其中“param % 1 === 0”的作用是确保param是整数。“===”的作用是进行精确的比较,如果单单用于“==”的话,Javascript会设法进行一些转换后比较,例如一个数字100和一个字符串"100",“100 == "100"”会返回true。在Microsoft AJAX Library里使用的几乎都是“===”。
我们可以看出,Microsoft AJAX Library的确正在设法构造一个良好的编程模型,很多内容都模仿了.NET Framework的方式。当然,对于Release版本来说,验证参数传递的代码都是多余的。事实上,在Release的Microsoft AJAX Library中也不存在那些Function._validateXXX函数。
另外,在Microsoft AJAX Library中也可以发现,每个方法都有XML形式的注释,注释的写法和含义与验证方式里提供的Attribute一一对应。根据官方的说法,它们能够在Visual Studio“Orcas”中使用这些信息提供的IntelliSense功能和其他信息。自然,注释也只是需要在Debug脚本中出现。
三、开发抽象类与接口规则
其实在官方的白皮书《Changes between the ASP.NET AJAX (Atlas) CTP and the v1.0 Beta/RTM Release》中已经对这部分内容有了讲述,而且我在《从Atlas到Microsoft ASP.NET AJAX(3) - Class and Type Definition, Reflection APIs》中也对这部分内容进行了翻译,因此在这里就不赘述了。
四、外部脚本文件命名规则
在使用ASP.NET AJAX时,一般使用ScriptManager来添加脚本文件而不是直接在页面里添加<script />元素。为了更好的配合ScriptManager管理Debug和Release脚本,外部脚本文件也需要遵守一定命名规则。
对于外部脚本文件的引用,如果ScriptReference的ScriptMode设为Debug,则会使用ScriptReference类的静态方法GetDebugPath(string)来处理脚本文件的路径,GetDebugPath(string)方法会调用ScriptReference类的静态方法ReplaceExtension将“.js”替换成“.debug.js”。因此我们在开发Debug脚本和Release脚本时,需要将Debug脚本的文件设为与Release脚本文件名对应的“.debug.js”文件。例如,如果Debug脚本的文件名为HelloWorld.js,则它对应的Release脚本的文件名则为“HelloWorld.debug.js”。
五、程序集内嵌脚本文件命名规则
将脚本内嵌在程序集中是分发控件的重要方法。与外部脚本命名规则相同,需要将Debug脚本的文件设为与Release脚本文件名对应的“.debug.js”文件。
注意:可能我对于第四条和第五条的表述有些模糊不清,事实上ScriptManager在选择脚本时的逻辑还是比较复杂的,可能我会使用一个独立章节进行这部分代码的分析。到那时,我们就会对ScriptManager对于脚本引用的行为有个清晰的认识了。
六、Release脚本
Release脚本是为生产环境开发的,因此应该对于文件进行必要的压缩,以尽可能地缩小脚本的体积。
关于“Sys$Net$$_WebMethod$_execute”这种命名方式,其主要的好处在于效率提升。
这涉及到了JavaScript中的一个最为基础,也是最为常见的效率知识:JavaScript中每一次点(.)操作都会遍历该对象下的所有子对象,用来找到点后面的对象,如果点非常多的话,那么效率方面将受到非常大的影响。