以下是由 Scott Fennell 发布的客座文章。Scott 是阿拉斯加安克雷奇的一位 WordPress 主题和插件开发者。在这里,他根据自己的喜好自定义了 WordPress 菜单系统的默认 HTML 输出,而不会破坏其已有的有用功能。
WordPress 导航菜单系统有很多我喜欢的地方,也有一些我非常不喜欢的地方。我不喜欢的地方恰好触及了我的痛点:标记膨胀和不符合 SMACCS 的类名。对我来说,深入研究以纠正这种情况是值得的。但是,我不想失去与管理界面的集成,也不想失去核心提供的动态类名,例如 current_page_item
或 current-menu-ancestor
。因此,我不会替换任何东西:我将扩展用于绘制导航菜单的 PHP 类:Walker_Nav_Menu
类。
我将通过构建一个插件来实现这一点,该插件将输出具有我想要的任何标记和类的导航菜单。在此过程中,我将停下来闻闻玫瑰花香 并使用 var_dump()
查看 WordPress 向我们公开的 PHP 变量。此插件将包含以下组件
- 一个主插件文件,用于注册插件并调用其他文件
- 一个用于输出菜单的短代码
- 一些 CSS、JS 和 SVG 用于执行显示/隐藏子菜单等操作
- 一个自定义 Walker 类,它将扩展核心
Walker_Nav_Menu
类
在这些组件中,除了最后一个组件之外,其他组件主要充当占位符。它们将提供最少的代码量以实现最小可行产品,我不会详细探讨它们。它们将为我构建自定义 Walker 类提供足够的基础,而这正是本文的重点。
假设
- 让我们在 twentyfifteen 主题上进行操作
- 如果任何其他插件处于活动状态,请确保它们不会导致 JS 或 PHP 错误。如有疑问,请停用它们
- 我在撰写本文时使用的是 WordPress 4.3.1。
插件
在进行过程中,我将引用完成的插件中的代码块。如果您想参考最终产品,甚至将其安装在 WordPress 测试站点上,可以从 我的 GitHub 存储库 中获取它。

短代码
该插件通过注册一个短代码 [csst_nav]
来工作。该短代码接受一个参数 which_menu
,您可以在其中通过提供导航菜单的 slug、ID 或标题来选择要输出的导航菜单。以下是一些示例,我碰巧有一个名为“法律链接”的菜单,其 slug 为 legal-links
,ID 为 5
[csst_nav]
[csst_nav which_menu='legal-links']
[csst_nav which_menu='Legal Links']
[csst_nav which_menu='5']

