详解 WordPress `wp_nav_menu()` 函数的源码:如何通过 `Walker` 类递归渲染菜单项的 HTML。

各位观众,晚上好!我是今天的讲师,咱们今天来聊聊 WordPress 菜单背后的“大功臣”—— wp_nav_menu() 函数,以及它如何巧妙地利用 Walker 类把菜单项们变成漂亮的 HTML 代码。准备好了吗?咱们要深入源码,看看这幕后的英雄是如何施展魔法的。

第一幕:wp_nav_menu() 函数:总指挥官登场

首先,wp_nav_menu() 函数是 WordPress 菜单显示的核心。它接收一个数组作为参数,这个数组里面包含了各种配置信息,比如菜单的 ID、菜单显示的位置(主题位置)等等。咱们先来看一个简单的例子:

<?php
wp_nav_menu( array(
    'theme_location' => 'primary', // 指定菜单的主题位置
    'menu_id'        => 'primary-menu', // 给菜单的 ul 标签添加 ID
    'container'      => 'nav',       // 用 nav 标签包裹菜单
    'container_class' => 'main-navigation', // 给 nav 标签添加 class
) );
?>

这段代码告诉 WordPress:“嘿,显示 ‘primary’ 这个主题位置的菜单,给 ul 标签一个 ‘primary-menu’ 的 ID,用 <nav> 标签包裹起来,并且给这个 <nav> 标签加上 ‘main-navigation’ 的 class。”

wp_nav_menu() 本身并不负责生成 HTML。它更像是一个指挥官,负责接收指令,然后把任务分派给更专业的士兵。

第二幕:Walker 类:HTML 渲染大师

这个“士兵”就是 Walker 类(或者它的子类)。Walker 类是一个抽象类,它的作用是遍历一个多维数组(通常是树形结构),并按照一定的规则生成 HTML。 WordPress 菜单项正是以树形结构存储的,所以用 Walker 类来处理简直完美。

WordPress 默认使用 Walker_Nav_Menu 类来渲染菜单。这个类继承自 Walker 类,并针对菜单项进行了专门的优化。

第三幕:深入 Walker_Nav_Menu 类:核心方法详解

Walker_Nav_Menu 类有几个关键方法,它们共同完成了菜单 HTML 的生成:

  1. start_lvl( &$output, $depth = 0, $args = array() ) 在开始一个子菜单(<ul> 标签)时调用。

  2. end_lvl( &$output, $depth = 0, $args = array() ) 在结束一个子菜单(</ul> 标签)时调用。

  3. start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) 在开始一个菜单项(<li> 标签)时调用。这是最核心的方法,负责生成每个菜单项的 HTML。

  4. end_el( &$output, $item, $depth = 0, $args = array() ) 在结束一个菜单项(</li> 标签)时调用。

让我们逐个分析这些方法,并结合代码示例,看看它们是如何工作的。

3.1 start_lvl():开启子菜单之旅

start_lvl() 方法负责生成子菜单的 <ul> 标签。它接收三个参数:

  • $output:引用传递的字符串,用于存储生成的 HTML。
  • $depth:当前菜单项的深度(层级)。
  • $args:一个包含各种配置信息的数组,与 wp_nav_menu() 传入的参数类似。
public function start_lvl( &$output, $depth = 0, $args = array() ) {
    $indent = str_repeat("t", $depth);
    $output .= "n$indent<ul class="sub-menu">n";
}

这段代码很简单:

  • $indent = str_repeat("t", $depth);:根据深度生成缩进,使 HTML 代码更易读。
  • $output .= "n$indent<ul class="sub-menu">n";:将 <ul> 标签添加到 $output 字符串中,并添加一个 sub-menu 的 class。

3.2 end_lvl():结束子菜单的旅程

end_lvl() 方法与 start_lvl() 相对应,负责生成子菜单的 </ul> 标签。参数与 start_lvl() 相同。

public function end_lvl( &$output, $depth = 0, $args = array() ) {
    $indent = str_repeat("t", $depth);
    $output .= "$indent</ul>n";
}

这段代码同样简单:

  • $indent = str_repeat("t", $depth);:根据深度生成缩进。
  • $output .= "$indent</ul>n";:将 </ul> 标签添加到 $output 字符串中。

3.3 start_el():菜单项的灵魂工程师

start_el() 方法是整个 Walker_Nav_Menu 类中最核心的方法。它负责生成每个菜单项的 <li> 标签以及其中的链接。它接收五个参数:

  • $output:引用传递的字符串,用于存储生成的 HTML。
  • $item:一个包含菜单项信息的对象。这个对象包含了菜单项的 ID、标题、链接、描述等等。
  • $depth:当前菜单项的深度。
  • $args:一个包含各种配置信息的数组。
  • $id:菜单项的 ID。
