分析 `wp_nav_menu()` 函数的源码,它是如何渲染出导航菜单的?

各位观众,各位听众,晚上好!我是今天的主讲人,江湖人称“代码老司机”,很高兴能和大家一起扒一扒WordPress的“老司机”函数——wp_nav_menu(),看看它到底是怎么把一个看起来简单的导航菜单给“揉”出来的。

今天咱们就来一次深度的代码解剖,保证让你看完之后,也能成为“菜单老司机”。咱们尽量用通俗易懂的语言,加上大量的代码示例,让大家彻底搞明白wp_nav_menu()的运行机制。

一、wp_nav_menu() 是个什么玩意儿?

首先,咱们得搞清楚wp_nav_menu()是干嘛的。简单来说,它就是一个函数,负责根据你在WordPress后台设置的导航菜单,生成HTML代码,并在你的网站前端显示出来。

就像你点外卖,你点的是“红烧肉盖饭”,外卖小哥送来的就是一份热气腾腾的“红烧肉盖饭”。wp_nav_menu()就相当于外卖小哥,你告诉它你要显示哪个菜单,它就给你生成对应的HTML代码。

二、参数大揭秘:wp_nav_menu() 都吃些什么?

wp_nav_menu()函数接受一个数组作为参数,这个数组里包含了各种选项,告诉函数你想怎么定制这个菜单。 就像你点外卖的时候可以备注“不要香菜”、“多放辣椒”一样。

常用的参数如下表所示:

参数名 类型 描述 默认值
menu string/int 指定要显示的菜单。可以是菜单的名称、ID或slug。
menu_class string 指定<ul>元素的CSS class。 menu
menu_id string 指定<ul>元素的ID。 menu-{menu slug}
container string 指定包裹菜单的容器元素。可以是divnav等。 div
container_class string 指定容器元素的CSS class。 menu-{menu slug}-container
container_id string 指定容器元素的ID。
before string 在每个菜单项链接之前添加的内容。
after string 在每个菜单项链接之后添加的内容。
link_before string 在菜单项链接文本之前添加的内容。
link_after string 在菜单项链接文本之后添加的内容。
echo bool 是否直接输出HTML代码。如果设置为false,则返回HTML代码字符串。 true
fallback_cb string 如果指定的菜单不存在,则调用的回调函数。 wp_page_menu
items_wrap string 用于包裹菜单项的HTML代码。使用%1$s表示菜单项的开始标记,%2$s表示菜单项的结束标记。 <ul id="%1$s" class="%2$s">%3$s</ul>
depth int 菜单的深度。0表示显示所有层级。 0
walker object 用于遍历菜单项的对象。可以自定义walker来控制菜单的渲染方式。
theme_location string 主题中注册的菜单位置。如果设置了此参数,则menu参数将被忽略。

举个例子:

<?php
$args = array(
    'theme_location'  => 'primary',
    'menu_class'      => 'nav-menu',
    'container'       => 'nav',
    'container_class' => 'main-nav',
    'fallback_cb'     => 'wp_page_menu',
);
wp_nav_menu( $args );
?>

这段代码的意思是:

  1. 使用主题位置为primary的菜单。
  2. <ul>元素的CSS class为nav-menu
  3. 使用<nav>元素包裹菜单。
  4. <nav>元素的CSS class为main-nav
  5. 如果找不到指定的菜单,则调用wp_page_menu函数。

三、源码剖析:wp_nav_menu() 到底是怎么工作的?

好了,参数咱们都了解了,现在咱们来深入源码,看看wp_nav_menu()是如何一步一步把菜单“揉”出来的。

