Hugo Config模塊構建實現源碼剖析
瞭然於胸 – collectModules時序圖
經過loadConfig
和applyConfigDefaults
,我們已經將用戶自定義信息和默認信息都歸置妥當,並且放在瞭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("%#v\n", 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` module\n", 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模塊構建的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- IntelliJ IDEA中Project與Module的概念以及區別
- Hugo 遊樂場內容初始化示例詳解
- vite+vue3.0+ts+element-plus快速搭建項目的實現
- docker volumes 文件映射方式
- 詳解Go 依賴管理 go mod tidy