Javascript中会经常用到setTimeout来推迟一个函数的执行,如:

setTimeout(function(){alert("Hello World");},1000)

会在执行到这句话后延迟1秒钟来弹出alert窗口。那么再看这一段:

function a() {
    setTimeout(function() {alert(1)}, 0);
    alert(2);
}
a();

注意这段代码中的setTimeout延迟设为了0,就是延迟0毫秒,貌似是不做任何延迟立刻执行,即1,2。但实际的执行结果确是2,1。为什么?这得从Javascript调用堆栈(call stack)和setTimeout的功能说起。

首先,JavaScript是单线程的,即同一时间只执行一条代码,所以每一个JavaScript代码执行块会“阻塞”其它异步事件的执行。其次,和其他的编程语言一样,Javascript中的函数调用也是通过堆栈实现的。在执行函数a的时候,a先入栈,如果不给alert(1)加setTimeout,那么alert(1)第2个入栈,最后是alert(2)。但现在给alert(1)加上setTimeout后,alert(1)就被加入到了一个新的堆栈中等待,并“尽可能快”的执行。这个尽可能快就是指在a的堆栈完成后就立刻执行,因此实际的执行结果就是先alert(2),再alert(1)。在这里setTimeout实际上是让alert(1)脱离了当前函数调用堆栈。看下面一个例子:

<input name="input" onkeydown="alert(this.value)" type="text" value="a" />

这样一段函数意图是每输入一个字符就把当前input里的所有字符都alert出来,但实际效果确是alert出按键之前的内容。这里,我们就可以利用setTimeout(0)来实现。

<input onkeydown="var me=this; setTimeout(function(){alert(me.value)}, 0)" name="input" type="text" value="a" />

这样当onkeydown事件触发的时候,alert就被放入了下一个调用堆栈,一旦onkeydown事件触发的堆栈关闭后就开始执行。当然浏览器还有个onkeyup事件也可以实现我们的需求。

这样的setTimeout用法在实际项目中还是会时常遇到。比如浏览器会聪明的等到一个函数堆栈结束后才改变DOM,如果再这个函数堆栈中把页面背景先从白色设为红色,再设回白色,那么浏览器会认为DOM没有发生任何改变而忽略这两句话,因此我们可以通过setTimeout把“设回白色”函数加入下一个堆栈,那么就可以确保背景颜色发生过改变了(虽然速度很快可能无法被察觉)。

总之,setTimeout增加了Javascript函数调用的灵活性,为函数执行顺序的调度提供极大便利。

推荐阅读jQuery作者John的一篇文章:How JavaScript Timers Work,你会对JavaScript单线程本质和setTimeout以及setInterval有更加深刻的理解。

在网上看了一篇不错的JavaScript基础知识文章,感谢Realazy的辛苦翻译,后来才得知原来Realazy就是我同事的朋友,晕,世界真小。(PS:CSDN上的名人孟子e章竟然也是我同事的哥们,ft。。。)

看了这篇文章才发现越是基础的东西越能展示水平,这也就是为什么很多朋友向我索取原代码我都没有回复,并非不想分享,而是觉得自己的作品还不足以达到供别人学习的程度,还需努力。

引用:http://realazy.org/blog/2007/07/18/scope-in-javascript/

作用域(scope)是JavaScript语言的基石之一,在构建复杂程序时也可能是最令我头痛的东西。记不清多少次在函数之间传递控制后忘记 this关键字引用的究竟是哪个对象,甚至,我经常以各种不同的混乱方式来曲线救国,试图伪装成正常的代码,以我自己的理解方式来找到所需要访问的变量。

这篇文章将正面解决这个问题:简述上下文(context)和作用域的定义,分析可以让我们掌控上下文的两种方法,最后深入一种高效的方案,它能有效解决我所碰到的90%的问题。

我在哪儿?你又是谁
JavaScript 程序的每一个字节都是在这个或那个运行上下文(execution context)中执行的。你可以把这些上下文想象为代码的邻居,它们可以给每一行代码指明:从何处来,朋友和邻居又是谁。没错,这是很重要的信息,因为 JavaScript社会有相当严格的规则,规定谁可以跟谁交往。运行上下文则是有大门把守的社区而非其内开放的小门。

我们通常可以把这些社会边界称为作用域,并且有充足的重要性在每一位邻居的宪章里立法,而这个宪章就是我们要说的上下文的作用域链(scope chain)。在特定的邻里关系内,代码只能访问它的作用域链内的变量。与超出它邻里的变量比起来,代码更喜欢跟本地(local,即局部)的打交道。

具体地说,执行一个函数会创建一个不同的运行上下文,它会将局部作用域增加到它所定义的作用域链内。JavaScript通过作用域链的局部向全局攀升方式,在特定的上下文中解析标识符。这表示,本级变量会优先于作用域链内上一级拥有相同名字的变量。显而易见,当我的好友们一起谈论”Mike West”(本文原作者)时,他们说的就是我,而非bluegrass singer 或是Duke professor, 尽管(按理说)后两者著名多了。

让我们看些例子来探索这些含义:

<script type="text/javascript">
var ima_celebrity = "Everyone can see me! I'm famous!",
the_president = "I'm the decider!";

function pleasantville() {
var the_mayor = "I rule Pleasantville with an iron fist!",
ima_celebrity = "All my neighbors know who I am!";

function lonely_house() {
var agoraphobic = "I fear the day star!",
a_cat = "Meow.";
}
}
</script>

我们的全明星,ima_celebrity, 家喻户晓(所有人都认识她)。她在政治上积极活跃,敢于在一个相当频繁的基层上叫嚣总统(即the_president)。她会为碰到的每一个人签名和回答问题。就是说,她不会跟她的粉丝有私下的联系。她相当清楚粉丝们的存在 并有他们自己某种程度上的个人生活,但也可以肯定的是,她并不知道粉丝们在干嘛,甚至连粉丝的名字都不知道。

