深入理解Hugo - 模板的生命周期

422 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情 ,祝福祖国,生日快乐 :)

Hugo事件风暴中,我们了解到Hugo的设计理念 - 给用户提供始终如一的轻松写作体现。 而实现这一理念则是以Golang Template为基础,开发出更多实用功能,让内容创造者专于内容创作的同时,还拥有良好的体验。

让我们回顾一下Golang Template的实现步骤: 1-golang-template.svg

再看看Hugo是如何围绕其展开的: 2.1-hugo-whole-process-map-go-template.svg

Hugo围绕着Golang Template做了很多设计,现在我们通过和模板相关的领域事件,一起来看看模板的生命周期,从而能够有更全面的理解。

Hugo模板生命周期领域事件

还是从领域事件入手,来看看有哪些关键事件和模板强相关: 6.0-template-ddd-events.svg

除了上面和Golang Template一一映射的事件外,还有更细节的事件,为了直观好看,我们把这些事件收集到一起来进行分析:

6.0.1-template-ddd-events-simple.svg

可以看到和模板强相关的事件,主要集中在创建HugoSites和Build阶段。 进一步细分,可发现模板生命周期可分为三个阶段:

  1. 开始阶段,包括注册回调,并选定模板服务提供方,并出发模板更新。
    • 注册layouts回调到HugoSites初始化字段
    • 设置默认模板提供方到配置项
    • 通知模板提供者开始更新
  2. 准备阶段,包括准备好模板执行器,收集模板相关的功能函数,解析Hugo内置和用户自定义模板,将所有模板存储到模板命名空间,以及用layout处理器连接layout和模板。
    • 新建模板执行器
    • 收集模板函数到函数映射
    • 收集文本函数到函数映射
    • 新建模板命名空间
    • 新建layout处理器
  3. 渲染阶段,准备好页面内容后,回调在开始阶段注册的layouts事项,通过layout处理器查找相应的模板,最终用模板进行渲染,生成站点页面
    • 为内容结点创建页面
    • 回调layout注册项进行初始化
    • 为页面查找模板
    • 用模板渲染页面

事件可以帮助我们清晰地了解到Hugo设计的模板生命周期。 下面我们再通过Hugo游乐场源码梳理一遍具体实现流程,好让我们能更立体地了解到模板生命周期,同时也可以为后面的代码实现讲解章节做好准备。

Template vs Layouts

我们先来看看Hugo中两个容易混淆的概念,Template和Layouts。

在创建自定义Hugo主题的时候,我们接触最多的就是Layouts。 官方文档解释Layouts就是用来当模板的,这样解释并没有问题,但这会让我们很容易产生一种Layouts既模板的错觉,并将Layouts和模板直接划上等号。

但真的是这样的吗,让我们从代码层面看看他们的关联:

6.2-layouts-vs-template.svg

通过查看新建主题的目录结构,我们会发现自动生成的的文件主要在layouts目录下,里面有首页模板,还有页头和页尾模板,等等。 在代码中,Layouts的出现通过都是以[]string字符串数组的形式出现的。 也就是说在代码中Layouts就是用来记录layout相关的文件路径信息的。

如果想将Layouts转换成Golang Template,首先需要将其转换成templateInfo。 并记录文件名,分析是否是文本类型,将layout文件内容以字符串存储在tempalte字段中。

其中是否是文本类型,涉及到Golang Template的设计知识。

Golang将Template按类型进行了划分。如HTML和Text,通过对HTML标签进行转换,最终也会被转换成Text。 说到底,通过对不同模板类型的转换,都会变成文本类型。

通过templateInfo,最终Hugo会生成真正的Hugo模板结构体templateState。 可以看出该结构体实现了Template接口。

所以我们可以得出结论:Layouts不等于Template,是制作Template的原材料。

弄清了Template和Layouts之间的关系,我们分别来看看Template生命周期中的开始、准备和渲染阶段。

开始阶段

6.1.1-template-cycle-start.svg

HugoSites中的init字段是hugoSitesInit类型的,其中就包含了lazy.init类型的layouts。 这样就可以在layouts字段注册一些回调方法,方便在时机成熟的时候回调。

同时,对于HugoSites而言,直接面对的是模板服务的提供商,所以需要在这个阶段将TemplateProvider作为提供商,设置在配置信息中。 等信息都准备妥当后,就可以通知模板服务提供商开始工作更新了。

准备阶段

6.1.2-template-cycle-prepare.svg

对外提供整体服务,并和Deps关联的是templateExec。 包含了texttemplate.executertemplateHandler,以及所有的模板功能函数。 颜色表明各结构之间的关联关系。

可以看出,texttemplate.executer包含了templateExecHelper,因为在执行的过程中,通过对模板的分析,可能会用上功能函数。

templateHander则需要处理和template相关的一些操作。 main字段是templateNamespace类型,里面存储了HTML和Text原型信息,并存储了由原型创建的所有templateStatetemplateStateMap中。 layoutHandler则是连接layout和template的关键,比如通过layout查询template时,就由layoutHandler全权负责。

渲染阶段

结合开始阶段和准备阶段一览:

6.1.3-template-cycle-start-and-prepare.svg

通过前期的准备和组织,我们来看看渲染阶段是怎么发生的:

6.3-template-cycle-execute.svg

页面渲染发生在site_render.go中,从pageRender正式开始。

总共分为两大步,一是page.resolveTemplate解析模板,拿到模板后再开始site.renderAndWritePage渲染和写入页面。

  1. 解析模板 因为Site组合了Deps,所以也和Deps一样,同样持有templateExec信息,通过调用templateExecLookupLayout方法,查询模板信息。 因为这些模板信息都已经存储在了templateNamespace里的templateStateMap中。

  2. 渲染页面 在pageRender中已经拥有了页面信息pageState,通过上一步又获取了模板信息,所以是时候开始真正地site.renderForTemplate渲染了。 还是通过templateExec调用Execute方法。 因为当前的执行器是texttemplate.executer类型,所以真正地执行是在texttemplate.executerExecuteWithContext方法中。 这里是直接用的Golang Template源码,而不是调用Golang的默认包。 因为Golang默认包中自带的功能函数,并不能完全满足Hugo的诉求。 在后续代码实现章节将会详细讲述,这里还是专注在基础架构的初步理解上。

小结

6.1-template-cycle.svg

从Golang Template应用示例开始,我们了解到了Golang中模板工作的基本流程。 这有助于我们进一步理解Hugo的设计和实现。

通过对Hugo领域事件中模板强相关的核心事件进行分析,我们将Hugo模板的生命周期大致分为三个阶段:开始、准备、渲染。

为了立体的理解模板生命周期,我们不仅从领域事件进行梳理,还从代码结构进行分析。 看到了Hugo是基于HTML和Text模板原型,帮助将所有的Layouts转换成Template,并存储在了Template命名空间中。 还看到为了拓展Golang Template的功能,Hugo将强大的自定义函数保存在了执行器中。 这让模板在渲染过程中,有了更多的帮手。 而这一切,都封装在了对外统一提供的服务templateExec中,不仅对内进行封装,还对外提供了便捷。

还有更多有意思的事情,比如Hugo为什么不能直接用Golang内置的Template包,而要独立维护? 我们也会在后续代码实现章节,进一步展开讲解。 和大家一起,一探究竟。