原因

近期,我正在筹备一个开源项目,该项目是一个能快速生成后台代码的工具,采用了Hertz+Gorm+Gen+Vben Admin技术栈。在开发过程中,我始终在思考如何实现框架与后续业务的分离,以使快速生成的业务代码更加清晰,且业务的修改不会对框架产生影响。这样也能更好地迭代这个开源项目,并在使用过程中不断完善。
在凝视着vscode,陷入沉思时,我突然灵光一闪,想到了插件化。目前许多工具都正在转变为插件化,它们在提供最基础的功能的同时,也允许用户以自己的方式增强这个工具。

开始

Go语言在插件化方面非常的方便,只需要将插件路由注册到主路由上就可以使用了。接下来我将实现过程记录一下:

一、在框架中加入插件功能

1、先创建一个Plugin 插件模式接口化文件

package plugin

import "github.com/cloudwego/hertz/pkg/route"

const (
	OnlyFuncName = "Plugin"
)

// Plugin 插件模式接口化
type Plugin interface {
	// Register 注册路由
	Register(group *route.RouterGroup)
	// RouterPath 用户返回注册路由
	RouterPath() string
}

2、然后批量初始化插件路由

package initialize

import (
	"fmt"
	……
)

//插件初始化,注册上面创建的接口文件
func PluginInit(group *route.RouterGroup, Plugin ...plugin.Plugin) {
	for i := range Plugin {
		PluginGroup := group.Group(Plugin[i].RouterPath())
		Plugin[i].Register(PluginGroup)
	}
}

// 安装插件
func InstallPlugin(Router *server.Hertz) {
	PublicGroup := Router.Group("")
	fmt.Println("无鉴权插件安装==》", PublicGroup)
	PrivateGroup := Router.Group("",middleware.JwtMiddleware.MiddlewareFunc())
	fmt.Println("鉴权插件安装==》", PrivateGroup)
	……
}

3、在主路由上调用上面的函数

package initialize

import (
	"context"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
)

func Routers() *server.Hertz {
	Router := server.Default()
	InstallPlugin(Router) // 安装插件
	//下面继续写主框架中的路由地址
	……
	return Router
}

4、在man中调用路由

package main

import (
	……
)

func main() {
	initialize.Routers()
}

二、插件实现方法

如果我要实现CMS的功能,
我在根目录创建了一个名字为plugint的文件夹,在文件夹中创建目录:

plugint
└── cms
    ├── README.MD
    ├── api
    │   └── cms_api.go
    ├── global
    │   └── global.go
    ├── main.go
    ├── model
    │   └── res
    │       └── article.go
    ├── router
    │   ├── cms_router.go
    │   └── enter.go
    └── service
        └── post.go

1、api:直接写接口方法

//cms_api.go 文件
package api

import(
	……
)

type CMSApi struct {
}
//提交文章
func (b *CMSApi) PostArticle(c context.Context, cx *app.RequestContext) {
	article := &res.ArticleRes{}
	if err := cx.BindJSON(&article); err != nil {
		hlog.Error("参数错误!", zap.Error(err))
		response.FailWithMessage(err.Error(), cx)
		return
	}
	response.OkWithData(article, cx)
}

2、model:写一些结构体

3、global:在插件中的全局变量,在这里只写了一个例子,全局保存Title的内容

package global

var GlobalConfig = new(res.ArticleRes) //GlobalConfig为model中的结构体

4、router路由

//cms_router.go文件

package router
import (
	……
)

type CMSRouter struct {
}

//初始化路由
func (s *CMSRouter) InitCMSRouter(group *route.RouterGroup) {
	genRouterWithoutRecord := group.Use(middleware.Cors())
	cmsApi := api.CMSApi{}
	{
		genRouterWithoutRecord.POST("post", cmsApi.PostArticle)
	}
}

5、main.go:很重要的文件,是插件的进出口:

package cms
import (
	……
)

type cmsPlugin struct {
}
  
//初始化插件,可以设置接受参数,这里只接受了一个Title,并将其保存到当前插件的全局变量中
func CreateCMSPlugin(Title string) *cmsPlugin {
	//这里可以传递配置参数,将参数放到当前插件的global中,作为插件全局变量使用
	global.GlobalConfig.Title = Title
	return &cmsPlugin{}
}

//注册路由
func (*cmsPlugin) Register(group *route.RouterGroup) {
	router.RouterGroupApp.InitCMSRouter(group)
}

//设置路由路径
func (*cmsPlugin) RouterPath() string {
	return "cms"
}

6、service:自己写逻辑吧

三、结果

在写好插件后,将插件的man.go中的初始化插件方法,注册到initialize/InstallPlugin中,就是第一步骤中的第2个中:

package initialize

……

// 安装插件
func InstallPlugin(Router *server.Hertz) {
	PublicGroup := Router.Group("")
	fmt.Println("无鉴权插件安装==》", PublicGroup)
	PrivateGroup := Router.Group("",middleware.JwtMiddleware.MiddlewareFunc())
	fmt.Println("鉴权插件安装==》", PrivateGroup)
	//添加到这里,如果无需jwt权限则使用PublicGroup
	PluginInit(PrivateGroup, cms.CreateCMSPlugin("传递参数"))
}

可以启动项目调用插件了,地址为:http://localhost:8888/cms/post

总结

这种方法的应用十分简单,且代码分离非常清晰。唯一的问题在于,插件的注册还需要手动添加到文件中。接下来,我将通过前端配置或寻找其他方式,使功能实现更加优雅。