首頁 > 軟體

Hugo Config模組構建實現原始碼剖析

2023-02-25 06:01:10

瞭然於胸 - collectModules時序圖

經過loadConfigapplyConfigDefaults,我們已經將使用者自定義資訊和預設資訊都歸置妥當,並且放在了Config Provider中,方便查用。

Hugo在拿到這些資訊後,立馬著手的事情就是collectModules,也就是收集模組資訊了。

正如上圖中loadModulesConfig所示,拿到設定資訊後,就進行解碼decodeConfig操作。 在我們的範例中,我們的專案用到了名為mytheme的主題,所以在專案設定資訊中,我們需要把主題新增到匯入項Imports中。

準備好了模組的設定資訊後,接下來就是要根據這些設定資訊,對模組進行處理了。

需要先準備好回撥函數beforeFinalizeHook,為什麼要準備這和個回撥函數呢? 我們先把這個疑問放一放,一會我們就能發現實際的觸發場景。

回撥函數設定好後,接著就開始收集模組了。 如上圖左上角所示,首先需要建立Module Client用來具體處理模組的收集工作。 為什麼要叫Client呢? 這是因為現在Hugo支援Golang的mod模式,意味著可以用go.mod來匯入主題,那我們就需要下載依賴包 - 主題工程來管理依賴了。 這樣來看,叫使用者端是不是就不難理解了。 在我們的範例中,主題目錄是用來做流程講解示範的,只有一個文字檔案,所以這裡的場景並不涉線上go模組載入。

使用者端設定好後,開始收集,如上圖中間所示,收集過程總共分四步:

  • 按設定遞迴收集所有模組 - Collect
  • 設定處於活躍狀態的模組 - setActiveMods
  • 觸發提前設定的回撥函數 - HookBeforeFinalize
  • 移除重複的掛載資訊 - Finalize

Collect

先為專案建立工程模組Project Module,然後開始遞迴收集模組:

func (c *collector) collect() {
   ...
   // c.gomods is [], GetMain() returns ni
   projectMod := createProjectModule(c.gomods.GetMain(), c.ccfg.WorkingDir, c.moduleConfig)
   // module structure, [project, others...]
   if err := c.addAndRecurse(projectMod, false); err != nil {
      c.err = err
      return
   }
   ...
}

這裡為什麼會用到遞迴呢? 因為在Hugo中,模組之間是有相互依賴的。 通過最開始的模組設定資訊也可以看出,我們把依賴的模組放在了Imports中,Project Module就需要匯入"mytheme"模組。 在實際情況中,"mytheme"有可能也是依賴於其它的主題,所以也需要匯入其它模組。

從上面時序圖右下方可以看到,addAndRecurse做了四件事:

  • 為匯入的模組建立模組資料夾,用來放置模組所有檔案
  • 應用主題設定,就像最開始解析專案模組的設定資訊一樣,看是否還需要匯入其它模組
  • 將模組新增到模組列表中
  • 為新模組重複上述步驟

這樣,我們就能順著專案模組的設定資訊,逐個將所有的模組資訊收集齊全了。

setActiveMods

遞迴收集完所有模組資訊後,需要根據使用者設定,進一步將禁用的模組給過濾到,留下這一次構建所需要的模組。

HookBeforeFinalize

過濾完模組後,在Finalize敲定前,是時候回撥我們之前設定好地回撥函數了。

除了載入多語言設定處,回撥函數所做的操作主要集中在上面時序圖的右下腳。 就是為專案模組準備好所有的掛載Mount,包括Content, Static, Layouts, Archetypes, Data, Assets, i18n,共七個元件。 其中Content和其它的元件有點不一樣。 因為Content掛載點和多語言一一對應,也就是說有幾種語言,就會有幾個內容目錄。

Finalize

等有了所有的模組的資訊,掛載點也收集完畢後,我們還要做一件事情。 那就是要保證這些掛載點在全域性視野下,沒有重複。

結合時序圖,我們進一步將其中的關鍵物件結構體,根據這些結構體的屬性和行為,按流程處理後所得到的最終結果放在一起,視覺化出來。 方便大家理解:

抽象總結 - 輸入不同型別的值,輸出標準的configProvider

在上圖中,通過下方輸出部分可以看出,一個模組設定項,對應一個模組。

在左邊的模組設定資訊中,包含了模組之間的依賴資訊。 在上面的範例中專案模組飽含了主題模組。

在右邊的模組範例中,首先要區分哪一個是專案模組,因為專案模組是站點構建的起點。 所以在模組中需要能標識身份資訊的欄位projectMod

如果從掛載Mounts的角度來看模組,那每個模組實際上就是一個合併後的根檔案系統。 Hugo將這個檔案系統用七個元件進行了劃分。

專案模組必需得包含這些資訊,但因為依賴於其它模組,所以需要將專案模組放在最後處理。 Hugo將專案模組放在了模組佇列的第一個,並用一個回撥函數幫助在合適的時間點,對專案模的掛載進行了統一的處理。

再用Input -> [?] -> Output模型來進行分析,可以抽象為以下模型:

主題資訊來源於使用者自定義資訊,作為輸入傳入收集模組功能單元。 在處理過程中,Hugo按Name, Module Config, Module, Mounts的對應關係,將模組相關資訊進行處理。 最終生成所有模組的資訊,並通過將這些資訊設定在Config Provider中,為後續的操作做好準備。

動手實踐 - Show Me the Code of collectModules

在知道collectModules的實現原理後。 按照我們的傳統,讓我們動動小手,用程式碼來總結程式碼,鞏固一下知識。

可以這裡線上嘗試,Show Me the Code, try it yourself