wp_nav_menu()函数的源码位于wp-includes/nav-menu-template.php文件中。 咱们简化一下,只保留核心逻辑,一步步分析:

  1. 参数合并与处理:

    wp_nav_menu()首先会把你传递的参数和默认参数合并,形成一个完整的参数数组。 这样可以确保所有需要的参数都有值。

    $defaults = array(
       'menu'            => '',
       'container'       => 'div',
       'container_class' => 'menu-{menu slug}-container',
       'container_id'    => '',
       'menu_class'      => 'menu',
       'menu_id'         => '',
       'echo'            => true,
       'fallback_cb'     => 'wp_page_menu',
       'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
       'depth'           => 0,
       'walker'          => '',
       'theme_location'  => ''
    );
    
    $args = wp_parse_args( $args, $defaults ); //合并参数
  2. 确定要显示的菜单:

    wp_nav_menu()会根据你提供的menu参数或theme_location参数,来确定要显示哪个菜单。 如果你设置了theme_location,它会优先使用theme_location指定的菜单。 如果没有设置,或者指定的菜单不存在,它会尝试使用menu参数指定的菜单。 如果还是找不到菜单,它会调用fallback_cb参数指定的回调函数,通常是wp_page_menu,用于显示页面菜单。

    if ( ! empty( $args['theme_location'] ) ) {
       $locations = get_nav_menu_locations();
       if ( isset( $locations[ $args['theme_location'] ] ) ) {
           $menu = wp_get_nav_menu_object( $locations[ $args['theme_location'] ] );
       }
    } elseif ( ! empty( $args['menu'] ) ) {
       $menu = wp_get_nav_menu_object( $args['menu'] );
    }
  3. 获取菜单项:

    确定了要显示的菜单之后,wp_nav_menu()会调用wp_get_nav_menu_items()函数,获取菜单的所有菜单项。 wp_get_nav_menu_items()会从数据库中读取菜单项的信息,包括菜单项的标题、链接、父级ID等等。

    $menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) );
  4. 使用 Walker 类遍历菜单项并生成HTML:

    这才是wp_nav_menu()最核心的部分。它会使用一个名为Walker的类,来遍历菜单项,并根据菜单项的信息生成HTML代码。

    Walker类是一个抽象类,你需要继承它,并重写它的方法,才能自定义菜单的渲染方式。 WordPress默认提供了一个Walker_Nav_Menu类,用于生成标准的导航菜单。

    if ( empty( $args['walker'] ) ) {
       $walker = new Walker_Nav_Menu;
    } else {
       $walker = $args['walker'];
    }
    
    $items = walk_nav_menu_tree( $menu_items, $args['depth'], $args );

    walk_nav_menu_tree()函数会递归地遍历菜单项,并调用Walker类的方法,生成HTML代码。 Walker类主要有以下几个方法:

    • start_lvl():在开始一个子菜单时调用。
    • end_lvl():在结束一个子菜单时调用。
    • start_el():在开始一个菜单项时调用。
    • end_el():在结束一个菜单项时调用。

    咱们来看一下Walker_Nav_Menu类的start_el()方法的简化版:

    function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
       $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';
    
       $class_names = $value = '';
    
       $classes = empty( $item->classes ) ? array() : (array) $item->classes;
       $classes[] = 'menu-item-' . $item->ID;
    
       $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
       $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
    
       $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth );
       $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
    
       $output .= $indent . '<li' . $id . $value . $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        : '';
    
       $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 . '"';
           }
       }
    
       $title = apply_filters( 'the_title', $item->title, $item->ID );
    
       $item_output = $args->before;
       $item_output .= '<a'. $attributes .'>';
       $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
       $item_output .= '</a>';
       $item_output .= $args->after;
    
       $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }

    这段代码的作用是:

    1. 根据菜单项的属性,生成<li>元素的CSS class和ID。
    2. 生成<a>元素的href、title、target等属性。
    3. 将菜单项的标题包裹在<a>元素中。
    4. 将所有的HTML代码拼接起来,并添加到$output变量中。

    end_el()方法则负责关闭<li>元素。

    通过Walker类的遍历,最终会生成一个完整的HTML字符串,包含了所有的菜单项。

  5. 输出HTML:

    最后,wp_nav_menu()会根据echo参数的值,决定是直接输出HTML代码,还是返回HTML代码字符串。 如果echo参数为true,则直接输出HTML代码。 如果echo参数为false,则返回HTML代码字符串。

    $output = apply_filters( 'wp_nav_menu', $items, $args );
    if ( $args['echo'] ) {
       echo $output;
    } else {
       return $output;
    }

四、自定义菜单:玩转 Walker 类

wp_nav_menu()的强大之处在于它的可定制性。 你可以通过自定义Walker类,来控制菜单的渲染方式。 就像你可以自己设计外卖的包装盒一样。

例如,你可以创建一个自定义的Walker类,来给每个菜单项添加一个图标:

class My_Walker_Nav_Menu extends Walker_Nav_Menu {
    function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        parent::start_el( $output, $item, $depth, $args, $id );
        $output = str_replace( '<a', '<a><i class="fa fa-home"></i> ', $output ); // 添加图标
    }
}

$args = array(
    'theme_location' => 'primary',
    'walker'         => new My_Walker_Nav_Menu(),
);
wp_nav_menu( $args );

这段代码的意思是:

  1. 创建一个名为My_Walker_Nav_Menu的类,继承自Walker_Nav_Menu
  2. 重写start_el()方法,在每个菜单项的链接之前添加一个Font Awesome的home图标。
  3. 在调用wp_nav_menu()时,将walker参数设置为My_Walker_Nav_Menu类的实例。

通过这种方式,你可以完全控制菜单的HTML结构,实现各种各样的定制效果。

五、案例分析:一个完整的自定义菜单

为了让大家更好地理解wp_nav_menu()的用法,咱们来分析一个完整的自定义菜单的例子。

假设我们需要创建一个如下的菜单:

<nav class="custom-menu">
    <ul>
        <li class="menu-item menu-item-home current-menu-item"><a href="/">首页</a></li>
        <li class="menu-item menu-item-about"><a href="/about">关于我们</a></li>
        <li class="menu-item menu-item-products"><a href="/products">产品</a>
            <ul class="sub-menu">
                <li class="menu-item menu-item-product-1"><a href="/products/product-1">产品1</a></li>
                <li class="menu-item menu-item-product-2"><a href="/products/product-2">产品2</a></li>
            </ul>
        </li>
        <li class="menu-item menu-item-contact"><a href="/contact">联系我们</a></li>
    </ul>
</nav>

我们可以使用以下代码来实现:

<?php

class Custom_Walker_Nav_Menu extends Walker_Nav_Menu {
    function start_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = str_repeat( "t", $depth );
        $output .= "n$indent<ul class="sub-menu">n";
    }

    function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';

        $class_names = $value = '';

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

        $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

        $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth );
        $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

        $output .= $indent . '<li' . $id . $value . $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        : '';

        $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 . '"';
            }
        }

        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';
        $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;

        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }
}

$args = array(
    'theme_location'  => 'primary',
    'container'       => 'nav',
    'container_class' => 'custom-menu',
    'menu_class'      => '', //不再需要默认的'menu'类
    'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>', //自定义ul的结构
    'walker'         => new Custom_Walker_Nav_Menu(),
);

wp_nav_menu( $args );

?>

这段代码的关键在于:

  1. 自定义Walker类: 我们创建了一个Custom_Walker_Nav_Menu类,继承自Walker_Nav_Menu,并重写了start_lvl()start_el()方法,用于控制菜单的HTML结构。
  2. 设置containercontainer_class参数: 我们将container参数设置为nav,并将container_class参数设置为custom-menu,用于指定包裹菜单的容器元素和CSS class。
  3. 设置menu_class参数为空: 我们将menu_class参数设置为空,因为我们不需要默认的menu类。
  4. 设置items_wrap参数: 我们自定义了items_wrap参数,修改ul的结构。
  5. walker参数设置为自定义的Walker类: 我们将walker参数设置为Custom_Walker_Nav_Menu类的实例,以便使用自定义的Walker类来渲染菜单。

六、总结:wp_nav_menu() 的奥秘

通过以上的分析,相信大家对wp_nav_menu()的运行机制有了更深入的了解。 简单总结一下:

  1. wp_nav_menu()是一个用于生成导航菜单的函数。
  2. 它接受一个数组作为参数,用于定制菜单的各种选项。
  3. 它会根据你提供的参数,确定要显示的菜单,并获取菜单的所有菜单项。
  4. 它使用Walker类遍历菜单项,并生成HTML代码。
  5. 你可以通过自定义Walker类,来控制菜单的渲染方式。

掌握了wp_nav_menu(),你就掌握了WordPress菜单的“命脉”,可以轻松地创建各种各样的自定义菜单,让你的网站更加个性化。

好了,今天的讲座就到这里。 希望大家都能成为“菜单老司机”,在WordPress的世界里自由驰骋! 感谢大家的聆听!

发表回复

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