public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
    $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';

    $classes = empty( $item->classes ) ? array() : (array) $item->classes;
    $classes[] = 'menu-item-' . $item->ID;

    /**
     * Filter the arguments for the nav menu item.
     *
     * @since 4.4.0
     *
     * @param array  $args  An array of arguments.
     * @param object $item  Menu item data object.
     * @param int    $depth Depth of menu item. Used for padding.
     */
    $args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );

    /**
     * Filter the CSS class(es) applied to a menu item's list item element.
     *
     * @since 3.0.0
     *
     * @param string[] $classes An array of CSS classes to be applied
     *                           to the menu's list item element.
     * @param object   $item      The current menu item.
     * @param WP_Term  $args      An array of {@see wp_nav_menu()} arguments.
     * @param int      $depth     Depth of menu item. Used for padding.
     */
    $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
    $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

    /**
     * Filter the ID applied to a menu item's list item element.
     *
     * @since 3.0.1
     *
     * @param string $menu_id The ID that is applied to the menu item's list item element.
     * @param object $item    The current menu item.
     * @param WP_Term $args    An array of {@see wp_nav_menu()} arguments.
     * @param int    $depth   Depth of menu item. Used for padding.
     */
    $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth );
    $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

    $output .= $indent . '<li' . $id . $class_names .'>';

    $atts = array();
    $atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
    $atts['target'] = ! empty( $item->target )     ? $item->target     : '';
    $atts['rel']    = ! empty( $item->xfn )        ? $item->xfn        : '';
    $atts['href']   = ! empty( $item->url )        ? $item->url        : '';

    /**
     * Filter the HTML attributes applied to a menu item's anchor element.
     *
     * @since 3.6.0
     *
     * @param array $atts {
     *     The HTML attributes applied to the menu item's `<a>` element, empty strings are ignored.
     *
     *     @type string $title  Title attribute.
     *     @type string $target Target attribute.
     *     @type string $rel    The rel attribute.
     *     @type string $href   The href attribute.
     * }
     * @param object $item  The current menu item.
     * @param WP_Term $args  An array of {@see wp_nav_menu()} arguments.
     * @param int    $depth Depth of menu item. Used for padding.
     */
    $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );

    $attributes = '';
    foreach ( $atts as $attr => $value ) {
        if ( ! empty( $value ) ) {
            $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
            $attributes .= ' ' . $attr . '="' . $value . '"';
        }
    }

    /** This filter is documented in wp-includes/post-template.php */
    $title = apply_filters( 'the_title', $item->title, $item->ID );

    /**
     * Filter the content of the link to a menu item.
     *
     * @since 3.0.0
     *
     * @param string $title The link text.
     * @param object $item  The current menu item.
     * @param WP_Term $args  An array of {@see wp_nav_menu()} arguments.
     * @param int    $depth Depth of menu item. Used for padding.
     */
    $title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );

    $item_output = $args->before;
    $item_output .= '<a'. $attributes .'>';
    $item_output .= $args->link_before . $title . $args->link_after;
    $item_output .= '</a>';
    $item_output .= $args->after;

    /**
     * Filter a menu item's starting output.
     *
     * The menu item's starting output only includes `$args->before`, the opening `<a>`
     * tag, the menu item's title, the closing `</a>` tag, and `$args->after`. Currently,
     * there is no filter for modifying the opening and closing `<li>` tags.
     *
     * @since 3.0.0
     *
     * @param string $item_output The menu item's starting HTML output.
     * @param object $item        Menu item data object.
     * @param int    $depth       Depth of menu item. Used for padding.
     * @param WP_Term $args        An array of {@see wp_nav_menu()} arguments.
     */
    $output .= apply_filters( 'walker_nav_menu_start_el', $indent . $item_output, $item, $depth, $args );
}

