Caddy 因其模块化架构而易于扩展。大多数类型的 Caddy 扩展(或插件)被称为模块,如果它们扩展或插入到 Caddy 的配置结构中。需要明确的是,Caddy 模块与 Go 模块是不同的(尽管它们也是 Go 模块)。
快速入门
Caddy 模块是任何在导入其包时注册为 Caddy 模块的命名类型。关键在于,模块总是实现了 caddy.Module
接口,该接口提供了它的名称和构造函数。
在新的 Go 模块中,将以下模板粘贴到 Go 文件中,并自定义你的包名、类型名和 Caddy 模块 ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package mymodule import "github.com/caddyserver/caddy/v2" func init() { caddy.RegisterModule(Gizmo{}) }
type Gizmo struct { }
func (Gizmo) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "foo.gizmo", New: func() caddy.Module { return new(Gizmo) }, } } ``` 然后从你的项目目录运行以下命令,你应该会在列表中看到你的模块:
|
xcaddy list-modules
…
foo.gizmo
…
1 2 3 4 5 6
| 恭喜,你的模块已经注册到 Caddy 中,并且可以在 Caddy 的配置文件中使用,只要它处于相同命名空间的任何地方。 在底层,xcaddy 实际上是创建了一个新的 Go 模块,该模块需要 Caddy 和你的插件(使用适当的 replace 来使用你的本地开发版本),然后添加一个导入以确保它被编译:
|
import _ “github.com/example/mymodule”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| ## 模块基础 Caddy 模块: 1. 实现 caddy.Module 接口以提供 ID 和构造函数。 1. 在适当的命名空间中具有唯一名称。 1. 通常满足某些对宿主模块有意义的接口。 **宿主模块** (或父模块)是加载/初始化其他模块的模块。它们通常为客模块定义命名空间。 **客模块** (或子模块)是被加载或初始化的模块。所有模块都是客模块。 ## 模块 ID 每个 Caddy 模块都有一个唯一的 ID,由命名空间和名称组成: - 完整的 ID 看起来像 foo.bar.module_name 。 - 命名空间是 foo.bar 。 - 名称是 module_name ,它必须在其命名空间中是唯一的。 模块 ID 必须使用 snake_case 约定。 ### 命名空间 命名空间类似于类,即命名空间定义了该命名空间中所有模块的共同功能。例如,我们可以期望 http.handlers 命名空间中的所有模块都是 HTTP 处理程序。因此,宿主模块可能会将命名空间中的客模块从 interface{} 类型断言为更具体、更有用的类型,例如 caddyhttp.MiddlewareHandler 。 客模块必须正确命名空间,以便宿主模块能够识别它,因为宿主模块会要求 Caddy 提供某个命名空间中的模块,以提供宿主模块所需的特定功能。例如,如果你编写了一个名为 gizmo 的 HTTP 处理程序模块,你的模块名称将是 http.handlers.gizmo ,因为 http 应用程序会在 http.handlers 命名空间中查找处理程序。 换句话说,Caddy 模块被期望根据其模块命名空间实现某些接口。按照这种约定,模块开发者可以说一些直观的话,例如:“http.handlers 命名空间中的所有模块都是 HTTP 处理程序。”更技术地说,这通常意味着:“http.handlers 命名空间中的所有模块都实现了 caddyhttp.MiddlewareHandler 接口。”因为方法集是已知的,所以可以断言并使用更具体的类型。 **查看将所有标准 Caddy 命名空间映射到其 Go 类型的表格。** caddy 和 admin 命名空间是保留的,不能用作应用名称。 要编写插入第三方宿主模块的模块,请查阅这些模块的命名空间文档。 ### 名称 命名空间内的名称很重要,对用户也很明显,但只要它是唯一的、简洁的,并且符合它的功能,它就不那么重要。 ## 应用模块 应用是具有空命名空间的模块,并且按照惯例成为它们自己的顶级命名空间。应用模块实现了 caddy.App 接口。 这些模块出现在 Caddy 配置的顶级 "apps" 属性中:
|
{
“apps”: {}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 示例应用是 http 和 tls 。它们的命名空间是空的。 为这些应用编写的客模块应该使用从应用名称派生的命名空间。例如,HTTP 处理程序使用 http.handlers 命名空间,TLS 证书加载器使用 tls.certificates 命名空间。 ## 模块实现 模块可以是几乎任何类型,但结构体是最常见的,因为它们可以保存用户配置。 ### 配置 大多数模块需要一些配置。只要你的类型与 JSON 兼容,Caddy 就会自动处理这些配置。因此,如果模块是结构体类型,它将需要在其字段上使用结构体标签,这些标签应该使用 Caddy 约定的 snake_case :
|
type Gizmo struct {
MyField string json:"my_field,omitempty"
Number int json:"number,omitempty"
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| 在结构体标签中使用 omitempty 选项将在 JSON 输出中省略该字段,如果它是其类型的零值。这有助于在将 JSON 配置从 Caddyfile 转换为 JSON 时保持 JSON 配置的简洁。 当模块被初始化时,它已经填充了其配置。也可以在模块初始化后执行额外的配置和验证步骤。 ### 模块生命周期 模块的生命周期从它被宿主模块加载时开始。以下是发生的情况: 1. 调用 New() 方法以获取模块值的实例。 1. 将模块的配置反序列化到该实例中。 1. 如果模块是 caddy.Provisioner ,则调用 Provision() 方法。 1. 如果模块是 caddy.Validator ,则调用 Validate() 方法。 1. 此时,宿主模块将得到已加载的客模块作为 interface{} 类型的值,因此宿主模块通常会将客模块断言为更有用的类型。请查阅宿主模块的文档,了解其命名空间中的客模块需要满足什么要求,例如需要实现哪些方法。 1. 当模块不再需要时,如果它是 caddy.CleanerUpper ,则调用 Cleanup() 方法。 请注意,在给定时间,你的模块的多个加载实例可能会重叠!在配置更改期间,新模块会在旧模块停止之前启动。在使用全局状态时要格外小心。使用 caddy.UsagePool 类型来帮助管理模块加载之间的全局状态。如果你的模块监听一个套接字,请使用 caddy.Listen*() 来获取支持重叠使用的套接字。 ### 配置 模块的配置将自动反序列化到其值中(当加载 JSON 配置时)。这意味着,例如,结构体字段将为你填充。 然而,如果模块需要额外的配置步骤,你可以实现可选的 caddy.Provisioner 接口:
|
// Provision 设置模块。
func (g *Gizmo) Provision(ctx caddy.Context) error {
// TODO: 设置模块
return nil
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 这是设置用户未提供的字段的默认值的地方(不是其类型的零值的字段)。如果字段是必需的,如果它未设置,你可以返回一个错误。对于具有零值含义的数字字段(例如,某些超时持续时间),你可能希望支持 -1 表示“关闭”,而不是 0 ,因此如果你的用户未配置它,你可以设置一个默认值。 这也是宿主模块通常加载它们的客/子模块的地方。 模块可以通过调用 ctx.App() 访问其他应用,但模块不能有循环依赖。换句话说,由 http 应用程序加载的模块不能依赖于 tls 应用程序,如果由 tls 应用程序加载的模块依赖于 http 应用程序。(这与 Go 中禁止导入循环的规则非常相似。) 此外,在 Provision 中应避免执行昂贵的操作,因为即使配置仅被验证,也会执行配置。在配置阶段,不要期望模块实际上会被使用。 #### 日志 了解 Caddy 中的日志记录方式。如果模块需要日志记录,请不要使用 Go 标准库中的 log.Print*() 。换句话说,**不要使用 Go 的全局日志记录器** 。Caddy 使用高性能、高度灵活的结构化 日志记录,使用 zap。 要在模块的 Provision 方法中发出日志,请获取一个日志记录器:
|
func (g *Gizmo) Provision(ctx caddy.Context) error {
g.logger = ctx.Logger() // g.logger 是一个 *zap.Logger
}
1 2 3 4 5 6 7
| 然后你可以使用 g.logger 发出结构化、分级的日志。有关详细信息,请参阅 zap 的 godoc。 ### 验证 希望验证其配置的模块可以通过实现可选的 caddy.Validator 接口来实现:
|
// Validate 验证模块是否具有可用的配置。
func (g Gizmo) Validate() error {
// TODO: 验证模块的设置
return nil
}
1 2 3 4 5 6 7 8
| Validate 应该是一个只读函数。它在 Provision() 方法之后运行。 ### 接口保护 Caddy 模块的行为是隐式的,因为 Go 接口是隐式满足的。只需在模块的类型中添加正确的方法,就可以使模块正确或错误地工作。因此,打错字或方法签名错误可能导致意外的(缺乏)行为。 幸运的是,有一个简单、无开销、编译时检查可以添加到代码中,以确保你添加了正确的方法。这些称为接口保护:
|
var _ InterfaceName = (*YourType)(nil)
1 2 3 4 5 6
| 将 InterfaceName 替换为你打算满足的接口,将 YourType 替换为你的模块类型的名称。 例如,像静态文件服务器这样的 HTTP 处理程序可能满足多个接口:
|
// 接口保护
var (
_ caddy.Provisioner = (*FileServer)(nil)
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
)
1 2 3 4 5 6 7 8
| 如果没有接口保护,可能会出现令人困惑的错误。例如,如果模块在使用之前需要进行配置,但你的 Provision() 方法有错误(例如拼写错误或签名错误),则不会进行配置,导致令人困惑的问题。接口保护很容易添加,并且可以防止这种情况。它们通常放在文件的底部。 ## 宿主模块 当模块加载自己的客模块时,它就成为了一个宿主模块。如果模块的功能可以以不同的方式实现,这是很有用的。 宿主模块几乎总是一个结构体。通常,支持客模块需要两个结构体字段:一个用于保存其原始 JSON,另一个用于保存其解码后的值:
|
type Gizmo struct {
GadgetRaw json.RawMessage json:"gadget,omitempty" caddy:"namespace=foo.gizmo.gadgets inline_key=gadgeter"
Gadget Gadgeter json:"-"
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 第一个字段(在这个例子中是 GadgetRaw )是原始的、未配置的 JSON 形式的客模块所在的地方。 第二个字段(Gadget )是最终配置后的值将被存储的地方。由于第二个字段不是面向用户的,我们使用结构体标签将其从 JSON 中排除。(如果你不需要其他包使用它,你也可以将其设为未导出字段,那么就不需要结构体标签了。)
原始模块字段上的 caddy 结构体标签帮助 Caddy 知道要加载的模块的命名空间和名称(组成完整的 ID)。它还用于生成文档。 结构体标签的格式非常简单:key1=val1 key2=val2 ... 对于模块字段,结构体标签看起来像:
|
caddy:"namespace=foo.bar inline_key=baz"
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| namespace= 部分是必需的。它定义了要查找模块的命名空间。 inline_key= 部分仅在模块的名称将与模块本身**内联** 时使用;这意味着值是一个对象,其中一个键是**内联键** ,其值是模块的名称。如果省略,则字段类型必须是 caddy.ModuleMap 或 []caddy.ModuleMap ,其中映射的键是模块名称。 ### 加载客模块 在配置阶段调用 ctx.LoadModule() 来加载客模块:
|
// Provision 设置 g 并加载其小工具。
func (g *Gizmo) Provision(ctx caddy.Context) error {
if g.GadgetRaw != nil {
val, err := ctx.LoadModule(g, “GadgetRaw”)
if err != nil {
return fmt.Errorf(“loading gadget module: %v”, err)
}
g.Gadget = val.(Gadgeter)
}
return nil
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 注意,LoadModule() 调用接受一个指向结构体的指针和字段名作为字符串。这看起来很奇怪,不是吗?为什么不直接传递结构体字段呢?这是因为根据配置的布局,有几种不同的方式可以加载模块。这种方法签名允许 Caddy 使用反射来确定加载模块的最佳方式,并且最重要的是,读取其结构体标签。 如果客模块必须明确由用户设置,你应该在尝试加载它之前返回一个错误,如果 Raw 字段为 nil 或为空。 注意如何断言加载的模块:g.Gadget = val.(Gadgeter) — 这是因为返回的 val 是一个 interface{} 类型,这并不太有用。然而,我们期望在声明的命名空间中(在我们的例子中是 foo.gizmo.gadgets ,来自结构体标签)的所有模块都实现了 Gadgeter 接口,因此这种类型断言是安全的,然后我们就可以使用它了! 如果你的宿主模块定义了一个新的命名空间,请确保像我们在这里所做的那样为开发者记录该命名空间及其 Go 类型。 ## 模块文档 注册模块以使新的 Caddy 模块出现在模块文档中,并可在 http: 中使用。注册可在 http: 进行。如果你还没有账户,请创建一个新账户,然后点击“注册包”。 ## 完整示例 假设我们想编写一个 HTTP 处理程序模块。这将是一个演示用途的中间件,它会在每次 HTTP 请求时将访问者的 IP 地址打印到一个流中。 我们还希望它可以通过 Caddyfile 配置,因为大多数人在非自动化情况下更喜欢使用 Caddyfile。我们通过注册一个 Caddyfile 处理程序指令来实现这一点,这是一种可以向 HTTP 路由添加处理程序的指令。我们还实现了 caddyfile.Unmarshaler 接口。通过添加这几行代码,这个模块可以通过 Caddyfile 配置!例如:visitor_ip stdout 。 以下是该模块的代码,带有解释性注释:
|
package visitorip
import (
“fmt”
“io”
“net/http”
“os”
“github.com/caddyserver/caddy/v2”
“github.com/caddyserver/caddy/v2/caddyconfig/caddyfile”
“github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile”
“github.com/caddyserver/caddy/v2/modules/caddyhttp”
)
func init() {
caddy.RegisterModule(Middleware{})
httpcaddyfile.RegisterHandlerDirective(“visitor_ip”, parseCaddyfile)
}
// Middleware 实现了一个 HTTP 处理程序,它将访问者的 IP 地址写入文件或流。
type Middleware struct {
// 要写入的文件或流。可以是 “stdout”
// 或 “stderr”。
Output string json:"output,omitempty"
w io.Writer
}
// CaddyModule 返回 Caddy 模块信息。
func (Middleware) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: “http.handlers.visitor_ip”,
New: func() caddy.Module { return new(Middleware) },
}
}
// Provision 实现了 caddy.Provisioner。
func (m *Middleware) Provision(ctx caddy.Context) error {
switch m.Output {
case “stdout”:
m.w = os.Stdout
case “stderr”:
m.w = os.Stderr
default:
return fmt.Errorf(“需要一个输出流”)
}
return nil
}
// Validate 实现了 caddy.Validator。
func (m Middleware) Validate() error {
if m.w == nil {
return fmt.Errorf(“没有写入器”)
}
return nil
}
// ServeHTTP 实现了 caddyhttp.MiddlewareHandler。
func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
m.w.Write([]byte(r.RemoteAddr))
return next.ServeHTTP(w, r)
}
// UnmarshalCaddyfile 实现了 caddyfile.Unmarshaler。
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // 消耗指令名称
// 需要一个参数
if !d.NextArg() {
return d.ArgErr()
}
// 保存参数
m.Output = d.Val()
return nil
}
// parseCaddyfile 从 h 中反序列化标记到新的 Middleware。
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var m Middleware
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
}
// 接口保护
var (
_ caddy.Provisioner = (*Middleware)(nil)
_ caddy.Validator = (*Middleware
)(nil)
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
_ caddyfile.Unmarshaler = (*Middleware)(nil)
)
