Spring Boot- 个人博客 - 博客详情页


在这里插入图片描述
__

1. 需求分析

完整的博客详情页如上述所示,页面中包含的部分主要分为三大块:

  • 博客详情:详情信息中包含如下的内容:

    • 作者
    • 更新时间
    • 浏览次数
    • 标签
    • 正文
    • 赞赏
    • 转载声明
  • 评论:评论部分首先设定为两级的层次结构,原始评论为父级,它下面相应的评论都归为子级评论。此外,评论还需要区分是博主还是访客发表,如果是博主需要显示博主标签和头像;如果是访客,显示的是同一的访客头像

  • 侧边栏:侧边栏包含三部分:

    • 目录:根据正文的Markdown语法的标题级别自动生成目录
    • 留言:跳转按钮,直接跳转到评论区
    • 二维码生成:根据博客生成二维码,访客可扫描二维码直接在移动端查看博客内容

    侧边栏部分的实现主要是插件的应用,不需要后端进行处理


2. 前端处理

博客详情页的前段时间设计如下,根据需求分析可知,主要分为三大部分:

<div id="waypoint" class="m-container-small m-padded-tb-big animated fadeIn">
    <div class="ui container">
        <!--博客正文的头部,显示作者、更新时间、浏览次数-->
        <div class="ui top attached segment">
            <div class="ui horizontal link list">
                <div class="item">
                    <img src="https://unsplash.it/100/100?image=1005" th:src="@{${blog.user.avatar}}"  alt="" class="ui avatar image">
                    <div class="content"><a href="#" class="header" th:text="${blog.user.nickname}">Forlogen</a></div>
                </div>
                <div class="item">
                    <i class="calendar icon"></i> <span th:text="${#dates.format(blog.updateTime,'yyyy-MM-dd')}">2020-06-11</span>
                </div>
                <div class="item">
                    <i class="eye icon"></i> <span th:text="${blog.views}">2</span>
                </div>
            </div>
        </div>
        <!--博客正文部分-->
        <div class="ui attached segment">
            <!--图片区域-->
            <img src="https://unsplash.it/800/450?image=1005" th:src="@{${blog.firstPicture}}" alt="" class="ui fluid rounded image">
        </div>
        <div class="ui  attached padded segment">
            <!--标题和flag-->
            <div class="ui right aligned basic segment">
                <div class="ui orange basic label" th:text="${blog.flag}">原创</div>
            </div>

            <h2 class="ui center aligned header" th:text="${blog.title}">关于刻意练习的清单</h2>
            <br>
            <!--正文部分-->
            <div id="content" class="typo  typo-selection js-toc-content m-padded-lr-responsive m-padded-tb-large" th:utext="${blog.content}">
            </div>

            <!--标签-->
            <div class="m-padded-lr-responsive">
                <div class="ui basic teal left pointing label" th:each="tag : ${blog.tags}" th:text="${tag.name}">方法论</div>
            </div>

            <!--赞赏-->
            <div th:if="${blog.appreciation}">
                <div class="ui center aligned basic segment">
                    <button id="payButton" class="ui orange basic circular button">赞赏</button>
                </div>
                <div class="ui payQR flowing popup transition hidden">
                    <div class="ui orange basic label">
                        <div class="ui images" style="font-size: inherit !important;">
                            <div class="image">
                                <img src="../static/images/wechat.jpg" th:src="@{/images/wechat.jpg}" alt="" class="ui rounded bordered image" style="width: 120px">
                                <div>支付宝</div>
                            </div>
                            <div class="image">
                                <img src="../static/images/wechat.jpg" th:src="@{/images/wechat.jpg}" alt="" class="ui rounded bordered image" style="width: 120px">
                                <div>微信</div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="ui attached positive message" th:if="${blog.shareStatement}">
            <!--转载声明-->
            <div class="ui middle aligned grid">
                <div class="eleven wide column">
                    <ui class="list">
                        <li>作者:<span th:text="${blog.user.nickname}">Forlogen</span><a href="#" th:href="@{/about}" target="_blank">(联系作者)</a></li>
                        <li>发表时间:<span th:text="${#dates.format(blog.updateTime,'yyyy-MM-dd HH:mm')}">2020-06-30 09:08</span></li>
                        <li>版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)</li>
                    </ui>
                </div>
                <div class="five wide column">
                    <img src="../static/images/wechat.jpg" th:src="@{/images/wechat.jpg}" alt="" class="ui right floated rounded bordered image" style="width: 110px">
                </div>
            </div>
        </div>
        <!--留言区域列表-->
        <div  class="ui bottom attached segment" th:if="${blog.commentable}">
            <div id="comment-container"  class="ui teal segment">
                <div th:fragment="commentList">
                    <div class="ui threaded comments" style="max-width: 100%;">
                        <h3 class="ui dividing header">评论</h3>
                        <div class="comment" th:each="comment : ${comments}">
                            <a class="avatar">
                                <img src="https://unsplash.it/100/100?image=1005" th:src="@{${comment.avatar}}">
                            </a>
                            <div class="content">
                                <a class="author" >
                                    <span th:text="${comment.nickname}">Matt</span>
                                    <div class="ui mini basic teal left pointing label m-padded-mini" th:if="${comment.adminComment}">博主</div>
                                </a>
                                <div class="metadata">
                                    <span class="date" th:text="${#dates.format(comment.createTime,'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span>
                                </div>
                                <div class="text" th:text="${comment.content}">
                                    How artistic!
                                </div>
                                <div class="actions">
                                    <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${comment.id},data-commentnickname=${comment.nickname}" onclick="reply(this)">回复</a>
                                </div>
                            </div>
                            <div class="comments" th:if="${#arrays.length(comment.replyComments)}>0">
                                <div class="comment" th:each="reply : ${comment.replyComments}">
                                    <a class="avatar">
                                        <img src="https://unsplash.it/100/100?image=1005" th:src="@{${comment.avatar}}">
                                    </a>
                                    <div class="content">
                                        <a class="author" >
                                            <span th:text="${reply.nickname}">小红</span>
                                            <div class="ui mini basic teal left pointing label m-padded-mini" th:if="${reply.adminComment}">博主</div>
                                            &nbsp;<span th:text="|@ ${reply.parentComment.nickname}|" class="m-teal">@ 小白</span>
                                        </a>
                                        <div class="metadata">
                                            <span class="date" th:text="${#dates.format(reply.createTime,'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span>
                                        </div>
                                        <div class="text" th:text="${reply.content}">
                                            How artistic!
                                        </div>
                                        <div class="actions">
                                            <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${reply.id},data-commentnickname=${reply.nickname}" onclick="reply(this)">回复</a>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <!--评论输入区-->
            <div id="comment-form" class="ui form">
                <input type="hidden" name="blog.id" th:value="${blog.id}">
                <input type="hidden" name="parentComment.id" value="-1">
                <div class="field">
                    <textarea name="content" placeholder="请输入评论信息..."></textarea>
                </div>
                <div class="fields">
                    <div class="field m-mobile-wide m-margin-bottom-small">
                        <div class="ui left icon input">
                            <i class="user icon"></i>
                            <input type="text" name="nickname" placeholder="姓名" th:value="${session.user}!=null ? ${session.user.nickname}">
                        </div>
                    </div>
                    <div class="field m-mobile-wide m-margin-bottom-small">
                        <div class="ui left icon input">
                            <i class="mail icon"></i>
                            <input type="text" name="email" placeholder="邮箱" th:value="${session.user}!=null ? ${session.user.email}">
                        </div>
                    </div>
                    <div class="field  m-margin-bottom-small m-mobile-wide">
                        <button id="commentpost-btn" type="button" class="ui teal button m-mobile-wide"><i class="edit icon"></i>发布</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!--侧边栏-->
