likes
comments
collection
share

深入理解Hugo - Config模块之源码实现源码实现 - 多读书,读好书 学习也有不同的阶段 - 了解,知道,会。

作者站长头像
站长
· 阅读数 29

源码实现 - 多读书,读好书

学习也有不同的阶段 - 了解,知道,会。 通过别人的讲解,我听明白了,就可以说了解了。 不仅听懂了,还能复述出来,这就是知道。 不仅知道,我还能动手实践,并且结果还不错,那就是会了。

拿写作举例。 通过看介绍写作的文章,能了解到,原来写作就是将自己的想法记录下来,这就了解了写作是怎么一回事。 经过思考总结,能够将刚才的文章复述出来,并且可以给别人解释写作是怎么一回事了,也就是知道了。 按照理解,开始动手实践了,还完整的表达了自己的想法,也就可以说会写作了。

源码学习也是一样。 希望自己能够通过读源码的方式,了解优秀开源项目是怎么工作的,知道其中的原理,并且将吸取的知识转换成实际动手的能力。

所有人都知道要多读书,尤其是好书。 优秀源码就像好书,那么多优秀的开源项目就躺在那,触手可及,可为什么读的人却不多呢?

我觉得主要原因有以下两点:

  • 代码量巨大,不知从何下手。好的开源项目通常都很庞大,没有人领进门,全靠自己,难度不小。
  • 编程语言设计的初衷是用来写的,并不是用来读的。 人类自然语言主要是用来交流,用在人和人之间。 而编程语言则是用在人和机器之间,工程师将自己的想法按计算机能懂的代码表达出来,让机器完成我们下达的指令。

探索步骤

为了让自己能更好掌握站点领域中Hugo的知识,我准备采用以下步骤对Hugo源码进行学习,以达到会的目的:

  1. 跬步千里。以可运行源码为基础,分块学习,专注在当前模块,减少知识负载,并可以通过源码实时验证自己的猜想。
  2. 了然于胸。用时序图将源码一层层展开,理解实现流程和设计细节。
  3. 抽象总结。用祖师爷冯诺依曼架构 - Input -> [?] -> Output,来对实现思路想进行总结,进一步帮助理解和总结。
  4. 动手实践。图示关键设计点,掌握精髓。用源码解释源码,对基础知识进行阐述,夯实基础。

Config 模块的执行流程

下面我们从源码实现的角度,详细的来看看Hugo是如何设计和实现配置模块的。

跬步千里 - Config模块源码

游乐场源码, 切换到01-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 .

得到运行结果如下: 深入理解Hugo - Config模块之源码实现源码实现 - 多读书,读好书 学习也有不同的阶段 - 了解,知道,会。

进一步查看LoadConfig函数,我们发现主要做了三件事:

  1. loadConfig,加载用户项目中的配置文件,如config.toml
  2. applyConfigDefaults,如果说上面是用户的自定义信息,那这里就是Hugo用到的默认信息。
  3. collectModules,加载完自定义信息和默认信息后,根据得到的模块信息,准备模块,并将解析过后的模块信息,也放入配置信息中。

在本章我们重点看第1步loadConfig,在后续的章节里会有第2步applyConfigDefaults,和第3步collectModules的介绍。

了然于胸 - loadConfig时序图

深入理解Hugo - Config模块之源码实现源码实现 - 多读书,读好书 学习也有不同的阶段 - 了解,知道,会。

从时序图中,我们可以清晰的看到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格式数据

深入理解Hugo - Config模块之源码实现源码实现 - 多读书,读好书 学习也有不同的阶段 - 了解,知道,会。

首先了解用户的需求,是将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
评论
请登录