该短代码是 wp_nav_menu()
函数的包装器,该函数接受大量参数。
这是我偏离默认值并执行“我更喜欢的方式”的地方
menu
:我想能够指定要获取哪个菜单。container
:我想要更少的标记,因此不需要容器元素。menu_class
:我喜欢类名。我将为我的插件和我要获取的菜单提供一些命名空间的类名。echo
:不,谢谢。我将返回菜单,而不是将其回显。items_wrap
:我将用<nav>
而不是默认的无序列表来包装项目。before
:我将每个菜单项作为<span>
打开,并删除 核心硬编码的<li>
。after
:我将用结束</span>
关闭每个菜单项,并删除 核心硬编码的</li>
。before_submenu
:我将每个子菜单作为<span>
而不是<ul>
打开。after_submenu
:我将用结束<span>
而不是结束</ul>
关闭每个子菜单。walker
:**这就是您阅读本文的原因。**我将告诉 WordPress 使用我们的自定义 Walker 类。
其中一些参数,例如 before_submenu
和 after_submenu
,实际上并没有随 wp_nav_menu()
一起提供。但这没关系,因为它们仍然会传递到 Walker 类,我可以在那里根据需要使用它们。
以下是代码中的外观
<?php
/**
* The main template tag for this class. Get a custom menu via our walker.
*
* @return string A WordPress custom menu, passed through our walker class.
*/
public function get() {
// The CSS class for our shortcode.
$class = strtolower( __CLASS__ );
// Get a menu from the db.
$which_menu = $this -> which_menu;
/**
* Args for a call to wp_nav_menu().
*
* Some of these args don't get used by wp_nav_menu() per se,
* but we're able to pass them through to our walker class, which does use them.
*/
$menu_args = array(
// Instead of wrapping each menu item as list item, let's do a span.
'after' => '',
// The closing markup after a submenu.
'after_submenu' => '',
// Instead of wrapping each menu item as list item, let's do a span.
'before' => '',
// The opening markup before a submenu.
'before_submenu' => '',
// Nope, we don't need extra markup wrapping our menu.
'container' => FALSE,
// Nope, let's return instead of echo.
'echo' => FALSE,
// Let's use a <nav> instead of a nested list.
'items_wrap' => '<nav role="navigation" class="%2$s">%3$s</nav>',
// Which menu to grab? Takes ID, name, or slug.
'menu' => $which_menu,
// CSS classes for our menu.
'menu_class' => "$class $class-$which_menu",
// Our custom walker.
'walker' => new CSST_Nav_Walker(),
);
// The main content of the shortcode is in fact a call to wp_nav_menu().
$out = wp_nav_menu( $menu_args );
return $out;
}
?>
好了,前言就讲到这里。现在是时候深入研究自定义 Walker 类了。我热爱详尽的细节!
自定义 Walker 类
这里存在某种层次结构
- 核心定义了一个极其通用的类:
Walker
。它的目的是迭代遍历复杂结构(如多维数组),并在该结构的每个成员上执行操作。 - 然后,核心定义了
Walker
的一个更具体的扩展,专门用于挖掘导航菜单:Walker_Nav_Menu
。 - 最后,我定义了自己的
Walker_Nav_Menu
扩展,并将其称为CSST_Nav_Walker
。
我的自定义 Walker 类将扩展核心 Walker_Nav_Menu
的以下方法
start_el()
,它附加菜单项的开始标记以及菜单项本身。end_el()
,它附加菜单项的结束标记。start_lvl()
,它附加子菜单的开始标记。end_lvl()
,它附加子菜单的结束标记。
这些都是一些超级通用的名称,对吧?这正是重点:我们继承自 Walker
,它旨在能够以任何原因迭代遍历任何类型的结构。在这种情况下,特殊性是敌人。让我们跳过抽象的命名法,找出每个方法对我们有什么作用!
start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 )
此方法绘制菜单项的开始 HTML 以及菜单项本身。它包含五个参数
&$output
,它是菜单的所有 HTML,直到“此”菜单项。当我说“此”菜单项时,请理解此方法会为每个菜单项调用一次。$item
,它是此菜单项的 WP Post 对象(菜单项实际上是nav_menu_item
帖子类型的帖子),以及一些特定于导航菜单的其他数据。$depth
,它跟踪我们在菜单中嵌套了多少层——例如嵌套的子菜单。$args
,它主要是一个wp_nav_menu()
参数数组。它包含我们在短代码回调中传递的参数,以及我们省略的所有默认值。$id
,在核心源代码中记录为当前菜单项的 ID,但我不知道它是否仍然受支持。
这些参数中的大多数都有些平淡无奇,但其中一些包含大量有用信息。让我来使用 var_dump()
!
&$output
请注意,此变量以一个&符号作为前缀,这意味着它是 通过引用传递 的。这意味着该方法不必返回任何内容,因为发生在此变量中的任何操作都会影响方法外部的变量。这也是为什么 var_dump()
会非常快地变得非常大的原因
var_dump( esc_html( $output ) );
会得到我们
<?php
string(0) ""
string(274) "
"
string(1066) "
”
这最终会产生大约 35kb 的 var_dump()
文本,因此我已对其进行了大幅截断。我只显示了前三个菜单项的部分内容。这是前面菜单项的标记,在每个菜单项处,这就是为什么我们要 将当前菜单项附加到它 的原因。
$item
此参数为我们提供了当前菜单项的 WP Post 对象,使其成为此方法中最有趣的一个参数。
wp_die( var_dump( $item ) )
提供给我们
<?php
object(WP_Post)#358 (40) {
["ID"] => int(68)
["post_author"] => string(1) "1"
["post_date"] => string(19) "2015-10-07 01:05:49"
["post_date_gmt"] => string(19) "2015-10-07 01:05:49"
["post_content"] => string(1) " "
["post_title"] => string(0) ""
["post_excerpt"] => string(0) ""
["post_status"] => string(7) "publish"
["comment_status"] => string(6) "closed"
["ping_status"] => string(6) "closed"
["post_password"] => string(0) ""
["post_name"] => string(2) "68"
["to_ping"] => string(0) ""
["pinged"] => string(0) ""
["post_modified"] => string(19) "2015-10-07 01:05:49"
["post_modified_gmt"] => string(19) "2015-10-07 01:05:49"
["post_content_filtered"] => string(0) ""
["post_parent"] => int(0)
["guid"] => string(33) "http://localhost/wp/csstnav/?p=68"
["menu_order"] => int(1)
["post_type"] => string(13) "nav_menu_item"
["post_mime_type"] => string(0) ""
["comment_count"] => string(1) "0"
["filter"] => string(3) "raw"
["db_id"] => int(68)
["menu_item_parent"] => string(1) "0"
["object_id"] => string(2) "50"
["object"] => string(4) "page"
["type"] => string(9) "post_type"
["type_label"] => string(4) "Page"
["url"] => string(28) "http://localhost/wp/csstnav/"
["title"] => string(10) "Front Page"
["target"] => string(0) ""
["attr_title"] => string(0) ""
["description"] => string(0) ""
["classes"] => array(8) {
[0]=> string(0) ""
[1]=> string(9) "menu-item"
[2]=> string(24) "menu-item-type-post_type"
[3]=> string(21) "menu-item-object-page"
[4]=> string(17) "current-menu-item"
[5]=> string(9) "page_item"
[6]=> string(12) "page-item-50"
[7]=> string(17) "current_page_item"
}
["xfn"] => string(0) ""
["current"] => bool(true)
["current_item_ancestor"] => bool(false)
["current_item_parent"] => bool(false)
}
非常棒,对吧?我们可以访问该帖子对象并获取大量有趣的内容,例如摘录、日期、分类法。哎呀,也许我们可以设计一种方法来为导航菜单项添加特色图片!除了我们通常在帖子中看到的这些值之外,还有几个新项目,例如 classes
。在那里可以找到动态 CSS 类的强大数组:例如 current-menu-item
之类的东西。同样值得注意的是 object
,它为我们提供了有关此菜单项链接到什么的详细信息:可能是页面或术语归档。
$depth
Depth 持续跟踪我们有多深地嵌套了子菜单。对此我没有任何用处,但我愿意停下来欣赏一下 核心 对它的处理方式:它们用它来添加制表符字符(实际上是“\t”),以便源代码更易读。至少我假设这就是原因。核心做得很好。
与其 var_dump()
$depth
,不如将其附加到每个项目的 &$output
中更具指导意义。您可以看到它如何跟踪每个项目的级别。