代码虽然有点长,但逻辑还是比较清晰的:

  1. 生成缩进: $indent = ( $depth ) ? str_repeat( "t", $depth ) : ''; 根据深度生成缩进。

  2. 处理 CSS 类:

    • $classes = empty( $item->classes ) ? array() : (array) $item->classes; 获取菜单项的 CSS 类,如果为空则创建一个空数组。
    • $classes[] = 'menu-item-' . $item->ID; 添加一个默认的 CSS 类,menu-item-{菜单项ID}
    • $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) ); 将 CSS 类数组转换成字符串,并应用 nav_menu_css_class 过滤器,允许修改 CSS 类。
    • $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : ''; 如果 CSS 类字符串不为空,则生成 class 属性。esc_attr() 函数用于转义 HTML 属性中的特殊字符,防止 XSS 攻击。
  3. 处理 ID:

    • $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth ); 生成菜单项的 ID,并应用 nav_menu_item_id 过滤器,允许修改 ID。
    • $id = $id ? ' id="' . esc_attr( $id ) . '"' : ''; 如果 ID 不为空,则生成 id 属性。
  4. 生成 <li> 标签: $output .= $indent . '<li' . $id . $class_names .'>'; 将缩进、ID 和 CSS 类添加到 <li> 标签中。

  5. 处理链接属性:

    • $atts['title'] = ! empty( $item->attr_title ) ? $item->attr_title : ''; 获取菜单项的 title 属性。
    • $atts['target'] = ! empty( $item->target ) ? $item->target : ''; 获取菜单项的 target 属性。
    • $atts['rel'] = ! empty( $item->xfn ) ? $item->xfn : ''; 获取菜单项的 rel 属性。
    • $atts['href'] = ! empty( $item->url ) ? $item->url : ''; 获取菜单项的 href 属性。
    • $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth ); 应用 nav_menu_link_attributes 过滤器,允许修改链接属性。
  6. 生成链接属性字符串: 将链接属性数组转换成字符串。

  7. 处理链接标题:

    • $title = apply_filters( 'the_title', $item->title, $item->ID ); 获取菜单项的标题,并应用 the_title 过滤器。
    • $title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth ); 应用 nav_menu_item_title 过滤器,允许修改链接标题。
  8. 生成链接:

    • $item_output = $args->before; 获取 wp_nav_menu() 传入的 $args 数组中的 before 属性,这个属性允许在链接前添加内容。
    • $item_output .= '<a'. $attributes .'>'; 生成 <a> 标签,并将链接属性添加到标签中。
    • $item_output .= $args->link_before . $title . $args->link_after; 在链接标题前后添加内容,这些内容也来自 $args 数组。
    • $item_output .= '</a>'; 关闭 <a> 标签。
    • $item_output .= $args->after; 获取 $args 数组中的 after 属性,这个属性允许在链接后添加内容。
  9. 应用 walker_nav_menu_start_el 过滤器: $output .= apply_filters( 'walker_nav_menu_start_el', $indent . $item_output, $item, $depth, $args ); 允许修改整个菜单项的 HTML。

3.4 end_el():菜单项的谢幕

end_el() 方法负责生成菜单项的 </li> 标签。

public function end_el( &$output, $item, $depth = 0, $args = array() ) {
    $output .= "</li>n";
}

这段代码很简单,就是将 </li> 标签添加到 $output 字符串中。

第四幕:display_element():递归的奥秘

Walker 类中最核心的方法是 display_element()。 这个方法负责递归遍历菜单项,并调用 start_lvl()start_el()end_el()end_lvl() 方法来生成 HTML。

public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
    if ( ! $element )
        return;

    $id_field = $this->db_fields['id'];

    // Display this element.
    if ( is_object( $args[0] ) )
       $args[0]->has_children = ! empty( $children_elements[ $element->$id_field ] );

    parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
}

display_element 实际上调用了父类 Walkerdisplay_element 方法,它会根据菜单项的层级关系,递归调用 start_elend_elstart_lvlend_lvl 方法。

第五幕:自定义 Walker 类:打造个性化菜单

Walker_Nav_Menu 类已经很强大了,但有时候我们还需要自定义菜单的 HTML 结构。这时候,我们可以创建一个自定义的 Walker 类,继承自 Walker_Nav_Menu 类,并重写其中的方法。

例如,我们可以创建一个 My_Walker_Nav_Menu 类,修改 start_el() 方法,给菜单项添加一个图标:

class My_Walker_Nav_Menu extends Walker_Nav_Menu {
    public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        parent::start_el( $output, $item, $depth, $args, $id );

        // 在链接前添加一个图标
        $output = str_replace( '<a', '<i class="fa fa-home"></i><a', $output );
    }
}

然后,在 wp_nav_menu() 函数中使用这个自定义的 Walker 类:

<?php
wp_nav_menu( array(
    'theme_location' => 'primary',
    'walker'         => new My_Walker_Nav_Menu(),
) );
?>

总结:wp_nav_menu()Walker 类的完美配合

wp_nav_menu() 函数和 Walker 类是 WordPress 菜单系统的两大支柱。wp_nav_menu() 负责接收配置信息,并将任务分派给 Walker 类。Walker 类负责遍历菜单项,并生成 HTML 代码。通过自定义 Walker 类,我们可以灵活地控制菜单的 HTML 结构,打造个性化的菜单。

表格总结:关键方法与作用

方法名称 作用
start_lvl() 在开始一个子菜单(<ul> 标签)时调用,负责生成 <ul> 标签。
end_lvl() 在结束一个子菜单(</ul> 标签)时调用,负责生成 </ul> 标签。
start_el() 在开始一个菜单项(<li> 标签)时调用,负责生成每个菜单项的 <li> 标签以及其中的链接。这是最核心的方法。
end_el() 在结束一个菜单项(</li> 标签)时调用,负责生成 </li> 标签。
display_element() 递归遍历菜单项,并调用 start_lvl()start_el()end_el()end_lvl() 方法来生成 HTML。 实际上调用了父类 Walkerdisplay_element 方法,它会根据菜单项的层级关系,递归调用相关方法。

希望今天的讲解能够帮助大家更好地理解 WordPress 菜单系统的工作原理。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注