而在欢乐市(pleasantville)内,市长(the_mayor)是众所周知的。她经常在她的城镇内散步,跟她的选民聊天、握手并亲吻小孩。因为欢乐市(pleasantville)还算比较大且重要的邻居,市长在她办公室内放置一台红色电话,它是一条可以直通总统的7×24热线。她还可以看到市郊外山上的孤屋(lonely_house),但从不在意里面住着的是谁。

而孤屋(lonely_house)是一个自我的世界。旷恐患者时常在里面囔囔自语,玩纸牌和喂养一个小猫(a_cat)。他偶尔会给市长(the_mayor)打电话咨询一些本地的噪音管制,甚至在本地新闻看到ima_celebrity后会写些粉丝言语给她(当然,这是pleasantville内的ima_celebrity)。

this? 那是虾米?
每一个运行上下文除了建立一个作用域链外,还提供一个名为this的关键字。它的普遍用法是,this作为一个独特的功能,为邻里们提供一个可访问到它的途径。但总是依赖于这个行为并不可靠:取决于我们如何进入一个特定邻居的具体情况,this表示的完全可能是其他东西。事实上,我们如何进去邻居家本身,通常恰恰就是this所指。有四种情形值得特别注意:

呼叫对象的方法
在经典的面向对象编程中,我们需要识别和引用当前对象。this极好地扮演了这个角色,为我们的对象提供了自我查找的能力,并指向它们本身的属性。

<script type="text/javascript">
var deep_thought = {
the_answer: 42,
ask_question: function () {
return this.the_answer;
}
};

var the_meaning = deep_thought.ask_question();
</script>

这个例子建立了一个名为deep_thought的对象,设置其属性 the_answer为42,并创建了一个名为ask_question 的方法(method)。当deep_thought.ask_question()执行时, JavaScript为函数的呼叫建立了一个运行上下文,通过”.“运算符把this指向被引用的对象,在此是deep_thought这个对象。之后这个方法就可以通过this在镜子中找到它自身的属性,返回保存在 this.the_answer中的值:42。

构造函数
类似地,当定义一个作为构造器的使用new关键字的函数时,this可以用来引用刚创建的对象。让我们重写一个能反映这个情形的例子:

<script type="text/javascript">
function BigComputer(answer) {
this.the_answer = answer;
this.ask_question = function () {
return this.the_answer;
}
}

var deep_thought = new BigComputer(42);
var the_meaning = deep_thought.ask_question();
</script>

我们编写一个函数来创建BigComputer对象,而不是直白地创建 deep_thought对象,并通过new关键字实例化deep_thought为一个实例变量。当new BigComputer()被执行,后台透明地创建了一个崭新的对象。呼叫BigComputer后,它的this关键字被设置为指向新对象的引用。这个函数可以在this上设置属性和方法,最终它会在BigComputer执行后透明地返回。

尽管如此,需要注意的是,那个deep_thought.the_question()依然可以像从前一样执行。那这里发生了什么事?为何this在the_question内与BigComputer内会有所不同?简单地说,我们是通过new进入BigComputer的,所以this表示“新(new)的对象”。在另一方面,我们通过 deep_thought进入the_question,所以当我们执行该方法时,this表示 “deep_thought所引用的对象”。this并不像其他的变量一样从作用域链中读取,而是在上下文的基础上,在上下文中重置。

函数呼叫
假如没有任何相关对象的奇幻东西,我们只是呼叫一个普通的、常见的函数,在这种情形下this表示的又是什么呢?

<script type="text/javascript">
function test_this() {
return this;
}
var i_w
onder_what_this_is = test_this();
</script>

在这样的场合,我们并不通过new来提供上下文,也不会以某种对象形式在背后偷偷提供上下文。在此, this默认下尽可能引用最全局的东西:对于网页来说,这就是 window对象。

事件处理函数
比普通函数的呼叫更复杂的状况,先假设我们使用函数去处理的是一个onclick事件。当事件触发我们的函数运行,此处的this表示的是什么呢?不凑巧,这个问题不会有简单的答案。

如果我们写的是行内(inline)事件处理函数,this引用的是全局window对象:

<script type="text/javascript">
function click_handler() {
alert(this); // 弹出 window 对象
}
</script>

<button id='thebutton' onclick='click_handler()'>Click me!</button>

但是,如果我们通过JavaScript来添加事件处理函数,this引用的是生成该事件的DOM元素。(注意:此处的事件处理非常简洁和易于阅读,但其他的就别有洞天了。请使用真正的addEvent函数取而代之):

<script type="text/javascript">
function click_handler() {
alert(this); // 弹出按钮的DOM节点
}

function addhandler() {
document.getElementById('thebutton').onclick = click_handler;
}

window.onload = addhandler;
</script>

<button id='thebutton'>Click me!</button>

复杂情况
让我们来短暂地运行一下这个最后的例子。我们需要询问deep_thought一个问题,如果不是直接运行click_handler而是通过点击按钮的话,那会发生什么事情?解决此问题的代码貌似十分直接,我们可能会这样做:

<script type="text/javascript">
function BigComputer(answer) {
this.the_answer = answer;
this.ask_question = function () {
alert(this.the_answer);
}
}

function addhandler() {
var deep_thought = new BigComputer(42),
the_button = document.getElementById('thebutton');

the_button.onclick = deep_thought.ask_question;
}

window.onload = addhandler;
</script>

很完美吧?想象一下,我们点击按钮,deep_thought.ask_question被执行,我们也得到了“42”。但是为什么浏览器却给我们一个undefined? 我们错在何处?

其实问题显而易见:我们给ask_question传递一个引用,它作为一个事件处理函数来执行,与作为对象方法来运行的上下文并不一样。简而言之,ask_question中的 this关键字指向了产生事件的DOM元素,而不是在BigComputer的对象中。DOM元素并不存在一个the_answer属性,所以我们得到的是 undefined而不是”42″. setTimeout也有类似的行为,它在延迟函数执行的同时跑到了一个全局的上下文中去了。

