ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手写 Go Web 框架:Beego REST 风格控制器基类设计与请求生命周期全解析

手写 Go Web 框架:Beego REST 风格控制器基类设计与请求生命周期全解析 文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载本篇以《Build Web Application with Golang》第 13.3 节en/13.3.md为核心围绕从零设计一个 REST 风格 MVC 控制器展开先厘清控制器在 MVC 中的角色再逐行拆解 Beego 控制器基类的struct interface双层设计、各方法默认实现以及Init → Prepare → method → Render → Finish的完整调用链最后通过继承基类编写MainController与index.tpl用最少代码输出 Hello, world。读完本文你将掌握该仓库第 13 章自研框架中控制器的设计思路并理解模板渲染与路由分发之间的底层联动。控制器在 MVC 中的角色MVCModel-View-Controller设计模式是当前 Web 应用中使用最广泛的框架模型。通过将 Model、View、Controller 三者分离可以让 Web 应用保持模块化、可维护、可测试、可扩展Model模型封装数据以及支配数据的一切业务逻辑例如访问权限规则、持久化、数据校验等。View视图负责数据的呈现。在 Web 应用场景中视图通常表现为模板被渲染为 HTML 后返回给客户端。Controller控制器Model 与 View 之间的胶水逻辑典型地拥有处理不同 URL 的方法。当路由把 URL 请求转发给控制器后控制器委托 Model 执行某些动作再通知 View 反映数据变化。需要特别强调的是并非所有请求都需要 Model 做逻辑处理或 View 做渲染。例如处理 HTTP 302 重定向时既不需要渲染视图也不需要 Model 参与计算但控制器的职责依然不可或缺。这正是一个轻量控制器基类需要同时覆盖渲染型与重定向型两类场景的原因——Render()是可选执行的方法而Redirect()则是独立封装的辅助能力。在本书第 13 章的整体规划中en/13.0.md、en/13.1.md控制器层是整个框架的核心枢纽上一节 13.2 自定义路由器 已经完成了把 URL 映射到控制器结构体的工作本节则要设计控制器本身。为什么需要 REST 风格 MVC 框架大多数传统 MVC 框架基于后缀动作映射suffix action mapping例如/user/add、/user/delete这类按动作后缀划分 URL 的方式。如今 REST 风格的 Web 架构越来越流行虽然可以通过过滤filter或重写rewriteURL 来实现 REST 风格地址但为什么不去直接设计一个 REST 风格的全新 MVC 框架呢本节的设计目标因此非常明确设计并实现一个基于控制器、REST 风格、从零构建的 MVC 框架用以简化 Web 应用开发甚至希望达到一行代码输出 Hello, world的效果。这种方法即 HTTP 动词的映射恰好与上一节路由器的设计一脉相承——路由器按请求方法GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS将请求分发到控制器对应的同名方法上。Beego 控制器基类的双层设计struct interface设计逻辑控制器基类需要两个组成部分一个结构体承载状态和一个接口类型定义契约。Controller struct承载请求上下文与渲染状态type Controller struct { Ct *Context Tpl *template.Template Data map[interface{}]interface{} ChildName string TplNames string Layout []string TplExt string }各字段的作用与默认状态如下字段类型作用默认状态由Init设置Ct*Context当前请求上下文封装ResponseWriter、Request以及路由参数Params由路由通过反射注入Tpl*template.Template模板对象基类中预留未显式初始化实际渲染路径主要使用ParseFiles动态解析Datamap[interface{}]interface{}注入模板的数据集合初始化为空 mapChildNamestring子控制器类型名用于推导默认模板名由路由反射传入类型名TplNamesstring模板文件名初始化为空字符串Layout[]string布局模板文件列表初始化为空切片TplExtstring模板扩展名固定为tplControllerInterface定义控制器生命周期的契约type ControllerInterface interface { Init(ct *Context, cn string) //Initialize the context and subclass name Prepare() //some processing before execution begins Get() //method GET processing Post() //method POST processing Delete() //method DELETE processing Put() //method PUT handling Head() //method HEAD processing Patch() //method PATCH treatment Options() //method OPTIONS processing Finish() //executed after completion of treatment Render() error //method executed after the corresponding method to render the page }这个接口之所以关键是因为上一节路由器注册路由时要求处理器必须是ControllerInterface类型见 13.2 中的Add(pattern string, c ControllerInterface)。只要子类实现了该接口就能直接使用基类控制器的全部方法并被注册进路由表。接口的每个方法都对应请求生命周期中的一环稍后详解。基类方法实现从初始化到渲染的每个细节Init上下文注入与默认状态func (c *Controller) Init(ct *Context, cn string) { c.Data make(map[interface{}]interface{}) c.Layout make([]string, 0) c.TplNames c.ChildName cn c.Ct ct c.TplExt tpl }Init是每个请求的必经入口它把路由解析出的Context含ResponseWriter、Request与路由参数Params注入控制器同时完成内部状态的初始化——Data与Layout被重置TplExt固定为tpl。参数cnChildName由路由层传入当前控制器的类型名为后续默认模板名推导做好准备。Prepare 与 Finish可覆写的空钩子func (c *Controller) Prepare() { } func (c *Controller) Finish() { }Prepare()在业务方法执行前被调用Finish()在执行完成后被调用二者在基类中均为空实现。继承者可以根据需要覆写它们例如在Prepare中做登录校验、权限检查在Finish中做资源释放或统计埋点。默认 HTTP 方法处理405 Method Not Allowedfunc (c *Controller) Get() { http.Error(c.Ct.ResponseWriter, Method Not Allowed, 405) } func (c *Controller) Post() { http.Error(c.Ct.ResponseWriter, Method Not Allowed, 405) } func (c *Controller) Delete() { http.Error(c.Ct.ResponseWriter, Method Not Allowed, 405) } func (c *Controller) Put() { http.Error(c.Ct.ResponseWriter, Method Not Allowed, 405) } func (c *Controller) Head() { http.Error(c.Ct.ResponseWriter, Method Not Allowed, 405) } func (c *Controller) Patch() { http.Error(c.Ct.ResponseWriter, Method Not Allowed, 405) } func (c *Controller) Options() { http.Error(c.Ct.ResponseWriter, Method Not Allowed, 405) }七个 HTTP 方法在基类中全部默认返回405 Method Not Allowed。需要提醒的是本节原文档的文字部分曾把这一默认行为写作默认返回 403 Forbidden但基类源码实际使用的是http.Error 状态码405Method Not Allowed阅读与引用时应以源码为准。这样的设计意味着子类只需要覆写自己支持的方法未覆写的方法自动对客户端返回 405从而天然实现了 REST 语义下的方法约束。Render两种渲染路径与模板命名规则func (c *Controller) Render() error { if len(c.Layout) 0 { var filenames []string for _, file : range c.Layout { filenames append(filenames, path.Join(ViewsPath, file)) } t, err : template.ParseFiles(filenames...) if err ! nil { Trace(template ParseFiles err:, err) } err t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data) if err ! nil { Trace(template Execute err:, err) } } else { if c.TplNames { c.TplNames c.ChildName / c.Ct.Request.Method . c.TplExt } t, err : template.ParseFiles(path.Join(ViewsPath, c.TplNames)) if err ! nil { Trace(template ParseFiles err:, err) } err t.Execute(c.Ct.ResponseWriter, c.Data) if err ! nil { Trace(template Execute err:, err) } } return nil }Render()根据是否设置了Layout走两条路径布局渲染路径len(c.Layout) 0把Layout中列出的所有布局文件拼接成绝对路径通过template.ParseFiles一次性解析再以c.TplNames为模板名调用ExecuteTemplate把Data注入后写出到ResponseWriter。这样既解析了布局文件也解析了内容页二者可以在模板中互相引用。普通渲染路径若TplNames为空则自动推导默认模板名——ChildName / Request.Method . TplExt。例如子控制器类型名为UserController、请求方法为 GET、扩展名为tpl默认模板就是UserController/GET.tpl随后解析ViewsPath下的该模板并Execute。两条路径都通过全局视图路径变量ViewsPath拼接模板的绝对路径任何解析或执行错误都会调用Trace日志函数记录该日志函数来自 13.4 日志系统属于 Beego 分级日志的 Trace 级。注意默认模板名按方法名推导正是为了贴合 REST 风格同一资源/user/:uid的 GET 与 POST 请求会分别落到UserController/GET.tpl与UserController/POST.tpl。Redirect封装上下文重定向func (c *Controller) Redirect(url string, code int) { c.Ct.Redirect(code, url) }Redirect是对Context.Redirect的薄封装用于处理无需 Model 参与、无需 View 渲染的请求——例如登录成功后跳转首页、提交表单后回到列表页。请求生命周期从 URL 到控制器方法的完整调用链基类实现接口的全部方法后路由层即可按固定顺序驱动一次请求。结合上一节 13.2 的ServeHTTP转发实现完整调用链如下Init() 初始化例程注入 Context 与子类名重置内部状态 ↓ Prepare() 预处理钩子每个继承子类可按需实现 ↓ method() 按请求方法分发GET → Get()POST → Post()PUT → Put() HEAD → Head()DELETE → Delete()PATCH → Patch()OPTIONS → Options() ↓ Render() 可选步骤根据全局变量 AutoRender 决定是否执行 ↓ Finish() 收尾钩子动作完成后执行每个继承子类可按需实现路由层在匹配到 URL 后通过反射创建控制器实例再依次反射调用Init、Prepare、与请求方法同名的方法、Render、Finish13.2 的 ServeHTTP 分发源码Init的入参包括新构造的Context以及route.controllerType.Name()即控制器类型名作为ChildName方法分发严格按r.Method字符串分支GET调Get、POST调Post、HEAD调Head、DELETE调Delete、PUT调Put、PATCH调Patch、OPTIONS调Options只有全局布尔变量AutoRender为true时才调用Render——这是文档中Render 是可选的、由 AutoRender 决定是否执行的底层依据无论是否渲染Finish最终都会被调用保证钩子的确定性。因此一次 GET 请求命中MainController的完整执行顺序是Init → Prepare → Get →AutoRender 开启时Render → Finish而若用户用 POST 访问一个只实现了Get()的控制器则会命中基类默认的Post()返回 405。实战指南继承基类写第一个控制器完成基类设计后就可以通过继承与按需覆写快速实现自己的请求处理逻辑。下面是为路由注册入口13.2 的 RegisterController 用法准备的MainControllerpackage controllers import ( github.com/astaxie/beego ) type MainController struct { beego.Controller } func (this *MainController) Get() { this.Data[Username] astaxie this.Data[Email] astaxiegmail.com this.TplNames index.tpl }MainController通过内嵌beego.Controller自动获得基类全部方法从而满足ControllerInterface接口可以直接注册到路由例如beego.BeeApp.RegisterController(/, controllers.MainController{})。它只覆写了Get()向Data注入Username与Email两个键值并指定渲染模板为index.tpl。当AutoRender为true时控制器会自动调用Render()渲染模板并响应而如果用户用 POST、HEAD 等其他 HTTP 方法访问同一资源则会返回 405 Method Not Allowed。运行结果如下index.tpl的完整内容如下可以看到模板解析模型数据非常简单——借助 Gotext/template的{{.字段名}}语法直接输出Data中注入的键值!DOCTYPE html html head titlebeego welcome template/title /head body h1Hello, world!{{.Username}},{{.Email}}/h1 /body /html页面最终呈现Hello, world!astaxie,astaxiegmail.com——控制器中赋值的Username与Email被模板渲染出来。这就是本节开头所说用一行业务代码输出 Hello, world的落地形态控制器只需填充Data与TplNames渲染由基类自动完成。从控制器到完整博客应用13.5 中的真实用法控制器基类的实战价值在 13.5 博客增删改系统 中得到完整印证。那里大量使用了本节设计的基类能力Layout与TplNames的组合例如IndexController.Get()中this.Data[blogs] models.GetAll()、this.Layout layout.tpl、this.TplNames index.tpl——这正是Render()布局路径的典型调用13.5 的 IndexControllerCtx.Params读取路由参数ViewController.Get()中strconv.Atoi(this.Ctx.Params[:id])参数来自 13.2 路由解析时写入Context.Params的 URL 动态参数Redirect完成写操作后的跳转NewController.Post()与EditController.Post()保存博客后调用this.Ctx.Redirect(302, /)回到首页对应本节Redirect封装的场景GET 展示 POST 提交双方法控制器NewController、EditController同时实现Get()与Post()分别渲染表单和接收提交这正是 REST 方法分发设计的直接收益。从Controller基类、ControllerInterface契约到路由层的反射调用、AutoRender开关、ViewsPath全局路径再到 13.5 中控制器模型的真实组合整个框架的控制器链路已经闭环路由器负责找到谁控制器基类负责怎么活子类只负责做什么。小结本节完成了自研框架中控制器层的设计用Controller结构体承载上下文与渲染状态用ControllerInterface接口固化请求生命周期用带默认实现的九个方法让子类只需覆写自己关心的部分。关键要点回顾REST 风格控制器把 HTTP 动词映射为同名方法未覆写的方法默认返回 405Render()支持布局渲染与普通渲染两条路径模板名可自动推导为ChildName/Method.tpl请求生命周期固定为Init → Prepare → method →AutoRender 开启时Render → Finish由路由层反射驱动内嵌基类即可获得全部能力配合Data与TplNames即可完成数据到模板的渲染。下一节 13.4 日志与配置 将为框架补齐日志分级与配置解析能力而 13.5 博客增删改 将把本节设计的控制器真正用于一个完整的博客系统。更多章节可查阅本书英文版目录。赞分享文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载相关推荐从零设计 RESTful MVC 控制器Beego 框架 Controller 基类与接口实现全解析从零设计 RESTful MVC 控制器Beego 框架 Controller 基类与接口实现全解析 本文以《Build Web Application wi文档教程从零构建 Go Web 框架 Beego路由设计、REST 控制器、模板渲染、日志配置与博客实战全解析从零构建 Go Web 框架 Beego路由设计、REST 控制器、模板渲染、日志配置与博客实战全解析 本篇技术指南以《Build Web Applicati文档教程Marmite模板系统深度解析如何自定义你的博客外观Marmite模板系统深度解析如何自定义你的博客外观 Marmite是一款基于Markdown的静态网站生成器它的强大之处在于其灵活的模板系统让用户能够轻上一篇10 分钟上手 res-downloader 跨平台资源嗅探下一篇ant-design-vue-pro中的权限系统动态权限与菜单生成实现创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表