Hello World
Spiga

深入Atlas系列:客户端代码编写规则分析与指南

2006-10-25 18:29 by 老赵, 4600 visits
  在RTM版本发布之后,我决定在“深入Atlas系列”的第一篇文章中讨论一下编写客户端代码的规则。

在RTM版本中,我们可以发现ASP.NET AJAX的客户端脚本引入了许多规则:有方法注释规则,有参数验证规则,而且对于Debug和Release模式下的脚本代码,甚至添加在程序集里的方式,也有相当严禁的规则。如果我们想要编写真正规范和严谨的代码或组件,了解这些规则是非常必要的。有了这些规则,用户在使用Debug模式进行开发和调试时可以得到更好的提示(比如Call Stack),下一版本的Visual Studio“Orcas”也会根据这些规则提供良好的IntelliSense功能。

另外,了解这些规则也有利于帮助开发人员阅读和理解客户端代码,这不也是我写“深入Atlas系列”的目的吗?在这片文章里,我将使用ASP.NET AJAX的脚本代码为范例,对开发规则进行一番描述。在某些时候也会对客户端和服务器端的部分代码进行简单的分析。


一、脚本方法命名规则

在这里,一些简单的规则我就不详细说明了,我在这里就简单地列举一下:
  1. 属性的get和set方法,使用“get_”和“set_”作为前缀。
  2. 事件的add和remove方法,使用“add_”和“remove_”作为前缀。
  3. 对于私有的方法,使用下划线“_”作为前缀。
  这里我想讲的其实是Debug脚本内大量使用的函数的“假名”。在Debug模式的脚本中,大量充斥着一种以“$”为分隔符的命名方法,这种方法被使用在包括构造函数之内的所有方法。例如:
Sys.Net._WebMethod方法Debug模式代码

当然,也有使用例如“.prototype.xxx = function xxx$xxx(...)”这样的定义方法,不过很明显,它们是完全相同的。

很容易得出这种命名的方式的规则:将一个方法的名称包含namespace,类名和方法名写全(例如:Sys.Net._WebMethod._execute),再将所有的点“.”替换成“$”(Sys$Net$$_WebMethod$_execute)。这样的命名规则有什么好处呢?从官方Whitepaper和试验结果来看,它们的作用是在调试时提供详细的Call Stack信息。

我们来做一个试验,先来写一个不使用这种命名方式代码。如下:
TraditionalNaming.js代码

下面是使用了命名规则的代码,如下:
AtlasNaming.js代码

代码没有任何功能,仅仅是为了调出调试窗口。在调试普通命名的脚本时,Call Stack显示所有的方法都是“JScript anonymous function”。如下:


这样就不能直观地看出Call Stack中的方法了。如果使用了上述的命名规则,就会大大改善这种情况。


不过很明显,这样的命名方式只是对于调试有用,在功能上却没有帮助。因此在Release的脚本中不应该使用这种命名方式,以减小脚本的体积。

Tips  在Debug模式的脚本中使用带有“假名”的命名方式。而在Release模式的脚本中取消这种方式的使用,以减小脚本的体积。



二、脚本方法参数验证和注释规则

在ASP.NET AJAX的Debug脚本中,可以看到几乎每个方法都会调用了Function._validateParams方法,作用就是对于参数的正确传递进行校验。由于Javascript的特点,在参数传递这个方面非常灵活。但是按照ASP.NET AJAX的官方说法,MS AJAX Library的目的是要建立一个良好的模型,因此为方法接收到参数进行严禁的检验可以说是重要的一步了。

进行参数检验的方法很简单,看一下脚本中的例子就可以得到简单地使用方式。在使用时会运用下面这个模式:
验证参数传递正确性代码模版

传递给Function_validateParams方法的第一个参数是需要被验证的真实参数,而第二个参数是一个数组,依次存放了对于每个参数的描述信息,其中name的作用仅仅是为了在出错提示中显示更加具体的信息,这个描述性的关键在于之后所有的attribute信息。它们可以有哪些?分别有些什么作用?既然文档中没有提供,那么我们通过阅读代码来获得答案。

首先,自然是Function_validateParams方法的代码。如下:
Function._validateParams方法代码

在上面的代码中,将错误对象e返回之前会调用它的popStackFrame方法。这个方法的目的是初始化错误对象的stack信息,它类似于.NET Framework中Exception的StackTrace。一个对象的popStackFrame方法被多次调用也不会出现问题,但是由于它会验证stack信息是否存在,因此只有第一次调用才是真正有效的。

在上面的代码的注释中,我提到了parameterArray这个信息。这是个很强大的东西,它的作用是在Javascript函数中产生类似C#中params关键字的效果:
public void Method(int i, params int[] paramArray)

    ……
}

这样,在产生代码的代码中也能够使用一一枚举的方式了。不过Javascript并没有提供自动组成一个Array对象的能力,因此只能自己从arguments对象中构造了。例如:
从arguments对象中构造参数数组