这个问题会在程序的所有角落时不时突然冒出,如果不细致地追踪程序的每一个角落的话,还是一个非常难以排错的问题,尤其在你的对象有跟DOM元素或者window对象同名属性的时候。

使用.apply()和.call()掌控上下文
在点击按钮的时候,我们真正需要的是能够咨询deep_thought一个问题,更进一步说,我们真正需要的是,在应答事件和setTimeout的呼叫时,能够在自身的本原上下文中呼叫对象的方法。有两个鲜为人知的JavaScript方法,apply和call,在我们执行函数呼叫时,可以曲线救国帮我们达到目的,允许我们手工覆盖this的默认值。我们先来看call:

<script type="text/javascript">
var first_object = {
num: 42
};
var second_object = {
num: 24
};

function multiply(mult) {
return this.num * mult;
}

multiply.call(first_object, 5); // 返回 42 * 5
multiply.call(second_object, 5); // 返回 24 * 5
</script>

在这个例子中,我们首先定义了两个对象,first_object和second_object,它们分别有自己的num属性。然后定义了一个multiply函数,它只接受一个参数,并返回该参数与this所指对象的num属性的乘积。如果我们呼叫函数自身,返回的答案极大可能是undefined,因为全局window对象并没有一个num属性除非有明确的指定。我们需要一些途径来告诉multiply里面的this关键字应该引用什么。而multiply的call方法正是我们所需要的。

call的第一个参数定义了在业已执行的函数内this的所指对象。其余的参数则传入业已执行的函数内,如同函数的自身呼叫一般。所以,当执行multiply.call(first_object, 5)时,multiply被呼叫,5传入作为第一个参数,而this关键字被设置为first_object的引用。同样,当执行multiply.call(second_object, 5)时,5传入作为第一个参数,而this关键字被设置为second_object的引用。

apply以call一样的方式工作,但可以让你把参数包裹进一个数组再传递给呼叫函数,在程序性生成函数呼叫时尤为有用。使用apply重现上一段代码,其实区别并不大:

<script type="text/javascript">

multiply.apply(first_object, [5]); // 返回 42 * 5
multiply.apply(second_object, [5]); // 返回 24 * 5
</script>

apply和call本身都非常有用,并值得贮藏于你的工具箱内,但对于事件处理函数所改变的上下文问题,也只是送佛到西天的中途而已,剩下的还是得我们来解决。在搭建处理函数时,我们自然而然地认为,只需简单地通过使用call来改变this的含义即可:

function addhandler() {
var deep_thought = new BigComputer(42),
the_button = document.getElementById('thebutton');

the_button.onclick = deep_thought.ask_question.call(deep_thought);
}

代码之所以有问题的理由很简单:call立即执行了函数(译注:其实可以用一个匿名函数封装,例如the_button.onclick = function(){deep_thought.ask_question.call(deep_thought);},但比起即将讨论的bind来,依然不够优雅)。我们给onclcik处理函数一个函数执行后的结果而非函数的引用。所以我们需要利用另一个JavaScript特色,以解决这个问题。

.bind()之美
我并不是 Prototype JavaScript framework的忠实粉丝,但我对它的总体代码质量印象深刻。具体而言,它为Function对象增加一个简洁的补充,对我管理函数呼叫执行后的上下文产生了极大的正面影响:bind跟call一样执行相同的常见任务,改变函数执行的上下文。不同之处在于bind返回的是函数引用可以备用,而不是call的立即执行而产生的最终结果。

如果需要简化一下bind函数以抓住概念的重点,我们可以先把它插进前面讨论的乘积例子中去,看它究竟是如何工作的。这是一个相当优雅的解决方案:

<script type="text/javascript">
var first_object = {
num: 42
};
var second_object = {
num: 24
};

function multiply(mult) {
return this.num * mult;
}

Function.prototype.bind = function(obj) {
var method = this,
temp = function() {
return method.apply(obj, arguments);
};

return temp;
}

var first_multiply = multiply.bind(first_object);
first_multiply(5); // 返回 42 * 5

var second_multiply = multiply.bind(second_object);
second_multiply(5); // 返回 24 * 5
</script>

首先,我们定义了first_object, second_object和multiply函数,一如既往。细心处理这些后,我们继续为Function对象的prototype定义一个bind方法,这样的话,我们程序里的函数都有一个bind方法可用。当执行multiply.bind(first_object)时,JavaScript为bind方法创建一个运行上下文,把this置为multiply函数的引用,并把第一个参数obj置为first_object的引用。目前为止,一切皆顺。

这个解决方案的真正天才之处在于method的创建,置为this的引用所指(即multiply函数自身)。当下一行的匿名函数被创建,method通过它的作用域链访问,obj亦然(不要在此使用this, 因为新创建的函数执行后,this会被新的、局部的上下文覆盖)。这个this的别名让apply执行multiply函数成为可能,而传递obj则确保上下文的正确。用计算机科学的话说,temp是一个闭包(closure),它可以保证,需要在first_object的上下文中执行multiply,bind呼叫的最终返回可以用在任何的上下文中。

这才是前面说到的事件处理函数和setTimeout情形所真正需要的。以下代码完全解决了这些问题,绑定deep_thought.ask_question方法到deep_thought的上下文中,因此能在任何事件触发时都能正确运行:

function addhandler() {
var deep_thought = new BigComputer(42),
the_button = document.getElementById('thebutton');

the_button.onclick = deep_thought.ask_question.bind(deep_thought);
}

漂亮。

转自:http://benchwang.spaces.live.com/blog/cns!1621B5CAD6EB680B!149.entry

Adam McCrea 写了篇使用 JavaScript 进行元编程的文章: Metaprogamming JavaScript。
该文用一个例子来说明元编程。例子很简单,一个 Form 包中两个下拉列表 country 和 state,业务需求是 country 下拉列表中选中“United States”则要显示 state 列表,否则隐藏该下拉列表。处理该逻辑的代码如下:

