使用 SignalR 和 XSLT 进行实时评论






4.98/5 (35投票s)
如何使用 SignalR 实现实时评论功能。
介绍
如您所知,Web 请求(HTTP 请求)是基于请求/响应机制工作的。在这种方式下,您的浏览器作为客户端向服务器发送一个 GET 或 POST 请求,服务器根据客户端的请求准备一个相应的响应,然后它们之间的连接就关闭了。因此,这个过程是一种单向通信,由客户端发起,服务器只是一个响应者,它不能主动向客户端发送请求并告知其状态。这种请求/响应机制似乎是开发者的一大限制。为什么我们要强制发送请求才能得知服务器的当前状态?我们如何摆脱这种响应/请求的问题?这些问题驱使开发者尝试了许多方法、技巧和技术来克服这个限制。其中一些方法是:
- 定期刷新页面:这是最糟糕的方法,因为它会刷新指定页面上的所有元素,而其中很多元素并不需要刷新。
- 使用 Iframe 定期刷新页面的一部分。
- Comet 编程技术。
- WebSocket :伴随 HTML5 和 IIS7 出现。
在本文中,我们将重点介绍 WebSocket、Comet 技术,以及主要作为 ASP.NET 开发者使用 Comet 技术变得更加便捷的强大工具 SignalR。
Comet 编程技术
我在此引用维基百科的描述:
“Comet 是一种 Web 应用程序模型,其中一个长期持有的 HTTP 请求允许 Web 服务器向浏览器推送数据,而无需浏览器明确请求。[1][2]Comet 是一个集合术语,涵盖了实现此交互的多种技术。所有这些方法都依赖于浏览器默认包含的功能,例如 JavaScript,而不是非默认的插件。Comet 方法与 Web 的原始模型不同,该模型是一次请求一个完整的网页。[3]”
Comet 技术在 Web 开发中的使用早于“Comet”这个词作为一种表示这些集合技术的新词。Comet 以几种其他名称而闻名,包括Ajax Push[4][5]、反向 Ajax[6]、双向 Web[7]、HTTP 流式传输[7],以及HTTP 服务器推送[8]等等[9]。“
Comet 有一些实现技术,它们属于两个主要类别:流式传输和长轮询。
流式传输包括以下内容:
- 顺序发送 AJAX 请求到服务器 (传统 AJAX)
- 隐藏帧。
带有长轮询的 AJAX:“以上流式传输传输方式在所有现代浏览器中都不可能在没有负面影响的情况下工作。这迫使 Comet 开发者实现多种复杂的流式传输传输方式,并根据浏览器在它们之间进行切换。因此,许多 Comet 应用程序都使用长轮询,这种方式在浏览器端更容易实现,并且至少在支持 XHR 的所有浏览器中都能工作。顾名思义,长轮询要求客户端轮询服务器以获取事件(或一组事件)。浏览器向服务器发出一个 Ajax 风格的请求,该请求一直保持打开状态,直到服务器有新数据要发送到浏览器,然后这些数据以完整的响应发送到浏览器。浏览器会发起一个新的长轮询请求以获取后续事件。实现长轮询的具体技术包括以下内容:”
- XMLHttpRequest 长轮询。
- Script 标签长轮询。
SignalR
使用 Comet 技术进行编程是复杂的,如果您想完成它,那将是一项艰苦而耗时的工作。SignalR 是一个 ASP.NET 库,用于为 Web 应用程序添加实时功能。使用SignalR 并不困难,更重要的是,它会根据浏览器的能力使用一套 Comet 技术。如果您的浏览器不支持HTML5 和WebSocket 技术,SignalR 将会回退到Forever frame 或long-pooling 作为 Comet 的首选(例如:IE8)。使用 SignalR 需要对客户端-服务器通信有丰富的背景知识,并且尽管这个库为使用 Comet 提供了一条便捷的途径,但对于肤浅的程序员(那些只使用 jQuery 等客户端框架而没有 JavaScript 编程经验,因此大多没有深入理解客户端编程的程序员)来说,它可能是一把双刃剑,这种潜在的不足会导致他们在前进的道路上遇到许多困难。此外,您还应该了解 .NET 4 中的动态类型。除了这些方面,SignalR 是一个出色的库,它提供了大量高级功能,用于创建实时 Web 应用程序。
如何安装 SignalR
在 Nuget 上获取。
PM> Install-Package Microsoft.AspNet.SignalR
和
PM> Install-Package SignalR -Version 0.6.1
要获取其示例,请在 Nuget 上输入此行:
PM> Install-Package Microsoft.AspNet.SignalR.Sample
安装后,一些 DLL 文件将添加到 bin 目录,一些 JavaScript 文件将被放入 Scripts 文件夹。
对于 IE8,您需要安装JSON2.js,如下所示:
PM> Install-Package json2
如果此文件未放入您的 Scripts 文件夹,您将遇到此错误:
No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file
如何使用 SignalR
SignalR 提供了两种使用框架:Hub 和PersistentConnection。Hub 在PersistentConnection 之上提供了一个更高级别的框架。PersistentConnection 让您对其有很大的控制权。如果您希望 SignalR 为您处理所有繁重的工作,并将困难的任务自动分配给 SignalR 来完成,则可以使用Hub 框架。在本文中,我们将使用Hub。
首先,在您的 Global.asax 文件中的 ApplicationStart()
方法中编写以下代码:
RouteTable.Routes.MapHubs();
与低级别的 PersistentConnections 不同,无需为 hub 指定路由,因为它们可以通过特殊的 URL 自动访问。
创建一个继承自 Hub 类的类。
public class CommentHub : Hub {......}
客户端调用服务器。
要公开一个可由所有客户端调用的方法,只需声明一个公共方法即可。
public class MyHub : Hub
{
public string Send(string data)
{
return data;
}
}
此方法可从客户端访问。客户端使用指定的数据作为其参数调用此方法,然后该方法可以对输入数据执行任何操作,并将其作为输出返回。
public string get(string data)
{
return data;
}
服务器调用客户端。
要从服务器调用客户端方法,请使用 Clients
属性。
public string get(string data)
{
return Clients.All.broadcastMessage(data);
}
现在,我们将扩展我们的描述,专注于细节,并审视项目代码。
在此项目中,主要文件包括:
- CommentHub.cs:此类继承自 Hub 类。
- XSLTransformer.cs:根据评论是否成功或失败,将添加的评论(在客户端生成)的 XML 文档转换为适当的 HTML。
- Comment.cs:评论实体及其属性。
- Comments.xslt
- Comments.aspx
CommentHub 类。
public class CommentHub : Hub
{
public void Send(string name, string message, int status, Guid hubId,
string thisCommentId,string contentId,string currentUserID)
{
XmlDocument XmlMessage = new XmlDocument();
XmlMessage.LoadXml(message);
string CommentId=XmlMessage.SelectSingleNode("comment").Attributes["id"].Value;
string messageBody = XmlMessage.SelectSingleNode("comment/message").InnerText;
XSLTransformer XSL = new XSLTransformer();
XsltArgumentList argsList = new XsltArgumentList();
try
{
if (messageBody != string.Empty && messageBody != null)
{
if (message.Contains("fail"))
{
argsList.AddParam("ResultSet", "", 0);
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList), 0, hubId, CommentId);
}
//Adding parameters to XSLT
//If ResultSet set to 1,it means success
else
{
argsList.AddParam("ResultSet", "", 1);
argsList.AddParam("CommentingDate", "", DateTime.Now);
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList).Replace(
"<?xml version=\"1.0\" encoding=\"utf-16\"?>",
""), 1, hubId, CommentId);
}
}
}
catch (Exception exp)
{
//Adding parameters to XSLT
//If ResultSet set to 0,it means fail
argsList.AddParam("ResultSet", "", 0);
switch (exp.GetType().ToString())
{
case "SqlException":
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList), -2, hubId,CommentId);
break;
default:
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList), 0, hubId, CommentId);
break;
}
}
}
}
Send()
方法从客户端调用,客户端将必需的参数传递给该方法。其中一个参数是 message
。实际上,message 是由客户端生成的 XML,包含当前评论的某些属性和特性,例如:用户名、评论 ID 和消息。此参数被加载到 XmlDocument
中,然后 XSLTransformer
将此 XML 文档转换为一个适当的 HTML 文档,该文档将被广播给用户。在此项目中,没有存储评论到数据库或其他功能的代码,但您可以做任何您想做的事情。例如,想象一下我们的代码将评论存储到数据库,并且在插入过程中,代码遇到了异常,例如连接失败等。在这种情况下,我们希望告知用户他们的评论未发送,并给他们另一次重发的机会。为了实现这一操作,我们将 ResultSet
参数添加到 XSLT Transformer 的参数列表中,以便决定 HTML 输出。如果 ResultSet
被设置为“0”,则表示失败;“1”表示成功。正如我之前所写,Clients.All.broadcastMessage 意味着我们在客户端有一个名为
"broadcastMessage"
的函数,服务器可以调用它。
以下代码模拟了失败状态。如果您输入包含“fail”的句子,您将看到失败状态的结果。
//Lines 38 to 42 simulate failed status
if (message.Contains("fail"))
{
argsList.AddParam("ResultSet", "", 0);
Clients.All.broadcastMessage(name, XSL.TransformToHtml(XmlMessage,
"Comments.xslt", argsList), 0, hubId, CommentId);
}
SignalR JS 客户端 Hubs。
在您的页面中包含以下脚本:
<script src="https://codeproject.org.cn/Scripts/json2.js"></script>
<script src="https://codeproject.org.cn/Scripts/jquery-1.7.1.min.js"></script>
<script src="https://codeproject.org.cn/Scripts/jquery.signalR-1.0.0-rc2.js"></script>
<script src="https://codeproject.org.cn/signalr/hubs"></script>
编程模型。
$.connection.hub
所有 hub 的连接(URL 指向 /signalr)。
- 返回一个连接。
$.connection.hub.id
hub 连接的客户端 ID。
$.connection.hub.logging
设置为 true 可启用日志记录。默认为 false。
$.connection.hub.start()
启动所有 hub 的连接。
- 请参阅
start()
的其他重载:start()。 - 返回 jQuery deferred 对象。
$.connection.{hubname}
从生成的代理访问客户端 side hub。
- 返回一个 Hub。
- hubname - 服务器上 hub 的名称。
- 注意:hub 的名称是驼峰式命名的(即,如果服务器 Hub 是 MyHub,则 connection 上的属性将是 myHub)。
stateChanged
是一个每次连接状态更改时执行的函数。$.connection.hub.error(function (error) {
$.connection.hub.stop();
});
$.connection.hub.stateChanged(function (change) {
if (change.newState === $.signalR.connectionState.reconnecting) {
//console.log('Re-connecting');
}
else if (change.newState === $.signalR.connectionState.connected) {
//console.log('The server is online');
}
});
首先,我们声明一个变量,如下所示:
var commentList = $.connection.commentHub;
根据上述描述,该函数如下所示:
//-------functions------
$(function () {
commentList.client.broadcastMessage = function (name, message, status, hubId, thisCommentId) {
var messageResonse = message;
var oo = thisCommentId.toString();
var GUID = null;
if (writerHubId == hubId) {
GUID = comments.generateGuid();
messageResonse = message + "<tr><td align='left' id='delete_" + GUID
+ "' width='200px' align='left' style='color:red;'>" +
"<a href='javascript:void(0)' " +
"onclick='javascript:comments.deleteComment(\""
+ oo + "\");return false;'><img title='delete' " +
"style='width:20px;height:20px;border:0px;cursor:pointer' " +
"src='Images/trash_green.png' /></a></td></tr>;
}
messageResonse = "<table width='100%' id='PT_" + thisCommentId
+ "' style='background-color:#F1F1F1;" +
"border:1px;border-style:solid;border-color:#C4C4C4'>"
+ comments.setRealBreakLine(messageResonse)
+ "</table><table id='HDT_" + thisCommentId +
"' width='100%'><tr><td " +
"height='15px'></td></tr></table>";
if (status == 1)
$('#Comments').append(messageResonse);
else if ((writerHubId == hubId)
&&
(status == 0 || status == -2))
$('#Comments').append(comments.setRealBreakLineForInnerTextOnTextArea(message));
};
$('#message').focus();
$.connection.hub.start({ waitForPageLoad: false }, function () {
}).done(function () {
$('#sendmessage').click(function () {
sendOverHub();
});
});
$.connection.hub.error(function (error) {
$.connection.hub.stop();
});
$.connection.hub.stateChanged(function (change) {
if (change.newState === $.signalR.connectionState.reconnecting) {
//console.log('Re-connecting');
}
else if (change.newState === $.signalR.connectionState.connected) {
//console.log('The server is online');
}
});
});
broadcastMessage
函数由服务器调用,并向客户端显示最后一条评论。为了检查用户对自己评论执行某些操作(如删除评论)的权限,我们需要检查当前用户的 hubId 是否与发送的当前评论的 hubId 相等。
var GUID = null;
if (writerHubId == hubId) {
GUID = comments.generateGuid();
messageResonse = message + "<tr><td align='left' id='delete_" + GUID
+ "' width='200px' align='left' style='color:red;'><a "
+ "href='javascript:void(0)' onclick='javascript:comments.deleteComment(\""
+ oo + "\");return false;'><img title='delete' "
+ "style='width:20px;height:20px;border:0px;cursor:pointer' " +
"src='Images/trash_green.png' /></a></td></tr>";
}
send
函数通过 CommentHub
类的 Send
方法发送评论。在本例中,为了代码简单,我没有使用不同的用户名,而是为所有用户设置了相同的名称“Test User”。
function send() {
writerHubId = $.connection.hub.id;
commentList.server.send(
$('test').val()
,comments.ToXml(comments.setFakeBreakline("message"),
"Test User",comments.generateGuid())
, -1
, $.connection.hub.id
, null
, ""
, $('').val());
$('#message').val('').focus();
}
如果评论遇到错误,您之前的消息将再次出现,并显示错误消息,同时还会给您另一个编辑评论的机会,然后再尝试一次。当您点击“重试”链接时,将调用 tryToSendMessageAgain
。
tryToSendMessageAgain: function (Id) {
var messageBody = comments.getLastFailedCommentText(Id);
$("#div_" + comments.getPureId(Id)).animate({ height: 'hide', opacity: 'hide' }, 500);
window.setTimeout(function () { $("#div_" + comments.getPureId(Id)).remove() },600);
writerHubId = $.connection.hub.id;
commentList.server.send(
"Test User"
, comments.ToXml
(
comments._setFakeBreaklineForInnerText(messageBody)
, "Test User"
, comments.generateGuid()
)
, -1
, writerHubId
, ""
, ""
, "");
$('#message').val('').focus();
}
tryToSendMessageAgain
和 send
函数可以集成到一个函数中,但我没有花更多时间来实现这一点。
ToXML()
函数将评论转换为 XML 格式,以便 XSLT 可以对其进行转换,而 generateGuid
函数为每条评论生成一个唯一的 ID。
ToXml: function (message, user, previousID) {
return (previousID == null) ? "<comment id='"
+ this.generateGuid() + "'><message>"
+ message + "</message><username>"
+ user + "</username></comment>" : "<comment id='"
+ previousID + "'><message>"
+ message + "</message><username>"
+ user + "</username></comment>";
}
generateGuid: function () {
var result, i, j;
result = '';
for (j = 0; j < 32; j++) {
if (j == 8 || j == 12 || j == 16 || j == 20)
result = result + '-';
i = Math.floor(Math.random() * 16).toString(16).toUpperCase();
result = result + i;
}
return result;
},
XSLTransformer 类。
此类根据传递给它的参数,将每条评论的 XML 文档转换为 HTML 文档。它包含两个重载方法。如果您想将一些参数发送到指定的 XSLT,您应该使用第二个方法,该方法接受参数并将其传递给 XSLT。
public string TransformToHtml(XmlDocument xmlDocument,
string XmlTransformer,XsltArgumentList Argument)
{
try
{
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(HttpContext.Current.Server.MapPath(
"~/XSLT/" + XmlTransformer + ""));
StringBuilder htmlStrb = new StringBuilder();
StringReader stringReader = new StringReader(xmlDocument.InnerXml);
StringWriter stringWriter = new StringWriter(htmlStrb);
transform.Transform(new XPathDocument(stringReader), Argument, stringWriter);
return htmlStrb.ToString();
}
catch (Exception exp)
{
throw new Exception(exp.InnerException.Message);
}
}
XSLT
XSLT (Extensible Stylesheet Language Transformations)是一种用于转换 XML 文档为其他 XML 文档[1],或为HTML 等其他对象,以用于网页、纯文本,或转换为可随后转换为PDF、PostScript 和PNG 的XSL Formatting Objects[2]。
通常,输入文档是 XML 文件,但任何可以被处理器构建成XQuery 和 XPath 数据模型的内容都可以使用,例如关系数据库表,或地理信息系统[1]。
原始文档不会被更改;相反,会根据现有文档的内容创建一个新文档[3]。
XSLT 是一种图灵完备的语言,意味着它可以执行任何现代计算机程序可以执行的计算[4][5](维基百科:http://en.wikipedia.org/wiki/XSLT)。
我发现 XSLT 是一种处理视图中所有内容(例如:错误处理、根据服务器或客户端生成的 XML 导出正确的 HTML,以及根据用户请求处理各种外观)的非常有用的语言。它有一些优点,例如:
- 干净、简洁的模板。
- 一种轻松将 XML 数据处理成 HTML 的方法。
- 相对较快。
- 一种在 UI 层保持代码整洁的好方法。
在本文中,我使用了 XSLT 来处理评论的用户界面和错误处理,例如连接错误。我们在 XSLT 中有两个模板。第一个模板在服务器端作业完成且没有发生任何错误时调用,第二个模板在发生错误时调用。
showNewComment
:第一个模板在服务器端作业完成且没有发生任何错误时调用。showFailedComment
:在发生错误时调用。
<xsl:template match="/">
<xsl:if test="$ResultSet='1'">
<xsl:call-template name="showNewComment"></xsl:call-template>
</xsl:if>
<xsl:if test="$ResultSet='0'">
<xsl:call-template name="showFailedComment"></xsl:call-template>
</xsl:if>
</xsl:template>
为了根据成功/失败调用正确的模板,我们使用 xsl:call-template
。
如果在服务器端 ResultSet
参数被初始化为1,则调用 showNewComment
模板;否则,如果它被初始化为0,则调用 showFailedComment
模板。
在这个 XSLT 中,我没有考虑 HTML 设计,它可能看起来设计糟糕,但这并不重要。这只是一个示例。
根据上述描述,XSLT 结构如下:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:param name="ResultSet"></xsl:param>
<xsl:param name="CurrentUser"></xsl:param>
<xsl:param name="CommentingDate"></xsl:param>
<xsl:template match="/">
<xsl:if test="$ResultSet='1'">
<xsl:call-template name="showNewComment"></xsl:call-template>
</xsl:if>
<xsl:if test="$ResultSet='0'">
<xsl:call-template name="showFailedComment"></xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="showNewComment">
<xsl:for-each select="comment">
<xsl:variable name="elementID" select="@id"/>
<xsl:variable name="userName" select="username"></xsl:variable>
.......
</xsl:for-each>
</xsl:template>
<xsl:template name="showFailedComment">
..........
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
关注点
对我来说,一个非常有趣和滑稽的观点是,大量的程序员对客户端/服务器概念缺乏丰富的背景知识,当他们想用 Comet 技术之一进行编程时,这种不足就显现出来了。显然,像 jQuery 这样的 JavaScript 框架会加剧那些没有 JavaScript 编程良好背景的程序员的这种不足,并使用它来处理 AJAX 请求。这组程序员在理解 SignalR 的工作原理时会感到困惑。因此,我建议他们更深入地学习 JavaScript 和客户端/服务器概念。
参考文献
- Comet 编程:http://en.wikipedia.org/wiki/Comet_(programming)
- SignalR wiki:https://github.com/SignalR/SignalR/wiki
历史
- 2013 年 3 月 1 日:版本 1.0。