自反+递归 实现评论的无限引用

自反+递归 实现评论的无限引用

引言

大家每天都在看博客,发表评论,实现一个评论系统也是一名Web开发者的基本要求。虽然评论只是一个很普通的功能,但是实现评论的引用,尤其是无限引用,却有一定的困难。身为“网易工程队”的正规军,同时又作为一名程序开发人员,有必要向大家展示一下“盖楼”的方法。

效果预览:http://www.tracefact.net/demo/NestedComment/Default.aspx

NOTE:本文使用 基于业务对象(List<Comment>)的筛选 来进行引用列表的搜寻,对数据库仅进行了一次读取。想也应该能想明白:不管是初始评论还是包含引用的评论都属于同一文章下,一次读取该文章下的评论,进行列表搜寻就可以了,为什么要多次读取数据库!?
  尽管如此,使用递归的效率依然是很低的,会进行频繁的方法调用,所以这篇文章的方法基本上只有实验价值,没有使用价值。可以考虑在Comment表中建一个字段,QuoteContent,用来保存引用的内容,QuoteContent可以使用文中的方法来获得。

评论引用的“传统方法”

称之为“传统方法”,是因为这种方法很多的论坛都在采用,比如说 蓝色理想。做法是在点引用的时候,在回帖人的正文中,加入代码比如“[quote]引用内容...[/quote]回复正文”,然后在输出的时候,将UBB代码用正则表达式替换成HTML代码。

这种方法的好处是取出数据速度比较快,直接从数据库读出再送显就可以了。缺点是写正则表达式比较麻烦,而且容易出错,比如一个[quote]的嵌套位置不正确,就会使表达式失效;还有就是会让数据库存储额外的数据(引用的内容也存储了)。

这种方法很多人可能都用过,我们就不讨论了,直接进入我们的正题。

自反关系的表结构

我们先介绍一下本文会频繁用到的两个术语:

  • 初始评论:表示这个评论没有引用其他任何评论。
  • 引用评论:表示这个评论包含对其他评论的引用。

数据表的结构是实现无限引用的前提,设计的不好就会很难实现。我们先看一下建表的脚本:

Create Table Comment
(
    Id         int identity(1,1)     Not Null,
    UserName   Varchar(200)          Not Null,
    Content    Varchar(2000)         Not Null,
    PostDate   DateTime              Not Null Default GetDate(),
    CommentId  Int                   Null,      -- 外键,自反关系
    ArticleId  Int                   Not Null

   Constraint pk_Comment Primary Key(Id)
   Constraint fk_Comment_Comment Foreign Key(CommentId) References Comment(Id)
)

  • Id:评论的Id。
  • UserName:通常情况下,这里是个int类型的UserId,引用一个User表,但在本文中简单起见,直接用Varchar类型。
  • Content:评论的内容。
  • PostDate:评论发表的时间。

这些我想都比较容易看懂,我们下来主要看CommentId和ArticleId:

CommentId:这是一个关键字段。这个字段引用了本身(Comment表)的Id字段,构成一个自反关系,它也是Comment表的外键。当一个评论是初始评论时,它为Null;当一个评论是引用评论时,它为该评论所引用的评论的Id。当我们需要获取某一个引用评论时,需要顺着它的CommentId纵深查找,直到找到CommmentId为Null的评论。

举例来说:如果我想显示Id为17的评论,我先看它的CommentId是否为Null,如果为Null,那么它是一个初始评论,直接返回;如果不为Null,则寻找Id等于它的CommentId的评论,找到以后,再检查这个评论的CommentId,重复之前的过程,一直找到CommentId为Null的评论为止。

ArticleId:这个大家应该比较熟悉了,它是评论所属的文章的Id。

NOTE:这里我想说一下,如果想建立外键(自反是一种特殊的外键)。那么外键所包含的字段要么为Null,要么为一个存在的主键。我发现很多人不喜欢用Null,他们也不建外键,如果让他们来实现上面的表结构,当一个评论是初始评论时,他会给CommentId 赋值为0,而不是Null。虽然这样也没什么错,但我个人很不喜欢,不够规范。

页面实现

