
Jekyll Generator 插件开发完全指南从数据注入到运行时动态生成页面【免费下载链接】jekyll:globe_with_meridians: Jekyll is a blog-aware static site generator in Ruby项目地址: https://gitcode.com/gh_mirrors/je/jekyll本文基于 Jekyll 官方文档中的 Generators 章节结合源码实现系统讲解 Jekyll 生成器Generator插件的工作原理与开发方法。读完本文你将能够编写自定义 Generator 插件在构建时向已有页面注入构建期计算的数据或在运行时动态创建整站页面如按分类自动生成目录页并理解生成器在构建流水线中的执行时机、优先级与安全模式safe mode下的行为边界。生成器是什么一段在构建流水线中运行的 Ruby 代码Jekyll 在把站点渲染成静态文件之前需要一套机制来基于你自己的规则创建额外内容——这正是生成器的定位。一个生成器就是Jekyll::Generator的子类它只需定义一个generate方法该方法接收一个Jekyll::Site实例作为参数generate的返回值会被忽略——所有工作都以副作用的形式完成修改内存中的站点结构页面、数据哈希等。从源码可以看到Jekyll::Generator本身几乎是空的# lib/jekyll/generator.rb module Jekyll Generator Class.new(Plugin) endlib/jekyll/generator.rb 中它只是Jekyll::Plugin的一个空子类。真正的能力来自Plugin基类与Jekyll::Site的构建流程。执行时机在内容清点之后、渲染之前官方文档明确说明生成器在 Jekyll 完成对现有内容的清点inventory之后、站点生成之前运行。这一点可以直接在Jekyll::Site#process中验证# lib/jekyll/site.rb def process return profiler.profile_process if config[profile] reset read # 从磁盘读取所有页面、文章、静态文件、数据 generate # 依次运行所有生成器 render cleanup write end也就是说当你的generate(site)被调用时站点已经完成读取带 front matter 的页面是Jekyll::Page的实例可通过site.pages访问静态文件是Jekyll::StaticFile的实例可通过site.static_files访问数据文件_data目录可通过site.data访问文章的分类与标签可通过site.categories、site.tags访问见 lib/jekyll/site.rb#L272-L274 中categories的实现。generate阶段由 lib/jekyll/site.rb#L190-L198 的Site#generate驱动它会遍历所有已实例化的生成器并逐个调用generate(self)同时用 debug 日志记录每个生成器的耗时# lib/jekyll/site.rb def generate generators.each do |generator| start Time.now generator.generate(self) Jekyll.logger.debug Generating:, #{generator.class} finished in #{Time.now - start} seconds. end nil end实例一向已有页面注入构建期计算的数据最简单的生成器用法是把构建时算好的值注入到模板的未定义变量中。官方文档的例子中模板reading.html有两个未定义变量ongoing和done由生成器在构建时赋值module Reading class Generator Jekyll::Generator def generate(site) book_data site.data[books] ongoing book_data.select { |book| book[status] ongoing } done book_data.select { |book| book[status] finished } # get template reading site.pages.find { |page| page.name reading.html} # inject data into template reading.data[ongoing] ongoing reading.data[done] done end end end这个例子的关键机制在于reading.data返回的就是渲染 Liquid 模板时用到的数据哈希。修改page.data等于在渲染前改写了页面的 front matter模板中的{{ page.ongoing }}、{{ page.done }}因此可以在渲染阶段取到值。由于Site#process中generate位于render之前这种注入对最终输出是生效的。这个模式适合一切数据在构建期即可确定、但来源不在 front matter 里的场景聚合_data中的多份数据、按状态过滤列表、预计算分页信息等。实例二运行时动态生成整站页面分类目录页更复杂的用法是让生成器在运行时创建全新的页面。文档给出的目标是为站点中每个已注册的分类创建一页渲染该分类下所有文章的列表。这类动态页面有两条设计要点由于页面在运行时才创建其内容、front matter 和其他属性都要由插件自己设计。因为目的是渲染某个分类下所有文档的列表所以输出文件的 basename 取index.html最合理能够用 front matter defaults 配置这些页面会非常理想因此给这些页面赋一个特定的type这里是categories很有价值。完整实现如下继承自文档中的SamplePlugin示例module SamplePlugin class CategoryPageGenerator Jekyll::Generator safe true def generate(site) site.categories.each do |category, posts| site.pages CategoryPage.new(site, category, posts) end end end # Subclass of Jekyll::Page with custom method definitions. class CategoryPage Jekyll::Page def initialize(site, category, posts) site site # the current site instance. base site.source # path to the source directory. dir category # the directory the page will reside in. # All pages have the same filename, so define attributes straight away. basename index # filename without the extension. ext .html # the extension. name index.html # basically basename ext. # Initialize data hash with a key pointing to all posts under current category. # This allows accessing the list in a template via page.linked_docs. data { linked_docs posts } # Look up front matter defaults scoped to type categories, if given key # doesnt exist in the data hash. data.default_proc proc do |_, key| site.frontmatter_defaults.find(relative_path, :categories, key) end end # Placeholders that are used in constructing page URL. def url_placeholders { :path dir, :category dir, :basename basename, :output_ext output_ext, } end end end逐段解析这个自定义 Page 子类构造函数Jekyll::Page实例的 URL 由若干属性拼出动态页面没有磁盘上的源文件因此这些属性必须手工初始化site是站点实例base指向站点源目录dir是页面驻留的目录这里直接取分类名basename/ext/name共同决定文件名为index.html。data哈希中预置了linked_docs键把该分类下的所有文章塞进去模板即可通过page.linked_docs遍历列表。front matter defaults 的接入data.default_proc是这段代码的精华。它给数据哈希挂了一个default_proc——当模板访问page中不存在的键时就会回退到site.frontmatter_defaults.find(relative_path, :categories, key)。这正是让动态页面能被_config.yml的 defaults 配置的机制。FrontmatterDefaults#find的签名是find(path, type, setting)按path页面相对路径与type这里传入:categories过滤匹配的 defaults 集合按作用域优先级返回对应设置的默认值。url_placeholders的覆盖Jekyll::Page#url_placeholders默认只提供:path、:basename、:output_ext三个占位符。自定义子类在此基础上追加了:category使得 permalink 模板中可以写:category这样的占位符并被正确替换——这就是为什么示例的 permalink 可以写成categories/:category/。用 front matter defaults 为生成的页面指定布局与输出路径生成器把页面生出来之后布局layout和输出路径permalink就完全交给配置文件管理了。文档给出的_config.yml配置# _config.yml defaults: - scope: type: categories # select all category pages values: layout: category_page permalink: categories/:category/这里scope.type: categories与生成器中frontmatter_defaults.find(relative_path, :categories, key)传入的类型符号一一对应每个动态生成的分类页渲染时取不到自身data中的layout/permalink就会命中这条 defaults 规则从而统一使用category_page布局、输出到categories/:category/路径。生成器负责造页面配置负责定规矩两者解耦。技术要点接口、优先级与安全模式官方文档以表格形式给出了生成器必须实现的方法——只有一个方法说明generate以副作用side-effect的形式生成内容在只实现一个方法之外源码层面还有两个值得了解的机制均继承自Jekyll::Plugin优先级priorityJekyll::Plugin定义了五级优先级 lib/jekyll/plugin.rb#L5-L11PRIORITIES { :low -10, :highest 100, :lowest -100, :normal 0, :high 10, }.freeze多个生成器共存时实例化顺序由优先级决定。Site#instantiate_subclasses在Site#setup阶段完成生成器的筛选、排序与实例化# lib/jekyll/site.rb def instantiate_subclasses(klass) klass.descendants.select { |c| !safe || c.safe }.tap do |result| result.sort! result.map! { |c| c.new(config) } end end两个细节值得注意排序发生在实例化之前result.sort!后map!成实例排序依据是Plugin基类中基于PRIORITIES值实现的优先级高的先排。因此在插件中可以通过priority :high之类的类方法声明执行顺序让数据准备型生成器先于依赖其结果的生成器运行每个生成器实例化时都会收到站点配置c.new(config)即你的initialize可以接收config参数读取自定义配置项。安全模式safe mode与safe声明instantiate_subclasses中的筛选条件!safe || c.safe揭示了安全模式的行为当--safe开启典型如托管平台构建环境时只有显式声明了safe true的生成器才会被实例化和执行。文档第二个示例中的safe true就是为此——声明我只操作站点内存结构不做文件读写等危险动作从而允许在 safe 模式下运行。这一筛选逻辑同样体现在插件的加载阶段PluginManager#require_plugin_files在非 safe 模式下 glob 加载插件目录下所有.rb文件而 gem 插件则受白名单whitelist配置约束见 lib/jekyll/plugin_manager.rb#L77-L87。插件文件的组织与加载_plugins目录与plugins_dir配置文档对插件文件组织给出了两条规则单文件生成器文件可以任意命名但必须使用.rb扩展名跨多文件的生成器应打包成 Ruby gem 发布发布目标为 rubygems.orggem 名取决于该站点的名称可用性——gem 名不可重复。Jekyll 默认在源目录下的_plugins目录查找插件这个默认值定义在 lib/jekyll/configuration.rb#L13plugins_dir _plugins,你可以通过配置文件中的plugins_dir键覆盖默认目录。从PluginManager#plugins_path的实现看这里有一个容易踩的坑def plugins_path if site.config[plugins_dir].eql? Jekyll::Configuration::DEFAULTS[plugins_dir] [site.in_source_dir(site.config[plugins_dir])] else Array(site.config[plugins_dir]).map { |d| File.expand_path(d) } end end当plugins_dir保持默认值_plugins时插件目录被解析为源目录内的相对路径site.in_source_dir(...)一旦你把plugins_dir改成别的名字配置值会被Array(...)展开并对每一项执行File.expand_path——即作为绝对/基于当前工作目录的路径处理可以配置多个目录数组形式且不再自动相对源目录解析。因此自定义plugins_dir时建议写成绝对路径或明确知道其展开基准。加载链路小结把上述机制串起来一次jekyll build中生成器相关的完整链路是以 lib/jekyll/commands/build.rb 的Build.process为入口Build.process创建Jekyll::Site并调用process_site(site)→site.process见 lib/jekyll/command.rb#L27-L34Site#setup先执行plugin_manager.conscientious_require加载主题依赖、_plugins下的.rb文件、白名单内的 gem 插件随后self.generators instantiate_subclasses(Jekyll::Generator)完成生成器的筛选、按优先级排序与实例化lib/jekyll/site.rb#L128-L135Site#read清点全部页面、文章、静态文件与数据文件Site#generate按顺序执行每个生成器的generate(site)——此时site.pages、site.data、site.categories等均已就绪生成器既可以改写已有页面的data也可以向site.pages追加全新的Jekyll::Page实例Site#render之后注入的数据与新生成的页面随站点一起被渲染输出。小结生成器是Jekyll::Generator的子类只要求实现一个generate(site)方法通过副作用修改站点内存结构返回值被忽略运行时机固定在Site#read内容清点之后、Site#render渲染之前因此可安全访问site.pages、site.static_files、site.data、site.categories两类典型用法向既有页面注入构建期数据改写page.data以及运行时动态创建Jekyll::Page子类实例需自行初始化dir/basename/ext/name/data并覆盖url_placeholders支持自定义 permalink 占位符动态页面通过data.default_proc接入front matter defaults用配置文件的defaultsscope.type统一控制其布局与输出路径单文件插件放在_plugins目录可用plugins_dir配置覆盖注意非默认值会被File.expand_path展开多文件插件应打包为 gemsafe true声明让插件在 safe 模式下仍会运行priority控制多个生成器间的执行顺序。【免费下载链接】jekyll:globe_with_meridians: Jekyll is a blog-aware static site generator in Ruby项目地址: https://gitcode.com/gh_mirrors/je/jekyll创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考