连载了十一篇,终于讲到了大家最关心的Caddy的插件开发。插件开发是一种提供给我们开发者定制化Caddy的能力,让我们可以根据自己的需求,通过插件的机制,扩展Caddy的功能,满足自己的需求。

在Caddy中,编写插件是非常容易的事情,这得益于Caddy模块化的架构。其实我们用到的很多指令功能,都是Caddy基于插件实现,这些官方实现的插件,称之为标准插件。

在Caddy中,是没有插件这个概念的,Caddy称之为模块,其实和插件是一样的,而我们对插件也更容易理解,所以我会沿用插件这个概念。

xcaddy

要开发Caddy插件,xcaddy是必不可少的利器,通过它你可以把自己想用的插件编译进Caddy中,生成你专属的Caddy二进制文件,这样这个Caddy二进制文件就包含了你想用的插件。是不是很方便?它提供了让你定制Caddy二进制文件的能力,是否需要额外的插件完全取决于你。

在使用xcaddy之前,你得先安装它,你可以从 https://github.com/caddyserver/xcaddy/releases 下载在二进制文件,也可以可以通过源代码安装它,因为它是开源的。

1
$ go install github.com/caddyserver/xcaddy/cmd/[email protected]

实现一个插件

要想开发一个插件,那么这个插件必须要实现 caddy.Module 接口,这个接口非常简单,只有一个 CaddyModule 方法:

1
2
3
type Module interface {
   CaddyModule() ModuleInfo
}

现在我们来开发我们自己的插件HelloWorld,首先先定义一个结构体实现caddy.Module 接口 。

1
2
3
4
5
6
7
8
type HelloWorld struct {
}
func (h HelloWorld) CaddyModule() caddy.ModuleInfo {
   return caddy.ModuleInfo{
      ID:  "http.handlers.hello_world",
      New: func() caddy.Module { return new(HelloWorld) },
   }
}

从以上代码可以看到,我们返回的 caddy.ModuleInfo 包含两个字段:

  1. ID :用于指定一个唯一的标识,它应该在合适的命名空间里是唯一的。
  2. New :是一个构造函数,用于返回一个 caddy.Module ,示例中就是我们的 HelloWorld 结构体。

这里的ID是有特殊构成的,比如这里的http.handlers.hello_world,前面的http.handlers部分是命名空间,hello_world表示插件的名字。你可以把命名空间理解为一个父类,它提供了这个命名空间里一些通用的功能,而插件是子类,继承了命名空间的很多通用的能力。

通过命名空间可以知道我们的HelloWorld插件用于caddy http处理的部分。Caddy定义了很多不同的命名空间,用于不同的功能。

注册插件

插件开发好之后,需要注册才能使用。Caddy提供了 RegisterModule 方法注册一个可扩展的插件。

1
2
3
func init() {
   caddy.RegisterModule(&HelloWorld{})
}

通常,我们会把插件的注册放在 init 方法中,这样通过 import 关键字导入这个 package 的时候,就自动注册好了插件。 现在通过 xcaddy list-modules 就可以看到我们刚刚注册的插件了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
➜ xcaddy list-modules 
......
tls.stek.standard

  Standard modules: 83

http.handlers.hello_world

  Non-standard modules: 1

  Unknown modules: 0

可以看到,我们已经注册了一个非标准的插件(模块),它的ID是 http.handlers.hello_world

插件初始化

好了,现在我们注册好了一个HelloWorld插件,但是它还是一个空壳,并不能做任何事情。现在我们为 HelloWorld 增加一个字段,用于在网页上显示文本。

1
2
3
type HelloWorld struct {
   Text string `json:"text,omitempty"`
}

有了 Text 字段后,我们先初始化它,给他一个默认值Hello 世界

1
2
3
4
5
// Provision sets up the module.
func (h *HelloWorld) Provision(ctx caddy.Context) error {
   h.Text = "Hello 世界"
   return nil
}

看到以上代码中的 Provision 方法了吗?它其实是 caddy.Provisioner 接口的方法:

1
2
3
type Provisioner interface {
   Provision(Context) error
}

在整个插件生命周期中,用于做一些初始化的工作,比如示例中就把 h.Text 的值设置为 Hello 世界

插件校验

我们已经初始化好了插件,但是是否初始化正确?或者是否忘记了,基于这个问题,Caddy为我们提供了校验的机制,让我们可以检测插件是否正常。

1
2
3
4
5
6
7
// Validate validates that the module has a usable config.
func (h HelloWorld) Validate() error {
   if h.Text == "" {
      return errors.New("the text is must!!!")
   }
   return nil
}

以上示例用于校验 h.Text 的值不能为空,相信你已经猜到了 Validate 也是一个接口的方法,通过以上示例 HelloWorld 结构体其实实现了 caddy.Validator 接口:

1
2
3
type Validator interface {
   Validate() error
}

Validate 方法也是插件生命周期中的一部分,会在加载插件的时候,被Caddy自动调用,用于对插件进行校验。

除了初始化和校验之外,Caddy还提供了caddy.CleanerUpper接口,用于当插件不再需要时,执行清理工作,比如文件的关闭等。大家可以看下相关文档,这里不再讲述。

注册指令

虽然我们已经做了很多工作,但是插件还是不能用,因为没有被调用,现在我们通过注册一个可以在Caddyfile中使用的指令,来触发该插件的调用,让网页可以显示我们刚刚初始化的 Hello 世界 文字。

因为我们的插件 HelloWorld 是用于HTTP的,所以需要注册HTTP指令:

1
2
3
4
5
6
7
8
func init() {
   caddy.RegisterModule(&HelloWorld{})
   httpcaddyfile.RegisterHandlerDirective("hello_world", parseCaddyfile)
}
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
   hw := new(HelloWorld)
   return hw, nil
}

在Caddy中,可以通过 httpcaddyfile.RegisterHandlerDirective 函数注册一个Handler处理的指令,它的第一个参数 hello_world 是在Caddyfile中使用的指令名字,就和我们经常使用的 rootfile_server 是一样的。

第二个参数是一个 UnmarshalHandlerFunc 函数类型,它的原型如下所示:

1
UnmarshalHandlerFunc func(h Helper) (caddyhttp.MiddlewareHandler, error)

所以呢,从 parseCaddyfile 函数可以推测出,我们的 HelloWorld 结构体要实现 caddyhttp.MiddlewareHandler 接口才可以。

1
2
3
4
5
6
7
8
func (h *HelloWorld) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
   err := next.ServeHTTP(w, r)
   if err != nil {
      return err
   }
   w.Write([]byte(h.Text))
   return nil
}

如果你用过Gin框架的中间件,应该会觉得很熟悉,caddyhttp.MiddlewareHandler 也是一个中间件,用于处理HTTP的请求和相应。以上示例代码的目的是把 h.Text 写到响应中,这样打开网页,就可以看到 Hello 世界 了。

接口实现检测

从以上示例中,你可以看到我们的 HelloWorld 结构体实现了好几个接口,这些接口只有正确的实现,Caddy才能加载、配置并且校验好我们的插件,所以我们需要在编译期就让编译器帮我们检查好是否实现了这些接口。

1
2
3
4
5
6
// Interface guards
var (
   _ caddy.Provisioner           = (*HelloWorld)(nil)
   _ caddy.Validator             = (*HelloWorld)(nil)
   _ caddyhttp.MiddlewareHandler = (*HelloWorld)(nil)
)

这其实是个编程的小技巧,称之为『接口守卫』不止可以用在这个示例中,其他你的Go语言代码中也可以使用。如果以上接口都正确实现了, 编译器就可以通过,如果没有实现,我们是无法编译成功的。

配置Caddyfile

好了,一些准备就绪,相信你也迫不及待的想体验自己编写的插件了吧?在体验之前,我们先来配置好我们的Caddyfile:

1
2
3
4
5
6
{
  order hello_world last
}
localhost {
  hello_world
}

为了演示我们的插件,所以整个Caddyfile比较简单,只写了必要的配置。 首先我们看第一个全局配置 order 配置,它是用来设置HTTP处理器指令的顺序的,我们的 hello_world 是新增的HTTP处理器指令,需要为它定好顺序,这样Caddy的路由才知道如何使用它处理HTTP。我们这里配置它作为做最后一个,也就是 last

然后就是在Site模块中使用这个 hello_world 指令即可。

现在,在终端中输入如下命令,编译并启动caddy:

1
2
## 我的Caddyfile是放在桌面的,你根据自己的Caddyfile路径指定
➜ xcaddy run -config ~/Desktop/Caddyfile

然后打开浏览器,输入 https://localhost/ 就可以看到 Hello 世界 了。

小结

这篇文章,从0到1带你完整的开发好了一个Caddy插件,相信你已经学会了,并且你也了解了开发一个插件所需要实现的接口以及注册,接下来,就按照文章中的示例,实现一个简单的、自己的插件吧。

下一篇,将会结合Caddyfile配置,来实现可以显示任意文本的插件,欢迎关注我的公众号 飞雪无情 ,我们下篇见。

本文为原创文章,转载注明出处,欢迎扫码关注公众号flysnow_org或者网站 https://www.flysnow.org/ ,第一时间看后续精彩文章。觉得好的话,请猛击文章右下角「在看」,感谢支持。

扫码关注