Skip to content

升级指南

介绍

DANGER

  • 不建议将使用 Goyave v4 的应用程序升级到 v5。Goyave v5 是框架的完全重写版本,与旧版本有很大不同。升级到 v5 将耗时且容易出错。
  • 本升级指南可能不完整。如果在升级应用程序时遇到未涵盖的内容,请提交 pull request。

INFO

  • 建议在开始升级过程之前阅读新文档和 v5 发布说明。
  • 本指南将引导您完成最小升级路径。升级后,您的应用程序不会完全匹配新的推荐目录结构和架构,但会像以前一样继续工作。
  • 如果您有任何问题或需要帮助,请随时在 discord 上提问。

准备工作

将现有应用程序升级到 v5 可能是一个漫长的过程。建议首先逐步重构应用程序的某些方面,以使过渡更容易。

首先,您可以在项目中同时导入 v4 和 v5,因为它们的主要版本增加导致导入路径不同。这样您可以部分开始使用新的和更新的工具。

sh
go get -u goyave.dev/goyave/v5

包标识符相同,因此在导入 v5 时需要添加别名

go
import goyave5 "goyave.dev/goyave/v5"
import validation5 "goyave.dev/goyave/v5/validation"
//...

应用程序中每个等同于 Goyave v5 中组件的元素都应重构为具有构造函数的 struct。例如,构造函数不再是一组简单的函数。这样做也有助于下一步的依赖解耦。

控制器

go
// http/controller/user/user.go
func Show(response *goyave.Response, request *goyave.Request) {
	//...
}

变为:

go
// http/controller/user/user.go
type Controller struct{
	goyave5.Component
}

func NewController() *Controller {
	return &Controller{}
}

func (ctrl *Controller) Show(response *goyave.Response, request *goyave.Request) {
	//...
}

INFO

不要忘记更新路由注册器。

验证规则

go
// http/validation/validation.go
func validateCustom(ctx *validation.Context) bool {
	return false
}

变为:

go
// http/validation/custom.go
type CustomValidator struct{ validation5.BaseValidator }

func (v *CustomValidator) Validate(ctx *validation.Context) bool {
	return false
}

func (v *CustomValidator) Name() string { return "custom" }

func (v *CustomValidator) IsType() bool { return true }          // 仅在需要时
func (v *CustomValidator) IsTypeDependent() bool { return true } // 仅在需要时

func Custom() *CustomValidator {
	return &CustomValidator{}
}

注册规则的方式变化:

go
validation.AddRule("password", &validation.RuleDefinition{
	Function:           validatePassword,
	RequiredParameters: 0,
})

变为:

go
validator := Custom()
validation.AddRule(validator.Name(), &validation.RuleDefinition{
	Function:           validator.Validate(),
	RequiredParameters: 0,     // v5 切换后不再需要
	IsType:             validator.IsType(),
	IsTypeDependent:    validator.IsTypeDependent(),
	ComparesFields:     false, // v5 切换后不再需要
})

中间件

go
// http/middleware/custom.go
func CustomMiddleware(param, column string, model interface{}) goyave.Middleware {
	return func(next goyave.Handler) goyave.Handler {
		return func(response *goyave.Response, request *goyave.Request) {
			next(response, request)
		}
	}
}

变为:

go
// http/middleware/custom.go
type Custom struct {
	goyave5.Component
}

func (m *Custom) Handle(next goyave.Handler) goyave.Handler {
	return func(response *goyave.Response, request *goyave.Request) {
		next(response, request)
	}
}

路由定义变化如下:

go
router.Middleware(middleware.Custom)

变为:

go
router.Middleware((&middleware.Custom{}).Handle)

状态处理器

go
// http/controller/status/custom.go
func CustomStatusHandler(response *goyave.Response, request *goyave.Request) {
	//...
}

变为:

go
// http/controller/status/custom.go
type CustomStatusHandler struct {
	goyave5.Component
}

func (*CustomStatusHandler) Handle(response *Response, request *Request) {
	//...
}

路由定义变化如下:

go
router.StatusHandler(status.CustomStatusHandler)

变为:

go
router.Middleware((&status.CustomStatusHandler{}).Handle)

依赖解耦

下一步是从组件解耦全局依赖。

检查所有使用 goyave.dev/goyave/v4/databasegoyave.dev/goyave/v4/langgoyave.dev/goyave/v4/configgoyave.Logger / goyave.ErrLoggergoyave.AccessLogger 的地方。

