深入理解Hugo - Config模块之源码实现源码实现 - 多读书,读好书 学习也有不同的阶段 - 了解,知道,会。
源码实现 - 多读书,读好书
学习也有不同的阶段 - 了解,知道,会。 通过别人的讲解,我听明白了,就可以说了解了。 不仅听懂了,还能复述出来,这就是知道。 不仅知道,我还能动手实践,并且结果还不错,那就是会了。
拿写作举例。 通过看介绍写作的文章,能了解到,原来写作就是将自己的想法记录下来,这就了解了写作是怎么一回事。 经过思考总结,能够将刚才的文章复述出来,并且可以给别人解释写作是怎么一回事了,也就是知道了。 按照理解,开始动手实践了,还完整的表达了自己的想法,也就可以说会写作了。
源码学习也是一样。 希望自己能够通过读源码的方式,了解优秀开源项目是怎么工作的,知道其中的原理,并且将吸取的知识转换成实际动手的能力。
所有人都知道要多读书,尤其是好书。 优秀源码就像好书,那么多优秀的开源项目就躺在那,触手可及,可为什么读的人却不多呢?
我觉得主要原因有以下两点:
- 代码量巨大,不知从何下手。好的开源项目通常都很庞大,没有人领进门,全靠自己,难度不小。
- 编程语言设计的初衷是用来写的,并不是用来读的。 人类自然语言主要是用来交流,用在人和人之间。 而编程语言则是用在人和机器之间,工程师将自己的想法按计算机能懂的代码表达出来,让机器完成我们下达的指令。
探索步骤
为了让自己能更好掌握站点领域中Hugo的知识,我准备采用以下步骤对Hugo源码进行学习,以达到会的目的:
- 跬步千里。以可运行源码为基础,分块学习,专注在当前模块,减少知识负载,并可以通过源码实时验证自己的猜想。
- 了然于胸。用时序图将源码一层层展开,理解实现流程和设计细节。
- 抽象总结。用祖师爷冯诺依曼架构 - Input -> [?] -> Output,来对实现思路想进行总结,进一步帮助理解和总结。
- 动手实践。图示关键设计点,掌握精髓。用源码解释源码,对基础知识进行阐述,夯实基础。
Config 模块的执行流程
下面我们从源码实现的角度,详细的来看看Hugo是如何设计和实现配置模块的。
跬步千里 - Config模块源码
➜ hugo-playground git:(main) git checkout 01-config
Switched to branch '01-config'
Your branch is up to date with 'origin/01-config'.
➜ hugo-playground git:(01-config)
通过将源码按配置模块进行裁剪,Config模块在我们面前也变得清晰,让我们在Hugo源码学习的道路上迈出了自信的一步。 通过命令行,我们可以看看具体的目录结构:
➜ hugo-playground git:(01-config) tree
.
├── LICENSE
├── README.md
├── command.sh
├── common
│ ├── maps
│ │ ├── maps.go
│ │ └── params.go
│ └── paths
│ └── path.go
├── config
│ ├── commonConfig.go
│ ├── compositeConfig.go
│ ├── configLoader.go
│ ├── configProvider.go
│ ├── defaultConfigProvider.go
│ └── env.go
├── go.mod
├── go.sum
├── hugo-playground
├── hugofs
│ ├── files
│ │ └── classifier.go
│ └── fs.go
├── hugolib
│ └── config.go
├── langs
│ ├── config.go
│ └── language.go
├── log
│ └── logger.go
├── main.go
├── modules
│ ├── client.go
│ ├── collect.go
│ ├── config.go
│ └── module.go
├── parser
│ └── metadecoders
│ ├── decoder.go
│ └── format.go
└── types
├── convert.go
└── types.go
13 directories, 30 files
总共也就13个文件夹,30个文件,其中还包含了一些Github和Golang工程的默认文件。
我们看看main.go
文件中主要干了什么:
...
// 1. config
log.Process("main", "load configurations from config.toml and themes")
cfg, _, err := hugolib.LoadConfig(
hugolib.ConfigSourceDescriptor{
WorkingDir: tempDir,
Fs: afs,
Filename: "config.toml",
},
)
fmt.Printf("%#v\n", cfg)
如果想要运行程序,查看输出的配置信息长啥样,也可以进入到工程根目录,简单运行命令:
go run .
得到运行结果如下:
进一步查看LoadConfig
函数,我们发现主要做了三件事:
loadConfig
,加载用户项目中的配置文件,如config.toml
。applyConfigDefaults
,如果说上面是用户的自定义信息,那这里就是Hugo用到的默认信息。collectModules
,加载完自定义信息和默认信息后,根据得到的模块信息,准备模块,并将解析过后的模块信息,也放入配置信息中。
在本章我们重点看第1步loadConfig
,在后续的章节里会有第2步applyConfigDefaults
,和第3步collectModules
的介绍。
了然于胸 - loadConfig时序图
从时序图中,我们可以清晰的看到loadConfig
被调用的环境。
由主函数发起调用,在hugolib/config.go
中先是构建好configLoader
,调用loadConfig
函数,将config.toml
文件转换成Map类型数据。
在loadConfig
中,通过函数名可以观察到Hugo的实现思路。
这也说明好的命名是多么的重要。
(温馨提示,在阅读下述流程函数时,可打开配置源码对照查看。)
FromFileToMap
,将目标文件config.toml
转换成Map类型的数据。loadConfigFromFile
,为了达到上面的目的,首先要从硬盘加载这个文件。UnmarshalFileToMap
,加载后,需要将字符弃,解组成Map类型的数据。UnmarshalToMap
,解组对应的输出格式要求,可能不一样,这里是要求解组成Map类型,还有可能是其它类型。FormatFromString
,从文件名获取文件格式toml
。UnmarshaTo
,通过获取的文件格式信息,以及文件数据信息,和对应的输出格式Map信息,解组总函数,知道该让谁具体负责了。toml.Unmarshal
,所有信息被传送到具体操作员toml
,可以外聘,也可以自己实现。Hugo选择了外聘github.com/pelletier/go-toml/v2
。
抽象总结 - 输入配置文件,输出Map格式数据
首先了解用户的需求,是将config.toml文件作为输入,要求输出Map类型的数据。
Hugo先是收集信息,包括文件数据data
,文件格式toml
,和输出类型map[string]any
,找到专业的人go-toml
,进行处理。
最终得到符合要求的Map信息。
了然于胸 - loadConfig时序图
在知道loadConfig
的实现原理后,我们再来动动小手,巩固一下知识。
可以这里线上尝试,Show Me the Code, try it yourself
代码里有注解说明,代码样例:
package main
import (
"bytes"
"fmt"
toml "github.com/pelletier/go-toml/v2"
"golang.org/x/tools/txtar"
"path/filepath"
"strings"
)
// 文件结构
// 文件名: config.toml
// 文件内容:theme = 'mytheme'
var files = "-- config.toml --\n" +
"theme = 'mytheme'"
// Format 文件格式类型
type Format string
// TOML 支持的格式,为简单示例,只支持TOML格式
const (
TOML Format = "toml"
)
func main() {
// 解析上面的文件结构
data := txtar.Parse([]byte(files))
fmt.Println("File start:")
// Input: 数据,格式,输出类型
var configData []byte
var format Format
m := make(map[string]any)
// 遍历解析生成的所有文件,通过File结构体获取文件名和文件数据
// f.Name 获取文件名
// f.Data 获取文件数据
// 如果是config.toml文件,则获取文件数据
for _, f := range data.Files {
if "config.toml" == f.Name {
configData = bytes.TrimSuffix(
f.Data, []byte("\n"))
format = FormatFromString(f.Name)
}
}
err := UnmarshalTo(configData, format, &m)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(m)
}
fmt.Println("File end.")
}
// FormatFromString turns formatStr, typically a file extension without any ".",
// into a Format. It returns an empty string for unknown formats.
// Hugo 实现
func FormatFromString(formatStr string) Format {
formatStr = strings.ToLower(formatStr)
if strings.Contains(formatStr, ".") {
// Assume a filename
formatStr = strings.TrimPrefix(
filepath.Ext(formatStr), ".")
}
switch formatStr {
case "toml":
return TOML
}
return ""
}
// UnmarshalTo unmarshals data in format f into v.
func UnmarshalTo(data []byte, f Format, v any) error {
var err error
switch f {
case TOML:
err = toml.Unmarshal(data, v)
default:
return fmt.Errorf(
"unmarshal of format %q is not supported", f)
}
if err == nil {
return nil
}
return err
}
程序输出结果:
# 解析后得到文件config.toml
# 准备Input:config file data, format, map[string]any
# 得到Output: map[theme:mytheme]
File start:
map[theme:mytheme]
File end.
Program exited.
转载自:https://juejin.cn/post/7155462124372770823