<div id="toolbar" class="m-padded m-fixed m-right-bottom" style="display: none">
    <div class="ui vertical icon buttons ">
        <button type="button" class="ui toc teal button" >目录</button>
        <a href="#comment-container" class="ui teal button" >留言</a>
        <button class="ui wechat icon button"><i class="weixin icon"></i></button>
        <div id="toTop-button" class="ui icon button" ><i class="chevron up icon"></i></div>
    </div>
</div>
<div class="ui toc-container flowing popup transition hidden" style="width: 250px!important;">
    <ol class="js-toc">
    </ol>
</div>
<div id="qrcode" class="ui wechat-qr flowing popup transition hidden "style="width: 130px !important;">
    <!--<img src="./static/images/wechat.jpg" alt="" class="ui rounded image" style="width: 120px !important;">-->
</div>

其中赞赏、转载声明和评论是否启用,取决于博客发布时的选择,具体体现在Blog对象的相应字段的值。如果在博客发布时启用,那么在详情页就像现实对应的部分;如果不启用,那么就不会显示。

从前面页面的设计来看,后端需向前端传递的数据主要是两个部分:

  • 博客对象Blog,前端需从中拿到各种字段的值使用
  • 评论对象Comment,前端拿到评论进行显示

此外,如果此时博主登录管理后台成功,还需要从session中获取到博主相关的信息。


3. 后端处理

3.1 博客详情