Tips  在描述参数信息时,可以使用parameterArray来指定一个参数为参数数组,例如“{name: "paramArray", type: Array, parameterArray: true}”。当然,只有出现在方法最后的参数才能是parameterArray。

接下面我们来分析一下Function._validateParameterCount方法的实现。如下:
Function._validateParameterCount函数代码

不做多余的分析了,可以看出,如果有了parameterArray,那么在检测参数数量时就允许出现任意多个参数。

在Function._validateParams方法的后段里,会调用Function._validateParameter方法来检验单个参数。如下:
Function._validateParameter方法代码

在代码的前半段(第16行之前)的是用来判断单个参数,可以为参数指定类型,是否为整型,是否允许为null等限制。

Tips  在构造一个参数的描述对象时,可以限制参数的类型,限制其是否为整型(仅仅通过type:Number指定参数类型的话,并不能避免浮点数作为参数的可能,因此需要integer作限制),是否允许为null。例如“{name: "intParam", type: Number, integer: true, mayBeNull: true}”表明了一个名为intParam的参数必须是一个整数,但是可以为null。。

在代码的后半段(第18行之后)就是用来判断一个Array参数的元素类型了,在这里会对数组内的每一个元素的类型进行检验。

Tips  在构造一个Array参数的描述对象时,除了使用type指定它的类型为Array之外,还能够使用elementType,elementInteger,elementMayBeNull来限制数组内元素的信息。例如“{name: "intArray", type: Array, elementType: Number, elementInteger: true, elementMayBeNull: true}”表明了一个能够存放整数或者null的数组参数。

Function._validateParameterType方法的作用很清晰。代码如下:
Function._validateParameterType方法代码

个人认为这段代码里对于枚举和标记的判断比较有特色,其中“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函数。

Tips  在Debug脚本中,为每个方法(至少是公开方法)添加参数验证,而在Release脚本中,则删除所有这些代码。

另外,在Microsoft AJAX Library中也可以发现,每个方法都有XML形式的注释,注释的写法和含义与验证方式里提供的Attribute一一对应。根据官方的说法,它们能够在Visual Studio“Orcas”中使用这些信息提供的IntelliSense功能和其他信息。自然,注释也只是需要在Debug脚本中出现。

Tips  在Debug版本中为每个方法(至少是公开方法)添加注释,而再Release版本中去除所有这些内容。



三、开发抽象类与接口规则

其实在官方的白皮书《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》中也对这部分内容进行了翻译,因此在这里就不赘述了。

Tips  在定义抽象类或接口时,对于Debug脚本,将抽象方法和接口方法全部设为Function.abstractMethod,并且再构造函数中添加“throw Error.notImplemented();”语句。在Release脚本中对于抽象函数或接口函数则不要定义任何内容。



四、外部脚本文件命名规则

在使用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”。

Tips  在开发外部Debug脚本和Release脚本时,将Debug脚本的文件名设为与Release脚本文件名对应的“.debug.js”文件。
在使用外部脚本文件进行开发时,将ScriptReference的DebugMode设为Debug,以能够使用Debug脚本,获得更多的支持(当然前提是外部脚本文件有正确的Debug版本)。



五、程序集内嵌脚本文件命名规则

将脚本内嵌在程序集中是分发控件的重要方法。与外部脚本命名规则相同,需要将Debug脚本的文件设为与Release脚本文件名对应的“.debug.js”文件。

Tips  在开发内嵌如程序集的Debug脚本和Release脚本时,将Debug脚本的文件名设为与Release脚本文件名对应的“.debug.js”文件。
在使用外部脚本文件进行开发时,将ScriptManager的DebugMode设为true会使所有的程序集脚本引用提供Debug脚本(ScriptReference的DebugMode会覆盖ScriptManager的DebugMode),以获得更多的支持(当然前提是程序集中有正确的Debug版本脚本)。


注意:可能我对于第四条和第五条的表述有些模糊不清,事实上ScriptManager在选择脚本时的逻辑还是比较复杂的,可能我会使用一个独立章节进行这部分代码的分析。到那时,我们就会对ScriptManager对于脚本引用的行为有个清晰的认识了。



六、Release脚本

Release脚本是为生产环境开发的,因此应该对于文件进行必要的压缩,以尽可能地缩小脚本的体积。

Tips  使用脚本压缩/混淆工具对于Release脚本进行压缩。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

