精简且高效的博客( 重访)






4.75/5 (22投票s)
博客引擎的替代方法

引言
前导码
首先,在我接触任何技术性内容之前,我想说两件事
- 非常感谢 Marc Clifton(这里也有他)。他关于一个精益高效博客引擎的文章 精益高效博客引擎 和 精益高效博客引擎,第二部分 是我写这篇文章的灵感来源。
- 这是我的第一篇文章,因此,我不是要求你们“手下留情”,而是告诉你们把它拆开,撕碎,嚼烂再吐出来。你们越是批评,我越高兴,因为这样我的下一篇文章就会更好。
附注:我在本文中多次提到 Marc Clifton。我并非崇拜他,但请允许我表明我完全尊重他高超的编程技巧。是的,技巧。
动机
有几个因素促使我写这篇文章。Marc的文章是其中一部分。我喜欢做事,即使别人认为这是在重新发明轮子,因为这能证明我能做到,而且我喜欢自己开创道路达成目标,这会让我成为一个更好的工程师。而且由于 我的网站 早该上线了,我把这次机会视为双重机会,既能开发我的网站,又能构建ABE。
好了,现在让我们开始,向大家介绍ABE并谈谈技术细节。
ABE - 使用代码
ABE 代表“另一个博客引擎”。作为开发者和工程师,我们知道通往某个既定目标的正确路径不止一条。选择哪条路可能取决于技术限制、约束、个人选择或编码风格。话虽如此,尽管市面上已有多个博客引擎 [Subtext, DotText, DasBlog 等等],Marc出于自己的原因选择了编写自己的博客引擎,而我也有许多相同的理由。
博客条目要求
通过学习其他博客引擎的经验,我将博客的功能列举如下:
- 标题与副标题
- 条目本身
- 博客编写日期
- 用于分类博客条目的标签
- 添加评论的功能
- 永久链接,以便访客可以引用博客条目,并可以直接导航到博客条目,而无需浏览数百篇条目
- 归档,用于访问较早的博客
还有其他一些让博客更完善的功能,例如友好的 URL 和 TrackBacks/PingBacks/LinkBacks。但这些超出了ABE当前阶段的范围。事实上,我选择暂时放弃让读者评论的功能。我确实希望人们能够评论我的博客条目,但一切都得循序渐进。
数据库
在进入文章的标记和代码之前,我将发布我使用的数据库图

后端是 SQL Server 2005。此配置应适用于任何 SQL2k5 版本。正如你们所见,这是一个非常简单的数据库。目前来说,它足以满足需求。事实上,它已超出所需,因为其中包含了尚未在此版本中包含的评论的基础。别担心,后续文章将讨论评论、登录、验证码和 RSS 订阅源(尽管订阅源已上线)。
UI
现在我们已将需求精简到构成博客的基本要素,并且由于我不太擅长艺术设计,我选择了以下外观:

我还为交替行提供了略有不同的外观,背景为令人愉悦的灰色。
博客条目控件
博客条目的架构:我不会指出标题、副标题或条目本身,这些都很明显,但你们会看到在左下角有两个值得注意的元素。类别标签和永久链接地址。在右下角,我们有以长格式显示的日期,以免混淆当前使用的格式。我将博客条目(我给它命名的)实现为一个用户控件,其标记如下:
<%@ control language="C#" autoeventwireup="true" codefile="blogEntry.ascx.cs"
inherits="controls_blogEntry" enableviewstate="true" %>
<asp:table runat="server" width="100%" id="blogEntryTable">
<asp:TableRow runat="server" CssClass="blogEntryTitleRow">
<asp:TableCell runat="server" ID="blogEntryIDCell" Visible="false"></asp:TableCell>
<asp:TableCell runat="server" ID="blogEntryTitleCell" CssClass="blogEntryTitle">
</asp:TableCell>
</asp:TableRow>
<asp:TableRow runat="server" CssClass="blogEntrySubTitleRow">
<asp:TableCell runat="server" ID="blogEntrySubTitleCell">
</asp:TableCell>
</asp:TableRow>
<asp:TableRow runat="server" CssClass="blogEntryDataRow">
<asp:TableCell runat="server" ID="blogEntryDataCell">blogEntryData</asp:TableCell>
</asp:TableRow>
<asp:TableRow runat="server" CssClass="TagsRow">
<asp:TableCell runat="server" ID="TagsCell" CssClass="TagsRow">
<div class="TagsRow">Tags Row</div>
</asp:TableCell>
</asp:TableRow>
<asp:TableRow runat="server" CssClass="blogEntryPermalinkRow">
<asp:TableCell runat="server" ID="PermalinkCell"><div>PermaLink</div>
</asp:TableCell>
</asp:TableRow>
<asp:TableFooterRow runat="server" CssClass="blogEntryFooterRow">
<asp:TableCell runat="server" ID="blogEntryFooterCell">
<div>footer row</div>
</asp:TableCell>
</asp:TableFooterRow>
</asp:table>
从标记可以看出,`ID` 和 `CssClass` 的名称相当长且非常具有描述性。在开发过程中我喜欢这样,因为它极大地简化了调试。但到了部署阶段,我实际上会使用更短的名称(通常是缩写),这可以大大减少通过网络(或无线传输)发送的文本量。这很麻烦,但在部署需要承受大量使用的东西时是值得的。阅读本文时,请始终记住,这不是高吞吐量的生产级质量。代码是在 5 天内编写的,而文章则花了近 3 周时间,几乎没有制定任何适当的规范。
总之,如果您在 HTML/XHTML 开发方面经验更丰富,可以跳过下一段;如果不是,我希望它能回答您关于 `DIV` 标签的疑问?您还可以看到我对 `<DIV></DIV>` 标签的大量使用(在此控件以及我网站的其他所有地方,只需查看页面源代码)。原因在于,`DIV`(顾名思义)是对页面进行逻辑划分。您可以对特定的 `DIV` 标签应用格式,而忽略其他 `DIV`。此外,由于 `DIV` 可以设置 `ID`,因此它非常适合动态内容,您能精确地知道将其放置在哪里。最后,`DIV` 标签会在前后添加一个换行符,从而强调它所在的区域。将控件的属性作为普通属性公开,更新每个条目都很简单。
/// <summary>
/// Exposes the BlogEntryID attribute of the underlying markup (blogEntryIDCell)
/// </summary>
public int BlogEntryID
{
get
{
return int.Parse(this.blogEntryIDCell.Text);
}
set
{
this.blogEntryIDCell.Text = value.ToString();
}
}
/// <summary>
/// Exposes the BlogEntryTitle attribute of the underlying markup
/// (blogEntryTitleCell)
/// </summary>
public string BlogEntryTitle
{
get
{
return this.blogEntryTitleCell.Text;
}
set
{
//this will be a link on to the page with the single blog, complete.
// Should be used as a permalink?
this.blogEntryTitleCell.Text = CreateLink(value);
}
}
(其他属性以此类推)由于我们有(或者说将要有)许多博客条目,因此我们显然需要一个数据源来检索一定数量(我选择了 10 个)的博客条目,而其余的则可以根据需要分页。对于存在的其他元素,博客条目控件(永久链接、标签和日期)如您所见,是内置在实际控件中的。现在,这其中有一个小技巧。这些链接中的每一个都会通过一个 `get` 请求链接回博客页面,该请求指定使用哪个过滤器来获取正确的条目或条目。因此,博客页面有四种可能的 `get` 请求:
表单 | 描述 | *示例 |
mustafacodes.net/blog.aspx | 原始页面,手动输入或通过导航栏访问,这是访问博客页面的默认方式。 | mustafacodes.net/blog.aspx |
mustafacodes.net/blog.aspx?id={tag ID} | ID,可以通过标题或永久链接访问。 | mustafacodes.net/blog.aspx?id=2 |
mustafacodes.net/blog.aspx?tag={tag text} | 标签,可以通过标签云或博客条目标签行中的标签访问。 | mustafacodes.net/blog.aspx?tag=personal |
mustafacodes.net/blog.aspx?date={month #, year #} | 日期,可以通过标签云下方的归档列表访问。 | mustafacodes.net/blog.aspx?date=12,2007 |
*我缩短了 URL,以便它能适应页面。
目前,我将永久链接保持原样,只是一个带有博客条目 ID 的 `GET` 请求。希望在下一篇文章中,我能实现 URL 重写,从而得到类似 _http://www.mustafacodes.net/blog/aparticularblogentry.aspx_ 的形式。
现在,我们只需要关心 `blog.aspx` 页面上的文章处理方式。我的 `Page_Load` 方法如下:
protected void Page_Load(object sender, EventArgs e)
{
try
{
if (!IsPostBack)
{
if (Request.QueryString.Count > 0)
{
eBlogAction blogAction = CheckKey(Request.QueryString);
switch (blogAction)
{
case eBlogAction.GetByID:
GetBlogByID();
break;
case eBlogAction.GetByTag:
GetBlogByTag();
break;
case eBlogAction.GetByDate:
GetBlogByDate();
break;
case eBlogAction.DefaultAction:
//anything else we simply transfer back to
//the default blogs
Server.Transfer("blog.aspx");
break;
default:
//anything unhandled previously we simply transfer back to
// the blogs not the error pages (for now)
Server.Transfer("blog.aspx");
break;
}
}
}
}
catch (Exception ex)
{
//log the error
using (ErrorLogger el = new DataLayer.ErrorLogger(ex,
ConfigurationManager.ConnectionStrings["DbConnection"].ConnectionString));
Response.Redirect("error.aspx", true);
}
}
加载数据
现在,我选择使用一个专门负责数据处理和显示博客条目列表的控件,名为 `blogEntries`。
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="blogEntries.ascx.cs"
Inherits="controls_blogEntries" %>
<%@ Register TagName="BlogEntry" Src="../controls/blogEntry.ascx" TagPrefix="uc" %>
<asp:Table runat="server" ID="blogEntries" Width="100%">
<asp:TableRow runat="server">
<asp:TableCell Width="100%" CssClass="blogEntriesBody" runat="server">
<asp:Repeater runat="server" ID="rptBlog">
<HeaderTemplate>
<table width="100%">
</HeaderTemplate>
<ItemTemplate>
<tr style="width: 100%;">
<td>
<div class="rptblogEntriesTemplate">
<uc:BlogEntry runat="server" ID="ItemBlogEntryItem" />
</div>
</td>
</tr>
</ItemTemplate>
<AlternatingItemTemplate>
<tr style="width: 100%;">
<td>
<div class="rptBlogAlternatingItemTemplate">
<uc:BlogEntry runat="server" ID="ItemBlogEntryItem" />
</div>
</td>
</tr>
</AlternatingItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
</asp:TableCell>
</asp:TableRow>
</asp:Table>
<asp:Table runat="server" ID="btnTable" Width="100%">
<asp:TableRow>
<asp:TableCell HorizontalAlign="Left">
<asp:LinkButton ID="rptPrev" runat="server" Text="<< Previous"
CommandName="rptPrev" Visible="false" />
</asp:TableCell>
<asp:TableCell HorizontalAlign="Right">
<asp:LinkButton ID="rptNext" runat="server" Text="Next >>"
CommandName="rptNext" Visible="false" />
</asp:TableCell>
</asp:TableRow>
</asp:Table>
如您所见,它主要由一个 `DataRepeater` 组成,该控件非常轻量级,在服务器端绑定到 `PagingDataSource`,这就是分页的实现方式。我不会在此处发布控件的完整后台代码,可以说它需要重写。另外请注意,`ItemTemplate` 和 `AlternatingItemTemplate` 之间唯一的区别是使用的 CSS 类。这是因为它们之间唯一的区别在于指定交替行的不同背景颜色。但是,我将发布 `ItemDataBound` 事件处理程序(及其简洁性)。
private void rptBlog_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
if(e.Item.ItemType == ListItemType.Item ||
e.Item.ItemType == ListItemType.AlternatingItem)
{
//pass the values from (DataRowView)e.Item.DataItem[];
controls_blogEntry cbe =
(controls_blogEntry)e.Item.FindControl("ItemBlogEntryItem");
DsBlog.tblBlogEntryRow ber =
(DsBlog.tblBlogEntryRow)((DataRowView)e.Item.DataItem).Row ;
if (ber != null)
{
cbe.BlogEntryID = ber.BlogEntryID;
cbe.BlogEntryTitle = ber.BlogTitle;
cbe.BlogEntrySubTitle = ber.BlogSubTitle;
cbe.BlogEntryData = string.Format("<div>{0}</div><br />", ber.BlogEntry);
string link = string.Format
("http://www.mustafacodes.net/blog.aspx?id={0}",
cbe.BlogEntryID);
cbe.PermaLink = string.Format("<div class=\"blogEntryPermalinkRow\">" +
"permalink: <a href=\"{0}\">{0}</a></div>", link);
cbe.BlogEntryFooter = ber.BlogDate.ToLongDateString();
string s = ber.Tags;
if(s.Length > 0)
{
cbe.Tags = s.Substring(1);
}
else
{
cbe.Tags = s;
}
}
}
}
博客
目前,我没有将服务器放在家里的奢侈。而且,正如之前讨论过的,我喜欢出于“我能做到”的原因而编码。所以,由于这是我的网站,我编写了一个 WinForm 应用程序,它只需接受我博客条目的 HTML 格式文本(非常感谢 Patrick Sears 和 Code-Frog [Rex] 向我推荐 FCKEditor 及其 WinForms 实现),并将其发送到一个 Web 服务,该服务接受数据并将其插入数据库。Web 服务和博客编辑器应用程序值得单独写一篇文章,所以稍后我会发布它们。但这是抢先看。

结论
这次快速且几乎无痛地涉足编写自己的博客引擎版本,我花了大约 5 天时间。写这篇文章花的时间要长得多。这对我来说,证明了引擎背后的概念很简单,而且与其他免费开源引擎相比,它们的大小完全没有必要——正如 Marc 在他的文章 这里 中提到的,其他引擎又大又笨重;嗯,Sub Text 在我的驱动器上足足有 71.6 MB 的源代码。我的完整网站的源代码,包括图片和其他所有东西(不包含数据库),只有区区 1.07 MB。诚然,ABE(目前)不提供相同的功能集,但两者之间约 70 倍的差异在我看来仍然很大。
如果您想尝试 ABE,请按照源代码 zip 文件中的 _installme.txt_ 文件中的步骤进行操作。如果您认为我需要包含某项功能,那么,请随时给我发邮件。如果您有任何问题,我很乐意为您解答。
我将继续在 ABE 上工作。我已承诺再写两篇文章,围绕 ABE 的使用以及将要实现的高级功能。所以请留意新的修订版和发布。