学习GoFrame框架,从头开始一步步搭建个人博客WEB应用(中)

前言

今天继续上一篇搭建个人博客的内容。之前只做了个数据库和url路由,我最近在写后续的内容时发现有很多调整的地方。所以首先做一下调整。

数据库调整

数据库进行了部分调整。

  • blog_auth表暂未做调整,因为目前还没做到后台管理,这个表暂时没用上,所以也没变动,表结构如下:
CREATE TABLE `blog_auth` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(10) DEFAULT '' COMMENT '账号',
  `password` varchar(10) DEFAULT '' COMMENT '密码',
    `url` varchar(10) NOT NULL COMMENT 'URL名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • blog_tag表未做调整,该表内容较少,结构简单,具体表结构如下:
CREATE TABLE `blog_tag` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT '' COMMENT '标签名称',
  `state` tinyint unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签管理';
  • blog_article表改动了2个字段名,具体表结构如下:
CREATE TABLE `blog_article` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `tag_id` int unsigned DEFAULT '0' COMMENT '标签ID',
  `title` varchar(100) NOT NULL DEFAULT '' COMMENT '文章标题',
  `desc` varchar(255) DEFAULT '' COMMENT '简述',
  `content` text COMMENT '内容',
  `author` varchar(10) COMMENT '作者',
  `date` int unsigned DEFAULT '0' COMMENT '创建时间',
  `state` tinyint unsigned DEFAULT '1' COMMENT '状态 0为删除、1为正常',
  `deleted` int unsigned DEFAULT '0' COMMENT '删除时间',
  PRIMARY KEY (`id`),
  KEY `fktid` (`tag_id`),
  CONSTRAINT `fktid` FOREIGN KEY (`tag_id`) REFERENCES `blog_tag` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章管理';

  • 执行完建库和建表后,需要执行dao将表结构导入goframe的model中。注意,在做这一步之前要在config/config/toml文件中配置好数据库的host、port、user、pass、name、type、charset。
gf gen dao -l "mysql:你的帐号:你的密码@tcp(127.0.0.1:3306)/blog"

路由调整

  • 关于命名匹配
    之前我使用命名匹配:name方式进行匹配,感觉有时会有问题重复响应的问题,访问/home时/:tag路由也会响应,感觉很奇怪。
    经认真查阅文档发现这种方式匹配规则相当于正则的([^/]+),即表示只要不是\的字符一个或多个都可以匹配,匹配太宽泛,造成路由多了以后容易出错。
  • 关于字段匹配
    我改用字段匹配{field}方式进行匹配(field}为自定义的匹配名称后未出现重复响应的问题,访问/home时/{tag}路由不会再重复响应。
    文档中说该规则类似正则([\w.-]+),表示匹配字符是“字母、数字、下划线_、小数点.、减号-”一个或多个,匹配范围略小一些,减少了出错几率。
  • blog/router/router.go内容如下:
package router

import (
	"blog/app/api"
	"blog/app/service"
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
)

func init() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.Middleware(service.Middleware.Ctx)  // 绑定context相关中间件
		group.REST("/admin", api.Admin)           // 后台管理入口
		group.GET("/", api.Home)                  // 博客首页
		group.GET("/{tag}/", api.Tag)            // 博客其他栏目入口
		group.GET("/{tag}/{article}", api.Show) // 文章入口
	})
}

中间件传递公共信息

正好最近写了一篇关于GoFrame框架中Context相关梳理及实例的博客,所以我打算先梳理这部分的内容,以下是写个人博客时关于中间件传递公共信息的代码:

blog/app/model/common.go文件

在该文件中定义了中间件context需要使用的struct和其初始化方法。因为博客被访问时文章列表、栏目列表经常需要查询数据库,若将文章列表、栏目列表存入context中可大大降低访问数据库的次数(仅仅在项目启动、改动文章列表和栏目列表时才需要重载model.List,执行数据库访问),大大提高系统运行效率:

  1. 定义公共信息数据结构,这里再叨叨几句,写代码之前要先理清思路,首先必须搞定数据结构:
package model

import (
	"github.com/gogf/gf/frame/g"
)

// Catalogs 博客栏目及文章简要信息
type Catalogs struct {
	Tags         []Tag  // 博客栏目id和名称数组
	ArticleSlice []Article  // 所有文章简要信息数组
	ArticleMap   map[int][]Article  // 键是博客栏目id、值是该栏目下文章简要信息数组的map
}

// Tag 博客栏目id和名称
type Tag struct {
	TagId   int  // 栏目id
	TagName string  // 栏目名称
}

