级别: 中级 Nicholas Chase (ibmquestions@nicholaschase.com), 自由撰稿人, 自由职业
2008 年 3 月 17 日 在这个分两部分的文章系列中,学习如何用 ECMAScript for XML(E4X)和 Prototype JavaScript 库创建一个简单的 Ajax mindreader 应用程序,这个程序实现 Twenty Questions 游戏,并可以在游戏过程中学习新东西。在第 1 部分中,我们创建了一个系统,它接受并分析一个现有的知识库,从而判断用户可能在想什么。在第 2 部分中,将学习在知识库中添加新信息,并使用 Prototype JavaScript 库把 Twenty Questions 应用程序与一个外部数据库集成起来,让一个用户在知识库中添加的新信息能够对其他用户有所帮助。
完成后的应用程序见 http://www.backstopmedia.com/examples/e4x.html。本系列假设您熟悉 XML 和 JavaScript 概念。如果需要了解背景知识,请参见 参考资料。还需要一个支持 E4X 的浏览器,比如 Firefox 1.5 或更高版本。
目前的状态
如果您还没有阅读第 1 部分,现在就应该这么做(参见 参考资料)。在第 1 部分中,我们创建了一个应用程序,它用问题(问题的答案主要是 "yes" 和 "no")分析一个知识库,判断用户正在想什么。知识库与清单 1 类似。
清单 1. 知识库示例
<knowledgebase>
<questions>
<question id='1'>
<display>Is it animal, vegetable, or mineral?</display>
<answerOption>Animal</answerOption><
answerOption>Vegetable</answerOption>
<answerOption>Mineral</answerOption>
</question>
...
</questions>
<targets>
<target id='1'>
<display>a house cat</display>
<answer questionid = '1'><answerValue1>Animal</
answerValue1></answer>
<answer questionid = '41'><answerValue41>No</
answerValue41></answer>
</target>
<target id='2'>
<display>a carrot</display>
<answer questionid = '1'><answerValue1>Vegetable</
answerValue1></answer>
<answer questionid = '44'><answerValue44>No</
answerValue44></answer>
</target>
...
</targets>
</knowledgebase>
|
 |
