news 2026/10/15 11:28:31

Middleman 分页实战:以 paginate-app 博客文章为例解析 frontmatter 数据源与 per_page 配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Middleman 分页实战:以 paginate-app 博客文章为例解析 frontmatter 数据源与 per_page 配置
  • 前端
  • 开发工具

【免费下载链接】middleman

Hand-crafted frontend development

项目地址:https://gitcode.com/gh_mirrors/mi/middleman
点击查看免费下载

本文以 middleman-core 仓库中paginate-app测试夹具(fixture)为核心,讲解 Middleman 静态博客分页功能的工作原理与配置方法。你将掌握博客文章 frontmatter(title/date/tags)如何成为分页数据源、per_page分页 API 的两种调用方式、分页模板变量(pagination / items / prev_page / next_page)的完整用法,以及分页背后Array#per_page与 Collections 扩展的源码实现。

一、关联文档与它在分页测试中的角色

在 paginate-app/source/blog/2011-02-02-test-article.html.markdown 中,存放着一篇典型的 Middleman 博客文章,其完整内容如下:

--- title: "Test Article" date: 2011-02-02 tags: bar --- Test Article Content

这个文件是 Middleman 分页功能测试夹具paginate-app的一篇输入数据。它由两部分组成:

  • frontmatter(YAML 前置数据块):声明了title: "Test Article"、date: 2011-02-02、tags: bar三个元数据字段,供分页逻辑按日期排序、按标签分组;
  • 正文内容:仅一行Test Article Content,用于验证文章被正确渲染进分页页面。

在paginate-app/source/blog/目录下共有 7 篇结构完全相同的测试文章(2011-01-01至2011-02-02),它们共同构成一个 7 篇规模的小型博客数据集。整个夹具只包含三种文件,职责非常清晰:

目录/文件职责
source/blog/*.html.markdown博客文章数据源(frontmatter + 正文)
source/archive/2011/index.html.erb分页目标模板,消费items、pagination等局部变量
config.rb分页配置(运行时由测试动态写入)

提示:paginate-app/config.rb在仓库中为空文件,实际的配置内容由 paginate.feature 中的 Cucumber 场景在测试运行时动态写入,这正是下面要展开讲解的完整配置。

二、把文章集合变成分页数据源:per_page 核心用法

在 paginate.feature 的“Basic configuration”场景中,第一段配置演示了最常用的分页写法——按日期倒序排列后每 5 篇一页:

articles = resources.select { |r| matcher = ::Middleman::Util::UriTemplates.uri_template('blog/2011-{remaining}') ::Middleman::Util::UriTemplates.extract_params(matcher, ::Middleman::Util.normalize_path(r.url)) } articles.sort { |a, b| b.data.date <=> a.data.date }.per_page(5) do |items, num, meta, is_last| page_path = num == 1 ? '/2011/index.html' : "/2011/page/#{num}.html" prev_page = case num when 1 nil when 2 '/2011/index.html' when 3 "/2011/page/#{num-1}.html" end next_page = is_last ? nil : "/2011/page/#{num+1}.html" proxy page_path, "/archive/2011/index.html", locals: { items: items, pagination: meta, prev_page: prev_page, next_page: next_page } end

逐段拆解这个配置:

  1. 筛选文章:通过resources.select配合Middleman::Util::UriTemplates的 URL 模板匹配,把source/blog/2011-{remaining}模式下的文章筛选出来,构成候选集合;
  2. 按日期倒序:sort { |a, b| b.data.date <=> a.data.date }用 frontmatter 中的date字段做倒序排列,保证最新文章排在最前;
  3. 分页:.per_page(5) do |items, num, meta, is_last|是分页的入口,块参数依次代表本页文章、页码(从 1 开始)、分页元数据、是否为最后一页;
  4. 生成页面:首页输出到/2011/index.html,后续页输出到/2011/page/2.html等,并通过proxy指令把每页数据以locals形式注入目标模板/archive/2011/index.html。

prev_page/next_page的取值逻辑值得留意:首页(num == 1)没有上一页返回nil;第二页的上一页指向首页/2011/index.html;其余页指向page/#{num-1}.html;最后一页由is_last判定,此时next_page为nil,避免生成不存在的“下一页”链接。

三、frontmatter 驱动标签分页:tags 字段的进阶玩法

同一个夹具中的文章全部带有tags: bar字段(如 2011-02-02-test-article.html.markdown 中的tags: bar),因此 paginate.feature 的同一场景还演示了按标签分组的完整流程:

def get_tags(resource) if resource.data.tags.is_a? String resource.data.tags.split(',').map(&:strip) else resource.data.tags end end def group_lookup(resource, sum) results = Array(get_tags(resource)).map(&:to_s).map(&:to_sym) results.each do |k| sum[k] ||= [] sum[k] << resource end end tags = articles .select { |resource| resource.data.tags } .each_with_object({}, &method(:group_lookup)) tags.each do |k, articles_in_tag| articles_in_tag.sort { |a, b| b.data.date <=> a.data.date }.per_page(2).each do |items, num, meta, is_last| page_path = num == 1 ? "/tags/#{k}.html" : "/tags/#{k}/page/#{num}.html" # ...与主分页相同的 prev_page / next_page / proxy 逻辑 end end

这里有三个值得注意的设计点:

  • get_tags对tags字段做了字符串与数组的双重兼容:当 frontmatter 中tags: bar是字符串时按逗号拆分并去空格;当它是数组时直接使用。这让同一套分页代码既能处理单标签文章,也能处理tags: [ruby, web]这种多标签写法;
  • group_lookup把文章按标签符号分组,得到{ bar: [文章列表], ... }这样的 Hash;
  • 标签分页用.per_page(2),每页只放 2 篇文章,第一页输出到/tags/bar.html,后续页输出到/tags/bar/page/2.html,与主分页共用/archive/2011/index.html作为模板。

自定义分页粒度是另一个亮点。paginate.feature 的“Custom pager method”场景演示了传入 Proc 而非数字的分页方式:

def items_per_page(all_items) [ all_items.shift(2), all_items ] end articles.sort { |a, b| b.data.date <=> a.data.date }.per_page(method(:items_per_page).to_proc).each do |items, num, meta, is_last| # ...同上,生成 /2011/index.html 与 /2011/page/2.html end

per_page接受一个把整份集合切分为若干页数组的 Proc:这里第一页取前 2 篇,第二页容纳剩余全部 5 篇。对照特征文件中的断言,第一页Article Count: 2、Page Start: 1、Page End: 2,第二页Article Count: 5、Page Start: 3、Page End: 7——说明每页的文章数量完全由自定义切分逻辑决定,而不是固定值。

四、模板侧消费分页数据:pagination 与 items 变量的约定

分页产物由 archive/2011/index.html.erb 渲染,它完整展示了模板侧可用的变量:

Paginate: <%= !!pagination %> Article Count: <%= items.length %> <% if pagination %> Page Num: <%= pagination.page_number %> Num Pages: <%= pagination.num_pages %> Per Page: <%= pagination.per_page %> Page Start: <%= pagination.page_start %> Page End: <%= pagination.page_end %> Next Page: '<%= sitemap.find_resource_by_destination_path(next_page).url if next_page %>' Prev Page: '<%= sitemap.find_resource_by_destination_path(prev_page).url if prev_page %>' <% end %> <% items.each do |article| %> <article> <%= article.data.title %> <%= article.url %> </article> <% end %>

各变量的含义与来源如下:

变量含义数据来源
pagination分页元数据对象(无分页时为 nil,可用!!pagination判断)config.rb 中locals传入的meta
items当前页的文章列表,items.length即本页文章数config.rb 中locals传入的items
pagination.page_number当前页码(从 1 开始)page_locals计算
pagination.num_pages总页数page_locals计算
pagination.per_page本页实际文章数page_locals计算
pagination.page_start/page_end本页文章在全集中的起始/结束序号(1 起始),用于渲染“第 X 到 Y 篇”page_locals计算
next_page/prev_page上/下一页的目标路径(字符串),由 config.rb 的 case 逻辑算出config.rb

Next Page: '<%= sitemap.find_resource_by_destination_path(next_page).url if next_page %>'一行展示了路径到 URL 的转换方式:next_page在 config.rb 中是形如/2011/page/2.html的目标路径,模板中通过sitemap.find_resource_by_destination_path找到对应资源后再取.url,从而在首页正确输出Next Page: '/2011/page/2.html'。

在 paginate.feature 的断言中可以验证这套约定的实际效果:

  • 访问/2011/index.html时:Article Count: 5、Page Num: 1、Num Pages: 2、Page Start: 1、Page End: 5、Next Page: '/2011/page/2.html',页面上能看到2011-01-03至2011-02-02共 5 篇文章,而最早的两篇(2011-01-01、2011-01-02)被正确地移到了第二页;
  • 访问/2011/page/2.html时:Article Count: 2、Page Start: 6、Page End: 7、Prev Page: '/2011/',只剩下两篇最早的旧文章;
  • 访问/tags/bar.html时:7 篇文章按每页 2 篇切成 3 页(Num Pages: 3),第一页只包含日期最新的两篇。

五、源码级原理:Array#per_page 与 Collections 扩展如何工作

分页能力并非 Middleman 模板渲染器的内置特性,而是通过 collections.rb 中一段“类级注入”实现的——它把per_page直接装进了 Ruby 原生Array:

require 'middleman-core/core_extensions/collections/pagination' # Super "class-y" injection of array helpers class Array include Middleman::Pagination::ArrayHelpers end

这解释了为什么 config.rb 里任何resources.select {...}产生的数组都能直接调用.per_page(...)——它本质上就是Array的一个实例方法。

其核心实现在 pagination.rb 中:

def per_page(per_page) return to_enum(__method__, per_page) unless block_given? parts = if per_page.respond_to? :call per_page.call(dup) else each_slice(per_page).reduce([]) do |sum, items| sum << items end end num_pages = parts.length collection = self current_start_i = 0 parts.each_with_index do |items, i| num = i + 1 meta = ::Middleman::Pagination.page_locals( num, num_pages, collection, items, current_start_i ) yield items, num, meta, num >= num_pages current_start_i += items.length end end

从实现可以看到三个关键设计:

  1. 双重入口:per_page参数既可以传数字(走each_slice(per_page)按固定大小切分),也可以传可调用对象(respond_to? :call分支直接调用切分函数),这正好对应 paginate.feature 中“Basic configuration”与“Custom pager method”两种场景;
  2. 无块时返回 Enumerator:return to_enum(__method__, per_page) unless block_given?让per_page在无块调用时返回可枚举对象,支持链式.each写法(自定义分页场景正是per_page(...).each do |...|);
  3. 每页 yield 四个参数:items(本页文章)、num(页码)、meta(分页元数据)、is_last(是否最后一页,即num >= num_pages),与 config.rb 中的块签名一一对应。

分页元数据由同文件的page_locals方法计算(pagination.rb):

def self.page_locals(page_num, num_pages, collection, items, page_start) per_page = items.length # Index into collection of the last item of this page page_end = (page_start + per_page) - 1 ::Middleman::Util.recursively_enhance(page_number: page_num, num_pages: num_pages, per_page: per_page, page_start: page_start + 1, page_end: [page_end + 1, collection.length].min, collection: collection) end

这里把page_start从 0 起始的内部游标转成 1 起始的展示序号(page_start + 1),并用[page_end + 1, collection.length].min确保最后一页的page_end不超过文章总数。模板中看到的Page Start: 6、Page End: 7正是这套计算的产物。

此外,collections.rb 还向模板暴露了一个便捷 helper:

helpers do def pagination current_resource.data.pagination end end

即模板中可以直接调用pagination取当前资源的分页元数据——这也是paginate-app的另一份模板 tag.html.erb 中paginate/page_articles等变量的相关基础设施(该模板还展示了通过 frontmatter 声明pageable: true与per_page: 2的另一种分页入口)。

六、把分页能力迁移到自己的 Middleman 项目

综合以上分析,要在自己的 Middleman 项目中落地同样的分页功能,只需三步:

  1. 准备文章数据源:按 2011-02-02-test-article.html.markdown 的格式,为每篇文章写好 frontmatter(至少包含title与date,需要标签归档时再加tags);
  2. 在 config.rb 中编写分页配置:用resources.select筛选文章 → 按data.date倒序 → 调.per_page(N)(或自定义切分 Proc)→ 在块内用proxy生成各页,并把items、pagination、prev_page、next_page通过locals传给模板;
  3. 编写分页模板:参照 archive/2011/index.html.erb,遍历items输出文章列表,并用pagination.page_number/num_pages/page_start/page_end渲染页码信息,用next_page/prev_page渲染上下页导航。

验证时可对照 paginate.feature 中的断言标准:确认首页/中间页/末页的Article Count、Page Start、Page End、Next Page、Prev Page输出符合预期,且每一页的文章互不重复、无遗漏。

  • 前端
  • 开发工具

【免费下载链接】middleman

Hand-crafted frontend development

项目地址:https://gitcode.com/gh_mirrors/mi/middleman
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 11:28:17

SpringBoot+Vue公共自行车管理系统设计与实现

1. 项目背景与核心价值 泗洪县公共自行车在线租赁管理系统是一个典型的"互联网绿色出行"解决方案。随着城市短途出行需求的快速增长&#xff0c;传统人工管理的公共自行车系统暴露出租还车效率低、调度不及时、数据统计困难等问题。这个毕业设计项目正是针对这些痛点…

作者头像 李华
网站建设 2026/10/15 11:28:07

iPhone文件传输全攻略:绕过iTunes的7种高效方案

1. 项目背景与需求解析作为一名长期使用iPhone的深度用户&#xff0c;我深刻理解文件传输这个看似简单却经常让人抓狂的痛点。每次需要把手机里的照片、视频或文档传到电脑时&#xff0c;iTunes那缓慢的传输速度和复杂的操作流程总让人望而却步。更不用说Windows系统下iTunes那…

作者头像 李华
网站建设 2026/10/15 11:27:47

零基础OpenClaw安装-你的私人顾问:从Node.js到TaoToken的完整配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 11:27:46

Netty实现高性能WebSocket服务:高并发低延迟实战指南

1. 为什么是 Netty 而不是 Spring WebSocket&#xff1f;——从一个真实压测场景说起 去年在做某高校实验室的物联网数据中台项目时&#xff0c;我们遇到一个典型瓶颈&#xff1a;前端需要同时接入 8000 台边缘设备&#xff0c;每台设备每 5 秒上报一次 JSON 格式的状态快照&am…

作者头像 李华
网站建设 2026/10/15 11:27:16

QRLSTM分位数回归在时序预测中的应用与Matlab实现

1. 项目背景与核心价值 在金融风控、电力负荷预测、医疗数据分析等领域&#xff0c;传统LSTM神经网络虽然能够捕捉时间序列数据的长期依赖关系&#xff0c;但其点预测特性无法反映预测结果的不确定性分布。这正是QRLSTM&#xff08;Quantile Regression Long Short-Term Memory…

作者头像 李华
网站建设 2026/10/15 11:26:57

impeccable:零配置、可验证的开发者工具交付标准

1. 项目概述&#xff1a;一个被误读的“完美”词&#xff0c;如何成为开发者工具链中的真实存在“impeccable”这个词在英语里本意是“无可挑剔的、完美的”&#xff0c;常用于形容服务、工艺或细节处理达到了极致水准。但放在当前技术语境下——尤其是与npx、CLI、browser ext…

作者头像 李华