首先我们来看对于博客内容部分的处理,表现层相应的处理方法为:

@Controller
public class IndexController {

    @Autowired
    private BlogService blogService;

    @GetMapping("/blog/{id}")
    public String blog(@PathVariable Long id, Model model) {
        model.addAttribute("blog", blogService.getAndConvert(id));
        return "blog";
    }
}

后端根据博客的id来查询相应的Blog对象,调用的是getAndConvert(),业务层的实现为:

public interface BlogService {

    // 博客内容格式转换  MakeDown -> HTML
    Blog getAndConvert(Long id);
}

由于数据库保存的博客内容为Markdown形式,如果想要在浏览器中正确的显示,就需要先将其转换为对应的HTML格式。接口实现类中方法的实现为:

@Service
public class BlogServiceImpl implements BlogService {

    @Autowired
    private BlogRepository blogRepository;
    
  
    @Override
    public Blog getAndConvert(Long id) {
        // 首先根据id查询对应的博客是否存在
        Blog blog = blogRepository.getOne(id);
        if (blog == null) {
            throw new NotFoundException("该博客不存在");
        }
        // 为了不改变博客数据库中保存的格式,首先获取博客的副本
        Blog b = new Blog();
        BeanUtils.copyProperties(blog,b);
        // 获取博客正文
        String content = b.getContent();
        // 调用工具类中的方法实现格式转换
        b.setContent(MakeDownUtils.markdownToHtmlExtensions(content));
		// 更新浏览次数,打开一次算一次浏览
        blogRepository.updateViews(id);
        return b;
    }

updateViews方法需要在持久层中自行定义,对应的实现为:

public interface BlogRepository extends JpaRepository<Blog, Long>, JpaSpecificationExecutor<Blog> {

    // 更新阅读次数,每次点击算一次
    @Transactional
    @Modifying
    @Query("update Blog b set b.views = b.views+1 where b.id = ?1")
    int updateViews(Long id);
}

另外,工具类的实现如下:

public class MakeDownUtils {
    /**
     * markdown格式转换成HTML格式
     * @param markdown
     * @return
     */
    public static String markdownToHtml(String markdown) {
        Parser parser = Parser.builder().build();
        Node document = parser.parse(markdown);
        HtmlRenderer renderer = HtmlRenderer.builder().build();
        return renderer.render(document);
    }

    /**
     * 增加扩展[标题锚点,表格生成]
     * Markdown转换成HTML
     * @param markdown
     * @return
     */
    public static String markdownToHtmlExtensions(String markdown) {
        //h标题生成id
        Set<Extension> headingAnchorExtensions = Collections.singleton(HeadingAnchorExtension.create());
        //转换table的HTML
        List<Extension> tableExtension = Arrays.asList(TablesExtension.create());
        Parser parser = Parser.builder()
                .extensions(tableExtension)
                .build();
        Node document = parser.parse(markdown);
        HtmlRenderer renderer = HtmlRenderer.builder()
                .extensions(headingAnchorExtensions)
                .extensions(tableExtension)
                .attributeProviderFactory(new AttributeProviderFactory() {
                    public AttributeProvider create(AttributeProviderContext context) {
                        return new CustomAttributeProvider();
                    }
                })
                .build();
        return renderer.render(document);
    }

    /**
     * 处理标签的属性
     */
    static class CustomAttributeProvider implements AttributeProvider {
        @Override
        public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
            //改变a标签的target属性为_blank
            if (node instanceof Link) {
                attributes.put("target", "_blank");
            }
            if (node instanceof TableBlock) {
                attributes.put("class", "ui celled table");
            }
        }
    }
}

这样经过后端处理就可以向前端传递处理后的结果,即给定id对应的Blog对象。注意,此时的Blog对象的content字段的内容已经是HTML格式。

3.2 评论

此外,后端还需要根据博客id字段的属性值来找到对应的评论信息,表现层对应的处理方法为:

@Controller
public class CommentController {

    @Autowired
    private CommentService commentService;

    @Autowired
    private BlogService blogService;

    @Value("${comment.avatar}")
    private String avatar;

    @GetMapping("/comments/{blogId}")
    public String comments(@PathVariable Long blogId, Model model) {
        model.addAttribute("comments", commentService.listCommentByBlogId(blogId));
        return "blog :: commentList";
    }
}

业务层中listCommentByBlogId方法的实现为:

public interface CommentService {

    List<Comment> listCommentByBlogId(Long blogId);
}
@Service
public class CommentServiceImpl implements CommentService {

    @Autowired
    private CommentRepository commentRepository;