虽然我曾经花了不少时间学习Web标准,但是以后我不会再过分地分散精力了,我的文章也不会讲述Css和Web标准,所以这里只给出实现并略做一点说明。

尽管本文中评论部分的页面是动态生成的HTML,但是我们往往需要先设计一下HTML,编写好样式表,然后才去写程序,我们看下一个无限引用的HTML代码可能是什么样的。在任意一个站点下创建一个页面NestedComment.aspx:

<div id="commentHolder">
    <div class='comment'>
       <p class='title'><span>2008-3-24 16:33:49 发表</span>内蒙古网友</p>
       <div>
           <div>
              <div><span>广州网友 原贴:</span><br />
                  向马XX同志荣升台湾省省长表示祝贺!
              </div>
              <span>四川网友 原贴:</span><br />
              四川人民发来贺电!
           </div>
           <span>陕西西安网友 原贴:</span><br />
           陕西网友发来贺电
       </div>
       <p>内蒙网友发来贺电</p>
    </div>
    <div class='comment'>略...</div>
    <div class='comment'>略...</div>
    <div class='comment'>略...</div>
    <div class='comment'>略...</div>
</div>

可以看到,每一条评论都包含在一个css Class为comment的div中,所有的div又包含在一个Id为commentHolder的div中,作为它们的容器。我们之后要生成的代码,将会以上面的HTML代码作为格式和模板。现在把它们注释掉,放置一个Repeater控件,代码如下:

<div id="commentHolder">
    <asp:Repeater runat="server" ID="rpComment" EnableViewState="false">
       <ItemTemplate>
           <%# GetContent(Container.DataItem) %>
       </ItemTemplate>
    </asp:Repeater>
</div>

注意到将EnableViewState设为了False,以及在ItemTemplate中放置了一个方法GetContent(),并将当前绑定的项目作为参数传递了进去,这些我们在后置代码中会再讲到。

我们再看一下Css样式:

<style type="text/css" >
    *{margin:0;padding:0;}
    body{margin:10px;font-size:14px;font-family:宋体}
    h1{font-size:26px;margin:10px 0 15px;}
    #commentHolder{width:540px;border-bottom:1px solid #aaa;}
    .comment{padding:5px 8px;background:#f8fcff;border:1px solid #aaa;font-size:14px;border-bottom:none;}
    .comment p{padding:5px 0;}
    .comment p.title{color:#1f3a87;font-size:12px;}
    .comment p span{float:right;color:#666}
    .comment div{background:#ffe;padding:3px;border:1px solid #aaa;line-height:140%;margin-bottom:5px;}
    .comment div span{color:#1f3a87;font-size:12px;}
</style>

后置代码

Comment 实体类

我们先创建一个实体类 Comment,这个类用于映射数据库中的表Comment:

public class Comment {
    private int id;
    private string userName;
    private string content;
    private DateTime postDate;
    private int commentId;
    private int articleId;

    public int Id {
       get { return id; }
       set { id = value; }
    }

    public string UserName {
       get { return userName; }
       set { userName = value; }
    }

    public string Content {
       get { return content; }
       set { content = value; }
    }

    public DateTime PostDate {
       get { return postDate; }
       set { postDate = value; }
    }

    public int CommentId {
       get { return commentId; }
       set { commentId = value; }
    }

    public int ArticleId {
       get { return articleId; }
       set { articleId = value; }
    }
}

评论的排序一般有两种:一种是最新评论在最上面,一种是最新评论在最下面。我个人比较喜欢最新评论在最上面这种,但是在引用评论中引用的评论列表肯定是最早的在最上面,所以我们需要实现列表的排序,一种是顺序,一种是倒序。关于如何实现列表排序,在 基于业务对象的排序 中已经很详细的写明了,这里就不再讨论,只给出代码。修改Comment类,添加如下代码:

public class Comment {

    //... 上面略

    public static CommentComparer GetComparer(bool isAscending) {
       return new CommentComparer(isAscending);
    }

    public static CommentComparer GetComparer() {
       return GetComparer(true);
    }
    // 嵌套类,用于排序
    public class CommentComparer : IComparer<Comment> {
       private bool isAscending;

       public CommentComparer(bool isAscending) {
           this.isAscending = isAscending;
       }

       public int Compare(Comment x, Comment y) {
           if (isAscending)
              return x.Id.CompareTo(y.Id);
           else
              return y.Id.CompareTo(x.id);
       }
    }
}

获取评论列表:GetList(int articleId)方法

我们接着在代码后置类中添加一个方法,GetList(int articleId),这个方法通常是根据文章Id(articleId)从数据库中获取这个文章下的所有评论,并返回一个List<Comment>列表对象。但是本文中,为了简单起见,我直接手动创建了这个列表对象(需要注意的是对于CommentId为Null的评论,我们将它的CommentId设为0,也可以使用 int?,这样int类型也可以设置为null,但我个人不大喜欢这样):

// 应该来自于数据库,这里直接 HardCoding 了
// articleId 是文章的Id,返回此文章下的所有评论
private List<Comment> GetList(int articleId)
{
    List<Comment> list = new List<Comment>();

    Comment cmt1 = new Comment();
    cmt1.Id = 15;                // 评论Id
    cmt1.ArticleId = articleId;     // 文章Id
    cmt1.CommentId = 0;             // 起始评论
    cmt1.Content = "向马XX同志荣升台湾省省长表示祝贺!";
    cmt1.PostDate = DateTime.Now.AddMinutes(-25);  // 25分钟前发表
    cmt1.UserName = "广州网友";       // 用户名
   
    Comment cmt2 = new Comment();
    cmt2.Id = 16;
    cmt2.ArticleId = articleId;
    cmt2.CommentId = 15;                // 引用id为15的评论
    cmt2.Content = "四川人民发来贺电!";      
    cmt2.PostDate = DateTime.Now.AddMinutes(-19);
    cmt2.UserName = "四川网友";

    Comment cmt3 = new Comment();
    cmt3.Id = 17;
    cmt3.ArticleId = articleId;
    cmt3.CommentId = 16;                // 引用id为16的评论
    cmt3.Content = "陕西人民发来贺电";    
    cmt3.PostDate = DateTime.Now.AddMinutes(-16);
    cmt3.UserName = "陕西西安网友";
   
    Comment cmt4 = new Comment();
    cmt4.Id = 18;
    cmt4.ArticleId = articleId;
    cmt4.CommentId = 0;                 // 又一则起始评论
    cmt4.Content = "希望台湾和平稳定发展。";
    cmt4.PostDate = DateTime.Now.AddMinutes(-13);
    cmt4.UserName = "黑龙江网友";
   
    Comment cmt5 = new Comment();
    cmt5.Id = 19;
    cmt5.ArticleId = articleId;
    cmt5.CommentId = 17;            // 引用Id为17的评论
    cmt5.Content = "宁夏人民发来贺电";
    cmt5.PostDate = DateTime.Now.AddMinutes(-8);
    cmt5.UserName = "宁夏网友";
   
    Comment cmt6 = new Comment();
    cmt6.Id = 20;
    cmt6.ArticleId = articleId;
    cmt6.CommentId = 18;            // 引用Id为18的评论
    cmt6.Content = "支持楼上";   
    cmt6.PostDate = DateTime.Now.AddMinutes(-5);
    cmt6.UserName = "加拿大网友";

    Comment cmt7 = new Comment();
    cmt7.Id = 21;
    cmt7.ArticleId = articleId;
    cmt7.CommentId = 17;            // 引用Id为17的评论
    cmt7.Content = "内蒙人民发来贺电";
    cmt7.PostDate = DateTime.Now.AddMinutes(-2);
    cmt7.UserName = "内蒙古网友";

    list.Add(cmt1);
    list.Add(cmt2);
    list.Add(cmt3);
    list.Add(cmt4);
    list.Add(cmt5);
    list.Add(cmt6);
    list.Add(cmt7);

    return list;
}

填充Repeater控件,Page_Load 事件代码

我们在Page_Load中调用GetList()方法,获取评论列表,将它按倒序排列,然后填充了Repeater控件:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
       List<Comment> list = GetList(16);       // 获取ArticleId为16的所有评论
       list.Sort(Comment.GetComparer(false));  // 倒序排列
       ViewState["List"] = list;           // 设置ViewState

       rpComment.DataSource = list;
       rpComment.DataBind();
    }
}

注意到,我们使用ViewState保存了列表,一会还会看到,我们会从ViewState中还原列表,此时,Comment对象必须被标记为可串行化,修改Comment类,在顶部添加Serializable特性:

[Serializable]
public class Comment { /*略*/}

获取输出:GetContent()方法

接下来我们需要编写我们的核心方法GetContent,它嵌入在Repeater控件的ItemTemplate中,并接受Container.DateItem作为参数,而Container.DateItem代表的是Repeater控件显示的一个数据项,也就是一个Comment类型实例,再进一步就是数据库表中的一行。

递归调用:AddComment()方法

在实现GetContent()方法之前,我们首先应该考虑如何根据一则评论,获取它所引用的所有评论。如果我们需要编写一个方法,那么这个方法需要接收哪些参数:

  1. 方法肯定需要当前显示的评论(Comment),然后才能根据这个评论所引用的评论的Id,也就是它的CommentId属性,去逐层深入地搜寻其他的Comment。
  2. 我们应该有一个列表来保存搜寻到的Comment,我们管这个列表叫 quoteList,它是List<Comment>类型。
  3. 我们需要传递搜寻的对象,也就是当前文章下的所有评论列表,也是一个List<Comment>类型。

再看看这个方法的流程应该是什么:

  1. 检查传递进来的评论,判断它的CommentId,如果为0,那么是起始评论,退出方法。
  2. 如果不是,搜索Id等于它的CommentId的评论;将找到的评论加入quoteList引用列表;然后再次调用方法,并传递找到的评论(递归调用)。直到找出CommentId为0的评论为止。

现在我们来看一下AddComment()方法的代码:

// 向quoteList中添加 符合条件的Comment
protected void AddComment(List<Comment> list, List<Comment> quoteList, Comment cmt)
{
    if (cmt.CommentId != 0)
    {
       Comment find = list.Find(new Predicate<Comment>(cmt.MatchRule));
       quoteList.Add(find);

       // 递归调用,只要CommentId不为零,就加入到引用评论列表
       AddComment(list, quoteList, find); 
    }else
       return;
}

上面的参数 list代表某一文章下的全部评论列表,cmt代表当前要显示的评论,quoteList代表当前要显示的评论所引用的评论列表。

列表搜寻:Predicate<T>(T obj)委托

注意上面,在找寻Id等于当前评论的CommentId的评论,我使用了list.Find()方法。

如何进行列表的搜寻,我在 基于业务对象的筛选 中已经详细介绍了,这里只给出实现过程,不再进行讲述。需要注意的是:在 基于业务对象的筛选 一文中,我是创建了一个类封装筛选的规则,而在本文中,我们将筛选规则,也就是MatchRule()方法(这个方法实现了Predicate<T>委托)直接写在了 Comment 类中,它的作用是:搜索列表,并返回Id与当前评论的CommentId相同的评论。

修改Comment类,添加如下代码:

// 实现 Predicate<T> 委托,搜索Id 等于当前评论的CommentId的评论
public bool MatchRule(Comment cmt) {
    return (this.commentId == cmt.id);
}

输出显示:GetContent()方法

OK,有了前面的铺垫,下面的工作就直白的多,我先给出代码,再做以说明:

// 根据当前的Comment得到HTML输出
protected string GetContent(object objComment)
{
    string output = "";
    List<Comment> list = (List<Comment>)rpComment.DataSource; // 获取全部列表

    Comment cmt = (Comment)objComment;             // 获取当前评论
    List<Comment> quoteList = new List<Comment>(); // 创建当前评论所引用的评论列表

    AddComment(list, quoteList, cmt);       // 为当前评论的引用列表添加项目

    quoteList.Sort(Comment.GetComparer());  // 对列表排序,顺序排列

    foreach (Comment quote in quoteList)    // 生成引用的评论列表
    {
       output = String.Format(
              "<div>{0}<span>{1} 原贴:</span><br />{2}</div>",
              output, quote.UserName, quote.Content);
    }

    // 添加当前引用
    output = String.Format(
           "<div class='comment'><p class='title'><span>{0} 发表</span>{1}</p>{2}<p>{3}</p></div>",
           cmt.PostDate, cmt.UserName, output ,cmt.Content);

    return output;
}

在这段代码中,我们获取了当前评论 cmt,然后又通过Repeater控件的DataSource属性获取了全部列表,创建了当前评论所引用的评论列表,然后调用了AddComment()方法,对引用列表填充了项目,然后通过Sort()方法将它按Id顺序排列。最后,我们遍历了引用列表,按照我们之前讲述的HTML代码产生了输出,然后返回到页面上。

测试:发表评论

现在所有的工作都做完了,但是为了更有趣一些,让我们为程序添加发表评论的功能。先在页面上添加如下代码:

<br />
引用评论 :
<asp:DropDownList ID="ddlCommentId" runat="server">
    <asp:ListItem>21</asp:ListItem>
    <asp:ListItem>20</asp:ListItem>
    <asp:ListItem>19</asp:ListItem>
    <asp:ListItem>18</asp:ListItem>
    <asp:ListItem>17</asp:ListItem>
    <asp:ListItem>16</asp:ListItem>
    <asp:ListItem>15</asp:ListItem>
</asp:DropDownList>

姓名:<asp:TextBox ID="txtUserName" runat="server" Width="100px"></asp:TextBox>
<br />
<asp:TextBox ID="txtContent" TextMode="MultiLine" runat="server" Height="92px" Width="300px"></asp:TextBox>
<asp:Button ID="btnSubmit" runat="server" Text="提交" OnClick="btnSubmit_Click" />

然后编写btnSubmit的Click事件:

// 按钮提交事件,通常是要保存到数据库
// 作为演示,这里使用ViewState进行持久化
protected void btnSubmit_Click(object sender, EventArgs e)
{
    // 从ViewState中获取 Comment列表
    List<Comment> list = ViewState["List"] as List<Comment>;

    Comment cmt = new Comment();
    cmt.ArticleId = 16;
    cmt.CommentId = Convert.ToInt32(ddlCommentId.SelectedValue);
    cmt.Content = txtContent.Text;
    cmt.Id = 15 + list.Count;       // 设置当前评论的Id
    cmt.PostDate = DateTime.Now;
    cmt.UserName = txtUserName.Text;

    // 将新评论的id添加到DropDownList中
    ListItem item = new ListItem(cmt.Id.ToString());     
    ddlCommentId.Items.Insert(0, item);
    ddlCommentId.SelectedIndex = 0;
   
    list.Add(cmt);                      // 添加新评论。
    list.Sort(Comment.GetComparer(false));  // 倒序排列回帖
    // ViewState["List"] = list;    这里是没有必要的,因为ViewState和list引用的是用同一个对象
    rpComment.DataSource = list;
    rpComment.DataBind();
}

我们从ViewState中获取了列表,然后根据用户输入创建新评论,然后添加在了列表中,最后,我们让Repeater控件再次绑定评论列表list。

注意在上面的代码中,有的人会在向列表中添加了评论后,对ViewState再次赋值:

ViewState["List"] = list;

这是没有必要的,因为ViewState和list引用的是同一个对象,当你在list上调用Add方法添加项目的时候,ViewState也已经添加了。你可以在上面的方法中添加如下代码来验证:

Label lbResult = new Label();
lbResult.Text = Object.ReferenceEquals(list, ViewState["List"]).ToString();
Page.Controls.Add(lbResult); // 返回True,说明ViewState与list引用了同一个对象

现在我们打开页面,然后添加评论,就会看到类似下面的显示:

总结

本文中,我们综合使用了前面学习的知识,实现了类似网易的无限评论引用功能。

我们先了解了评论引用的传统实现方式,然后介绍了数据库表结构、创建了数据库表的映射类Comment。接着,我们根据静态页面创建了页面,并添加了样式。在后置代码中我们实现了主要的逻辑,包括列表排序、筛选,根据当前评论递归地查找所有引用的评论,以及如何产生输出。

最后,我们添加了发表评论的功能,对程序进行了测试。

感谢阅读,希望这篇文章能给你带来帮助!

posted @ 2008-03-24 23:08 张子阳. 阅读(2649) 评论(41)  编辑 收藏 所属分类: Asp.NetDesign & Pattern

  回复  引用  查看    
#1楼 2008-03-24 23:18 | wingoo      
很喜欢楼主的文章
很好很详细:)
  回复  引用  查看    
#2楼 [楼主]2008-03-24 23:21 | 张子阳.      
@wingoo

谢谢支持 ~~
  回复  引用  查看    
#3楼 2008-03-24 23:48 | 酷咖啡      
严重支持
  回复  引用  查看    
#4楼 2008-03-25 00:06 | 560889223      
这个……可以实现同一个评论同时引用两个不同的其他评论么?
  回复  引用  查看    
#5楼 [楼主]2008-03-25 00:20 | 张子阳.      
@560889223

呵呵,不能 =.= 这里是缺陷了,但并不是不能实现,把CommentId 从Comment表中抽象出来建一个连接表就可以了,让这个表包含两个字段,一个是评论Id,一个是评述所引用的评论的Id。用这两个字段做复合主键。然后让Comment表的Id去和这个表的评论Id关联。本来是一对一的自反关系就变成一对多的自反关系了。当然程序也需要做些许改动。

  回复  引用  查看    
#6楼 2008-03-25 01:17 | 曲滨*銘龘鶽      
哥们这种圈套圈的方式太丑陋了现在看来,好多网站都套好几100圈
有没有,什么替代风格或方式,可以改观一下效果?
  回复  引用  查看    
#7楼 [楼主]2008-03-25 01:21 | 张子阳.      
@曲滨*銘龘鶽

没有想到更好的,就姑且先圈套圈好了,套上100圈我觉得很搞笑,就好像盖楼房一样。

显示上只要修改HTML代码和CSS,算法上没有什么变动。

BTW:WY就很无聊,很适合闲聊。。。

  回复  引用  查看    
#8楼 2008-03-25 02:45 | 怪怪      
啥叫"网易工程队"? 新名词?
  回复  引用  查看    
#9楼 2008-03-25 07:28 | 金色海洋(jyk)      
其实可以用"后加载"的方式来处理,就像csdn的左面的树一样,鼠标点了一下在去加载,当然加载的时候不能刷新整个页面了。

表结构稍微修改一下

Id int identity(1,1) Not Null,
UserName Varchar(200) Not Null,
Content Varchar(2000) Not Null,
PostDate DateTime Not Null Default GetDate(),
CommentId varchar(1500) Not Null, -- 记录引用的记录的ID,比如:1,3,20,30,5,26
ArticleId Int Not Null


CommentId 这样这个字段就可以记录多个引用了,好像可以弥补你说的那个缺陷。

可能你的思路是一下子把所有的n层的引用一次都显示出来,引用多个评论不好做,改成“后加载”就好实现了。


  回复  引用  查看    
#10楼 2008-03-25 07:29 | 金色海洋(jyk)      
对了,没有仔细阅读,如果有不正确的地方请多原谅,谢谢。
  回复  引用  查看    
#11楼 2008-03-25 08:17 | 飞无痕落无声      
不错,写的挺好,向您学习~
  回复  引用  查看    
#12楼 2008-03-25 08:31 | Q.Lee.lulu      
--引用--------------------------------------------------
怪怪: 啥叫&quot;网易工程队&quot;? 新名词?
--------------------------------------------------------
同问!!

很详细的文章,支持一个!!

  回复  引用  查看    
#13楼 [楼主]2008-03-25 08:33 | 张子阳.      
@怪怪

就是在网易评论中盖楼的网友,你可以去网易看一看,选一个热门新闻,然后点评论进去 ···

  回复  引用  查看    
#14楼 [楼主]2008-03-25 08:38 | 张子阳.      
@金色海洋(jyk)

引用多个评论不难实现,数据库可以像你所说的那样改,另外将Comment类的CommentId字段改为List<int>类型,保存引用了的评论的Id。
但是让页面后加载(用户点击才展开评论)是很不错的完善,我没有实现,现在是进行一个列表搜寻,一次找出然后显示。以后无聊了可以考虑实现一下,网易已经实现了(也是最近才实现的),感兴趣你可以去它的评论看看,应该就是那个意思。

BTW:我一般不喜欢在数据库一个字段中存储多个值,我可能会采取这种方式:把CommentId 从Comment表中抽象出来建一个连接表就可以了,让这个表包含两个字段,一个是评论Id,一个是评述所引用的评论的Id。用这两个字段做复合主键。然后让Comment表的Id去和这个表的评论Id关联。本来是一对一的自反关系就变成一对多的自反关系了。
这样的优点是:Comment类的CommentId很好实现(已经改为List<int>类型),数据库结构严谨。缺点是需要多建一张表,连接查询降低效率。


  回复  引用  查看    
#15楼 2008-03-25 08:40 | Charly      
不错,收藏起来。
  回复  引用  查看    
#16楼 2008-03-25 09:04 | 戏水      
通俗易懂 内容详实 高屋建瓴 与时俱进,l---- 好!
  回复  引用  查看    
#17楼 2008-03-25 09:22 | 北京SEO网站优化 [未注册用户]
很详细
  回复  引用  查看    
#18楼 2008-03-25 09:45 | 看客 [未注册用户]
@金色海洋(jyk)
--引用--------------------------------------------------
金色海洋(jyk): 其实可以用&quot;后加载&quot;的方式来处理,就像csdn的左面的树一样,鼠标点了一下在去加载,当然加载的时候不能刷新整个页面了。

--------------------------------------------------------


csdn左边的树可不是后加载的.........是一次性加载的

  回复  引用  查看    
#19楼 2008-03-25 09:54 | 杨正祎(阿一)      
cnbeta好像就是这样做的。
  回复  引用  查看    
#20楼 [楼主]2008-03-25 09:57 | 张子阳.      
@戏水
@Charly

谢谢~!

@杨正祎(阿一)

呵呵,不知道,我只参考了网易的视觉效果。

  回复  引用  查看    
#21楼 2008-03-25 10:38 | 奡 [未注册用户]
会不会造成数据库负担?
对于每一条引用了其他回复的回复,都需要在数据库中查询引用了的回复。
  回复  引用  查看    
#22楼 2008-03-25 10:40 | future001 [未注册用户]
楼主是网易的?挺NB呀!
  回复  引用  查看    
#23楼 2008-03-25 10:45 | airwolf2026      
好东西,收藏咯.
  回复  引用  查看    
#24楼 [楼主]2008-03-25 11:09 | 张子阳.      
@奡

你明显没有看文章哈,为了减轻数据库负担,我只读了一次数据库,之后的操作都基于列表进行,对数据库的依赖降低到了最小。你属于我所认为的那种“把操作都交给数据库”的一类人。

推荐你看下这篇文章:
http://www.cnblogs.com/JimmyZhang/archive/2008/03/18/1110708.html

  回复  引用  查看    
#25楼 [楼主]2008-03-25 11:10 | 张子阳.      
@future001

我不是网易的哈,我只是经常浏览网易,我说我是“网易工程队”,意思是说我时常在网易新闻的评论里跟大伙儿一起“盖楼”。

  回复  引用  查看    
#26楼 2008-03-25 12:06 | yezizhe      
写的很好,而且跟前面的知识点也结合起来,又复习了一次啊。哈哈~
数据库设计真的有好大学问,正如这个例子,当一个评论是初始评论时,给CommentId 赋值为0的话处理起来比较方便,但是数据库严谨性就不是很好。
然后我也不喜欢在数据库一个字段中存储多个值,怪怪的。。。哈
  回复  引用  查看    
#27楼 2008-03-25 12:29 | 金色海洋(jyk)      
@看客 [未注册用户]
好久没上csdn了,旧版的树是点击之后才加载的,新版的吗,好象还是,只是效果好了很多。

因为我把网线拔掉后,再点击就展不开了。
  回复  引用  查看    
#28楼 2008-03-25 12:32 | tc.net [未注册用户]
没有考虑到性能问题 像网易 顶个几十楼 这样的存储就的查询数据库几十次,推荐做法以空间换时间 直接把引用的评论内容包括到本评论
  回复  引用  查看    
#29楼 [楼主]2008-03-25 12:37 | 张子阳.      
@tc.net

我晕,哥们你有没有看文章啊,这篇文章所采用的方法只读了一次数据库,可以看下这篇文章,更换一下思维:

http://www.cnblogs.com/JimmyZhang/archive/2008/03/18/1110708.html


  回复  引用  查看    
#30楼 2008-03-25 12:46 | WEBBER      
你的文章总是很实用...顶
  回复  引用  查看    
#31楼 2008-03-25 15:45 | 怪怪      
@张子阳.
果然是专有名词, 说不准将来进字典了。

我去看过, 很黄很暴力 :)
  回复  引用  查看    
#32楼 2008-03-25 17:59 | 汉广      
这种对服务器压力太大了吧,还是空间换时间比较好。网易的估计不是这样干的
:-)
  回复  引用  查看    