Event.observe(window, “load”, function() {
Event.observe($(“country”), “change”, function() {
if ($F(“country”) == “United States”)
$(“state-field”).show();
else
$(“state-field”).hide();
});
});

这里用到的对象和方法是 Prototype 对 JavaScript 的扩展。Prototype 是旨在简化动态 Web 程序开发的 JavaScript Framework. 详见 http://www.prototypejs.org/。在上面代码中:Event.observe 在IE下相当于 attachEvent,用于注册事件处理函数。这是注册了 window.onload 和 country.onchange 事件处理程序。$() 是 document.getElementById() 的别名。$F() 会取出 form 中元素当前值。show() 和 hide() 是在 html 元素上增加的方法。通过 prototype 在 html 元素基础上扩展了很多方法。如果想运行上面代码,需要下载 prototype.js 并放置在 html 页面相同目录。完整代码如下:

<html>
<head>
<title>Metaprogramming JavaScript – Example 1</title>
<script src=”prototype.js”></script>
<script type=”text/javascript” charset=”utf-8″>
Event.observe(window, “load”, function() {
Event.observe($(“country”), “change”, function() {
if ($F(“country”) == “United States”)
$(“state-field”).show();
else
$(“state-field”).hide();
});

});
</script>
</head>

<body>
<form>
<p id=”country-field”>
<label for=”country”>Country</label>
<select id=”country”>
<option>United States</option>
<option>Canada</option>
<option>Somewhere Else</option>
</select>
</p>

<p id=”state-field”>
<label for=”state”>State</label>
<select id=”state”>
<option>Ohio</option>
<option>Michigan</option>
<option>Kentucky</option>
</select>
</p>
</form>
</body>
</html>

这些代码满足这样简单的需要没有什么问题。然而随着需求变化,代码会变得繁琐。假如增加如下需求:
增加 province 字段
当 country 为 “Canada” ,显示 province
当 country 为 “United States”,显示 state
当 state 是 “Ohio” 或 “Michigen”,显示 Brutus(很可爱的logo)
根据以上需求,代码修改为:

Event.observe(window, “load”, function() {
Event.observe($(“country”), “change”, function() {
var country = $F(“country”);
if (country == “United States”) {
$(“us-state-field”).show();
$(“province-field”).hide();
} else if (country == “Canada”) {
$(“province-field”).show();
$(“us-state-field”).hide();
} else {
$(“us-state-field”).hide();
$(“province-field”).hide();
}
});

Event.observe($(“us-state”), “change”, function() {
var state = $F(“us-state”);
if (state == “Ohio” || state == “Michigan”)
$(“brutus”).show();
else
$(“brutus”).hide();
});

});

新程序使用先前的事件处理和字段检查方式,尝试扩展程序以满足新的需求。然而这样有几个问题,第一个问题这个代码有两个 bug:1)页面初始载入时所有字段都显示出来了,2)隐藏 state 字段不会,不会自动隐藏 Brutus。后者可称为多米诺(domino)bug,因为如果有其他元素依赖于动态显示和隐藏的字段会导致级联影响。

第二个问题是代码的可读性。不容易立即看出这段代码想达到的目的,因而需要很仔细的检查是否遗漏或者不严格符合需求。当需求发生变化,很希望准确地知道代码什么地方需要修改,当作了修改后,需要有信心确信不会影响其它功能。元编程概念就是用于解决这个问题。

Pragmatic Programmer(实用主义程序员?) 把元编程描述为江代码中的细节抽出来放到元数据中。元数据通常为配置文件或者其他的数据源,当然元数据也可以是可执行代码,只要把“what”(是什么)和“how”(如何做)清晰分离开。该方法的思路是使用易于描述问题论域的词汇来编写代码。问题论域即要描述的“what”。这种类型语言称为领域专用语言(domain specific language),缩写为 DSL。在上面例子中,“what”是显示和隐藏 form 中字段的规则。目前的实现是把规则和规则的实现耦合在一起。随着需求变化和特性增加,代码会变得越来越不清晰。如果能够将这些规则抽出来,规则修改不再依赖于实现,会减少应对需求变化的苦恼。如果使用一种比 JavaScript 更接近问题论域的语言来描述规则,会将问题大幅简化。

编写 DSL 可以从考虑描述业务领域使用的词汇入手。这些词汇有时会因为描述人角色不同而不同。例如程序员、用户、业务分析员就会各不相同。在作出上述假设前,可以考虑一个问题:为什么这些不同角色要使用不同词汇呢?不同词汇在沟通中导致信息失真。通过找到在各组人之间通用的词汇,可以消除信息传递,也就可避免信息失真。这种公共的词汇通常可以在需求说明书中找到:

show us-state when country is “United States”
show province when country is “Canada”
show Brutus when state is “Ohio” or “Michian”
DSL 应该尽可能与上述描述一致。

有可能直接使用上述描述。上述描述并不是可执行的 JavaScript 代码,但可以放在文本文件中,使用 JavaScript 读出来进行解析。这种方法虽然可行,但可能过于复杂。如果使用纯文本作为 DSL,它需要和英语同样的灵活性。但我们希望把它作为数据,它需要遵循一些严格的规则。如果 DSL 本身就是可执行代码,可以将问题简化。不足的一面是非程序开发人员可能不会使用这种 DSL。实际上让非开发人员写DSL也是不现实的目标。如果程序元写好了,业务人员能够看懂或者能够修改,就很不错了。怎样处理例子中的需求,把它转成可执行代码?这可能是最难的一步了,具有很强的主管性。在 JavaScript 中,有两个建议:把方法串起来组成类似于句子的结构,灵活的使用方法名。沿着这个思路,可将上面的业务规则描述如下:

show(“us-state-field”).when(“country”).is(“United States”);

/>show(“province-field”).when(“country”).is(“Canada”);
show(“brutus”).when(“us-state”).is(“Ohio,Michigan”);

方法名“when”和“is”本身表意不完整,但在 DSL 上下文中,这些方法串起来,能够表意很好。可以看出 show() 返回一个对象,该对象具有方法 when();when() 返回一个对象,该对象具有方法 is()。下面给出能够描述第一条规则的简单实现:

