65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (22投票s)

2008年1月4日

CDDL

9分钟阅读

viewsIcon

100053

downloadIcon

649

博客引擎的替代方法

BlogEntry02.png

引言

前导码

首先,在我接触任何技术性内容之前,我想说两件事

  1. 非常感谢 Marc Clifton这里也有他)。他关于一个精益高效博客引擎的文章 精益高效博客引擎精益高效博客引擎,第二部分 是我写这篇文章的灵感来源。
  2. 这是我的第一篇文章,因此,我不是要求你们“手下留情”,而是告诉你们把它拆开,撕碎,嚼烂再吐出来。你们越是批评,我越高兴,因为这样我的下一篇文章就会更好。

附注:我在本文中多次提到 Marc Clifton。我并非崇拜他,但请允许我表明我完全尊重他高超的编程技巧。是的,技巧。

动机

有几个因素促使我写这篇文章。Marc的文章是其中一部分。我喜欢做事,即使别人认为这是在重新发明轮子,因为这能证明我能做到,而且我喜欢自己开创道路达成目标,这会让我成为一个更好的工程师。而且由于 我的网站 早该上线了,我把这次机会视为双重机会,既能开发我的网站,又能构建ABE。
好了,现在让我们开始,向大家介绍ABE并谈谈技术细节。

ABE - 使用代码

ABE 代表“另一个博客引擎”。作为开发者和工程师,我们知道通往某个既定目标的正确路径不止一条。选择哪条路可能取决于技术限制、约束、个人选择或编码风格。话虽如此,尽管市面上已有多个博客引擎 [Subtext, DotText, DasBlog 等等],Marc出于自己的原因选择了编写自己的博客引擎,而我也有许多相同的理由。

博客条目要求

通过学习其他博客引擎的经验,我将博客的功能列举如下:

  • 标题与副标题
  • 条目本身
  • 博客编写日期
  • 用于分类博客条目的标签
  • 添加评论的功能
  • 永久链接,以便访客可以引用博客条目,并可以直接导航到博客条目,而无需浏览数百篇条目
  • 归档,用于访问较早的博客

还有其他一些让博客更完善的功能,例如友好的 URL 和 TrackBacks/PingBacks/LinkBacks。但这些超出了ABE当前阶段的范围。事实上,我选择暂时放弃让读者评论的功能。我确实希望人们能够评论我的博客条目,但一切都得循序渐进。

数据库

在进入文章的标记和代码之前,我将发布我使用的数据库图

DbDiag.PNG
图 1:数据库图。

后端是 SQL Server 2005。此配置应适用于任何 SQL2k5 版本。正如你们所见,这是一个非常简单的数据库。目前来说,它足以满足需求。事实上,它已超出所需,因为其中包含了尚未在此版本中包含的评论的基础。别担心,后续文章将讨论评论、登录、验证码和 RSS 订阅源(尽管订阅源已上线)。

UI

现在我们已将需求精简到构成博客的基本要素,并且由于我不太擅长艺术设计,我选择了以下外观:

BlogEntry01.png
图 2:博客条目外观和感觉。

我还为交替行提供了略有不同的外观,背景为令人愉悦的灰色。

博客条目控件

博客条目的架构:我不会指出标题、副标题或条目本身,这些都很明显,但你们会看到在左下角有两个值得注意的元素。类别标签和永久链接地址。在右下角,我们有以长格式显示的日期,以免混淆当前使用的格式。我将博客条目(我给它命名的)实现为一个用户控件,其标记如下:

<%@ 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 SearsCode-Frog [Rex] 向我推荐 FCKEditor 及其 WinForms 实现),并将其发送到一个 Web 服务,该服务接受数据并将其插入数据库。Web 服务和博客编辑器应用程序值得单独写一篇文章,所以稍后我会发布它们。但这是抢先看。

BlogEntry03.png

结论

这次快速且几乎无痛地涉足编写自己的博客引擎版本,我花了大约 5 天时间。写这篇文章花的时间要长得多。这对我来说,证明了引擎背后的概念很简单,而且与其他免费开源引擎相比,它们的大小完全没有必要——正如 Marc 在他的文章 这里 中提到的,其他引擎又大又笨重;嗯,Sub Text 在我的驱动器上足足有 71.6 MB 的源代码。我的完整网站的源代码,包括图片和其他所有东西(不包含数据库),只有区区 1.07 MB。诚然,ABE(目前)不提供相同的功能集,但两者之间约 70 倍的差异在我看来仍然很大。

如果您想尝试 ABE,请按照源代码 zip 文件中的 _installme.txt_ 文件中的步骤进行操作。如果您认为我需要包含某项功能,那么,请随时给我发邮件。如果您有任何问题,我很乐意为您解答。

我将继续在 ABE 上工作。我已承诺再写两篇文章,围绕 ABE 的使用以及将要实现的高级功能。所以请留意新的修订版和发布。

© . All rights reserved.