20 条回复

  1. Dflying Chen
    *.*.*.*
    链接

    Dflying Chen 2006-10-25 20:34:00

    关于“Sys$Net$$_WebMethod$_execute”这种命名方式,其主要的好处在于效率提升。
    这涉及到了JavaScript中的一个最为基础,也是最为常见的效率知识:JavaScript中每一次点(.)操作都会遍历该对象下的所有子对象,用来找到点后面的对象,如果点非常多的话,那么效率方面将受到非常大的影响。

  2. 老赵
    admin
    链接

    老赵 2006-10-25 20:45:00

    @Dflying Chen
    在这里的作用只是为了在Call Stack里更加清楚,因为我们在访问时的确还是通过点“.”来访问的,那种命名方式在效率改进上没有任何的价值,只是相当于在“根”上多保留了一个函数的引用而已。Release脚本中也不存在这个方式。:)
    不过,我们的确应该避免使用不必要的“.”,例如在遍历一个数组时,先保留它的长度再使用for来遍历,这样可以提高效率。

  3. Dflying Chen
    *.*.*.*
    链接

    Dflying Chen 2006-10-25 20:53:00

    @Jeffrey Zhao
    恩,懂了,xiexie

  4. 老赵
    admin
    链接

    老赵 2006-10-25 21:09:00

    @Dflying Chen
    :)

  5. en[匿名][未注册用户]
    *.*.*.*
    链接

    en[匿名][未注册用户] 2006-10-25 22:53:00

    :)

  6. 小蜗牛
    *.*.*.*
    链接

    小蜗牛 2006-10-25 22:57:00

    看来规则很重要的。

  7. cathsfz
    *.*.*.*
    链接

    cathsfz 2006-10-25 23:21:00

    Preview的DragDropList+DraggableItem拖动起来比CTP慢,感觉上如此。有人有兴趣去做一个profile测试看看结果吗?

    另外是否有什么推荐的js压缩及混淆工具?

  8. 老赵
    admin
    链接

    老赵 2006-10-26 00:37:00

    @cathsfz
    可能需要看一下现在的代码,我看过当时的代码,现在还有些印象。
    至于js压缩工具我一直用破解的似乎不太好推荐……有免费的吗?

  9. 浪子
    *.*.*.*
    链接

    浪子 2006-10-26 08:54:00

    @Jeffrey Zhao
    codeproject上有

  10. cathsfz
    *.*.*.*
    链接

    cathsfz 2006-10-26 12:07:00

    @Jeffrey Zhao
    DragDrop的效率,或许体现了Whitepaper中所说的“enclosure在IE中效率稍高,而prototype在Firefox中效率稍高”吧,确实是IE中有明显的延迟感,但在Firefox中则爽快了。

    我现在试了一下在App_Init中用$create(),发现超好用,能够比得上xml-script。首先$create()能够帮你调用initialize()和beginUpdate()/endUpdate(),这样写javascript就不至于明显比写xml-script要多几句废话。其次是声明属性、事件用JSON的形式,论结构和美观也不比XML差。而且behavior现在能够脱离control而存在了,直接用$create声明关联到DomElement的behavior很方便。

  11. 老赵
    admin
    链接

    老赵 2006-10-26 13:54:00

    相当赞同后面那段话,$create将一系列操作封装起来了,能够将信息很集中的处理,省去了相当多的代码,很好用的。以前我必须自己写一个component加到xml-script中去,:)

  12. 戴尔网站[未注册用户]
    *.*.*.*
    链接

    戴尔网站[未注册用户] 2006-10-26 16:37:00

    非常好

  13. 蛙蛙池塘
    *.*.*.*
    链接

    蛙蛙池塘 2006-11-30 22:10:00

    几乎一点也看不懂,郁闷,下面一句话你可能写错了。

    例如,如果Release脚本的文件名为HelloWorld.js,则它对应的Release脚本的文件名则为“HelloWorld.debug.js”。

  14. 老赵
    admin
    链接

    老赵 2006-11-30 22:30:00

    @蛙蛙池塘
    的确写错了,谢谢指正。:)

  15. yxf[未注册用户]
    *.*.*.*
    链接

    yxf[未注册用户] 2007-01-09 17:06:00

    thank you

  16. 老赵
    admin
    链接

    老赵 2007-01-09 19:26:00

    @yxf
    :)

  17. 孤叶(学习.net框架)
    *.*.*.*
    链接

    孤叶(学习.net框架) 2007-02-14 17:32:00

    首先,自然是Function_validateParams方法的代码。如下:
    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)];



    //
    // 取到当前参数的描述,可能有parameterArray,所以最多使用最后一个参数描述信息
    15 var expectedParam = expectedParams[Math.min(i, expectedParams.length - 1)];
    中为什么不直接用
    expectedParams[i];

  18. 老赵
    admin
    链接

    老赵 2007-02-14 17:57:00

    @孤叶(学习.net框架)
    因为可能会出现类似于C#里这样的情况:
    method(int a, param object[] objs) { }

  19. 孤叶(学习.net框架)
    *.*.*.*
    链接

    孤叶(学习.net框架) 2007-02-14 18:13:00

    @Jeffrey Zhao
    暂时记下
    哈,不过还不能理解为什么要这样,就算是用method(int a, param object[] objs) { }
    传参

  20. 老赵
    admin
    链接

    老赵 2007-02-14 18:17:00

    @孤叶(学习.net框架)
    比如:expectedParams的长度为5,下标为0到4。
    实际传了8个参数,其中下标4到7的参数都必须符合expectedParams[4]中定义的样式。
    为了通用,就取i和expectedParams.length - 1中较小的那个。:)

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我