对于语言,我们将创建一个简单的适配器,作为全局语言包的代理。由于其结构特性,我们将能够将其作为依赖项用于我们的组件。以后很容易用实际的 v5 语言实现替换:

go
// lang/lang.go
package lang

import glang "goyave.dev/goyave/v4/lang"

type Languages struct{}

func (l *Languages) Get(lang string, line string, placeholders ...string) string {
	return glang.Get(lang, line, placeholders...)
}

现在让我们将所有这些全局依赖项移动到结构体字段

go
type Controller struct{
	goyave5.Component
	db *gorm.DB
	lang *lang.Languages
	customConfigEntry string
}

func NewController(db *gorm.DB, lang *lang.Languages, customConfigEntry string) *Controller {
	return &Controller{
		db:                db,
		lang:              lang,
		customConfigEntry: customConfigEntry,
	}
}

INFO

  • 对于配置,建议仅传递实际值而不是配置对象。
  • 不要忘记更新之前使用组件的位置以及它们的初始化方式。

切换到 v5

  • 将导入 goyave.dev/goyave/v4 替换为 goyave.dev/goyave/v5 并删除之前定义的所有别名。
  • 删除之前实现的语言适配器。同时从组件依赖项中删除它。
  • 通过直接传递中间件和状态处理器而不是它们的 Handle 方法来更新路由定义。
  • 删除所有 validation.AddRule() 的使用。
  • 从您的组件中
    • 如果您有权访问 goyave.Request,请使用 request.Lang.Get() 而不是语言适配器。否则,只需将 lang 的使用替换为 Lang()(可通过 goyave.Component 组合访问)。
    • 您不一定需要删除 db 依赖项。但如果您想删除,现在可以从 component.DB() 访问它。
    • 确保您的组件已初始化。如果它们没有通过诸如 router.Controller()router.Middleware() 等方法传递给框架,它们可能不会被初始化,这将阻止它们访问服务器的资源。
  • 日志记录现在使用结构化日志。将 Println() 的使用替换为 Info()

初始化

  • 如果您的配置是手动加载的,它现在返回一个 *config.Config 和一个 error,而不仅仅是一个错误。
  • 创建一个新的 *goyave.Server,注册路由以及启动、关闭和信号钩子。
  • 使用 server.Start() 启动服务器。
  • *goyave.Error 已被移除。这意味着服务器不再返回退出代码。您可以使用您选择的退出代码。返回的退出代码并没有真正带来价值。日志中的错误消息更重要。
  • 服务器返回的错误现在总是 *errors.Error 类型。应该使用 Goyave *slog.Logger 记录这些错误,或者如果您还没有访问记录器,可以这样记录:
go
fmt.Fprintln(os.Stderr, err.(*errors.Error).String())

示例

go
goyave.RegisterStartupHook(func() {
	goyave.Logger.Println("Server is listening")
})
goyave.RegisterShutdownHook(func() {
	goyave.Logger.Println("Server is shutting down")
})

if err := goyave.Start(route.Register); err != nil {
	os.Exit(err.(*goyave.Error).ExitCode)
}

变为:

go
server, err := goyave.New(opts)
if err != nil {
	fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
	os.Exit(1)
}

server.Logger.Info("Registering hooks")
server.RegisterSignalHook()

server.RegisterStartupHook(func(s *goyave.Server) {
	server.Logger.Info("Server is listening", "host", s.Host())
})

server.RegisterShutdownHook(func(s *goyave.Server) {
	s.Logger.Info("Server is shutting down")
})

server.Logger.Info("Registering routes")
server.RegisterRoutes(route.Register)

if err := server.Start(); err != nil {
	server.Logger.Error(err)
	os.Exit(2)
}

配置

以下配置条目的更改可能会影响您的应用程序:

  • server.protocolserver.httpsPortserver.tls移除:协议仅为 http,因为 TLS/HTTPS 支持已被移除,因为 Goyave 应用程序大多数时候部署在代理后面。
  • server.timeout 已被拆分server.writeTimeoutserver.readTimeoutserver.idleTimeoutserver.readHeaderTimeoutserver.websocketCloseTimeout
  • server.maintenance移除
  • database 条目不再有默认值。它们之前使用 MySQL 的默认值。
  • 新条目 database.defaultReadQueryTimeoutdatabase.defaultWriteQueryTimeout 为数据库操作添加了超时机制。如果您有长查询,请增加它们的值。设置为 0 以禁用超时。
  • auth.jwt.rsa.password移除