    @Override
    public List<Comment> listCommentByBlogId(Long blogId) {
        Sort sort = Sort.by("createTime");
        // 首先根据博客id和指定的排序方式从数据库中找到对应的评论
        List<Comment> comments = commentRepository.findByBlogIdAndParentCommentNull(blogId, sort);
        // 最后分级显示
        return eachComment(comments);
    }
}

持久层的方法实现为:

public interface CommentRepository extends JpaRepository<Comment,Long> {

    List<Comment> findByBlogIdAndParentCommentNull(Long blogId, Sort sort);
}

查询需要依赖两个字段的信息:Comment对象的parentComment的内容和Blog对象的id。其中parentComment和Comment中的replyComments存在多对一关系,从而后续可以实现分级显示。

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "t_comment")
public class Comment {

    ...
        
    @OneToMany(mappedBy = "parentComment")
    private List<Comment> replyComments = new ArrayList<>();

    @ManyToOne
    private Comment parentComment;
}

经过上述的过程获得到评论列表后,需要进一步的处理便于分级显示,对应的方法为:

//存放迭代找出的所有子代的集合
private List<Comment> tempReplys = new ArrayList<>();

private List<Comment> eachComment(List<Comment> comments) {
    List<Comment> commentsView = new ArrayList<>();
    for (Comment comment : comments) {
        Comment c = new Comment();
        BeanUtils.copyProperties(comment,c);
        commentsView.add(c);
    }
    //合并评论的各层子代到第一级子代集合中
    combineChildren(commentsView);
    return commentsView;
}

private void combineChildren(List<Comment> comments) {

    for (Comment comment : comments) {
        List<Comment> replys1 = comment.getReplyComments();
        for(Comment reply1 : replys1) {
            //循环迭代,找出子代,存放在tempReplys中
            recursively(reply1);
        }
        //修改顶级节点的reply集合为迭代处理后的集合
        comment.setReplyComments(tempReplys);
        //清除临时存放区
        tempReplys = new ArrayList<>();
    }
}

private void recursively(Comment comment) {
        tempReplys.add(comment);//顶节点添加到临时存放集合
        if (comment.getReplyComments().size()>0) {
            List<Comment> replys = comment.getReplyComments();
            for (Comment reply : replys) {
                tempReplys.add(reply);
                if (reply.getReplyComments().size()>0) {
                    recursively(reply);
                }
            }
        }
    }

经过处理后,将结果传递给前端进行渲染即可。

最后需要进行处理的就是评论信息的提交部分,评论信息包含评论内容、姓名和邮箱三部分。前端信息传递到后端需要JavaScript进行相应的处理,此外还有表单的验证等,这里不重点关注前端的逻辑。当后端获取到评论信息后,就需要将其保存到数据库中,表现层对应的处理方法为:

@Controller
public class CommentController {

    @Autowired
    private CommentService commentService;

    @Autowired
    private BlogService blogService;

    // 此时为POST请求,评论信息包装为Comment对象
    // 评论人信息根据session中保存的user获取
    @PostMapping("/comments")
    public String post(Comment comment, HttpSession session) {
        // 首先获取到对应博客的id
        Long blogId = comment.getBlog().getId();
        // 根据博客id获取到对应的博客,设置Comment对象相应字段的值
        comment.setBlog(blogService.getBlog(blogId));
       	// 判断评论的是博主还是访客
        User user = (User) session.getAttribute("user");
        if (user != null) {
 			// 如果是博主,除了设置头像外,还需要设置adminComment字段为true
            comment.setAvatar(user.getAvatar());
            comment.setAdminComment(true);
        } else {
            // 否则只需要选择访客头像
            // adminComment默认为false,表示为访客评论
            comment.setAvatar(avatar);
        }
        // 最后保存评论,重定向到评论显示部分
        commentService.saveComment(comment);
        return "redirect:/comments/" + blogId;
    }
}

其中saveComment方法在持久层上仍然是Jpa帮我们自动实现的,业务层只需要调用使用即可。

public interface CommentService {

    Comment saveComment(Comment comment);
}

方法的实现如下:

@Transactional
@Override
public Comment saveComment(Comment comment) {
    // 首先看是不是一级评论
    Long parentCommentId = comment.getParentComment().getId();
    if (parentCommentId != -1) {
        // 如果不是,还需要找到它的父级评论,然后设置parentComment字段的值
        comment.setParentComment(commentRepository.getOne(parentCommentId));
    } else {
        // 否则无需设置
        comment.setParentComment(null);
    }
    // 最后设置评论时间,保存评论
    comment.setCreateTime(new Date());
    return commentRepository.save(comment);
}

4. footer