程式碼裡有註解說明,程式碼樣例:

package main
import "fmt"
type Mount struct {
   // relative path in source repo, e.g. "scss"
   Source string
   // relative target path, e.g. "assets/bootstrap/scss"
   Target string
   // any language code associated with this mount.
   Lang string
}
type Import struct {
   // Module path
   Path string
}
// Config holds a module config.
type Config struct {
   Mounts  []Mount
   Imports []Import
}
type Module interface {
   // Config The decoded module config and mounts.
   Config() Config
   // Owner In the dependency tree, this is the first module that defines this module
   // as a dependency.
   Owner() Module
   // Mounts Any directory remappings.
   Mounts() []Mount
}
type Modules []Module
var modules Modules
// moduleAdapter implemented Module interface
type moduleAdapter struct {
   projectMod bool
   owner      Module
   mounts     []Mount
   config     Config
}
func (m *moduleAdapter) Config() Config {
   return m.config
}
func (m *moduleAdapter) Mounts() []Mount {
   return m.mounts
}
func (m *moduleAdapter) Owner() Module {
   return m.owner
}
// happy path to easily understand
func main() {
   // project module config
   moduleConfig := Config{}
   imports := []string{"mytheme"}
   for _, imp := range imports {
      moduleConfig.Imports = append(moduleConfig.Imports, Import{
         Path: imp,
      })
   }
   // Need to run these after the modules are loaded, but before
   // they are finalized.
   collectHook := func(mods Modules) {
      // Apply default project mounts.
      // Default folder structure for hugo project
      ApplyProjectConfigDefaults(mods[0])
   }
   collectModules(moduleConfig, collectHook)
   for _, m := range modules {
      fmt.Printf("%#vn", m)
   }
}
// Module folder structure
const (
   ComponentFolderArchetypes = "archetypes"
   ComponentFolderStatic     = "static"
   ComponentFolderLayouts    = "layouts"
   ComponentFolderContent    = "content"
   ComponentFolderData       = "data"
   ComponentFolderAssets     = "assets"
   ComponentFolderI18n       = "i18n"
)
// ApplyProjectConfigDefaults applies default/missing module configuration for
// the main project.
func ApplyProjectConfigDefaults(mod Module) {
   projectMod := mod.(*moduleAdapter)
   type dirKeyComponent struct {
      key          string
      component    string
      multilingual bool
   }
   dirKeys := []dirKeyComponent{
      {"contentDir", ComponentFolderContent, true},
      {"dataDir", ComponentFolderData, false},
      {"layoutDir", ComponentFolderLayouts, false},
      {"i18nDir", ComponentFolderI18n, false},
      {"archetypeDir", ComponentFolderArchetypes, false},
      {"assetDir", ComponentFolderAssets, false},
      {"", ComponentFolderStatic, false},
   }
   var mounts []Mount
   for _, d := range dirKeys {
      if d.multilingual {
         // based on language content configuration
         // multiple language has multiple source folders
         if d.component == ComponentFolderContent {
            mounts = append(mounts, Mount{Lang: "en", Source: "mycontent", Target: d.component})
         }
      } else {
         mounts = append(mounts, Mount{Source: d.component, Target: d.component})
      }
   }
   projectMod.mounts = mounts
}
func collectModules(modConfig Config, hookBeforeFinalize func(m Modules)) {
   projectMod := &moduleAdapter{
      projectMod: true,
      config:     modConfig,
   }
   // module structure, [project, others...]
   addAndRecurse(projectMod)
   // Add the project mod on top.
   modules = append(Modules{projectMod}, modules...)
   if hookBeforeFinalize != nil {
      hookBeforeFinalize(modules)
   }
}
// addAndRecurse Project Imports -> Import imports
func addAndRecurse(owner *moduleAdapter) {
   moduleConfig := owner.Config()
   // theme may depend on other theme
   for _, moduleImport := range moduleConfig.Imports {
      tc := add(owner, moduleImport)
      if tc == nil {
         continue
      }
      // tc is mytheme with no config file
      addAndRecurse(tc)
   }
}
func add(owner *moduleAdapter, moduleImport Import) *moduleAdapter {
   fmt.Printf("start to create `%s` modulen", moduleImport.Path)
   ma := &moduleAdapter{
      owner: owner,
      // in the example, mytheme has no other import
      config: Config{},
   }
   modules = append(modules, ma)
   return ma
}

輸出結果:

# collect theme as module
start to create `mytheme` module
# project module has no owner with default mounts
&main.moduleAdapter{projectMod:true, owner:main.Module(nil), mounts:[]main.Mount{main.Mount{Source:"mycontent", Target:"content", Lang:"en"}, main.Mount{Source:"data", Target:"data", Lang:""}, main.Mount{Source:"layouts", Target:"layouts", Lang:""}, main.Mount{Source:"i18n", Target:"i18n", Lang:""}, main.Mount{Source:"archetypes", Target:"archetypes", Lang:""}, main.Mount{Source:"assets", Target:"assets", Lang:""}, main.Mount{Source:"static", Target:"static", Lang:""}}, config:main.Config{Mounts:[]main.Mount(nil), Imports:[]main.Import{main.Import{Path:"mytheme"}}}}
# theme module owned by project module with no import in the example
&main.moduleAdapter{projectMod:false, owner:(*main.moduleAdapter)(0xc000102120), mounts:[]main.Mount(nil), config:main.Config{Mounts:[]main.Mount(nil), Imports:[]main.Import(nil)}}
Program exited.

以上就是Hugo Config模組構建實現原始碼剖析的詳細內容,更多關於Hugo Config模組構建的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com