请求

  • request.ToStruct() 被移除,使用 typeutil.Convert() 代替。
  • request.Data 现在是 any 而不是 map[string]any。您应该在使用前进行安全的类型断言。
  • 查询数据不再在 request.Data 中,现在拆分到 request.Query 中。
  • Request.Request().Context() 可以替换为 request.Context()
  • request.URI() 重命名为 request.URL()
  • 请求访问器如 Has()String()Numeric() 等都被移除。
  • request.CORSOptions() 被移除。您可以通过路由元数据访问 CORS 选项:request.Route.Meta[goyave.MetaCORS].(*cors.Options)

响应

  • response.HandleDatabaseError(db) 变为 !response.WriteDBError(err)
    • WriteDBError() 如果有错误则返回 true,并且您应该 return。这与 HandleDatabaseError 相反
  • response.Error()response.JSON()response.String() 等不再返回错误。
  • response.Redirect() 被移除。您可以使用 http.Redirect(response, request.Request(), url, http.StatusPermanentRedirect) 替换。
  • 模板渲染被移除。response.Render()response.RenderHTML() 不再可用。如果您使用它们,现在应该手动渲染并使用 response.Write()
  • response.GetError() 现在返回 *errors.Error 而不是 any
  • response.GetStacktrace() 被移除。您现在可以从错误本身访问堆栈跟踪。
  • response.File()response.Download() 现在将文件系统作为第一个参数。使用 &osfs.FS{} 以保持之前的行为。

错误处理

  • 尝试移除 panic 的使用。相反,让您的方法/函数返回错误,一直传递到 HTTP 处理程序,该处理程序将使用 response.Error()
  • 在返回错误的任何地方使用错误包装

路由

  • 路由注册器现在将服务器作为参数:func Register(server *goyave.Server, router *goyave.Router)
  • request.Params 变为 request.RouteParams
  • request.Route() 变为 request.Route
  • router.Route() 现在将字符串切片作为第一个参数,而不是管道分隔的方法列表。
  • route.Validate 现在拆分为两个:route.ValidateQuery()route.ValidateBody()
  • 路由算法略有变化,以防止两个前缀以相同字符开头的子路由器之间发生冲突。此外,当一个子路由器匹配但它的任何路由都不匹配时,不会检查其他子路由器(无法回头)。
  • 解析中间件不再是核心中间件。您需要将其添加为全局中间件:router.GlobalMiddleware(&parse.Middleware{})
  • router.Static() 现在将文件系统作为第一个参数,并返回生成的 *Route

数据库

  • database.View 被移除,因为在测试更改后它不再有任何用处。
  • 数据库初始化器被移除,在 server.New() 之后和 server.Start() 之前对数据库进行更改。
  • database.RegisterModel() 被移除,在移除自动迁移和测试更改后,它不再有任何用处。
  • 自动迁移被移除。如果您愿意,仍然可以使用 Gorm 的自动迁移,但不鼓励这样做。
  • database.Paginator 现在接受一个表示要分页的模型的泛型参数。
  • Gorm 实例现在使用 Goyave 记录器而不是默认的记录器。如果您调整了 Gorm 记录器,请确保更新您的实现,最好使用 *database.Logger

本地化

  • 数组元素的验证消息键已更改。将 .array 替换为 .element
  • 类型相关的验证器现在也支持 object 类型。
  • lang.Get(request.Lang) 变为 request.Lang.Getcomponent.Lang().Get()
  • fields.json 现在是 map[string]string。不再有带有 "name" 或 "rules" 的对象。

验证

  • 查询和正文的验证现在分为两部分:
    • route.ValidateQuery()route.ValidateBody()。其中任何一个的第一次调用都会自动将验证中间件添加到路由中。
    • 验证错误响应正文略有不同:
      • 键现在是 error 而不是 validationError,以与其余错误处理程序保持一致。
      • 以下是新格式的示例。高亮行显示了差异:
json
{
  "error": {
    "body": {
      "fields": {
        "user": {
          "fields": {
            "name": {
              "errors": ["名称不能超过 255 个字符。"]
            },
            "roles": {
              "errors": ["角色不能超过 2 项。"],
              "elements": {
                "2": {
                  "errors": ["角色元素必须具有以下值之一:viewer、admin、moderator。"]
                }
              }
            }
          },
          "errors": ["用户必须是一个对象"]
        }
      }
    },
    "query": {
      "fields": {
        "group": {
          "errors": ["组 ID 是必需的。"]
        }
      }
    }
  }
}
  • validation.Validatevalidation.ValidateWithExtra 变为 validation.Validate(opts)
    • validation.Options 包含几个选项,以及外部依赖项,如语言、数据库、配置等。这些将传递给验证器,以便它们可以像任何常规组件一样访问它们。
    • isJSON 变为 ConvertSingleValueArrays,它做同样的事情,但逻辑不同,因此布尔值将相反。
    • 此函数现在还返回一个错误切片。这些不是验证错误,而是实际的执行错误。
go
data := map[string]any{
	"string": "hello world",
    "number": 42,
}

ruleSet := validation.RuleSet{
	"string": validation.List{"required", "string"},
	"number": validation.List{"required", "numeric", "min:10"},
}

errors := validation.Validate(data, ruleSet, true, request.Lang)

变为:

go
// 这里的 "ctrl" 是一个组件

ruleSet := validation.RuleSet{
	{Path: validation.CurrentElement, Rules: validation.List{validation.Required(), validation.Object()}},
	{Path: "string", Rules: validation.List{validation.Required(), validation.String()}},
	{Path: "number", Rules: validation.List{validation.Required(), validation.Float64(), validation.Min(10)}},
}

opt := &validation.Options{
	Data:                     data,
	Rules:                    ruleSet,
	Now:                      request.Now,
	ConvertSingleValueArrays: false,
	Language:                 request.Lang,
	DB:                       ctrl.DB().WithContext(request.Context()),
	Config:                   ctrl.Config(),
	Logger:                   ctrl.Logger(),
	Extra:                    map[any]any{},
}
validationErrors, errs := validation.Validate(opt)
  • 验证不再像以前那样总是在最后执行。现在它按照与常规中间件相同的排序规则执行:您现在可以选择验证在中间件堆栈中何时发生。确保在权限/认证中间件之后调用 ValidateBody/ValidateQuery
  • 字段验证的顺序现在是有保证的,并且可以由开发人员控制。确保您的规则集或自定义规则不依赖于需要转换(float64/int)的其他字段。相应地调整验证顺序或您的自定义规则。
  • PostValidationHooks 被移除。使用在验证中间件之后执行的中间件来获得相同的效果。
  • 正在验证的数据的根元素现在可以是任何东西,不一定是对象。确保在所有路径 validation.CurrentElement 的请求上添加 Object() 验证器。
  • 一些结构被重命名,以考虑到根元素并不总是对象这一事实:
    • validation.Errors 现在是 validation.FieldsErrors
    • validation.FieldErrors 现在是 validation.Errors
  • 验证 "rule" 现在命名为 "validator"。

自定义规则

  • ctx.Data 现在是 any 而不是 map[string]any。如果需要,请使用安全的类型断言。
  • ctx.Extra["request"] 变为 ctx.Extra[validation.ExtraRequest{}]
  • ctx.Extra 不再仅限于当前验证器。在 Options.Extra 中给出的相同引用在所有验证器之间共享。确保您的自定义验证器不会冲突。
  • 验证器实例意味着重用。确保不要在验证器内持久化任何数据。
  • ctx.Valid() 变为 ctx.Invalid(因此布尔值被反转)。
  • ctx.Rule 被移除。ctx.Rule.Params 变为验证器结构字段。值被传递给验证器构造函数。
  • 不要在验证器内部 panic。如果您需要报告错误,请使用 Context.AddError()
go
func validateCustom(ctx *validation.Context) bool {
	if !ctx.Valid() {
		return false
	}

	value, err := strconv.ParseInt(ctx.Rule.Params[0], 10, 64)
	if err != nil {
		panic(err)
	}

	ok, err := checkValue(ctx.Value, value)
	if err != nil {
		panic(err)
	}
	return ok
}

变为:

go
type CustomValidator struct{
	validation.BaseValidator
	Value int
}

func (v *CustomValidator) Validate(ctx *validation.Context) bool {
	ok, err := checkValue(ctx.Value)
	if err != nil {
		ctx.AddError(errors.New(err))
		return false
	}
	return false
}