常用缩写词
- Ajax:Asynchronous JavaScript™ and XML
- DOM:Document Object Model
- HTML:Hypertext Markup Language
- JSON:JavaScript Object Notation
- XML:Extensible Markup Language
|
|
为了进行这种分析,我们使用一个算法提出问题、排除与答案不相符的所有内容、在余下的问题中选择最相关的问题并再次提问,直到只剩下一项。
这时候,应用程序猜测用户心里想的就是最后一项。如果猜测正确,应用程序就重新开始。但是,如果猜测错了,就要做更多的工作了。
猜测错误
如果应用程序找到了最后的可能项,而这一项是错误的,就需要让系统知道 用户想的究竟是什么。例如,如果用户想的是 “芹菜” 而系统猜的是 “胡萝卜”,系统就需要知道有芹菜这一项。它还需要知道如何区分芹菜和胡萝卜。
第一个任务是查明正确答案是什么。表单很简单,见清单 2。
注意:如果您还没有按照第 1 部分去做,那么通过 参考资料 下载目前的应用程序。
清单 2. 新目标表单
<div id="targetFormDiv" style="position: absolute; top: 50px;
visibility: hidden; width: 100%;">
<form id="targetForm" name="targetForm">
OK, what is it? It's <input type="text" name="newTargetDisplay"
id="newTargetDisplay" />/>
<input type="button" onclick="submit_new_target()" value="Teach me!" />
</form>
</div>
|
当用户告诉应用程序猜测错误时,要显示这个表单,它会调用 get_new_target() 函数,见清单 3。
清单 3. 获得新目标
function get_new_target(){
document.getElementById("guessDiv").style.visibility = "hidden" ;
document.getElementById("targetFormDiv").style.visibility = "visible" ;
}
|
结果是图 1 所示的新目标表单。
图 1. 新目标表单
在框中输入一个新项(比如 “a lion”)并单击 Teach me!,它就会运行 submit_new_target() 函数,见清单 4。
清单 4. 提交新目标
...
show_form("targetFormDiv");
}
var newTarget;
function submit_new_target(){
newTarget = document.getElementById("targetForm").elements[0].value;
document.getElementById("newTarget").innerHTML = newTarget;
document.getElementById("oldTarget").innerHTML = currentGuessText;
hide_form("targetFormDiv");
show_form("answerFormDiv");
}
function hide_form(divName){
...
|
注意,这里声明了 newTarget,所以在从表单获得新目标之后可以使用它。然后在 answerFormDiv 中设置这一信息(见清单 5)并显示它。
清单 5. answerFormDiv
<div id="answerFormDiv" style="position: absolute; top: 50px;
visibility: hidden; width: 100%;">
<form id="answerForm" name="answerForm" >
What question distinguishes <span id="newTarget"></span>
from <span id="oldTarget"></span>?<br />
<input type="text" name="newQuestion" value="" id="newQuestion" /><br />
What is the correct answer for this item?
<select id="newAnswer" name="newAnswer">
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<input type="button" value="Add Question" onclick="add_new_question()" />
</form>
</div>
|
结果见图 2。
图 2. 获得新问题
现在需要把这些数据添加到知识库中。
补充知识库的内容和使用 JavaScript 变量
把新项添加到数据库中的第一步是,创建并添加一个新的问题元素,见清单 6。
清单 6. 添加新问题
...
var nextQuestionId = 3;
var nextTargetId = 5;
function add_new_question(){
var newQuestion = document.getElementById("answerForm").elements[0].value;
var newAnswer = document.getElementById("answerForm").elements[1].value;
thisQuestionId = nextQuestionId;
nextQuestionId++;
var newQuestionXML = <question id={thisQuestionId}>
<display>{newQuestion}</display>
<answerOption>Yes</answerOption>
<answerOption>No</answerOption>
</question>;
var newQuestionElement = new XML(newQuestionXML);
knowledgeBase.questions.appendChild(newQuestionElement);
}
|
在获得新问题和答案之后,就该创建新元素了。可以从 nextQuestionId 变量获得新问题的 ID,然后需要更新这个变量。
接下来,创建新元素。仍然使用第 1 部分中使用过的 XML 语法(参见 参考资料),但是在这里可以使用 JavaScript 变量语法插入新的 ID 和 display 值。注意,ID 属性不带引号;它是一个值;如果加上引号,就会得到 "{thisQuestionId}" 而不是 "3"。
创建 XML 之后,可以从 XML 生成一个 XML() 对象,然后使用 appendChild() 方法把它添加到问题元素中。
然后把新的答案添加到原来的目标中。换句话说,如果应用程序猜测 “a house cat”,而用户说它是 “a lion”,就需要告诉知识库:如果对 “Is it wild?” 针对 “a house cat” 的答案是 “No”(见清单 7)。
清单 7. 添加新目标
...
var newQuestionElement = new XML(newQuestionXML);
knowledgeBase.questions.appendChild(newQuestionElement);
//Add new answer to old target
var oldAnswer = "Yes";
if (newAnswer == "Yes"){
oldAnswer = "No";
}
var oldTargetNewAnswer = <answer questionid={thisQuestionId}></answer>;
oldTargetNewAnswer["answerValue"+thisQuestionId] = oldAnswer;
knowledgeBase..target.(@id==currentGuessId).appendChild(oldTargetNewAnswer);
...
|
首先,根据用户提交的信息获得原来的答案和新答案,并创建一个新的答案元素。在这里使用两种不同的技术。
首先使用变量插入,这在前面已经见过了。
第二种方法是使用散列表示法。注意,这里隐式地创建 oldTargetNewAnswer.answerValue3 元素;如果它不存在,E4X 就创建它。
最后,选择知识库中的所有目标元素,过滤出与 currentGuessId 对应的目标,并在其中添加新的答案元素。这样就更新了现有目标。
现在需要创建新目标。这个新目标元素应该包含应用于它的所有以前的答案,所以最简便的方法是复制原来的目标,见清单 8。
清单 8. 创建新目标
...
knowledgeBase..target.(@id==currentGuessId).appendChild(oldTargetNewAnswer);
var oldTargetElement = new XML();
oldTargetElement = knowledgeBase..target.(@id==currentGuessId);
//Clone old target
var newTargetElement = oldTargetElement.copy();
newTargetElement.@id = nextTargetId;
nextTargetId++;
newTargetElement.display = newTarget;
var newAnswerElement = newTargetElement.answer.(@questionid == thisQuestionId);
newAnswerElement["answerValue"+thisQuestionId] = newAnswer;
knowledgeBase.targets.appendChild(newTargetElement);
hide_form("answerFormDiv");
start_over();
}
|
首先,获得原来的目标元素的引用。然后,使用 copy() 方法创建这个元素的拷贝。在此之后,需要把 ID 和 display 重新设置为新的值。最后,需要把最近的答案元素的值改为新的值。
现在,可以把新元素添加到知识库中。
现在,可以用 start_over() 从头开始游戏。除非删除了 alert() 语句,否则在选择 “Animal” 之后就可以看到新的问题和目标,见图 3。
图 3. 新值
所以,直到重新装载页面之前,添加的所有新项和问题都会起作用;您只要心里想着添加的新项,并相应地回答问题,就会看到它们。
但糟糕的是,重新装载页面之后,知识库就会回到原来的状态。另外,用户只能看到自己添加的新项。如果每个人都能够看到别人添加的新项,那不是很棒吗?
改进后的新算法
为了让玩家能够利用所有人添加的新信息,需要设置一个外部数据库。对于本文,我们使用一个 MySQL 数据库和 PHP,但是因为实际的实现与本文的主题无关,所以这里不讨论实现方法。(可以从 参考资料 下载 SQL 文件和 PHP 脚本来维护和生成知识库。)以原来的算法为基础,添加数据库集成之后,就形成下面的算法:
- 从数据库中获取最新的知识库。
- 问用户他想的项是 “animal, vegetable, or mineral?”。
- 排除所有与这个问题的回答不相符的项。
- 如果只剩下最后一项,就询问用户这个猜测是否正确。
如果正确,就从数据库重新装载知识库并重新开始游戏。
如果不正确,就询问用户究竟是什么项,并要求用户提供一个可以区分正确答案和错误答案的问题。
- 把这个问题发送到数据库并返回新问题的 ID
- 使用新的问题 ID 在原来的目标和新目标中添加答案,并把新目标添加到知识库中
- 如果仍然有多个项,就要确定一个问题,这个问题应该应用于当前范围中尽可能多的项。(这是关键;这样可以排除尽可能多的可能选择。)
- 提出这个问题。
- 返回到第 3 步。
应用程序和数据库之间交互的关键是 Prototype JavaScript 库。
Prototype 简介
我们将主要使用 Prototype JavaScript 库中的 Ajax 功能,但是实际上这个库的功能非常全面。Prototype 提供四个方面的特性:
- 类管理:Prototype 支持更轻松地创建和扩展类和对象。
- DOM 管理:Prototype 支持更轻松地连接页面元素,尤其对于表单,并提供了执行显示或隐藏元素等任务的简便方法。
- JSON:Prototype 可以快速可靠地转换 JavaScript Object Notation,包括从字符串直接生成对象。
- Ajax:Prototype 的 Ajax 功能简化了从外部 URL 请求数据并在页面上显示信息的过程。Prototype 还包含一个定期更新器,但是本文并不使用它。
为了使用 Prototype 类和方法,从 Prototypejs.org Web 站点下载最新的文件并把它添加到 HTML 页面中,见清单 9。
清单 9. 在 HTML 页面中添加 Prototype
<html>
<head>
<title>E4X mindreader</title>
<script type="text/javascript" src="prototype.js"></script>
<script type="text/javascript; e4x=1" src="e4x.js"></script>
...
|
我们先从一些比较简单的任务开始。
表单管理
尽管肯定可以使用 DOM 操作 Web 页面的内容,但是 Prototype 提供了许多简便的函数。例如,可以使用 $() 函数访问一个元素,所以表达式 $('answerFormDiv') 引用 ID 为 answerFormDiv 的元素。可以使用这个功能简化第 1 部分中的许多 DOM 操作。例如,可以把 document.getElementById("displayQuestion").innerHTML = questionDisplay ; 替换为 $("displayQuestion").innerHTML = questionDisplay ;。
还可以使用 $F() 函数简化对表单元素的访问。例如,可以把 newTarget = document.getElementById("targetForm").elements[0].value; 替换为 newTarget = $F("newTargetDisplay");。
这些函数使用 HTML 元素的 ID 属性,所以一定要在 HTML 文件中包含 ID 属性。
现在看看如何集成这个应用程序和后端数据库。
请求知识库
使用 Prototype 的 Ajax.Request 对象请求知识库。可以使用这个对象发送 HTTP 请求并根据结果调用一个新函数。在这个示例中,希望请求一个代表知识库的 XML 字符串,然后使用这个字符串创建一个 XML 对象,见清单 10。
清单 10. 在 e4x.js 中请求知识库
function get_knowledge_base(){
new Ajax.Request("knowledgebase.php",
{method: "get",
parameters: {getkb: 'YES'},
onSuccess: function(transport){
kstring = transport.responseText;
knowledgeBase = new XML(kstring);
start_over();
}
})
}
|
在创建新的 Ajax.Request 对象时,要传递许多数据。首先,要提供 HTTP 请求的 URL。可以指定主机名,但是一般来说 Ajax 请求必须来自与发出请求的页面相同的服务器和端口,所以我们只使用相对 URL。
还需要指定希望使用 GET 方法,并指定一个值为 YES 的 getkb 参数。可以使用 parameters 参数设置多个值,稍后将会见到。
这个函数最重要的部分可能是 onSuccess 处理函数,它告诉对象在请求成功时应该做什么。Prototype 定义了 7 个不同的处理函数,所以可以处理几乎任何情况。当请求成功时,脚本提取出请求的文本(请记住,这仅仅是代表文档的 XML),并使用它创建一个新的 XML() 对象 knowledgeBase。完成这些之后,只需调用 start_over() 函数开始显示页面。
如果保存这个文件并重新装载页面,就会看到所有功能都与以前一样,只有两点不同。首先,如果仍然显示警告框,您会看到现在是由数据库在控制知识库中的数据。第二,如果别人更新了集成的数据库,您也会看到比以前更多的目标。
我们来看看如何输入新数据。
发送新问题
在使用 Prototype 中的 Ajax 功能时,一定要认识到一点:它们是异步的。您能做的只是发送请求并牢记下一步操作;您必须告诉它在完成时应该做什么。因此,需要把 add_new_question() 函数分成两部分。首先,发送问题,但是在返回 questionid 之前无法添加目标,见清单 11。
清单 11. 发送问题
function add_new_question(){
var newQuestion = $F('newQuestion');
var newAnswer = $F('newAnswer');
var thisQuestionId;
new Ajax.Request("knowledgebase.php",
{method: "get",
parameters: {getkb: 'NO', question: newQuestion},
onSuccess: function(transport){
thisQuestionId = transport.responseText;
finish_adding_new_question(newQuestion, newAnswer, thisQuestionId);
}
})
}
|
这非常简单。首先获取新问题和答案,然后把它发送给 PHP 文件。注意,在 parameters 中使用了多个以逗号分隔的名称-值对。
现在,等待脚本返回并结束这个函数。当脚本返回时,它会提供新问题的 ID,所以可以转到这个例程的第二部分,见清单 12。
清单 12. 完成新问题的添加过程
function finish_adding_new_question(newQuestion, newAnswer, thisQuestionId){
var newQuestionXML = <question id={thisQuestionId}>
<display>{newQuestion}</display>
<answerOption>Yes</answerOption>
<answerOption>No</answerOption>
</question>;
var oldAnswer = "Yes";
if (newAnswer == "Yes"){
oldAnswer = "No";
}
new Ajax.Request("knowledgebase.php",
{method: "get",
parameters: {addAnswers: 'YES',
old_target_id: currentGuessId,
old_answer: oldAnswer,
target_display: newTarget,
question_id: thisQuestionId,
new_answer: newAnswer},
onSuccess: function(transport){
get_knowledge_base();
}
})
}
|
与前面一样,创建问题元素并更新原来的答案和新答案。完成之后,可以发出一个新请求。注意,这个请求包含多个参数。它把所有信息发送给 PHP 脚本,脚本获取所有信息并在数据库中添加新目标。
这个例程返回之后,就可以请求知识库并获得最新的信息。
结束语和以后的改进
我们在这里构建的应用程序肯定还算不上人工智能,但是它能够给用户提供娱乐。每当用户心里想着知识库中没有的项玩这个游戏时,应用程序会在数据库中添加该项以及适当的问题和答案。通过使用 Prototype JavaScript 库,在每次玩游戏时应用程序都请求知识库的新拷贝,所以总会获得最新的信息。
当然,这个解决方案也有问题;到编写本文时,知识库的内容还不丰富。如果许多人玩这个游戏并添加许多项,那么下载完整的数据库内容可能不现实,必须设法减少下载时间。
目前,有几种方法。最简单的方法是只发送相关的集合。例如,可以对第一个问题 “animal, vegetable, mineral” 进行硬编码,然后只下载相关的三分之一知识库。最终,如果数据库变得非常大了,可以考虑在服务器而不是浏览器中进行处理;但是本文的意图只是使用 E4X 和 Prototype 开发小型 mindreader 应用程序,在服务器上进行处理超出了本文的范围。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 第 2 部分示例代码 | x-e4xpart2code.zip | 5KB | HTTP |
|---|
参考资料 学习
获得产品和技术
-
Prototype 库:下载 Ajax 库并使用这个 JavaScript 框架和易用的工具集进行类驱动的动态 Web 应用程序开发。
-
IBM 试用版软件:使用这些可以从 developerWorks 直接下载的试用版软件构建您的下一个开发项目。
讨论
关于作者  | |  | Nicholas Chase 曾经参与多家公司的网站开发,包括 Lucent Technologies、Sun Microsystems、Oracle 和 Tampa Bay Buccaneers。Nick 曾经做过高中物理教师、低放射性废弃设备管理员、在线科幻杂志的编辑、多媒体工程师、Oracle 教员以及一家交互通信公司的首席技术官。他出版了多部著作,包括 XML Primer Plus(Sams)。他还是 InterSection Unlimited 的合伙人,这家公司从事 Second Life 内容和应用程序的创建。在 Second Life 中,他的名字是 Chase Marellan。 |
对本文的评价
|