BlogEngine.Net 的成功不仅在于它的架构设计,它的代码实现细节也都是很经典的,每个结构分割的很清晰很自然,希望大家多多品位一下。在这篇文章里我将给大家介绍一下 BlogEngine.Net的Web实现上的几个亮点,包括Web.config,Ajax的运用等。
Web.config中的几个结点说明
让我们看一下appSettings结点中的各个选项的含义,以便您对整个BlogEngine.Net的认识更加清晰。
BlogEngine.FileExtension:在这里我们可以自定义Url请求的后缀名称,默认是.aspx。您可以定义自己喜欢的扩展名,例如.extguo,那么对于文章等生成的Url就成了类似http://HostName/CategoryName/PostTitle.extguo的形式。这个结点的使用在很多地方都可以见到,例如:
Post类中的RelativeLink
1public string RelativeLink
2{
3 get
4 {
5 string slug = Utils.RemoveIllegalCharacters(Slug) + BlogSettings.Instance.FileExtension;
6
7 if (BlogSettings.Instance.TimeStampPostLinks)
8 return Utils.RelativeWebRoot + "post/" + DateCreated.ToString("yyyy/MM/dd/", CultureInfo.InvariantCulture) + slug;
9
10 return Utils.RelativeWebRoot + "post/" + slug;
11 }
12}
BlogSettings.Instance.FileExtension就是读取这个结点获得的扩展名。
BlogEngine.VirtualPath:主要是为一些文章等的链接而服务的。我们可以使用虚拟目录安装我们的BlogEngine.Net,那么我们只要设置一下这里就可以得到和直接在根目录下一样的效果。我们需要注意一下Utils关于Url处理的部分,其中:
Utils中的RelativeWebRoot
1public static string RelativeWebRoot
2{
3 get
4 {
5 if (_RelativeWebRoot == null)
6 _RelativeWebRoot = VirtualPathUtility.ToAbsolute(ConfigurationManager.AppSettings["BlogEngine.VirtualPath"]);
7
8 return _RelativeWebRoot;
9 }
10}
就是获得相对Web目录。
BlogEngine.MobileDevices:它的值是一个正则表达式,主要是对移动设备做出判断,并给移动设备使用设置的主题。它在这里被使用:
Utils中的IsMobile判断
1private static readonly Regex MOBILE_REGEX = new Regex(ConfigurationManager.AppSettings.Get("BlogEngine.MobileDevices"), RegexOptions.IgnoreCase | RegexOptions.Compiled);
2
3/**//// <summary>
4/// Gets a value indicating whether the client is a mobile device.
5/// </summary>
6/// <value><c>true</c> if this instance is mobile; otherwise, <c>false</c>.</value>
7public static bool IsMobile
8{
9 get
10 {
11 HttpContext context = HttpContext.Current;
12 if (context != null)
13 {
14 HttpRequest request = context.Request;
15 if (request.Browser.IsMobileDevice)
16 return true;
17
18 if (!string.IsNullOrEmpty(request.UserAgent) && MOBILE_REGEX.IsMatch(request.UserAgent))
19 return true;
20 }
21
22 return false;
23 }
24}
之后在BlogSetting的Theme中返回。
BlogEngine.AdminRole:管理员角色名,可以动态配置管理员角 色,也就是说系统对是否是管理员的判断中的管理员名称实际上是从这儿获得的。比如有一天我们想把某个角色改为管理员,那么就直接修改这儿就行了,不过这儿 需要的数据肯定也是角色管理中的一个角色名称。
StorageLocation:数据的存储位置,主要是给数据存储是XML格式时使用。在DataStore.cs文件中StorageLocation方法会涉及到。
BlogEngine.HardMinify: 对于JavaScript脚本指定强行压缩(去除一些不必要的字符)的文件名称。我们知道BlogEngine.Net中对于JavaScript请求都 是通过JavaScriptHandler进行的,JavaScriptHandler有一个HardMinify来判断是否已经指定强行压缩。 JavaScriptHandler中会涉及到这部分的引用。注意对WebResource.axd的请求在BlogBasePage中替换成对于 js.axd的请求,之后JavaScriptHandler的处理是使用RetrieveRemoteScript来输出脚本的。
Ajax的运用
BlogEngine.Net的很多部分使用原生的Ajax完成。例如 评论的打分,widget的管理等部分。我们在讲述BlogBasePage时提到的AddLocalizationKeys实际上就是初始化页面脚本的 一些变量,之后使用AddJavaScriptInclude(Utils.RelativeWebRoot + "blog.js")引入blog.js,如果是管理员(Widget可被管理)又使用 AddJavaScriptInclude(Utils.RelativeWebRoot + "admin/widget.js")引入widget.js,使用 AddStylesheetInclude(Utils.RelativeWebRoot + "admin/widget.css")引入widget样式,注意先后顺序,因为widget.js需要使用核心的blog.js。blog.js中这段代码就是Ajax调用的核心:
Ajax核心
1/// <summary>
2/// Creates a client callback back to the requesting page
3/// and calls the callback method with the response as parameter.
4/// </summary>
5function CreateCallback(url, callback)
6{
7 var http = GetHttpObject();
8 http.open("GET", url, true);
9
10 http.onreadystatechange = function()
11 {
12 if (http.readyState == 4)
13 {
14 if (http.responseText.length > 0 && callback != null)
15 callback(http.responseText);
16 }
17 };
18
19 http.send(null);
20}
21
22/// <summary>
23/// Creates a XmlHttpRequest object.
24/// </summary>
25function GetHttpObject()
26{
27 if (typeof XMLHttpRequest != 'undefined')
28 return new XMLHttpRequest();
29
30 try
31 {
32 return new ActiveXObject("Msxml2.XMLHTTP");
33 }
34 catch (e)
35 {
36 try
37 {
38 return new ActiveXObject("Microsoft.XMLHTTP");
39 }
40 catch (e) {}
41 }
42
43 return false;
44}
在线评论预览与评论的提交
结合blog.js与User controls\CommentView.ascx,我们看一下它的处理过程。CommentView可以显示文章的评论列表和提交新评论。当点击 Preview时,会调用blog.js中的ToggleCommentMenu:
评论的预览与提交
1// Shows the preview of the comment
2function ToggleCommentMenu(element)
3{
4 element.className = 'selected';
5 if (element.id == 'preview')
6 {
7 $('compose').className = '';
8 $('commentCompose').style.display = 'none';
9 $('commentPreview').style.display = 'block';
10 $('commentPreview').innerHTML = '<img src="' + KEYwebRoot + 'pics/ajax-loader.gif" alt="Loading" />';
11 var argument = $('commentPreview').innerHTML;
12 AddComment(true);
13 }
14 else
15 {
16 $('preview').className = '';
17 $('commentPreview').style.display = 'none';
18 $('commentCompose').style.display = 'block';
19 }
20}
21
22function EndShowPreview(arg, context)
23{
24 $('commentPreview').innerHTML = arg;
25}
26
27function AddComment(preview)
28{
29 var isPreview = preview == true;
30 if (!isPreview)
31 {
32 $("btnSaveAjax").disabled = true;
33 $("ajaxLoader").style.display = "inline";
34 $("status").className = "";
35 $("status").innerHTML = KEYsavingTheComment;
36 }
37
38 var author = nameBox.value;
39 var email = emailBox.value;
40 var website = websiteBox.value;
41 var country = countryDropDown ? countryDropDown.value : "";
42 var content = contentBox.value;
43 var notify = $("cbNotify").checked;
44 var captcha = captchaField.value;
45
46 var callback = isPreview ? EndShowPreview : AppendComment;
47 var argument = author + "-|-" + email + "-|-" + website + "-|-" + country + "-|-" + content + "-|-" + notify + "-|-" + isPreview + "-|-" + captcha;
48
49 WebForm_DoCallback('ctl00$cphBody$CommentView1',argument, callback,'comment',null,false);
50
51 if (!isPreview && typeof OnComment != "undefined")
52 OnComment(author, email, website, country, content);
53}
54
请注意isPreview的引入与它的逻辑,这个Preview实际上也是需要回调到服务器端的程序的,之后生成预览的Render,当 点击Save时isPreview为false,这时回调服务器端的代码时才真正的保存,然后浏览器回调客户端的AppendComment完成一些初始 化工作。CommentView是一个UserControl并实现了 ICallbackEventHandler接口,这个接口有两个方法GetCallbackResult和RaiseCallbackEvent,在 RaiseCallbackEvent中我们可以看出,提交的评论参数使用"-|-"作为分隔符,经过一系列处理最后将这个新评论的呈现给了 _Callback,_Callback由GetCallbackResult返回。浏览端使用 WebForm_DoCallback('ctl00$cphBody$CommentView1',argument, callback,'comment',null,false);在这部分里我们只要实现ICallbackEventHandler接口就行了,实现的 细节都已经有.Net提供了,感兴趣细节的朋友可以查一查ASP.NET的回调方面的资料。
Widget排序的实现
当我们用鼠标拖住一个Widget移动到某个位置再放开鼠标时,发现这个Widget在WidgetZone中的排序被永久存储了,甚至 在你清空cookie时依然生效,BlogEngine.Net是在服务器端进行的存储,DataStore的be_WIDGET_ZONE中的 Widget的顺序就是在页面上Widget的显示顺序。这实际上是通过间接调用WidgetEditor.aspx中的MoveWidgets方法实现 的(前文提及过)。那么在浏览器端是如何处理这些排序问题的呢?
在widget.js中的大部分代码都是用来处理拖放排序的,initdragableElements主要完成使某个Widget可以 被拖动的初始化工作,widget.js的最后一句代码addLoadEvent(initdragableElements)使其在页面加载时执行。注 意initdragableElements中:
拖动事件的Hookup
1document.body.onmousemove = moveDragableElement;
2document.body.onmouseup = stop_dragDropElement;
3document.body.onselectstart = cancelSelectionEvent;
4document.body.ondragstart = cancelEvent;
5window.onresize = repositionDragObjectArray;
当鼠标onmouseup时执行stop_dragDropElement,在stop_dragDropElement中通过调用 saveData,saveData使用上文提到的CreateCallback完成服务器端的回调,其中saveString就是已排序的字符串,主要 用于发送给服务器端程序处理。
总结
实际上在BlogEngine.Net的Web站点中还有很多比较有用的东西值得我们去研究,例如FilterByApml的实现,对文 章的评分,邮件的发送等,这里就不做更多的讨论。关于在线评论预览我个人觉得没有必要去回调服务器,完全可以由客户端搞定,不知道大家是怎么想的。那个 Widget的排序一直都深深的吸引着我,这种处理我见得很少,所以拿出来分享。
在本系列的最后一篇文章中我会就BlogEngine.Net的优秀部分和不太推荐的部分做一个总结,并谈一下我对它的感受,希望大家继续关注。
坚持写完,坚持看完。
上一篇:BlogEngine.Net架构与源代码分析系列part13:实现分析(上)——HttpHandlers与HttpModules
返回到目录