func Custom(value int) *CustomValidator {
	return &CustomValidator{
		Value: value
	}
}
  • 验证中占位符的概念已更改:
    • 占位符不再是全局函数(替换函数):validation.SetPlaceholder() 被移除。所有内置占位符因此也被移除。除非您按照下面的说明将它们添加回来,否则您的一些自定义规则的验证错误消息可能会损坏。
    • :field 占位符保持不变。
    • 每个验证器返回自己的占位符关联切片,并实现 MessagePlaceholders(*validation.Context) []string
go
validation.SetPlaceholder("value", func(fieldName, language string, ctx *validation.Context) string {
	return ctx.Rule.Params[0]
})

变为:

go
func (v *CustomValidator) MessagePlaceholders(_ *validation.Context) []string {
	return []string{
		":value", strconv.Itoa(v.Value),
	}
}

规则集

  • 规则集的定义已完全更改。
    • 规则集不再意味着重用。应该为每个请求生成一个新的规则集。这就是为什么 route.ValidateBody()route.ValidateQuery()函数作为参数:func(*Request) validation.RuleSet
    • 规则(现在称为 "validators")现在是结构实例。它们不再由字符串标识。
    • 规则集不再有任何替代语法。
    • 规则集是切片。验证的顺序是函数返回的切片的顺序。唯一的例外是数组元素,它们总是在其父元素之前执行,以便可以递归地验证数组并正确转换。
    • validation.CurrentElement 现在在任何地方都有效,甚至在组合之外。正在验证的根元素可以是任何东西,不一定是对象。每个规则集都应包含一些用于 validation.CurrentElement 路径的验证器。
  • 验证规则的文件约定从 request.go 更改为 validation.go
  • 使用组合时,组合规则集中的验证器将相对于元素执行。ctx.Data 将不等于根数据,而是等于与组合规则集链接的父元素。这会影响比较规则,其比较路径现在将是相对的
go
// http/controller/request.go
var (
	InsertRequest validation.RuleSet = validation.RuleSet{
		"email":    validation.List{"required", "string", "email", "between:3,100", "unique:users"},
		"username": validation.List{"required", "string", "between:3,100", "unique:users"},
		"image":    validation.List{"nullable", "file", "image", "max:2048", "count:1"},
		"password": validation.List{"required", "string", "between:6,100"},
	}
)

变为:

go
// http/controller/validation.go
import (
	"gorm.io/gorm"
	"goyave.dev/goyave/v5"
	v "goyave.dev/goyave/v5/validation"
)

