上一节,我们完成了文章的发布功能和图片上传功能,但是还没有将文章展示出来。这一节我们来介绍如何展示文章和增加浏览量计数问题。

文章详情页面html代码

{% include "partial/header.html" %}
<div class="layui-container index">
    <div class="layui-row layui-col-space15">
        <div class="layui-col-md8">
            <div class="layui-card article-detail">
                <div class="layui-card-body">
                    <h1 class="title">{{article.Title}}h1>
                    <div class="meta">
                        {% if article.Category %}<span><a href="/?category_id={{article.CategoryId}}">{{article.Category.Title}}a>span>{% endif %}
                        <span>{{stampToDate(article.CreatedTime, "2006-01-02")}}span>
                        <span>{{article.Views}} 阅读span>
                        {% if hasLogin %}<span><a href="/article/publish?id={{article.Id}}">编辑a>span>{% endif %}
                    div>
                    <div class="article-body">
                        {{article.ArticleData.Content|safe}}
                    div>
                div>
            div>
            <div class="layui-card">
                <div class="layui-card-body">
                    <div class="article-prev-next">
                        {% if prev %}
                        <li>上一篇:<a href="/article/{{prev.Id}}">{{prev.Title}}a>li>
                        {% endif %}
                        {% if next %}
                        <li>下一篇:<a href="/article/{{next.Id}}">{{next.Title}}a>li>
                        {% endif %}
                    div>
                div>
            div>
        div>

        <div class="layui-col-md4">
            {% include "partial/author.html" %}
        div>
    div>
div>
{% include "partial/footer.html" %}

文章详情页面我们采用左右结构,左边显示文章标题、文章内容,上下篇文章等主要信息。右边则显示跟文章相关的最新文章、相关文章等内容。

左边显示的信息中,我们注意到显示文章分类使用的是{{article.Category.Title}},这是因为我们定义文章模型的时候,article.Category 它指向的是文章分类的模型,article.Category.Title 就能访问到文章分类的名称了。

同时,这里的文章发布时间,我们使用了{{stampToDate(article.CreatedTime, "2006-01-02")}}来显示。stampToDate是我们前面自定义的模板函数,它可以将时间戳按照给定的格式格式化输出。这里我们将文章发布的时间戳按照"2006-01-02"的格式来输出显示。

文章内容的输出我们使用标签{{article.ArticleData.Content|safe}}。这里同样地,article.ArticleData 指向的是文章内容表article_data模型,通过article.ArticleData.Content可以读取到文章的内容。这里我们使用了模板语言的|safe过滤标签,如果我们不使用safe标签的话,模板解析的时候,是为了安全,会对HTML标签和JS等语法标签进行自动转义,防止xss攻击。转义后,它就不是html了,这样不符合我们文章富文本内容输出的要求,因此这里我们需要使用safe来阻止它自动转义。

文章详情页控制器函数

文章详情页控制器我们写在controller/article.go 中。我们在article.go中添加ArticleDetail()函数:

func ArticleDetail(ctx iris.Context) {
	id := ctx.Params().GetUintDefault("id", 0)
	article, err := provider.GetArticleById(id)
	if err != nil {
		NotFound(ctx)
		return
	}
    _ = article.AddViews(config.DB)

	ctx.ViewData("article", article)
	ctx.View("article/detail.html")
}

这个函数,首先通过ctx.Params().GetUintDefault()来获取文章的id,我们根据文章id来读取文章,如果文章不存在,则使用NotFound()函数,输出404错误。检查完文章后,我们再使用ctx.ViewData("article", article)将article注入到模板中,这样模板就能使用article这个变量了。然后通过ctx.View("article/detail.html")来将控制器和文章详情模板关联起来。

这里我们使用了provider.GetArticleById(id),这个函数是需要访问数据库读取文章内容的,因此我们将它抽离到provider目录中。我们打开provider/article.go,在里面添加GetArticleById()函数:

func GetArticleById(id uint) (*model.Article, error) {
	var article model.Article
	db := config.DB
	err := db.Where("`id` = ?", id).First(&article).Error
	if err != nil {
		return nil, err
	}
	//加载内容
	article.ArticleData = &model.ArticleData{}
	db.Where("`id` = ?", article.Id).First(article.ArticleData)
	//加载分类
	article.Category = &model.Category{}
	db.Where("`id` = ?", article.CategoryId).First(article.Category)

	return &article, nil
}

这里我们从数据库根据文章id读取article信息的时候,先使用Preload("ArticleData")来将文章的内容表信息也关联的读进来。

我们设置article模型的时候,category不是和文章表通过外键关联,因此我们需要单独将文章分类加载进来。

增加文章访问量

上面我们在用户访问到文章详情页的时候,使用了_ = article.AddViews(config.DB)来增加文章的浏览量。这里为了直观,我们默认访问一次页面就当做增加一个浏览量。实际项目应用中,我们可能还需要根据用户的ip、ua来做处理,对搜索引擎的蜘蛛做区别处理,也要做访问记录等信息,而不是单纯的累加。

上面我们使用的是article.AddViews(),说明这是一个文章模型的内置方法,因此我们在model/article.go 中,增加AddViews()方法:

func (article *Article) AddViews(db *gorm.DB) error {
	article.Views = article.Views + 1
	db.Model(Article{}).Where("`id` = ?", article.Id).Update("views", article.Views)
	return nil
}

这里我们需要由上层传入db对象。因此db对象是在config中的,如果我们在model中直接访问config,就会导致嵌套依赖,会导致golang项目编译不通过。而我们这里更新文章浏览量使用的是Update函数,Update函数可以直接更新对应字段,而不会在更新后再次读取表信息,可以减少一次查询操作。

配置文章详情页面路由

上面文章详情页面准备好了,我们还需要添加文章路由,才能让用户访问到文章详情页面,我们在route/base.go 中的article分组下,添加article.Get("/{id:uint}", controller.ArticleDetail),最终article分组的代码如下:

article := app.Party("/article", controller.Inspect)
{
    article.Get("/{id:uint}", controller.ArticleDetail)
    article.Get("/publish", controller.ArticlePublish)
    article.Post("/publish", controller.ArticlePublishForm)
}

这里我们使用了{id:uint}来获取文章id。这里我们定义id的类型为无符号整形数字。这么定义后,在文章详情控制器中可以通过id := ctx.Params().GetUintDefault("id", 0)来获取。

验证结果

我们重启一下项目,我们先在浏览器中访问http://127.0.0.1:8001/article/1来看看效果,验证下文章发布过程是否正常。如果不出意外可以看到这样的画面: 

design-detail

完整的项目示例代码托管在GitHub上,需要查看完整的项目代码可以到github.com/fesiong/goblog 上查看,也可以直接fork一份来在上面做修改。