#33楼 2008-03-25 18:09 | 金色海洋(jyk)      
其实如果不用分页显示的话,那么引用的信息都在楼下呢,用js的方式在前台“copy”,是不是更节省服务器的资源呢?

  回复  引用  查看    
#34楼 [楼主]2008-03-25 20:42 | 张子阳.      
@汉广

嗯,网易的应该不是这样,它动辄就几千的回复,肯定不能这样。但是网易的评论发表了以后不能再进行编辑,从难度上降低了好多。空间换时间,将引用内容直接送进数据库,如果提供编辑的话,多个引用的情况下可能就比较麻烦。

即使是使用将引用内容存入数据库这种做法,在搜寻引用的评论列表的时候,文中的方法还是可以提供参考:-)


  回复  引用  查看    
#35楼 [楼主]2008-03-25 20:43 | 张子阳.      
@金色海洋(jyk)

的确是很省服务器资源,但是用js去实现估计很难很复杂。

  回复  引用  查看    
#36楼 2008-03-26 13:20 | 金色海洋(jyk)      
好象用 Div1.innerHTML = Div2.innerHTML 就可以实现。

Div2 里面是引用的回复所在的 div,

好象用 Div1.innerHTML = Div2.innerHTML 就可以实现。

Div2 里面是引用的回复所在的 div,一级的引用是没有问题的,多级的还没有测试。

 