func InsertRequest(_ *goyave.Request) v.RuleSet {
	return v.RuleSet{
		{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
		{Path: "email", Rules: v.List{
			v.Required(), v.String(), v.Email(), v.Between(3, 100),
			v.Unique(func(db *gorm.DB, val any) *gorm.DB {
				return db.Table("users").Where("email", val)
			}),
		}},
		{Path: "username", Rules: v.List{
			v.Required(), v.String(), v.Between(3, 100),
			v.Unique(func(db *gorm.DB, val any) *gorm.DB {
				return db.Table("users").Where("username", val)
			}),
		}},
		{Path: "image", Rules: v.List{
			v.Required(), v.File(), v.Image(), v.Max(2048), v.FileCount(1),
		}},
		{Path: "password", Rules: v.List{
			v.Required(), v.String(), v.Between(6,100),
		}},
	}
}

规则

如前所述,"rules" 现在命名为 "validators"。其中一些发生了显著变化,但大多数保持了相同的行为。

  • 数字规则现在让您选择确切的 Go 类型。新的验证器将自动检查输入值是否适合相应的类型。
    • integer 变为:Int()Int8()Int16()Int32()Int64()Uint()Uint8()Uint16()Uint32()Uint64()
    • numeric 变为:Float32()Float64()
  • Array() 不再有类型参数。要验证数组元素,请添加一个匹配数组元素的路径条目。
  • Size() 验证器及其衍生品,如 Min()Max() 等,现在也适用于对象,并将验证其键的数量。

认证

  • 认证中间件现在使用路由元数据来识别路由是否需要认证。您现在可以将认证中间件用作全局中间件,并使用 SetMeta(auth.MetaAuth, true) 标记每个路由器或路由。
  • 认证器中间件现在接受一个泛型参数,代表认证用户的 DTO。
  • 认证器现在使用构造函数并依赖于服务。如果您像这样初始化它们 &auth.JWTAuthenticator{},您现在应该使用 auth.NewJWTAuthenticator(userService)
  • 结构标签 auth:"username"auth:"password" 不再使用。认证现在使用用户服务。有关详细信息,请参阅认证文档
  • auth.FindColumns 被移除。
  • 自定义认证器现在接受一个代表认证用户 DTO 的泛型参数,以及一个允许它们获取用户的用户服务参数。有关详细信息,请参阅认证文档
  • 对受密码保护的 RSA 密钥的支持已被放弃。

Websockets

  • Websockets 现在使用带有控制器接口的 New()。Websocket 控制器现在应该是实现方法 Serve(*websocket.Conn, *goyave.Request) error组件
  • 以下 websocket 选项现在是 websocket 控制器可以实现的接口:UpgradeErrorHandlerErrorHandlerCheckOriginHeaders
  • 有关详细信息,请参阅 websocket 文档

测试

  • 新的 testutil 包包含测试实用程序。
  • 您不再被迫使用 testify。您现在可以使用您选择的测试框架。
  • goyave.TestSuite 被移除。
    • 您可以在不使用套件的情况下生成测试请求和响应,使用 testutil.NewTestRequest()testutil.NewTestResponse()
    • 使用 testutil.NewTestServer()
    • 不再需要运行服务器(并监听端口)来测试您的路由。有关详细信息,请参阅测试文档
    • GOYAVE_ENV 环境变量不再自动设置。
    • 工作目录不再自动更改。
    • 测试现在可以安全地并行运行。
  • database.Factory 现在接受一个表示要生成的模型的泛型参数。生成器函数现在应该返回实际模型类型的指针,而不是 any

示例

go
suite.RunServer(route.Register, func() {
	resp, err := suite.Get("/hello", nil)
	suite.Nil(err)
	suite.NotNil(resp)
	if resp != nil {
		defer resp.Body.Close()
		suite.Equal(200, resp.StatusCode)
		suite.Equal("Hi!", string(suite.GetBody(resp)))
	}
})

变为:

go
server := testutil.NewTestServerWithOptions(t, goyave.Options{Config: config.LoadDefault()})
server.RegisterRoutes(route.Register)

request := httptest.NewRequest(http.MethodGet, "/hello", nil)
response := server.TestRequest(request)
defer response.Body.Close()
// 断言

杂项

  • goyave.dev/filter 库已更新,部分设计已更改。
    • DefaultSort 现在是一个选项。使用它而不是在过滤之前更改请求的数据/查询。
    • Scope() 现在使用 *filter.Request 而不是直接从 HTTP 请求读取。使用 filter.NewRequest(request.Query) 创建一个。
    • Scope() 现在返回一个错误而不是数据库实例结果。
    • filter 查询字段现在始终是一个 []string 切片(不是 string)。
    • 验证错误消息名称添加了 "goyave-filter-" 前缀。如果您覆盖了这些消息,请确保更新条目的名称。
  • fsutil.File.Data 被移除。您应该打开并读取 fsutil.File.Header.Open()
  • fsutil 包中的函数现在将文件系统作为参数。
  • util/walk
    • walk.Path.Walk() 回调现在接受一个指针 *walk.Context
  • 通用和组合访问记录器现在输出到结构化记录器。如果您有解析日志的服务,应相应更新。
  • goyave.Loggergoyave.ErrLoggergoyave.AccessLogger 被移除。如果您使用自定义记录器,请确保它支持结构化日志记录,并替换 server.Logger
  • Gzip 中间件被移除,并由 compress.Middleware 替换。有关其新用法的信息,请参阅文档
  • util/reflectutil 包被移除。
  • util/sliceutil 包被移除。使用 samber/lo 代替。
  • util/typeutil 的函数 ToFloat64()ToString() 被移除。
  • goyave.BaseURL()goyave.ProxyBaseURL() 被移除。使用 server.BaseURL()server.ProxyBaseURL() 代替。
  • goyave.GetRoute() 被移除。使用 router.GetRoute() 代替。可以通过 request.Route.GetParent() 从请求中检索路由器。
  • goyave.EnableMaintenance()goyave.DisableMaintenance()goyave.IsMaintenanceEnabled() 被移除。
  • CORS 中间件现在是全局的,意味着它在请求的生命周期中更早执行。