博客要有文章展示,首先得有发文章的地方。因此我们在做完登录功能之后,接着现在就开始做文章发布功能了。
文章发布功能包含了2块内容,一块是文章的创建,另一块是分类的创建。
文章发布页面
我们在template文件夹下创建一个article文件夹,并在里面新建一个publish.html:
{% include "partial/header.html" %}
<div class="layui-container">
<div class="publish">
<div class="layui-form">
<input type="hidden" name="id" value="{{article.Id}}">
<div class="layui-form-item">
<label class="layui-form-label">文章标题label>
<div class="layui-input-block">
<input type="text" name="title" value="{{article.Title}}" autocomplete="off" class="layui-input">
div>
div>
<div class="layui-form-item">
<label class="layui-form-label">文章分类label>
<div class="layui-input-block">
<input type="text" name="category_name" value="{{article.Category.Title}}" autocomplete="off"
class="layui-input" list="category_name">
<datalist id="category_name">
{% for item in categories %}
<option value="{{item.Title}}">option>
{% endfor %}
datalist>
div>
div>
<div class="layui-form-item">
<label class="layui-form-label">关键词label>
<div class="layui-input-block">
<input type="text" name="keywords" value="{{article.Keywords}}" placeholder="多个请用英文,隔开" autocomplete="off" class="layui-input">
div>
div>
<div class="layui-form-item">
<label class="layui-form-label">文章描述label>
<div class="layui-input-block">
<textarea name="description" placeholder="默认提取文章前150个字" class="layui-textarea" rows="3">{{article.Description}}textarea>
div>
div>
<div class="layui-form-item">
<label class="layui-form-label">文章内容label>
<div class="layui-input-block">
<textarea name="content" class="layui-textarea" id="text-editor">{{article.ArticleData.Content}}textarea>
div>
div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="article-publish">确认提交button>
<button type="reset" class="layui-btn layui-btn-primary">重置button>
div>
div>
div>
div>
div>
{% include "partial/footer.html" %}
文章发布页面我们需要有如下字段:
- 文章ID
id
这是一个隐藏的字段,因为它不能被编辑和更改。如果没有id或id为0的时候,默认认为这是一篇新文章。 - 文章标题
title
文章标题即是文章显示的标题。 - 文章分类
category_name
这里面的文章分类采用分类名称来显示,我们规定每个分类的名称不一样,如果相同的分类名称,我们认为它是同一个分类。这里还通过datalist
标签,来支持下拉显示曾经使用过的分类名称,可以直接选中使用。 - 文章关键词
keywords
文章关键词主要是为了做seo优化使用的,关键词一般是文章标题的部分文字。当需要设置多个关键词时一般使用英文逗号,
隔开。 - 文章简介
description
文章简介我们使用textarea
多行输入框来显示,可以简单的介绍这边文章。一般文字不超过150个。如果不填写的话,则程序要自动从文章内容中抽取前150字作为文章简介。 - 文章内容
content
文章内容使用富文本编辑框来展示,为了简便,我们之间使用layui框架的layedit编辑器。这个编辑器简单,但功能不是太多,先凑合着使用。
鉴定提交表单是js
html页面往往是需要结合js来达到更多的交互目的的。我们也一样,需要使用layui的form表单组件来监听页面表单信息的提交来完成表单信息处理。因为我们的发布页面还需要使用富文本编辑器layedit来支持文章内容可视化编辑。因此我们在app.js 文件中,添加实例化编辑器和表单提交监听代码:
//实例化layui编辑器
if($('#text-editor').length) {
editorIndex = layedit.build('text-editor', {
height: 450
});
}
//发布文章
form.on('submit(article-publish)', function(data){
let postData = data.field;
postData.id = Number(postData.id)
if(!postData.title) {
return layer.msg("请填写文章标题");
}
//同步编辑器内容
layedit.sync(editorIndex);
postData.content = $('#text-editor').val();
$.post("/article/publish", postData, function (res) {
if(res.code === 0) {
layer.alert(res.msg, function(){
window.location.href = "/article/" + res.data.id;
});
} else {
layer.msg(res.msg);
}
}, 'json');
return false;
});
我们上面的html代码中,给填写文章内容的textarea标签,定义了#text-editor
id,因此我们使用layedit.build('text-editor',{height: 450})
来初始化编辑器,并将编辑器的高度初始化为450px高。这样编辑器就初始化完成了。
监听提交表单的时候,需要做一些简单处理,首先是将id转换成整形postData.id = Number(postData.id)
,再判断有没有填写文章标题,如果不填写文章标题,则不允许提交。没有文章标题嘛,不能作为独立的文章来提交,否则文章列表展示的时候,没东西展示,用户就会点击不到。
这里还有一个很重要的一步,因为编辑器不是自动同步的,我们还需要在提交前,手动同步一遍编辑器的内容到textarea,并重新获取textarea输入框的内容,才能提交到后台,否则数据可能不是最新的。
post表单提交到后台后,根据后端返回的状态,来判断是否发布成功,如果发布成功,则弹出一个alert窗口,提示发布成功了,并在用户点击确定的时候,跳到文章详情页面中去。
文章发布页面控制器
我们还需要将页面和控制器绑定。因此,我们在controller目录下,创建article.go文件,并添加ArticlePublish(ctx iris.Context)
方法:
func ArticlePublish(ctx iris.Context) {
//发布必须登录
if !ctx.Values().GetBoolDefault("hasLogin", false) {
InternalServerError(ctx)
return
}
id := uint(ctx.URLParamIntDefault("id", 0))
if id > 0 {
article, _ := provider.GetArticleById(id)
ctx.ViewData("article", article)
}
ctx.View("article/publish.html")
}
发布页面需要做权限判断,这里我们通过ctx.Values().GetBoolDefault("hasLogin", false)
获取前面中间件Auth(ctx iris.Context)
设置的hasLogin
值,如果hasLogin
值为true表示登录了,否则认为没有登录。没有登录的时候,我们使用InternalServerError(ctx)
服务器错误控制器来显示内容。
在发布页面呈现之前,我们先判断页面路径中,是否包含有文章id,如果有,我们认为这是在修改文章,则先从数据库中读出文章来,并注入到页面中。
id := uint(ctx.URLParamIntDefault("id", 0))
if id > 0 {
article, _ := provider.GetArticleById(id)
ctx.ViewData("article", article)
}
接着我们通过ctx.View("article/publish.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不是和文章表通过外键关联,因此我们需要单独将文章分类加载进来。
文章发布处理逻辑控制器
文章发布控制器和文章页面已经准备好了,我们还需要有一个处理逻辑的控制器。
我们先定义一个接收前端提交文章内容字段的结构体。我们在request文件夹中,新增一个article.go文件,并添加如下代码:
package request
type Article struct {
Id uint `form:"id"`
Title string `form:"title" validate:"required"`
CategoryName string `form:"category_name" validate:"required"`
Keywords string `form:"keywords"`
Description string `form:"description"`
Content string `form:"content" validate:"required"`
File string `form:"file"`
}
这里面的字段均为前端页面提交上来的字段,这里不做更多的解释。其中多出一个File字段,是因为layedit编辑器中,图片上传部分包含了一个隐藏的file表单,导致提交的时候,它也会跟过来。如果我们在这里不定义它,则使用Article结构体读取提交的内容的时候,它会报错。所以在这里定义,但实际上我们并没有使用它。
我们接着在article.go中,添加ArticlePublishForm(ctx iris.Context)
函数:
func ArticlePublishForm(ctx iris.Context) {
//发布必须登录
if !ctx.Values().GetBoolDefault("hasLogin", false) {
ctx.JSON(iris.Map{
"code": config.StatusFailed,
"msg": "登录后方可操作",
})
return
}
var req request.Article
if err := ctx.ReadForm(&req); err != nil {
ctx.JSON(iris.Map{
"code": config.StatusFailed,
"msg": err.Error(),
})
return
}
var category *model.Category
var err error
//检查分类
if req.CategoryName != "" {
category, err = provider.GetCategoryByTitle(req.CategoryName)
if err != nil {
category = &model.Category{
Title: req.CategoryName,
Status: 1,
}
err = category.Save(config.DB)
if err != nil {
ctx.JSON(iris.Map{
"code": config.StatusFailed,
"msg": err.Error(),
})
return
}
}
}
var article *model.Article
if req.Id > 0 {
article, err = provider.GetArticleById(req.Id)
if err != nil {
ctx.JSON(iris.Map{
"code": config.StatusFailed,
"msg": err.Error(),
})
return
}
if article.ArticleData == nil {
article.ArticleData = &model.ArticleData{}
}
article.ArticleData.Content = req.Content
} else {
article = &model.Article{
Title: req.Title,
Keywords: req.Keywords,
Description: req.Description,
Status: 1,
ArticleData: &model.ArticleData{
Content: req.Content,
},
}
}
//提取描述
if req.Description == "" {
htmlR := strings.NewReader(req.Content)
doc, err := goquery.NewDocumentFromReader(htmlR)
if err == nil {
textRune := []rune(strings.TrimSpace(doc.Text()))
if len(textRune) > 150 {
article.Description = string(textRune[:150])
} else {
article.Description = string(textRune)
}
}
}
if category != nil {
article.CategoryId = category.Id
}
err = article.Save(config.DB)
if err != nil {
ctx.JSON(iris.Map{
"code": config.StatusFailed,
"msg": err.Error(),
})
return
}
ctx.JSON(iris.Map{
"code": config.StatusOK,
"msg": "发布成功",
"data": article,
})
}
逻辑处理控制器不需要页面输出,所以这里不需要使用ctx.View()
方法。这里面我们使用ctx.JSON()
方法,来输出json字符串。
同样地,文章发布逻辑处理控制器也需要进行权限判断,如果没有登录,则用json返回提示登录:
if !ctx.Values().GetBoolDefault("hasLogin", false) {
ctx.JSON(iris.Map{
"code": config.StatusFailed,
"msg": "登录后方可操作",
})
return
}
这里一定要注意,这里ctx.JSON()
输出内容后,我们需要阻断后续的运行,一定要使用return
来阻止下面的代码继续执行,否则它会继续执行下去,导致逻辑错误。
上面我们将前端提交的内容读入到了req中:
var req request.Article
if err := ctx.ReadForm(&req); err != nil {
ctx.JSON(iris.Map{
"code": config.StatusFailed,
"msg": err.Error(),
})
return
}
接着,根据提交上来的分类名称,我们需要先判断这个分类名称是否已经创建了,如果没有创建,则先创建分类到数据库中。
分类准备后之后,我们再创建article模型,根据提交的数据检查是否包含有id,如果有,则表示这是一条更新记录,从数据库中读取源文章信息,如果没有id,则创建一个新的文章。
var article *model.Article
if req.Id > 0 {
article, err = provider.GetArticleById(req.Id)
if err != nil {
ctx.JSON(iris.Map{
"code": config.StatusFailed,
"msg": err.Error(),
})
return
}
if article.ArticleData == nil {
article.ArticleData = &model.ArticleData{}
}
article.ArticleData.Content = req.Content
} else {
article = &model.Article{
Title: req.Title,
Keywords: req.Keywords,
Description: req.Description,
Status: 1,
ArticleData: &model.ArticleData{
Content: req.Content,
},
}
}
这里要注意下,我们的文章模型中,ArticleData字段是一个外键字段,它关联article_data表,我们通过在这里赋值,它会将内容自动插入到article_data表中。
接着检查前端提交的文章简介有没有内容,如果没有的话,我们则使用goquery
包来将html标签过滤掉,只保留纯文字内容,同时将空格清理掉,再截取前150个字作为文章简介。因为涉及到汉字,一个汉字占3-4个字节,不能直接从字符串中截取,我们需要先将字符串转换成rune类型,否则会导致截取的结果不准确导致乱码,字数也不对。
//提取描述
if req.Description == "" {
htmlR := strings.NewReader(req.Content)
doc, err := goquery.NewDocumentFromReader(htmlR)
if err == nil {
textRune := []rune(strings.TrimSpace(doc.Text()))
if len(textRune) > 150 {
article.Description = string(textRune[:150])
} else {
article.Description = string(textRune)
}
}
}
接着我们在判断提交的信息中有没有分类,如果有分类,则将这篇文章关联到分类中,我们将分类的id赋值给文章的CategoryId字段:
if category != nil {
article.CategoryId = category.Id
}
最后就是文章的入库过程了:
err = article.Save(config.DB)
这个save方法为文章模型的内置方法,因此,我们需要在model/article.go文件中,添加这个方法:
func (article *Article) Save(db *gorm.DB) error {
if article.Id == 0 {
article.CreatedTime = time.Now().Unix()
}
if err := db.Debug().Save(article).Error; err != nil {
return err
}
if article.ArticleData != nil {
article.ArticleData.Id = article.Id
if err := db.Debug().Save(article.ArticleData).Error; err != nil {
return err
}
}
return nil
}
这个方法很简单,先是判断这是不是一个新文章,如果是,则添加上发布时间,同时更新文章的更新时间。因为每次保存,我们都认为这是一次更新。最后调用db.Save()
方法,来完成文章的入库。
配置文章发布路由
上面文章发布页面和文章逻辑处理都准备好了,我们现在还不能直接访问到发布页面。因此我们还需要给它添到路由中。我们打开route/base.go文件,在Register中添加发布文章的路由:
article := app.Party("/article", controller.Inspect)
{
article.Get("/publish", controller.ArticlePublish)
article.Post("/publish", controller.ArticlePublishForm)
}
同样的,我们使用了路由分组功能,将article归纳为一个组。这里面,我们将文章发布页面和文章逻辑处理路径都设置为同一个路径,但是他们分别绑定不同的请求方法,当我们使用get请求访问/article/publish
的时候,它是发布页面,当使用post请求访问/article/publish
的时候,则是文章发布逻辑处理控制器来处理了。
至此,我们就可以访问到发布页面和正常发布文章了。当然前提是先登录。
验证结果
我们重启一下项目,我们先在浏览器中访问http://127.0.0.1:8001/admin/login
来完成登录,接着在浏览器中访问http://127.0.0.1:8001/article/publish
看看效果,验证下文章发布过程是否正常。如果不出意外可以看到这样的画面:
完整的项目示例代码托管在GitHub上,需要查看完整的项目代码可以到github.com/fesiong/goblog 上查看,也可以直接fork一份来在上面做修改。