// Article 文章简要信息
type Article struct {
	TagId      int  // 栏目id
	ArtId      int  // 文章id
	ArtName    string  // 文章标题
	ArtSummary string  // 文章摘要
	Date       int  // 文章发布日期
}
  1. 定义全局共享变量List及其初始化方法。注意该全局变量中的值不涉及计算,只涉及重载,所以不存在数据不安全的情况。另外该全局变量是给context使用的,且它是大量读极少写的类型。
var List *Catalogs

// Catalog 从数据库中获取栏目信息和文章信息,返回指向Catalogs结构的指针
func Catalog() *Catalogs {
	// 从blog_tag表中获取文章栏目信息
	tags, err := g.Model("blog_tag").Fields("id", "name").Where("state = 1").All()
	if err != nil {
		return nil
	}
	// 从blog_tag和blog_article左连表中获取填充Article结构所有字段的相关信息
	articles, err := g.Model("blog_tag g").LeftJoin("blog_article a", "g.id=a.tag_id").Fields("g.id gid", "g.name", "a.id aid", "a.title", "a.desc", "a.date").Where("a.state = 1").OrderDesc("a.date").All()
	if err != nil {
		return nil
	}
	tagList := make([]Tag, 0)  // 填充Catalogs结构Tags字段的信息
	for _, i := range tags {
		tagItem := Tag{
			TagId:   i["id"].Int(),
			TagName: i["name"].String(),
		}
		tagList = append(tagList, tagItem)
	}  // 将数据库中查询到的信息先存入tagList,预备填入Catalogs结构
	artList := make([]Article, 0)  // 填充Catalogs结构ArticleSlice字段的信息
	artMap := make(map[int][]Article)  // 填充Catalogs结构ArticleMap字段的信息
	var artItem Article
	for _, i := range articles {
		artItem = Article{
			TagId:      i["gid"].Int(),
			ArtId:      i["aid"].Int(),
			ArtName:    i["title"].String(),
			ArtSummary: i["desc"].String(),
			Date:       i["date"].Int(),
		}
		artList = append(artList, artItem)
		artMap[i["gid"].Int()] = append(artMap[i["gid"].Int()], artItem)
	}  // 将数据库中查询到的信息先存入artList和artMap,预备填入Catalogs结构
	return &Catalogs{
		Tags:         tagList,
		ArticleSlice: artList,
		ArticleMap:   artMap,
	}  // 将从数据库中查询到的信息整理到Catalogs结构体中,将该结构体的指针返回给调用者
}


// 文件载入时的初始化方法
func init() {
	List = Catalog()
}

blog/app/model/context.go文件

该文件目前内容较少,只放了上面定义的List,以后可以根据需要扩充功能,再补充内容:

package model

const (
	// ContextKey 上下文变量存储键名
	ContextKey = "ContextKey"
)

// Context 请求上下文结构
type Context struct {
	List *Catalogs // 文章目录
}

blog/app/service/context.go文件

该文件内容大部分是参考官方模板写的,自己写了一个用来返回文章列表的函数。

package service

import (
	"blog/app/model"
	"context"
	"github.com/gogf/gf/net/ghttp"
	"strconv"
)

// Context 上下文管理服务
var Context = contextService{}

type contextService struct{}

// Init 初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改。
func (s *contextService) Init(r *ghttp.Request, customCtx *model.Context) {
	r.SetCtxVar(model.ContextKey, customCtx)
}

// Get 获得上下文变量,如果没有设置,那么返回nil
func (s *contextService) Get(ctx context.Context) *model.Context {
	value := ctx.Value(model.ContextKey)
	if value == nil {
		return nil
	}
	if localCtx, ok := value.(*model.Context); ok {
		return localCtx
	}
	return nil
}

// SetUser 将上下文信息设置到上下文请求中,注意是完整覆盖
func (s *contextService) SetUser(ctx context.Context) {
	s.Get(ctx).List = model.Catalog()
}

// ListTable 根据参数决定返回所有文章列表或指定栏目下的文章列表
func (s *contextService) ListTable(ctx context.Context,tid string) []model.Article{
	tNum,err := strconv.Atoi(tid)
	if err !=nil{
		return nil
	}  // 传参不是栏目id时会返回nil
	value := ctx.Value(model.ContextKey)
	if tNum==0{  // tid参数是字符串0时表示需要所有文章的列表
		if localCtx, ok := value.(*model.Context); ok {
			return localCtx.List.ArticleSlice
		}
	}else{  // tid参数是字符串其他数字时表示需要该数字对应栏目下所有文章的列表
		if localCtx, ok := value.(*model.Context); ok {
			return localCtx.List.ArticleMap[tNum]
		}
	}
	return nil
}