function show(fieldToDisplay){
return {
when : function(field){
return {
is: function(value){
if($F(field) == value){
$(fieldToDisplay).show();
}
else{
$(fieldToDisplay).hide();
}
}
}
}
};
};

完整代码如下:

<html>
<head>
<title>Metaprogramming JavaScript – Example 1</title>
<script src=”prototype.js”></script>
<script type=”text/javascript” charset=”utf-8″>
function show(fieldToDisplay){
return {
when : function(field){
return {
is: function(value){
if($F(field) == value){
$(fieldToDisplay).show();
}
else{
$(fieldToDisplay).hide();
}
}
}
}
};
};

Event.observe(window, “load”, function() {

Event.observe($(“country”), “change”, function() {
show(“us-state-field”).when(“country”).is(“United States”);
});

});

</script>
</head>

<body>
<form>
<p id=”country-field”>
<label for=”country”>Country</label>
<select id=”country”>
<option>United States</option>
<option>Canada</option>
<option>Somewhere Else</option>
</select>
</p>

<p id=”us-state-field”>
<label for=”state”>State</label>
<select id=”us-state”>
<option>Ohio</option>
<option>Michigan</option>
<option>Kentucky</option>
</select>
</p>
</form>
</body>
</html>

以上只是一个简单示意,在后续文章中会给出完整的实现。

The purpose of creating sustained messages is to provide totally loosely coupled architecture. In the case of sustained messages the UI components can be loaded asynchronously in any arbitrary order. As soon as a component that subscribes to a subject is loaded (first after a sustained message with the same subject is published) the event dispatcher will publish again the same message to the subscriber. Consider that this is a 1 to 1 publishing so that an already existing subscriber dose not receives same message multiple times. However, this pub sub mechanism differs from GI pub sub only in the persisting the messages. It is the responsibility of developer to unsubscribe an object or function from an event published by this object if it is not needed any more. Subscribing an object or function to a type of event multiple times causes that the function is executes multiple times if a message is published.

这两天尝试使用ASP.NET AJAX Extensions给ThinkPage的PageRank查询增加了文本框自动提示功能。当用户在输入网站地址的文本框中输入的时候会自动弹出一个下拉菜单,显示最近查询过的网站,并不断根据输入文本的内容缩小范围。比如:当输入“w”的时候,会列出所有w开头的网址,当输入到“www.f”的时候,就只显示www.felixwoo.com了。当点击下拉菜单中的一个网址后,会自动完成文本框中的内容。整个过程和IE的地址栏效果类似。显示效果如图:
http://www.felixwoo.com/wp-content/uploads/attachments/200611/04_123834_autocomplete.jpg

这个功能是通过ASP.NET AJAX AutoCompleteExtender实现的,只需要几行代码即可搞定。
首先需要在页面上添加asp:AutoCompleteExtender标记,别忘了之前要有asp:ScriptManager的声明。

<asp:ScriptManager ID="ScriptManager" runat="server" />
<asp:AutoCompleteExtender ID="AutoCompleteExtender1" TargetControlID="searchtext"
runat="server" ServiceMethod="GetCompletionList"
ServicePath="~/SearchAutoComplete.asmx" MinimumPrefixLength="1" />

其中TargetControlID为输入网址的文本框的ID,ServicePath为得到网站列表的webservice地址,ServiceMethod即那个webservice中的具体方法,MinimumPrefixLength=1意思是当输入一个字符的时候即开始提示。

在SearchAutoComplete.asmx中要做的就是从数据库中返回最近查询过的网站,并通过prefixText参数来过滤出只以prefixText开头的网站,这样才能实现逐级提示的功能。SearchAutoComplete.asmx代码如下:

[WebMethod]
public string[] GetCompletionList(string prefixText, int count)
{
List<string> list = DataProvider.GetURLList();
foreach (string s in list)
{
if (s.StartsWith(prefixText))
{
list.Add(s);
}
}
return list.ToArray();
}

其中List<string> list是声明的了一个string的范型,这个是.net 2.0中新增的功能,避免了原来使用ArrayList带来的装箱和拆箱的性能消耗。DataProvider.GetURLList()从从数据库返回所有网站列表,返回类型自然也是List<string>。其他的代码都很简单,相信一看就明白了。

就短短的几行代码就可以实现如此有趣和实用的功能,不能不说ASP.NET AJAX为我们做了太多。

最近研究了一下URL Rewrite,大家可以看到我的博客上文章的链接已经变成了http://www.felixwoo.com/article_212.html 这样的静态页面地址,其实是利用URL Rewrite做的一个地址重写,将上面的静态地址映射到了http://www.felixwoo.com/article.asp?id=212 这样做的好处是更有利于搜索引擎的查询和收录,而且显得更为友好。