所谓footer就是每个页面的最底部显示的信息,它是被所有的页面共用的部分,如下所示:footer

其中包含五部分信息:

  • 博主的联系方式:包括CSDN、GitHub和邮箱
  • 友链
  • 博客的一些统计信息:文章总数、阅读量、评论数
  • 博客简介
  • 版权申明

它对应的前端设计如下:

<!--底部-->
<footer th:fragment="footer" class="ui inverted vertical segment m-padded-tb-massive">
    <div class="ui center aligned container">
        <div class="ui inverted divided stackable grid">
            <!--博主联系方式-->
            <div class="three wide column">
                <div class="ui inverted link list">
                    <a href="https://github.com/" class="ui github circular icon button" data-content="点击跳转GitHub"><i class="github icon"></i></a>
                    <a href="https://forlogen.blog.csdn.net/" class="ui csdn circular icon button" data-content="点击跳转CSDN" ><i class="copyright outline icon"></i></a>
                    <a href="#" class="ui email circular icon button" data-content="18dyliang@stu.edu.cn"><i class="envelope icon"></i></a>
                </div>
            </div>
            <!--友情链接-->
            <div class="three wide column">
                <h4 class="ui inverted header m-text-thin m-text-spaced " >友情链接</h4>
                <div class="ui inverted link list">
                    <a href="https://www.csdn.net/" class="item m-text-thin">CSDN</a>
                    <a href="https://github.com/" class="item m-text-thin">GitHUb</a>
                    <a href="https://www.cnblogs.com/" class="item m-text-thin">博客园</a>
                    <a href="https://www.zhihu.com/" class="item m-text-thin">知乎</a>
                </div>
            </div>
            <!--博客统计信息-->
            <div class="three wide column">
                <h4 class="ui inverted header m-text-thin m-text-spaced " >博客信息</h4>
                <div id="statistic-container">
                    <div  th:fragment="statisticList" class="ui inverted center aligned link list" style="align-content: center;margin-top: 10px">
                        <div class="center aligned m-text-thin">
                            文章总数: <h2 class="ui orange header m-inline-block m-margin-top-null" th:text="${blogCount}" style="font-size:medium;"> 14 </h2></div>
                        <div class="m-text-thin">
                            访问总数: <h2 class="ui orange header m-inline-block m-margin-top-null" th:text="${blogView}" style="font-size:medium;"> 14 </h2></div>
                        <div class="m-text-thin">
                            评论总数: <h2 class="ui orange header m-inline-block m-margin-top-null" th:text="${blogComment}" style="font-size:medium;"> 14 </h2></div>
                    </div>
                </div>
            </div>
            <!--博客简介-->
            <div class="seven wide column">
                <h4 class="ui inverted header m-text-thin m-text-spaced ">Blog</h4>
                <p class="m-text-thin m-text-spaced m-opacity-mini">这是我的个人博客、会分享关于编程、写作、思考相关的任何内容,希望可以给来到这儿的人有所帮助...</p>
            </div>
        </div>

        <div class="ui center aligned inverted section divider"></div>
        <p class="m-text-thin m-text-spaced m-opacity-tiny">Copyright © 2016 - 2017  Designed by Forlogen</p>
        <p class="m-text-thin m-text-spaced m-opacity-tiny">本站托管于
            <a href="https://cn.aliyun.com/" style="color: #aac6e3">阿里云</a>
        </p>
    </div>
</footer>

那么只有博客统计信息部分需要后端查询,然后返回相应的值。表现层对应的处理方法是:

@Controller
public class FooterController {

    @Autowired
    private BlogService blogService;

    @Autowired
    private CommentService commentService;

    @GetMapping("/footer/statistic")
    public String statistic(Model model){
        model.addAttribute("blogCount", blogService.countBlog());
        model.addAttribute("blogView", blogService.numberOfViews());
        model.addAttribute("blogComment", commentService.numberOfComment());

        return "_fragments :: statisticList";
    }
}

对应在持久层就是一些自定义查询的实现,如下所示:

public interface BlogRepository extends JpaRepository<Blog, Long>, JpaSpecificationExecutor<Blog> {    
    @Query("select sum(b.views) from Blog b")
    Long numberOfviews();
}
@Service
public class BlogServiceImpl implements BlogService {

    @Autowired
    private BlogRepository blogRepository;

    @Override
    public Long countBlog() {
        return blogRepository.count();
    }
}
@Service
public class CommentServiceImpl implements CommentService {

    @Autowired
    private CommentRepository commentRepository;


    @Override
    public Long numberOfComment() {
        return commentRepository.count();
    }
}

5. 总结

完结撒花啦~

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值