start_el()
方法的 $depth 演示。$args
$args
应该看起来很熟悉:它主要是我在短代码中 传递给 wp_nav_menu()
的值数组。此外,还有我省略的任何参数的默认值。
var_dump( esc_html( $args ) );
会得到我们
<?php
object(stdClass)#341 (16) {
["menu"] => string(13) "a-nested-menu"
["container"] => bool(false)
["container_class"] => string(0) ""
["container_id"] => string(0) ""
["menu_class"] => string(31) "csst_nav csst_nav-a-nested-menu"
["menu_id"] => string(0) ""
["echo"] => bool(false)
["fallback_cb"] => string(12) "wp_page_menu"
["before"] => string(0) ""
["after"] => string(0) ""
["link_before"] => string(0) ""
["link_after"] => string(0) ""
["items_wrap"] => string(46) "%3$s"
["depth"] => int(0)
["walker"] => object( CSST_Nav_Walker )#339 (5) {
["icon"] => string(96) "
<svg class='csst_nav_svg-icon'>
<use xmlns:xlink='http://www.w3.org/1999/xlink' xlink:href='#csst_nav_svg-icon'></use>
</svg>
"
["tree_type"]=> array(3) {
[0] => string(9) "post_type"
[1] => string(8) "taxonomy"
[2] => string(6) "custom"
}
["db_fields"] => array(2) {
["parent"] => string(16) "menu_item_parent"
["id"] => string(5) "db_id"
}
["max_pages"] => int(1)
["has_children"] => bool(false)
}
["theme_location"] => string(0) ""
}
值得注意的是 walker
参数。您可以看到它命名了我们的 Walker 类,甚至捕获了我们作为类成员保存的 SVG 图标!walker
参数下的其他项目对于我们自定义导航菜单的目的而言,要么未使用,要么不感兴趣。
$id
$id
似乎让人有些失望。它始终为 0。甚至不会为你转储它。
start_el()
参数的实际用途
让我们从核心在 Walker_Nav_Menu -> start_el()
中所做的操作开始。如上所述,它们使用 $depth
来 添加制表符,似乎是为了追求更易读的源代码。多么精湛的工艺!此外,你最好相信他们会从 $item
中获取这些 CSS 类。
在我的自定义版本中,我有两个增值功能。首先,我有机会根据自己的编码偏好构建菜单项。例如,我碰巧讨厌三元运算符。其次,我有机会为 WordPress 为菜单项生成的 CSS 类命名空间。current-menu-item
将变为 csst_nav-current_menu_item
。我通过将 CSS 类传递给 自定义方法 来实现此目的,该方法会重命名类并将其传递回来。它们会带有我们项目的开头,以及连字符和下划线等方面更一致的格式。
这就是 start_el()
的全部内容!关于菜单项的起始 HTML,我没有什么更多要说的。但现在它已经打开了,我们最好将其关闭。
end_el( &$output, $item, $depth = 0, $args = array() )
end_el()
是 “一个非常短的方法:它所做的只是附加菜单项的结束 HTML。它承载与 start_el()
相同的参数,除了 $id
,它省略了 $id
。此外,&$output
将比我们在 start_el()
中遇到它时更大,因为当前 $item
已附加到其中。这些参数在我的 start_el()
讨论中已 var_dump()
过,所以我不会再赘述。
至于实际用法,有趣的是,核心只是打印一个结束的 li
。相反,我正在回溯到 $args
以使用我在创建短代码时通过 after
参数 指定的标记 来关闭元素。
start_lvl( &$output, $depth = 0, $args = array() )
这个奇怪命名的家伙 的目的是在我们要遍历的结构中开始一个新的“级别”。这听起来很抽象,但幸运的是我们手头有一个非常熟悉的例子:在导航菜单中,新级别只是一个子菜单!
此方法承载三个参数,&$output
、$depth
和 $args
,这些参数都在上面进行了 var_dump()
。至于用法,核心借此机会为子菜单打开一个新的 ul
,并带有缩进的源代码。非常好。但是,很多时候我发现自己对子菜单的处理方式不满意。例如,我想添加一个切换图标来指示存在子菜单。我希望子菜单使用我的标记和 CSS 类。而且,我希望子菜单在单击切换时作为显示/隐藏响应。这是进行这些 自定义 的绝佳时机。
好时光:我们的子菜单已打开,子菜单项将通过上面的 start_el()
和 end_el()
附加到其中。如果此子菜单项内部还有子菜单,没问题。这些也将通过 start_lvl()
附加。一旦全部完成,我们就需要关闭子菜单。
end_lvl( &$output, $depth = 0, $args = array() )
其他元素
我的自定义 Walker 确实还有一些其他元素:一个构造函数和几个属性。我使用构造函数来调用我的 SVG 图标类并为子菜单获取切换图标。我将图标保存为类上的一个属性,以便我的其他方法可以轻松地使用它。
核心的 Walker_Nav_Menu
类也有一些其他元素。
- 一个名为
$tree_type
的神秘属性,甚至核心 也没有使用 它。源代码将其记录为“类处理的内容”,而var_dump()
为我们提供了<?php array(3) { [0]=> string(9) "post_type" [1]=> string(8) "taxonomy" [2]=> string(6) "custom" } ?>
这,呃,随便吧。
- 一个名为
$db_fields
的属性,它有点不透明。var_dump()
为我们提供了<?php array(2) { ["parent"] => string(16) "menu_item_parent" ["id"] => string(5) "db_id" } ?>
对此,我 认输了。如果您能弄清楚这些是如何使用的以及我们如何利用它们来做一些有趣的事情,请在评论中留下您的想法!
资源和后续步骤
Walker
及其继承者不像 WordPress 的其他部分那样被广泛讨论或记录,这也是促使我撰写本文的原因之一。但是,有一些先前的工作可用。当我看到 Bootstrap 导航菜单的这个移植 时,我第一次对 Walker 深入研究产生了兴趣。而且,不出所料,手册 也提供了一些示例。
本文主要讨论的是如何控制导航项目和子菜单周围的类名和标记,但还有许多其他可能性。也许我们可以访问$item
并获取特色图片或一些文章元数据,如果$item
恰好链接到一篇文章。如果它恰好链接到一个分类归档,也许我们想要从即将推出的term_meta系统中获取一些内容。你甚至可以做一些完全不同的事情,比如使用你最喜欢的jQueryUI小部件或图片滑块所需的标记和类输出菜单项。试试看,祝你var_dump()
愉快!
WordPress的导航遍历器很糟糕。为什么不直接输出自定义HTML而不使用它呢?
这是
getItems()
辅助函数这些项目只是WordPress文章,它们包含你可能需要的所有内容。
感谢你的评论!
我真的很喜欢你的方法,在我看来它完全合理。一些需要考虑的事情,它们可能对你特定的情况有说服力,也可能没有
1) 我认为对于没有子菜单的菜单,你说的很有道理。对于确实有子菜单的菜单,我不确定你如何处理这种情况。
2) 你错过了将你的导航菜单项公开给一些核心过滤器的机会。
3) 你的实现需要指定菜单位置,而不是能够传递菜单slug、ID或名称。
4) 你的实现要求用户每次使用辅助函数时都包装菜单,而不是能够假设一个默认的包装器。
公平的观点。让我解决其中一些问题
1) 大多数网站通常不会嵌套超过一两层,这在同一个视图中保持整洁。如果你需要嵌套超过两层,你可以将菜单放入递归视图中。你还需要一个循环来从扁平的菜单项数组中创建一个嵌套数组。
3)
wp_get_nav_menu_object()
接受菜单ID、名称或slug。你只需跳过该位置查找即可。#2和#4完全正确。