blog/app/service/middleware.go文件

这是中间件获取数据和传递数据的方法,大部分内容是按照官方案例写的:

package service

import (
	"blog/app/model"
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
)

// Middleware 中间件管理服务
var Middleware = middlewareService{}

type middlewareService struct{}

// Ctx 自定义上下文对象
func (s *middlewareService) Ctx(r *ghttp.Request) {
	// 初始化,务必最开始执行
	customCtx := model.Context{
		List: model.List,
	}
	Context.Init(r, &customCtx)

	// 给模板传递上下文对象中的键值对
	r.Assigns(g.Map{
		"tags":customCtx.List.Tags,
	})

	// 执行后续中间件
	r.Middleware.Next()
}

blog/router/router.go文件

绑定中间件:其实这个文件的内容在上面router.go文件中已经写了,这里再重复一下,这一步千万别漏:

package router

import (
	"blog/app/api"
	"blog/app/service"
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
)

func init() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.Middleware(service.Middleware.Ctx)  // 绑定context相关中间件
		group.REST("/admin", api.Admin)           // 后台管理入口
		group.GET("/", api.Home)                  // 博客首页
		group.GET("/{tag}/", api.Tag)            // 博客其他栏目入口
		group.GET("/{tag}/{article}", api.Show) // 文章入口
	})
}

api展示数据

这部分相对来说反而是最简单的了,因为难搞的内容在中间件部分已经搞定了。

blog/app/api/home.go

package api

import (
	"blog/app/service"
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
)

func Home(r *ghttp.Request) {
	artList := service.Context.ListTable(r.Context(), "0")  // itd是0时表示获取所有栏目的文章列表
	err := r.Response.WriteTpl("home.html", g.Map{
		"tid":     "0",     // 当前栏目是主页,home
		"artList": artList, // 当前栏目文章清单
	})
	if err != nil {
		r.Response.Writeln(err)
		return
	}
}


blog/app/api/tag.go

package api

import (
	"blog/app/service"
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
)

func Tag(r *ghttp.Request) {
	tag,ok :=  r.Get("tag").(string)
	if !ok{
		r.Response.Writeln("该栏目不存在!")
		return
	}
	artList :=service.Context.ListTable(r.Context(),tag)
	err := r.Response.WriteTpl("tag.html", g.Map{
		"tid":   tag,  // 当前栏目名
		"artList":   artList,  // 当前栏目文章清单
	})
	if err != nil {
		r.Response.Writeln(err)
		return
	}
}

blog/app/api/show.go

package api

import (
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/net/ghttp"
)


func Show(r *ghttp.Request) {
	tag :=  r.Get("tag")
	article := r.Get("article")
	art, err := g.Model("blog_article").Fields("id","title", "content").Where("id=? and tag_id=?", article, tag).One()
	if err != nil {
		r.Response.Writeln(err)
		return
	}
	if len(art) == 0 {
		r.Response.Writeln("指定文章不存在")
		return
	}
	err = r.Response.WriteTpl("show.html", g.Map{
		"tid":   tag,  // 当前栏目名
		"md":    art["content"],  // 文章内容
		"title": art["title"].String(),  // 文章标题
		"aid":   article,  // 当前文章编号
	})
	if err != nil {
		r.Response.Writeln(err)
		return
	}
}

以上内容是goframe框架搭建个人博客后端要写的内容。接下来要写的是html展示页。然后后台管理的部分等过段时间再整理。

html模板部分

我对前端掌握得也不是很好,目前暂且用template模板渲染写一下页面。待以后再优化页面和ajax调接口取数据。
分层编写html模板,这样可以提高复用率。

blog/template/home.html

{{include "/module/head.html" .}}
{{include "/module/menu.html" .}}
{{include "/module/left.html" .}}
{{include "/body/home.html" .}}
{{include "/module/right.html" .}}
{{include "/module/tail.html" .}}

blog/template/tag.html

{{include "/module/head.html" .}}
{{include "/module/menu.html" .}}
{{include "/module/left.html" .}}
{{include "/body/tag.html" .}}
{{include "/module/right.html" .}}
{{include "/module/tail.html" .}}

blog/template/show.html

{{include "/module/head.html" .}}
{{include "/module/menu.html" .}}
{{include "/module/left.html" .}}
{{include "/body/show.html" .}}
{{include "/module/right.html" .}}
{{include "/module/tail.html" .}}