具体实现方法如下:
1、下载ISAPI Rewrite组件
点击下载
2、下载后解压到任意文件夹,如果是Windows 2003系统的IIS6,则需要给该文件夹IIS_WPG帐号的读取权限。
3、在IIS右键点击一个站点,属性,ISAPI筛选器,添加,筛选器名称Rewrite,可执行文件就点浏览找到你解压的目录Rewrite.dll加上即可。
4、重启IIS后应该就能支持URL重写规则了
5、修改解压目录中的httpd.ini这个文件,这个文件保存了URL重写的规则。
添加一行 RewriteRule /article_(\d+)\.html /article\.asp\?id=$1
意思就是将所有article-n.html的地址都转向article.asp?id=n这个地址
熟悉正则表达式的朋友也可以恨灵活的创建出自己的重写规则。

  上一篇文章提到了我在开发显示Exchange未读邮件数的WebPart中遇到了身份凭据传递的问题。因为Exchange的WebDAV是根据不同的用户返回不同邮箱的数据,当然就是非匿名的。输入身份信息一种方法是在网页上放置一个输入用户名和密码的Form,这样构建的NetworkCredential不存在传递的问题。但是,由于我的网络是基于Windows域方式的,每个用户要登录到域中,因此使用Windows集成身份验证能够让用户在SharePoint中使用当前的身份凭据登录,这样比输入用户名密码更加方便和友好。也就是说,为了免去用户再次输入Exchange用户名和密码,需要把客户端当前登录的Credential(用户身份凭据)传递到Exchange服务器上。然而,我的Exchange服务器和SharePoint网站服务器是两台不同的服务器,如果要使用集成身份验证并使用Windows和SharePoint默认的NTLM身份验证协议是无法将当前HttpContext中的身份凭据从Web服务器传递到Exchange服务器上的。而且也不可能从当前的身份凭据中获得用户名和密码,所以也不可能自己构建一个可传递的NetworkCredential。这就是著名的“Double Hop”双跳问题。

  当然解决这个问题有很多方法,比如可以用到SharePoint的单点登录Single-Sign-On(SSO)。但是每个用户需要在第一次使用之前先输入一次自己的Exchange密码储存到SSO数据库中,而且如果更改了Exchange密码需要再同时修改SSO数据,也比较麻烦。所以最好的解决方案就是使用Kerberos身份验证协议代替NTLM身份验证协议。

NTLM身份验证协议之所以无法传递身份凭据是由该协议本身的限制决定的,下图解释了原因。

http://www.felixwoo.com/wp-content/uploads/attachments/200606/20_233404_ntlm.jpg
  其实说“身份凭据传递”是不太恰当的,实际上是让Web服务器账户模拟客户端的用户来再对Exchange服务器发送请求。从上图中我们可以看到NTLM协议无法让Web服务器模拟客户端,因此Exchange收到的是一个未知用户的请求,所以会返回401身份验证错误。

  在这种情况下只能使用Kerberos协议,因为该协议最重要的特性之一就是可以委派。也就是说Web服务器可以模拟客户端对Exchange服务器发送请求。如下图所示。

http://www.felixwoo.com/wp-content/uploads/attachments/200606/20_234626_kerberos.jpg

  另外,为了增加安全性,Windows Server 2003中还为Kerberos增加了约束委派特性。可以指定委派的服务,那么没有指定的服务将无法使用委派。如下图所示。

http://www.felixwoo.com/wp-content/uploads/attachments/200606/20_235759_kerberos2.jpg

那么,如何启用Kerberos身份验证代替NTLM呢?步骤如下:

一、以SharePoint为例,首先要修改IIS的配置文件以启用Kerberos验证。
如果安装了SPS SP2以后则可以不用手动修改IIS配置文件,在SPS创建网站的时候就会有身份验证协议的选项。

二、为Web服务运行的帐号配置服务主体名称(SPN)
一般SPS的服务都是以一个域用户身份运行的,那么应该为该用户设置SPN。

三、为Web服务器运行的帐号配置委派信任关系
该步骤一定要设置为约束委派,我之前一直试验用非约束委派结果一直失败。

为SPS启用Kerberos身份验证的具体方法和说明可参考http://support.microsoft.com/default.aspx?scid=kb;zh-CN;832769

以上为我的案例中的步骤,不同情况配置情况不尽相同。在配置Kerberos委派的过程中有几个关键点。

1、IIS要设置为Windows集成验证
2、一定要确定客户端使用的是Kerberos身份验证协议,这个可以通过抓包软件或者Kerberos Tray来监视。注:Kerberos使用88端口。
3、如果Web服务是基于本地系统或者网络服务的则无需设置SPN,否则一定要为域用户设置SPN。
4、如果Web服务是基于本地系统或者网络服务的则要在Active Directory中为该Web服务的这台计算机设置委派信任关系,否则要为Web服务所基于的域用户设置委派信任关系,而且一定要设置成为约束委派!
5、Web.config中要有<authentication mode="Windows" />和<identity impersonate="true" />

  更详细的信息、设置方法和故障排除方法可以参考http://www.microsoft.com/china/technet/prodtechnol/windowsserver2003/technologies/security/tkerbdel.mspx 。这篇文章虽然有点长不过可谓字字珠玑,我就是在认真研读后才最终解决我的问题的。

http://www.felixwoo.com/wp-content/uploads/attachments/200606/21_170515_login.jpg
  这两天为SharePoint开发了一个获取Exchange未读邮件数的WebPart。存取Exchange信息可以有很多种方法,象MAPI、CDO和WebDAV。我采用的就是WebDAV方法。WebDAV实际上是基于HTTP的一种协议,返回的标准XML数据。优点基于国际标准、方便远程应用、调试方便而且适用于任何程序语言,那缺点主要就是要自己处理返回的XML结果。
  我的开发平台是.NET,主要的思路是使用System.Net.HttpWebRequest类来获取远程的WebDAV,然后分析返回的XML,提取出其中的“未读邮件”节点的数字。其中很重要的一点便是身份验证。因为Exchange的WebDAV是根据不同的用户返回不同邮箱的数据,当然就是非匿名的。因此,需要把客户端当前登录的Credential(用户身份凭据)传递到Exchange服务器上。然而我的Exchange服务器和SharePoint网站服务器是两台不同的服务器,如何才能把客户端的身份凭据通过Web服务器传递到Exchange服务器上呢?这个问题浪费了我不少时间才解决,我将再下一篇文章《使用Kerberos解决身份凭据传递问题》中详细介绍解决方法。
  参考了Exchange SDK中的Sample,获取Exchange信息的主要代码如下:

private int GetUnReadMailCount()
{
string url=“http://mail.felixwoo.com/exchange/”; //指定Exchange服务器地址
System.Net.HttpWebRequest Request;
System.Net.WebResponse Response;
System.Net.CredentialCache MyCredentialCache;
string strUserName = “wuf”; //指定登录的用户名
string strRootURI = url+strUserName ; //得到要访问邮箱的WebDAV地址
string strPassword = “123456”; //指定该用户的密码
string strDomain = “felixwoo.com”; //指定域名
string strQuery ="";
byte[] bytes = null;
System.IO.Stream RequestStream = null;
System.IO.Stream ResponseStream = null;
XmlDocument ResponseXmlDoc = null;
XmlNodeList HrefNodes= null;
XmlNodeList SizeNodes= null;
int count=0;
try
{
  // 用SQL查询WebDAV返回结果中的unreadcount节点.
  strQuery = "<?xml version=\"1.0\"?><D:searchrequest xmlns:D = \"DAV:\" >"
   + "<D:sql>SELECT \"DAV:displayname\",\"urn:schemas:httpmail:unreadcount\" FROM \"" + strRootURI + "\""
   + "</D:sql></D:searchrequest>";

  // 创建新的CredentialCache对象,构建身份凭据
  MyCredentialCache = new System.Net.CredentialCache();
  MyCredentialCache.Add( new System.Uri(strRootURI),
   "NTLM",
   new System.Net.NetworkCredential(strUserName, strPassword, strDomain)
   );

  // Create the HttpWebRequest object.
  Request = (System.Net.HttpWebRequest)HttpWebRequest.Create(strRootURI);

  // 指定HttpWebRequest的身份凭据,此处为关键所在。如果使用之前
  // 创建的MyCredentialCache,则这个身份凭据是可以从Web服务器传递
  // 到Exchange服务器的,但是这样带来的问题也很明显,就是不能够自
  // 动获取当前登录到域的用户的身份。即便已经成功登录到域,那也只
  // 能通过form再次输入用户名密码。因此,我在这里用的是
  // Request.Credentials = CredentialCache.DefaultCredentials,
  // 这样便可以获得当前用户的凭据,但是这样带来的问题便是上面提到的
  // 身份凭据无法传递的问题,解决方法请关注下篇文章。
  Request.Credentials = MyCredentialCache;

  // 指定WebDAV的SEARCH方法
  Request.Method = "SEARCH";

  // Encode the body using UTF-8.
  bytes = Encoding.UTF8.GetBytes((string)strQuery);

  // Set the content header length. This must be
  // done before writing data to the request stream.
  Request.ContentLength = bytes.Length;

  // Get a reference to the request stream.
  RequestStream = Request.GetRequestStream();

  // Write the SQL query to the request stream.
  RequestStream.Write(bytes, 0, bytes.Length);

  // Close the Stream object to release the connection
  // for further use.
  RequestStream.Close();

  // Set the content type header.
  Request.ContentType = "text/xml";

  // Send the SEARCH method request and get the
  // response from the server.
  Response = (HttpWebResponse)Request.GetResponse();

  // Get the XML response stream.
  ResponseStream = Response.GetResponseStream();

  // 创建XmlDocument对象,并获取收件箱的unreadcount节点的值
  ResponseXmlDoc = new XmlDocument();
  ResponseXmlDoc.Load(ResponseStream);
  HrefNodes = ResponseXmlDoc.GetElementsByTagName("a:displayname");
  SizeNodes = ResponseXmlDoc.GetElementsByTagName("d:unreadcount");
  for(int i=0;i<HrefNodes.Count;i++)
  {
   if(HrefNodes[i].InnerText=="收件箱")
    count=int.Parse(SizeNodes[i].InnerText);
  }
  ResponseStream.Close();
  Response.Close();
}
catch(Exception)
{
  // Catch any exceptions. Any error codes from the SEARCH
  // method request on the server will be caught here, also.
  return -1;
}
return count;
}

单位的门户网站构建于SharePoint,即时通讯平台是Live Communication Server(LCS)。由于LCS并不自带查询聊天记录的功能,所以为SharePoint开发了一个查询LCS聊天记录的WebPart。在此记录一下创建的过程。

http://www.felixwoo.com/wp-content/uploads/attachments/200606/15_151026_lcshistory.jpg

一、创建前的准备
1、服务器端安装Sharepoint Portal Server
2、开发客户端安装Visual Studio .NET
3、安装WebPart模版Sharepoint Web Part Library Template for Visual Studio .NET
下载地址:http://www.microsoft.com/downloads/details.aspx?FamilyId=14D5D92F-C3A6-407C-AAD7-B8C41A4991BE&displaylang=en。在安装这个模版的时候需要指定Microsoft.SharePoint.dll的位置,需要从服务器上把该文件拷贝到开发端的本地。

二、开发步骤
1、首先创建查询LCS记录User Control。LCS的聊天记录保存在默认名为LcsLog的数据库中,其中会用到messages和users两张表,在messages中保存了聊天记录的内容,但是其中的发送和接受人都是以id形式表现,需要用此id再到users中查到相应的Email,最后再利用DirectoryService从Active Directory中获取对应的用户名称。最后保存为History.ascx。

2、创建 Web Control Library 项目,相关的引用和namespace等都会自动创建好。

3、在项目中通过“添加现有项…”菜单,将第一步中建立的UserControl的三个文件全部加入到此项目中。打开“History.ascx.cs”文件,将此文件第一行指定的namespace改成和此项目一致的“LCS”,打开“History.ascx”文件,将第一行的“<%@ Control>”标签中的“CodeBehind”属性删除,“Inherits”属性的值改为“LCS.History”。

4、转到步骤2创建的“WebPart1.cs”文件,定义一个用来保存UserControl的对象:private System.Web.UI.Control _innerControl; 然后重载CreateChildControls()方法

protected override void CreateChildControls()
{
this._innerControl = this.Page.LoadControl("/bin/History.ascx");
this.Controls.Add(this._innerControl);
}

再重载RenderWebPart()方法,输出载入的User Control:

protected override void RenderWebPart(HtmlTextWriter output)
{
this.EnsureChildControls();
this._innerControl.RenderControl(output);
}

5、最后编辑“WebPart1.dwp”,配置WebPart。编辑SPS虚拟站点上的“web.config”,添加“<SafeControl>”标签以信任我们的WebPart。在VS.NET中编译,生成最终的“LCS.dll”。将此(.dll)和History.ascx拷贝到SPS虚拟站点跟目录的“bin”目录下。在SPS站点页面中导入WebPart1.dwp。这样查询LCS聊天记录的WebPart就可以使用了。