<div id="div1"><div id="div_1"></div>评论1</div> <hr>
<div id="div2"><div id="div_2"></div>评论2</div> <hr>
<div id="div3"><div id="div_3"></div>评论3</div> <hr>
<div id="div4"><div id="div_4"></div>评论4</div><hr>

<input type="button" onclick="a()">
<script language="javascript">
function a(){
var div1 =  document.getElementById("div1");
var div2 =  document.getElementById("div2");
var div3 =  document.getElementById("div3");

var div_2=  document.getElementById("div_2");
var div_3=  document.getElementById("div_3");

div_2.innerHTML = "引用:" + div1 .innerHTML  ;
div_3.innerHTML =  "引用:" + div2 .innerHTML ;
div_4.innerHTML =  "引用:" + div3 .innerHTML ;


}
</script>

基本原理就是这样了。



  回复  引用  查看    
#37楼 [楼主]2008-03-28 23:23 | 张子阳.      
递归的效率是很低的,会进行频繁的方法调用,所以这篇文章的方法基本上只有实验价值,没有使用价值。

可以考虑在Comment表中建一个字段,QuoteContent,用来保存引用的内容,QuoteContent可以使用文中的方法来获得。

  回复  引用  查看    
#38楼 2008-04-09 13:22 | 飘风 [未注册用户]
继续关注
  回复  引用  查看    
#39楼 2008-04-20 01:07 | jillzhang      
@张子阳.
递归的效率是很低的,会进行频繁的方法调用,所以这篇文章的方法基本上只有实验价值,没有使用价值。

可以考虑在Comment表中建一个字段,QuoteContent,用来保存引用的内容,QuoteContent可以使用文中的方法来获得。
----------------------------------------------------------
第归是程序最常用的算法,很多经典算法都用到第归,有些也只是第归的改进,所以我觉得这篇文章的做法还是实用的。
  回复  引用  查看    
#40楼 2008-04-20 01:08 | jillzhang      
因为在此种应用中,你这种方法是最正点的,其他的,比如直接将引用内容存储到数据库或者ubb那种是不太规范
  回复  引用  查看    
#41楼 2008-04-29 17:13 | saii [未注册用户]
感觉楼主就是天才!写的太好了!

标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-04-01 23:36 编辑过