blog/template/module/head.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>皛心的博客</title>
    <link rel="stylesheet" href="/layui/css/layui.css">
    <script src="/layui/layui.js"></script>
    <link rel="stylesheet" href="/editor.md/css/editormd.min.css">
    <link rel="stylesheet" href="/editor.md/css/matchesonscrollbar.css">
    <link rel="stylesheet" href="/editor.md/css/dialog.css">
    <link rel="stylesheet" href="/editor.md/css/codemirror.min.css">
    <script src="/editor.md/js/jquery.min.js"></script>
    <script src="/editor.md/js/editormd.js"></script>
    <script src="/editor.md/js/marked.min.js"></script>
    <script src="/editor.md/js/prettify.min.js"></script>
    <style>
        body{
            background: url("/image/bg9.jpg") fixed!important;
        }
    </style>
</head>
<body>

blog/template/module/menu.html

<div class="layui-container">
    <ul class="layui-nav">
        <li class="layui-nav-item"><a href="/">皛心的博客</a></li>
        {{range $_, $elem := .tags}}
        <li class="layui-nav-item"><a href="/{{$elem.TagId}}">{{$elem.TagName}}</a></li>
        {{end}}
    </ul>
</div>

blog/template/module/left.html

内容暂时为空,将来可以按需求写显示在页面左侧的内容

blog/template/module/right.html

内容暂时为空,将来可以按需求写显示在页面右侧的内容

blog/template/module/tail.html

    </div>
</div>


</body>
</html>

blog/template/body/home.html

这里涉及到一些模板语言里的range循环,看不懂的建议参看一下官方文档。注意在循环体中不能直接访问全局变量,需要写$.变量名。

<div class="layui-container">
    <div class="layui-row">
        <div class="layui-col-md12">
            {{range $_, $elem := .artList}}
            <div class="layui-font-20" style="border: 0;">
                <a href="/{{$elem.TagId}}/{{$elem.ArtId}}"><span class="layui-font-20" style="color: blue"> {{$elem.ArtName}}</span></a>
            </div>
            <div class="layui-font-16" style="border: 0;padding-top:0;padding-bottom: 0">
                {{$elem.ArtSummary}}<br>
                创建于:{{$elem.Date | date "Y年m月d日 H时i分s秒"}}
            </div>
            <hr class="layui-border-green">
            {{end}}
        </div>

blog/template/body/tag.html

这里涉及到模板语言里的if else条件判断。

<div class="layui-container ">
    <div class="layui-row">
        <div class="layui-col-md12">
            <!-- 内容主体区域 -->
            {{ if eq (.artList|len) 0}}
                <div class="layui-font-20" style="border: 0;">
                    <span class="layui-font-20" style="color: black"> 抱歉,该栏目下暂无文章!</span>
                </div>
            {{ else}}
                {{range $_, $elem := .artList}}
                <div class="layui-font-20" style="border: 0;">
                    <a href="/{{$elem.TagId}}/{{$elem.ArtId}}"><span
                            class="layui-font-20" style="color: blue"> {{$elem.ArtName}}</span></a>
                </div>
                <div class="layui-card-body layui-font-16" style="border: 0;padding-top:0;padding-bottom: 0">
                    {{$elem.ArtSummary}}<br>
                    创建于:{{$elem.Date | date "Y年m月d日 H时i分s秒"}}
                </div>
                <hr class="layui-border-green">
                {{end}}
            {{end}}
        </div>

blog/template/body/show.html

这里有很多是javascript的内容,功能是不显示.md字符串,调用editor.md插件将其渲染成网页格式并展示。

<div class="layui-container ">
    <div class="layui-row">
        <div class="layui-col-md12 layui-text" style="padding: 15px;">
            <!-- 内容主体区域 -->
            <h1 class="article-title" style=" text-align:center ">{{.title}}</h1><br>
            <div id="hide" style="display: none">
                {{.md}}
            </div>
            <div id="test-markdown-view">
                <!-- Server-side output Markdown text -->
            </div>
            <div id="content"></div>
            <script>
                var md = document.getElementById("hide").innerText
                $(function () {
                    var testView = editormd.markdownToHTML("test-markdown-view", {
                        markdown: md, // Also, you can dynamic set Markdown text
                        htmlDecode: false,  // Enable / disable HTML tag encode.
                        htmlDecode: "style,script,iframe",  // Note: If enabled, you should filter some dangerous HTML tags for website security.
                    });
                });
                document.getElementById("test-markdown-view").style.backgroundColor="transparent";
            </script>
        </div>

最后

博客还用到很多静态文件,我这提供百度网盘链接,以便大家下载学习。
链接: https://pan.baidu.com/s/1gJFc4aW8B9Tj14gA-yJo2g 密码: 1f10

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值