本文只是大体讲解了一下整个过程,具体关于开发Webpart的教程可以下载Kaneboy写的PPT教程:
http://www.gotdotnet.com/workspaces/releases/releasedownloadhandler/download.aspx/7741ec2a-e7ab-41da-baef-a8efd2658895/85aefa87-fb76-4b32-ab12-31a4e647878a/download.zip

最近使用微软的Sharepoint Portal Server(SPS)为单位开发了新的门户网站,因为是微软自家的产品,因此与Active Directory、Exchange、LCS等都有很好的整合。在整个部署过程中发现有很多值得记录下来的经验,一方面用来备忘,一方面与朋友分享。

一、关于固定IP地址的问题
在安装完Sharepoint后,首先要对一个网站进行扩展,也就是新建一个门户网站。经过我的反复测试这里扩展的网站最好不要指定固定IP地址,否则在日后的使用中会出现很多问题,如Frontpage无法编辑页面,WEB内容编辑器webpart无法使用等。不知道这是SPS的bug还是我的bug…

二、关于匿名访问与用户登录
SPS更适用于一个公司或团队内部,并不对外开放。但是对外开放的需求也是很常见的,我们需要的就是对内部人员开放权限,对外部匿名用户只有阅读权限。SPS也有这样的匿名访问设置,但是设置以后,我们会发现内部用户也变成了匿名用户,连管理员都无法对网站进行管理。解决这个问题有两种方法,一是为网站设置两个入口,即一套数据承载两个入口,一个匿名一个需要身份验证。具体参见http://sps.forever.net.cn/oceanstudy/SharePoint/如何匿名访问SPS门户站点/anyosps.html 。另外也有一个更好的方法,就是在页面上放置一个“登录”按钮,这样不用设置两个入口,在匿名访问的页面上点击登录即可输入用户名和密码,更符合我们的习惯。设置方法如下:
step 1:将SPS开启匿名访问(IIS里开匿名,再在SPS的网站设置里开启匿名)
step 2:在c:\inetpub\wwwroot(这是对应SPS网站的虚拟目录)下的web.config里加入一个key:
<appSettings>
<add key=”SPS-EnforceIISAnonymousSetting” value=”false” />
</appSettings>
step 3:把WSS网站的登陆按钮用FrontPage2003直接拷贝到SPS网站上需要的位置上。这个按钮其实就是一个webcontrol:
<WebPartPages:AuthenticationButton runat=”server”/>
这样,登陆按钮就可以使用了。

三、浴火重生的Frontpage
曾几何时,Frontpage已经被我深深的遗忘和抛弃,Dreamweaver在很多方面都超越了他。但是微软就是微软,Frontpage因为SPS而重获新生。SPS的页面都是可以通过Frontpage来编辑的,比通过页面编辑功能更多,而且可以个性化定制。比如可以跨区域添加webpart、添加blog部件等。但是适用Frontpage编辑后页面的Ghosting Page特性会被破坏,对Ghosting Page的修改会记录在Docs表的Content字段中,默认时该字段为Null,但当我们修改后,该字段中就会用来存储我们的修改信息。要想还原其实就很简单了,只要把该字段置为空就行了。

四、如何去掉最上面的“帮助”链接
SPS页面最上方的“帮助”链接是写在webcontrol里的,我们无法通过常规方法修改掉。不过也有变通的方法,可以通过js脚本来完成。
基本操作过程如下:
1 定位OWSBROWS.JS文件,这个文件在%SystemRoot%\Program Files\Common Files\Microsoft Shared\Web Server Extensions\60\template\layouts\2052目录中
2 在该文件中加入一个Window Load事件响应函数,以删除帮助链接,源代码如下转载自MSD2D:

window.attachEvent(“onload”, new Function(“DelHelp_OnLoad();”));
function DelHelp_OnLoad()
{
try{
var aTags = document.getElementsByTagName(“A”);
for(var j=0;j<aTags.length;j++){

var aTag = aTags(j);
if(aTag.innerText==”帮助”)
{
aTag.innerText=”";
break;
}
}
}
catch(e)
}
}

3 保存后刷新网页即可看到效果

另:OWSBROWS.JS作用于所有网页,修改之后SPS所有网页都将没有帮助菜单。注意编辑OWSBROWS.JS文件时注意该文件是UTF-8格式,注意保持格式,特别是中文版SPS,我一开始没有注意,始终不能使aTag.innerText==”帮助”条件成立。 修改之后的效果你可以看到帮助链接一闪之后就消失了,这是因为实际上SPS是将该链接生成并送到浏览器了,而加入的代码在页面加载时在将该链接去掉所以看不到,实际上在页面的HTML源代码中还是看得到这个链接的。

五、如何为SPS的列表库设置权限
很多用户发现SPS权限设置的最小单位是区域,而不能想WSS一样为区域中的列表或文档库设置权限。其实是可以的,只是微软把这个功能给屏蔽了。我们分析数据库后就可以发现SPS的区域信息和WSS的网站存在一个表中,而他们的列表库也是存在另外一个表中。这样对比字段后就可以发现其中的奥秘。具体修改方法如下:
修改XXX_Site数据库中的Webs表的对应区域的记录的SecutiryPrivoder字段的值为NULL,在Portal站点中修改列表配置的页面中就会自动出现修改列表权限的链接。还应该注意webs表和webpart表中AnoymousPermMask字段的数值,该字段用来控制是否能够匿名访问,设置为0则无法匿名访问,196609则允许匿名访问。

六、关于公文流转
SPS 2003目前还没有自带公文流转的功能,kaneboy开发了一个webpart,不过使用效果也不是很好,还是让我们期待Office 12吧,新SPS将原生集成OA流程。

京ICP备05053527号
经过26次查询历时3.429秒终于生成了此页面
Powered by WordPress & Designed by Felix © 2008