如何加载和显示分层结构化的评论
本教程将向您展示如何使用 RESTFul 服务和 JavaScript 加载和显示分层结构的评论。
引言
什么是分层评论?分层评论是一种以倒置树的形式显示评论的方式。也就是说,在顶层,有一个或多个节点。每个顶层节点将有零个或多个子节点。然后,每个子评论将有零个或多个孙子评论。孙子评论也是如此,每个评论将有零个或多个曾孙子评论,依此类推。
可视化此内容最简单的方法是想象 Windows 操作系统中的 TreeView
UI 控件。如果您从未接触过 Win32 编程或 C# WinForms,这里还有另一种可视化分层评论显示方式的方法——对话串。这是来自 wikipedia.org 的截图。
在 1996 年至 2000 年之间,这种显示论坛消息的方式最为流行。到 2002 年,大多数都转向了不同的显示方式(按时间顺序排列的评论列表,最新或最旧的评论在顶部)。如今,论坛消息的分层评论显示几乎已经绝迹。然而,这种类型的博客评论或新闻评论的显示仍然很普遍。您甚至可以在 Microsoft Outlook 中为电子邮件对话启用此类显示格式。我相信上面的截图是 Outlook 的早期版本。
这对我很重要吗?自从我发现第一个论坛以来,我就一直对复制这项技术感兴趣。起初,它似乎非常困难。然后我忘记了它(就像失去了兴趣一样)。最近,我有一些空闲时间,所以尝试复制这项技术。经过一番思考(没有去网上找答案),它竟然很简单。于是我进行了实现,并在本教程中讨论这项技术。
算法概述
分层评论的加载/显示没有秘诀。您只需要深度优先搜索 (DFS) 算法。如果您不知道这是什么,请查阅一下。此算法的难点在于使用递归。如果您不想使用递归,也可以使用堆栈来实现。我不想用 JavaScript 实现堆栈数据结构。所以我就用了递归。此时,您只需要了解算法是什么,以及要使用哪种编程技术:深度优先搜索和递归。更多关于实现的内容将在后面的部分进行解释。
架构概述
本文包含一个示例项目。这个示例项目是一个基于 Spring Boot 的 Web 应用程序。当用户的浏览器运行此应用程序时,它将通过 RestFUL Web 服务从后端获取两组评论,然后页面使用 JavaScript 以正确的层级结构渲染它们。这是截图。
有两组评论,每组都有一条根评论,然后在其下有多个子评论、孙子评论,以及可能的曾孙子评论。
该应用程序分为两部分,一部分是后端 Web 服务。该 Web 服务检索所有评论,然后将所有评论组织成正确的父子结构,然后再发送回前端进行渲染显示。这里分离的关注点是:
- 后端服务负责根据父子层级组织评论。
- 前端应用程序将假定评论已按正确的层级结构组织,并使用 DFS(深度优先搜索)在页面上渲染评论。
后端 Web 服务使用 Spring Boot 和 Spring REST 为前端提供一些模拟数据。将有一个服务对象将评论组织成正确的层级结构。然后,评论列表将作为 JSON 列表返回。
前端是一个使用 RequireJS、StapesJS 和 JQuery 编写的 JavaScript 应用程序。为了渲染页面,需要进行大量的 DOM 操作。JQuery 在这方面是最好的。StapesJS 和 RequireJS 只是为了更好地组织应用程序。我还添加了 axios 来与后端服务交互,获取数据,或者尝试处理错误。
评论的数据模型
我做的假设是,评论与博客文章、页面或任何可评论的对象相关联。这使我能够用一个简单的查询获取所有评论。在这个示例应用程序中,没有数据库查询。如果您想实现应用程序的其余部分,请注意这一点。
这是 Comment 数据模型对象的完整代码列表。
package org.hanbo.boot.rest.models;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
public class CommentModel
{
private String articleId;
private String commentId;
private String parentCommentId;
private List<CommentModel> childComments;
private String content;
private String commenterName;
private String commenterEmail;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date createDate;
public String getArticleId()
{
return articleId;
}
public void setArticleId(String articleId)
{
this.articleId = articleId;
}
public String getCommentId()
{
return commentId;
}
public void setCommentId(String commentId)
{
this.commentId = commentId;
}
public String getContent()
{
return content;
}
public void setContent(String content)
{
this.content = content;
}
public String getCommenterName()
{
return commenterName;
}
public void setCommenterName(String commenterName)
{
this.commenterName = commenterName;
}
public String getParentCommentId()
{
return parentCommentId;
}
public void setParentCommentId(String parentCommentId)
{
this.parentCommentId = parentCommentId;
}
public List<CommentModel> getChildComments()
{
return childComments;
}
public void setChildComments(List<CommentModel> childComments)
{
this.childComments = childComments;
}
public String getCommenterEmail()
{
return commenterEmail;
}
public void setCommenterEmail(String commenterEmail)
{
this.commenterEmail = commenterEmail;
}
public Date getCreateDate()
{
return createDate;
}
public void setCreateDate(Date createDate)
{
this.createDate = createDate;
}
public CommentModel()
{
this.childComments = new ArrayList<CommentModel>();
}
}
如代码所示,此类类型具有 articleId
,这使我能够一次性获取所有评论。接下来,每条评论都有 commentId
,此属性唯一标识评论本身。每个评论对象都有一个 parentCommentId
。这是父评论的引用。如果评论是根评论,则其 parentCommentId
将设置为 null
。此属性也是我按层级顺序重新组织评论的方式。最后,有 childComments
。此列表属性用于重新组织期间,收集当前评论的所有直接子评论。此类中的其他属性用于显示目的。
获取评论并重新组织
评论的获取和重新组织在一个服务对象中完成。我为此服务对象声明了一个接口。像这样:
package org.hanbo.boot.rest.services;
import java.util.List;
import org.hanbo.boot.rest.models.CommentModel;
public interface CommentsService
{
List<CommentModel> getArticleComments();
}
这是接口的完整实现。
package org.hanbo.boot.rest.services;
import java.util.Date;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.hanbo.boot.rest.models.CommentModel;
import org.springframework.stereotype.Service;
@Service
public class CommentsServiceImpl
implements CommentsService
{
@Override
public List<CommentModel> getArticleComments()
{
List<CommentModel> allCmnts = dummyComments();
List<CommentModel> reorganizedCmnts = reorganizeComments(allCmnts);
return reorganizedCmnts;
}
private List<CommentModel> reorganizeComments(List<CommentModel> allCmnts)
{
Map<String, CommentModel> mappedComments
= sortCommentsIntoMap(allCmnts);
addChildCommentsToParent(allCmnts, mappedComments);
List<CommentModel> retVal = topMostComments(allCmnts);
return retVal;
}
private List<CommentModel> topMostComments(List<CommentModel> allCmnts)
{
List<CommentModel> retVal = new ArrayList<CommentModel>();
if (allCmnts != null)
{
for (CommentModel cmnt : allCmnts)
{
if (cmnt != null)
{
if (cmnt.getParentCommentId() == null || cmnt.getParentCommentId().equals(""))
{
retVal.add(cmnt);
}
}
}
}
return retVal;
}
private void addChildCommentsToParent(List<CommentModel> allCmnts,
Map<String, CommentModel> mappedComments)
{
if (allCmnts != null && mappedComments != null)
{
for (CommentModel cmnt : allCmnts)
{
if (cmnt != null)
{
String parentCmntId = cmnt.getParentCommentId();
if (parentCmntId != null && !parentCmntId.equals("") &&
parentCmntId.length() > 0)
{
if (mappedComments.containsKey(parentCmntId))
{
CommentModel parentCmnt = mappedComments.get(parentCmntId);
if (parentCmnt != null)
{
parentCmnt.getChildComments().add(cmnt);
}
}
}
}
}
}
}
private Map<String, CommentModel> sortCommentsIntoMap(List<CommentModel> allCmnts)
{
Map<String, CommentModel> mappedComments = new HashMap<String, CommentModel>();
if (allCmnts != null)
{
for (CommentModel cmnt : allCmnts)
{
if (cmnt != null)
{
if (!mappedComments.containsKey(cmnt.getCommentId()))
{
mappedComments.put(cmnt.getCommentId(), cmnt);
}
}
}
}
return mappedComments;
}
private List<CommentModel> dummyComments()
{
List<CommentModel> retVal = new ArrayList<CommentModel>();
CommentModel modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser1@goomail.com");
modelAdd.setCommenterName("Test User1");
modelAdd.setCommentId("05e6fe83ab3f4f3bbf2e5ab75eda277b");
modelAdd.setContent("The first comment for this article");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId(null);
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser2@goomail.com");
modelAdd.setCommenterName("Test User2");
modelAdd.setCommentId("c2769bfd2d0a49a0920737d854e43c53");
modelAdd.setContent("The first child comment for this article");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("05e6fe83ab3f4f3bbf2e5ab75eda277b");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser2@goomail.com");
modelAdd.setCommenterName("Test User2");
modelAdd.setCommentId("b22b8a8cce0b4aa196d5e54e902be761");
modelAdd.setContent("The second child comment for this article");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("05e6fe83ab3f4f3bbf2e5ab75eda277b");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser3@goomail.com");
modelAdd.setCommenterName("Test User3");
modelAdd.setCommentId("4d457f242dd34cef89835067c45a7d3f");
modelAdd.setContent("The first grand child comment for this article");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("b22b8a8cce0b4aa196d5e54e902be761");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser3@goomail.com");
modelAdd.setCommenterName("Test User3");
modelAdd.setCommentId("f009e0879b0e4538b4f45788ca5e0adc");
modelAdd.setContent("The second grand child comment for this article");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("b22b8a8cce0b4aa196d5e54e902be761");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser3@goomail.com");
modelAdd.setCommenterName("Test User3");
modelAdd.setCommentId("3b51af94ad7944359967f7df6436a9b0");
modelAdd.setContent("The third grand child comment for this article");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("b22b8a8cce0b4aa196d5e54e902be761");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser3@goomail.com");
modelAdd.setCommenterName("Test User3");
modelAdd.setCommentId("2c6d0abd27a2404caeef29f3dc1049cd");
modelAdd.setContent("The fourth grand child comment for this article");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("c2769bfd2d0a49a0920737d854e43c53");
retVal.add(modelAdd);
//----------------
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser3@goomail.com");
modelAdd.setCommenterName("Test User3");
modelAdd.setCommentId("ef0d7c94fb6948f383835def70c09a79");
modelAdd.setContent("This is a second comment on the same article.");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId(null);
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser5@goomail.com");
modelAdd.setCommenterName("Test User5");
modelAdd.setCommentId("fcf5c1f63ec84f65bcac7fcd44fd0509");
modelAdd.setContent("Child comment #1 of the second comment of the same article.");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("ef0d7c94fb6948f383835def70c09a79");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser5@goomail.com");
modelAdd.setCommenterName("Test User5");
modelAdd.setCommentId("f0e383e91bb9456abf13d0dc1f7d1ba7");
modelAdd.setContent("Child comment #2 of the second comment of the same article.");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("ef0d7c94fb6948f383835def70c09a79");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser5@goomail.com");
modelAdd.setCommenterName("Test User5");
modelAdd.setCommentId("8d26b047dedf4d948cc87b72fb55bba4");
modelAdd.setContent("Child comment #3 of the second comment of the same article.");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("ef0d7c94fb6948f383835def70c09a79");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser6@goomail.com");
modelAdd.setCommenterName("Test User6");
modelAdd.setCommentId("f7e1c2cfbe00474ab5caafc8497641ea");
modelAdd.setContent("Grand child comment #1 of the second comment of the same article.");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("f0e383e91bb9456abf13d0dc1f7d1ba7");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser7@goomail.com");
modelAdd.setCommenterName("Test User7");
modelAdd.setCommentId("c5de23d8609f4d819d235dc6867f7917");
modelAdd.setContent("Grand child comment #2 of the second comment of the same article.");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("8d26b047dedf4d948cc87b72fb55bba4");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser7@goomail.com");
modelAdd.setCommenterName("Test User7");
modelAdd.setCommentId("1d8a871aebbe486595e3d9d3aecb8713");
modelAdd.setContent
("Grand child comment #3 of the second comment of the same article.");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("8d26b047dedf4d948cc87b72fb55bba4");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser8@goomail.com");
modelAdd.setCommenterName("Test User8");
modelAdd.setCommentId("f700db39af674f939165a4f6799668ec");
modelAdd.setContent("Great Grand child comment #1
of the second comment of the same article.");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("c5de23d8609f4d819d235dc6867f7917");
retVal.add(modelAdd);
modelAdd = new CommentModel();
modelAdd.setArticleId("525d346cbd784180bd09bc8a52be3e1a");
modelAdd.setCommenterEmail("testuser8@goomail.com");
modelAdd.setCommenterName("Test User8");
modelAdd.setCommentId("05eddfc462834adf84a2e4a4b7c81b06");
modelAdd.setContent("Great Grand child comment #2 of the
second comment of the same article.");
modelAdd.setCreateDate(new Date());
modelAdd.setParentCommentId("1d8a871aebbe486595e3d9d3aecb8713");
retVal.add(modelAdd);
return retVal;
}
}
此服务对象执行几项操作。
- 实现类中的最后一个方法
dummyComments()
准备所有模拟数据。这些是未组织好的评论,都与同一篇博客文章或页面相关。 - 倒数第二个方法是
sortCommentsIntoMap()
。此方法的作用是将所有评论放入一个HashMap
对象,键是评论 ID,值是评论本身。 - 从底部算起的第三个方法是
addChildCommentsToParent()
。此方法将对评论进行排序,并将子评论添加到父评论中。这是通过使用原始列表并借助HashMap
集合来完成的。两个集合具有相同的对象引用,因此可以通过反向查找将子评论添加到父评论。 - 从底部算起的第四个方法是
topMostComments()
。它找出所有的根评论。这些是具有子评论但没有父评论引用的评论。调用此方法时,评论的重新组织已经完成。 - 下一个方法(紧接上一个)是
reorganizeComments()
。此方法调用所有其他方法来根据父子关系重新组织所有评论,然后找出根评论,放入列表中并返回。 - 顶层方法是 REST 控制器将调用的
public
方法。它首先调用dummyComments()
方法返回未组织评论的列表。然后调用reorganizeComments
方法来组织评论,使其成为正确的层级结构。最后,返回根评论列表。
此实现中最重要的方法是 sortCommentsIntoMap()
和 addChildCommentsToParent()
。sortCommentsIntoMap()
将遍历未组织的评论并将它们放入 HashMap
中。通过这个 HashMap
,更容易将子评论链接到父评论。这就是 addChildCommentsToParent()
方法的作用。重新组织完成后,我需要做的就是找到根评论并将它们作为列表返回,这正是 topMostComments()
方法所做的。
用于返回评论的 RESTFUL API 控制器定义在名为 CommentsController
的类中,该类位于 src/main/java/org/hanbo/boot/rest/controllers 子文件夹中。您可以自行查看其功能。
这些就是后端服务所需的所有内容。接下来是前端渲染。
分层评论渲染
后端 Web 服务可以将评论组织成层级结构。接下来,前端将检索根评论并在正确的层级顺序中显示所有评论。本节将解释如何做到这一点。
在正确的位置渲染评论是其中最困难的部分。在此解决方案之前,我从未有时间考虑过解决方案。现在我考虑过了,最明显的解决方案是使用深度优先搜索 (DFS) 算法。
DFS 的目的是树遍历,访问树的每个节点。这个想法很简单,在任何给定节点,首先访问它的所有子节点。在每个子节点上,该节点成为当前节点,重复相同的过程,先访问子节点。通过这种递归,它将遍历所有子节点、孙子节点、曾孙子节点,依此类推。一旦没有更多要访问的子节点,就将当前节点标记为已访问。使用此算法的棘手之处在于何时渲染消息以及何时渲染子评论。结果证明这相当于访问子评论与将当前节点标记为已访问。我选择在访问完所有子节点后渲染当前评论的消息。我想只要子评论被收集为 DOM 元素并附加为当前评论的子节点,就不会有什么问题。
在深入 DFS 的实现之前,我将首先展示 JavaScript 中从后端服务获取评论的服务对象。
define (["axios"], function (axios) {
var svc = {
loadAllComments: function(articleId) {
reqUrl = "/allComments";
return axios.get(reqUrl);
}
};
return svc;
});
正如我之前提到的,我为整个应用程序使用了 RequireJS。define()
函数创建了一个可重用的组件,可以将其注入到其他组件中。此组件执行一项操作,向硬编码的 URL 发送 GET
请求。然后将 Promise 返回给调用者。调用者将处理响应。这定义在名为 commentService.js 的文件中。
接下来,我们将进入前端应用程序的核心,即对重组后的评论集合进行实际渲染。这是整个源代码,都在名为 forum.js 的文件中。
define(["jquery", "bootstrap", "stapes", "underscore", "commentsService"],
function($, boot, Stapes, _, commentsService) {
var articleComments = Stapes.subclass({
$el: null,
cmntsArea: null,
constructor : function() {
var self = this;
self.$el = $("#articleComments");
self.cmntsArea = self.$el.find("#allCmnts");
},
allComments : function() {
commentsService.loadAllComments().then(function(results) {
if (results && results.status === 200) {
if (results.data && results.data.length > 0) {
console.log(results.data);
var allCmnts = renderComments(results.data);
if (allCmnts != null) {
for (var i = 0; i < allCmnts.length; i++)
self.cmntsArea.append(allCmnts[i][0]);
}
}
} else {
console.log("error occurred.");
}
}).catch(function(error) {
if (error) {
console.log(error);
}
console.log("HTTP error occurred.");
}).finally(function() {
});
}
});
function renderComments(comments) {
var allCmnts = [];
if (comments != null && comments.length > 0) {
for (var i = 0; i < comments.length; i++) {
var cmnt = renderComment(comments[i]);
if (cmnt != null) {
allCmnts.push(cmnt);
}
}
}
return allCmnts;
}
function renderComment(comment) {
var retVal = $("<div></div>");
if (comment != null) {
var childComments = null;
if (comment.childComments != null &&
comment.childComments.length > 0) {
childComments = renderChildComments(comment.childComments);
}
if (childComments == null) {
childComments = [];
}
var content = renderCommentContent(comment);
var cmntFrame= $("<div class='row'></div>").append(
$("<div class='col-xs-12'></div>").append(
$("<div class='thumbnail'></div>").append(content).append(childComments)
)
);
retVal = cmntFrame;
}
return retVal;
}
function renderChildComments(childComments) {
var childCmnts = [];
if (childComments != null && childComments.length > 0) {
for (var i = 0; i < childComments.length; i++) {
var cmnt = renderComment(childComments[i]);
if (cmnt != null) {
childCmnts.push(cmnt[0]);
}
}
}
return childCmnts;
}
function renderCommentContent(comment) {
var retVal = $("<div style='margin: 12px 10px;'></div>");
if (comment != null) {
var cmntContentTmpl = "<div class='row'>" +
"<div class='col-xs-12'>" +
"<p><%= postContent %></p>" +
"</div>" +
"<div class='col-xs-6'>" +
"<p><strong>Posted by <i class='glyphicon glyphicon-user'></i>
<%= commenterName %></strong></p>" +
"</div>" +
"<div class='col-xs-6 text-right'>" +
"<p><strong>Posted on <i class='glyphicon glyphicon-calendar'></i>
<%= postDate %></strong>" +
"</div>" +
"</div>";
var cmntContentTmpl = _.template(cmntContentTmpl);
var contentToAdd = cmntContentTmpl({
commenterName: comment.commenterName,
postDate: comment.createDate,
postContent: comment.content
});
retVal.append($(contentToAdd));
}
return retVal;
}
return {
run: function () {
console.log("forum run.");
var cmnts = new articleComments();
cmnts.allComments();
}
};
});
这是整个项目中最大的代码文件。在此文件中,我使用 StapesJS、underscore JS 和 JQuery 定义了一个组件。它将在页面上找到一个区域,并添加显示分层结构化评论的 DOM。此组件的主要方法是 allComments()
。它加载后端的所有评论,然后运行渲染。这是:
allComments : function() {
commentsService.loadAllComments().then(function(results) {
if (results && results.status === 200) {
if (results.data && results.data.length > 0) {
console.log(results.data);
var allCmnts = renderComments(results.data);
if (allCmnts != null) {
for (var i = 0; i < allCmnts.length; i++)
self.cmntsArea.append(allCmnts[i][0]);
}
}
} else {
console.log("error occurred.");
}
}).catch(function(error) {
if (error) {
console.log(error);
}
console.log("HTTP error occurred.");
}).finally(function() {
});
}
在此方法 allComments()
中,从后端服务返回评论列表后,它会调用内部方法 renderComments()
。此方法接受评论列表并进行渲染。其中,渲染被委托给另一个实现了 DFS 的方法。
function renderComments(comments) {
var allCmnts = [];
if (comments != null && comments.length > 0) {
for (var i = 0; i < comments.length; i++) {
var cmnt = renderComment(comments[i]);
if (cmnt != null) {
allCmnts.push(cmnt);
}
}
}
return allCmnts;
}
基于 DFS 的渲染通过两个方法实现,第一个是 renderComment()
。它接受一条评论,并尝试渲染它本身及其所有后代。此方法如下:
function renderComment(comment) {
var retVal = $("<div></div>");
if (comment != null) {
var childComments = null;
if (comment.childComments != null &&
comment.childComments.length > 0) {
childComments = renderChildComments(comment.childComments);
}
if (childComments == null) {
childComments = [];
}
var content = renderCommentContent(comment);
var cmntFrame= $("<div class='row'></div>").append(
$("<div class='col-xs-12'></div>").append(
$("<div class='thumbnail'></div>").append(content).append(childComments)
)
);
retVal = cmntFrame;
}
return retVal;
}
这是实现 DFS 搜索和递归的地方。如果评论没有任何子评论,我就会创建一个空数组并将空数组作为子 DOM 追加到当前评论的 DOM 元素中。如果有任何子评论,则该方法会调用另一个方法 renderChildComments()
。renderChildComments()
方法会将子评论渲染为 DOM 元素列表。渲染完子评论后,它会调用 renderCommentContent()
来渲染评论内容。最后,当子评论列表和当前评论内容都可用时,它们将作为父评论和子评论连接在一起。然后将返回最终结果。
这是 renderChildComments()
方法的代码。
function renderChildComments(childComments) {
var childCmnts = [];
if (childComments != null && childComments.length > 0) {
for (var i = 0; i < childComments.length; i++) {
var cmnt = renderComment(childComments[i]);
if (cmnt != null) {
childCmnts.push(cmnt[0]);
}
}
}
return childCmnts;
}
上面列出的代码很简单,对于每个子评论,只需递归调用 renderComment()
方法即可。返回的结果将是渲染的 HTML DOM 节点。我将所有 DOM 节点收集到一个数组中并返回数组。请注意这一点:
if (cmnt != null) {
childCmnts.push(cmnt[0]);
}
我之所以必须这样做,是因为我使用 JQuery 构建 DOM 对象。输出将是一个数组。如果我不取出数组中的元素,而是将 JQuery 对象添加回返回的数组,那么我将返回一个数组的数组。这将无法正确显示。这就是为什么我必须使用数组索引并获取 HTML 节点放入要返回的数组中。
让我在这里快速总结一下,renderComment()
方法和 renderChildComments()
方法构成了一个完成 DFS 算法的递归。如果您仍然不理解,请运行应用程序,然后逐步调试此递归。随着时间的推移,它就会清晰起来。
关于这个组件,我最后想讲的是评论内容的渲染。UnderscoreJS
是一个非常棒的实用库。它提供了各种各样的方法,可以让生活变得非常轻松。其中一种方法是 _.template()
。它可以将常规字符串转换为字符串模板。模板本身是一个方法,它接受一个对象,并使用该对象的属性替换字符串模板中的占位符来创建最终字符串。您可以在名为 renderCommentContent()
的方法中看到这一点。
在此方法 renderCommentContent()
中,字符串模板的定义方式如下:
var cmntContentTmpl = "<div class='row'>" +
"<div class='col-xs-12'>" +
"<p><%= postContent %></p>" +
"</div>" +
"<div class='col-xs-6'>" +
"<p><strong>Posted by <i class='glyphicon glyphicon-user'></i>
<%= commenterName %></strong></p>" +
"</div>" +
"<div class='col-xs-6 text-right'>" +
"<p><strong>Posted on <i class='glyphicon glyphicon-calendar'></i>
<%= postDate %></strong>" +
"</div>" +
"</div>";
这是创建实际模板的方式,然后用对象属性的值替换占位符。
var cmntContentTmpl = _.template(cmntContentTmpl);
var contentToAdd = cmntContentTmpl({
commenterName: comment.commenterName,
postDate: comment.createDate,
postContent: comment.content
});
这就是关于评论渲染的所有内容。在下一节中,我想介绍配置 RequireJS 的技术,以便 BootStrap 和 JQuery 可以在 HTML 页面中只加载一次。
使用 RequireJS 进行应用程序配置
当我第一次使用 RequireJS 时,我不知道如何将其与 JQuery 和 BootStrap 的 JS 文件进行配置。在我制作的第一个教程中,这两个文件出现在同一个 HTML 文件中的不同位置。有一种更好的配置方法,可以使这两个 JS 文件在 HTML 文件中只出现一次。
主应用程序的源代码可以在 app.js 文件中找到,它看起来像这样:
requirejs.config({
paths: {
jquery: "/assets/jquery/js/jquery.min",
bootstrap: "/assets/bootstrap/js/bootstrap.min",
stapes: "/assets/stapes/stapes-min-1.0.0",
axios: "/assets/axios/axios.min",
underscore: "/assets/underscore/underscore-min-1.9.2",
commentsService: "/assets/app/commentsService",
forum: "/assets/app/forum"
},
shim: {
bootstrap: {
deps: ["jquery"]
}
}
});
require([ "forum" ], function (forum) {
forum.run();
});
使用 RequireJS,我需要使用所谓的 shim 配置。我需要它的原因是 bootstrap 的 JavaScript 文件不是用 RequireJS 支持编写的。因此,为了解决这个问题,需要 shim 配置。配置基本上意味着它就像这样简单:
shim: {
bootstrap: {
deps: ["jquery"]
}
}
还没完。在 HTML 页面上,我仍然必须设置要加载的 JavaScript 文件。请看一下 index.html 文件。JavaScript 文件包含如下:
<html>
...
<body>
<script type="text/javascript" src="/assets/requirejs/require.js"></script>
<script type="text/javascript" src="/assets/app/app.js"></script>
</body>
</html>
如所示,只有两个 JavaScript 文件被添加到 index.html 文件中。当它在浏览器(如 Chrome)中加载时,页面将注入所有其他 JavaScript 文件。这是截图,渲染完成后,您只能通过“检查”页面来看到这些注入的文件。
这是页面中两个 JavaScript 文件的截图。
如何测试此应用程序
下载源代码后,请转到 src/main/resources/static/assets 文件夹,并将 *.sj 文件重命名为 *.js。
这是一个基于 Spring Boot 的应用程序,要编译整个项目,请在可以找到 POM.xml 的基本文件夹中运行以下命令:
mvn clean install
等待构建完成,它将成功。然后使用以下命令运行应用程序:
java -jar target/hanbo-forumtest-1.0.1.jar
等待应用程序启动。然后打开 Chrome 或 Firefox,导航到以下 URL:
https://:8080
页面加载完成后,它将以分层顺序显示所有评论,就像本教程中的第二个截图所示,如下所示:
如果您能看到这个,那么示例应用程序就构建成功了。
摘要
本教程解释了如何加载分层评论并在网页上正确渲染它们。需要解决两个技术问题。一是如何加载评论,然后将它们组织成层级结构。这在后端 Web 服务中完成。如教程所示,我使用了一个 HashMap
和多个循环迭代来将子评论添加到父评论中。
更大的问题是如何在前端应用程序获取评论后进行渲染。在本教程中,我描述了使用深度优先搜索来遍历评论树并以正确的层级结构渲染所有评论。实现使用了递归来完成评论遍历。此外,本教程还展示了如何在运行时构建 HTML DOM 节点。最后,我展示了如何使用 RequireJS shim 配置加载 JavaScript 文件。
我知道本教程使用了一些非主流的 JavaScript 库。它们用于实现解决方案。只要您了解其工作原理,就可以使用其他库轻松实现相同的解决方案。
历史
- 2020年7月16日 - 初稿