分析 WordPress `wp_nav_menu()` 函数源码:如何与 `wp_get_nav_menu_items()` 协同渲染菜单。

大家好,我是你们今天的菜单品鉴师,啊不,是WordPress菜单源码分析师。今天咱们就来好好扒一扒 WordPress 的 wp_nav_menu() 这个大厨,看看它如何与 wp_get_nav_menu_items() 这个食材供应商配合,最终端出一盘秀色可餐的菜单。

开胃小菜:wp_nav_menu() 的基本用法和参数

首先,咱们得知道 wp_nav_menu() 是干啥的。简单来说,它就是用来在 WordPress 主题中显示导航菜单的。你只要在你的主题模板文件中调用它,它就能根据你的设置,把菜单渲染出来。

<?php
wp_nav_menu( array(
    'theme_location'  => 'primary', // 菜单位置,需要在主题 functions.php 中注册
    'menu'            => '', // 指定要显示的菜单 ID 或名称,如果 theme_location 有值,则忽略
    'container'       => 'div', // 菜单容器标签
    'container_class' => 'menu-primary-container', // 容器 class
    'container_id'    => '', // 容器 ID
    'menu_class'      => 'menu', // 菜单 ul 标签 class
    'menu_id'         => '', // 菜单 ul 标签 ID
    'echo'            => true, // 是否直接输出,false 则返回 HTML 字符串
    'fallback_cb'     => 'wp_page_menu', // 如果菜单不存在,则使用的回调函数
    'before'          => '', // 菜单链接前的内容
    'after'           => '', // 菜单链接后的内容
    'link_before'     => '', // 链接文本前的内容
    'link_after'      => '', // 链接文本后的内容
    'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>', // 菜单 ul 标签的 HTML 结构
    'depth'           => 0, // 菜单深度,0 表示不限制
    'walker'          => '', // 用于遍历菜单项的 Walker 对象
) );
?>

上面的代码片段展示了 wp_nav_menu() 的基本用法。可以看到,它接收一个数组作为参数,这个数组定义了菜单的各种属性,比如位置、容器、样式等等。

参数 描述
theme_location 指定菜单位置,需要在主题的 functions.php 文件中使用 register_nav_menu() 函数注册。这是最常用的参数,因为它允许你在 WordPress 后台的“外观” -> “菜单”中,将一个菜单分配到特定的主题位置。
menu 指定要显示的菜单 ID 或名称。如果设置了 theme_location,则此参数将被忽略。通常不推荐直接使用菜单 ID 或名称,而是使用 theme_location,因为它更灵活,允许用户在后台自由地分配菜单。
container 指定菜单容器的 HTML 标签。默认为 'div',也可以设置为 'nav''span' 等等。如果不需要容器,可以设置为 false
container_class 指定菜单容器的 CSS class。可以根据需要自定义 CSS 样式。
container_id 指定菜单容器的 HTML ID。
menu_class 指定菜单 <ul> 标签的 CSS class。
menu_id 指定菜单 <ul> 标签的 HTML ID。
echo 指定是否直接输出菜单 HTML。如果设置为 true(默认值),则直接输出;如果设置为 false,则返回 HTML 字符串,可以将其存储在变量中,并在需要的时候输出。
fallback_cb 指定一个回调函数,当指定的菜单不存在时,该函数会被调用。默认为 wp_page_menu(),它会显示一个基于页面结构的简单菜单。可以自定义一个回调函数,来显示一个默认菜单或提示信息。
before 在每个菜单项链接之前添加的内容。
after 在每个菜单项链接之后添加的内容。
link_before 在每个菜单项链接文本之前添加的内容。
link_after 在每个菜单项链接文本之后添加的内容。
items_wrap 指定菜单 <ul> 标签的 HTML 结构。%1$s 会被替换为 menu_id%2$s 会被替换为 menu_class%3$s 会被替换为菜单项的 HTML。可以自定义这个参数来修改菜单的 HTML 结构。
depth 指定菜单的深度。0 表示不限制深度,1 表示只显示顶级菜单项,2 表示显示顶级菜单项和一级子菜单项,以此类推。
walker 指定一个用于遍历菜单项的 Walker 对象。Walker 对象用于自定义菜单项的 HTML 输出。可以自定义一个 Walker 类,来完全控制菜单的 HTML 结构。

主菜上桌:wp_nav_menu() 的源码剖析

现在,咱们来深入 wp_nav_menu() 的源码,看看它是如何工作的。

function wp_nav_menu( $args = array() ) {
    static $menu_id_slugs = array();

    $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',
        'before'          => '',
        'after'           => '',
        'link_before'     => '',
        'link_after'      => '',
        'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
        'depth'           => 0,
        'walker'          => '',
        'theme_location'  => ''
    );

    $args = wp_parse_args( $args, $defaults );
    $args = (object) $args;

    // 允许插件修改参数
    $args = apply_filters( 'wp_nav_menu_args', $args );

    // 获取菜单对象
    $menu = wp_get_nav_menu_object( $args->menu );

    // 如果指定了主题位置,并且没有指定菜单,则尝试从主题位置获取菜单
    if ( ! $menu && $args->theme_location && ( $locations = get_nav_menu_locations() ) && isset( $locations[ $args->theme_location ] ) ) {
        $menu = wp_get_nav_menu_object( $locations[ $args->theme_location ] );
    }

    // 如果菜单不存在,则调用 fallback_cb
    if ( ! $menu ) {
        if ( $args->fallback_cb && is_callable( $args->fallback_cb ) ) {
            return call_user_func( $args->fallback_cb, (array) $args );
        }
        return false;
    }

    $menu_id = $menu->term_id;

    // 构建菜单容器
    $container = 'nav' === $args->container ? 'nav' : $args->container;
    $container_id = $args->container_id ? ' id="' . esc_attr( $args->container_id ) . '"' : '';

    if ( $container ) {
        $container_class = 'menu-'. $menu->slug .'-container';
        $container_class = apply_filters( 'wp_nav_menu_container_class', $container_class, $args, $menu );
        $container_class = ' class="' . esc_attr( $container_class ) . '"';

        echo '<' . $container . $container_id . $container_class . '>';
    }

    // 构建菜单 ul 标签
    $menu_id_slug = sanitize_title( $menu->slug );
    if ( isset( $menu_id_slugs[ $menu_id_slug ] ) ) {
        $menu_id_slugs[ $menu_id_slug ]++;
        $menu_id_slug .= '-' . $menu_id_slugs[ $menu_id_slug ];
    } else {
        $menu_id_slugs[ $menu_id_slug ] = 0;
    }

    $menu_id = $args->menu_id ? $args->menu_id : 'menu-' . $menu_id_slug;

    $wrap_id      = $menu_id;
    $wrap_class   = $args->menu_class;

    $items_wrap = apply_filters( 'wp_nav_menu_items_wrap', $args->items_wrap, $args, $menu );
    $wrap = sprintf( $items_wrap, esc_attr( $wrap_id ), esc_attr( $wrap_class ), '%3$s' );

    // **核心步骤:获取菜单项**
    $nav_menu_args = array(
        'menu'        => $menu,
        'container'   => false,
        'items_wrap'  => false,
        'echo'        => false,
        'walker'      => $args->walker,
        'depth'       => $args->depth,
    );

    $nav_menu = wp_get_nav_menu_items( $menu->term_id, $nav_menu_args );

    // 允许插件修改菜单项
    $nav_menu = apply_filters( 'wp_nav_menu_objects', $nav_menu, $args );

    $items = '';
    $output = '';

    if ( $nav_menu ) {
        $args = (object) $args;
        $sorted_menu_items = array();
        $menu_items = wp_list_sort( $nav_menu, array( 'menu_order' => SORT_ASC, 'ID' => SORT_ASC ) );

        $sorted_menu_items = wp_nav_menu_items( $menu_items, $args );

        unset($menu_items);

        $items .= walk_nav_menu_tree( $sorted_menu_items, $args->depth, $args );

        unset($sorted_menu_items);

        if ( ! empty( $items ) ) {
            $output = apply_filters( 'wp_nav_menu', $wrap, $args, $menu );
            $output = str_replace('%3$s', $items, $output);
        }
    }

    if ( $container ) {
        echo $output;
        echo '</' . $container . '>';
    } else {
        echo $output;
    }

    return $output;
}

咱们来一步步解读这个大厨的操作流程:

  1. 参数解析与准备:

    • wp_parse_args():将传入的参数与默认参数合并,确保所有需要的参数都有值。
    • apply_filters( 'wp_nav_menu_args', $args ):允许插件修改传入的参数,增加了灵活性。
    • wp_get_nav_menu_object():根据传入的 menu 参数(可以是菜单 ID、名称或 slug)获取菜单对象。如果 menu 参数为空,但指定了 theme_location,则尝试从主题位置获取菜单。
  2. 菜单验证与回退:

    • 如果找不到菜单,并且设置了 fallback_cb,则调用 fallback_cb 指定的回调函数。wp_page_menu() 是一个常用的回退函数,它会根据页面结构生成一个简单的菜单。
  3. 容器构建:

    • 根据 containercontainer_classcontainer_id 参数,构建菜单的容器 HTML 标签。
  4. <ul> 标签构建:

    • 生成唯一的菜单 ID,并根据 menu_classmenu_id 参数,构建菜单的 <ul> 标签的 HTML 结构。
  5. 获取菜单项(重头戏):

    • wp_get_nav_menu_items() 这是关键的一步,调用 wp_get_nav_menu_items() 函数,从数据库中获取菜单项。传入的参数包括菜单 ID 和一个参数数组,用于指定获取菜单项的方式。
  6. 菜单项处理:

    • apply_filters( 'wp_nav_menu_objects', $nav_menu, $args ):允许插件修改获取到的菜单项。
    • wp_list_sort(): 对菜单项进行排序,通常按照 menu_order 字段进行升序排序。
    • wp_nav_menu_items(): 对菜单项进行预处理,为后续的 Walker 类做准备。
    • walk_nav_menu_tree():使用 Walker 类遍历菜单项,生成最终的 HTML 结构。Walker 类是一个用于遍历树状结构的通用类,可以自定义 Walker 类来完全控制菜单项的 HTML 输出。
  7. 输出:

    • 将生成的 HTML 结构插入到容器中,并输出到页面。

配料准备:wp_get_nav_menu_items() 的详解

现在,咱们来详细看看 wp_get_nav_menu_items() 这个食材供应商是如何工作的。

function wp_get_nav_menu_items( $menu, $args = array() ) {
    $menu_id = ( is_object( $menu ) ) ? $menu->term_id : (int) $menu;
    $locations = get_nav_menu_locations();
    $location = array_search($menu_id, $locations);
    $args = (object) wp_parse_args( $args, array() );

    $items = wp_cache_get( "nav_menu_items:$menu_id", 'nav_menu' );
    if ( false === $items ) {
        $items = array();

        $args = array(
            'post_type'         => 'nav_menu_item',
            'posts_per_page'    => -1,
            'tax_query'         => array(
                array(
                    'taxonomy'  => 'nav_menu',
                    'field'     => 'term_id',
                    'terms'     => array( $menu_id ),
                ),
            ),
            'orderby'           => 'menu_order',
            'order'             => 'ASC',
            'suppress_filters' => true,
        );

        $posts = get_posts( $args );

        if ( $posts ) {
            update_post_caches( $posts );
            $items = array_map( 'wp_setup_nav_menu_item', $posts );
            wp_cache_add( "nav_menu_items:$menu_id", $items, 'nav_menu' );
        }
    }

    if ( $items ) {
        $items = apply_filters( 'wp_get_nav_menu_items', $items, $menu, $args );
    }

    return $items;
}
  1. 参数准备:

    • wp_parse_args():解析传入的参数。
    • $menu_id:获取菜单 ID。
  2. 缓存机制:

    • wp_cache_get():尝试从缓存中获取菜单项。如果缓存中存在,则直接返回缓存的菜单项,避免重复查询数据库。
  3. 数据库查询:

    • 如果缓存中不存在菜单项,则构建一个 WP_Query 对象,查询 nav_menu_item 类型的文章,这些文章属于指定的菜单(通过 tax_query 参数指定)。
    • get_posts():执行查询,获取菜单项文章。
  4. 菜单项对象化:

    • update_post_caches():更新文章缓存。
    • array_map( 'wp_setup_nav_menu_item', $posts ):将查询到的文章对象转换为菜单项对象。wp_setup_nav_menu_item() 函数会为每个文章对象添加一些额外的属性,使其更方便在菜单中使用。
  5. 缓存存储:

    • wp_cache_add():将获取到的菜单项存储到缓存中,以便下次使用。
  6. 过滤:

    • apply_filters( 'wp_get_nav_menu_items', $items, $menu, $args ):允许插件修改获取到的菜单项。
  7. 返回:

    • 返回菜单项数组。

菜品加工:Walker 类的作用

Walker 类是 WordPress 中用于遍历树状结构的通用类。在菜单渲染中,Walker 类用于遍历菜单项,并生成最终的 HTML 结构。

WordPress 默认提供了一个 Walker_Nav_Menu 类,用于渲染菜单。你可以自定义 Walker 类,来完全控制菜单的 HTML 输出。

class My_Custom_Walker extends Walker_Nav_Menu {
    function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        // 在这里自定义菜单项的 HTML 输出
        $output .= '<li class="my-custom-menu-item">';
        $output .= '<a href="' . esc_attr( $item->url ) . '">' . esc_html( $item->title ) . '</a>';
        $output .= '</li>';
    }
}

上面的代码片段展示了一个自定义 Walker 类的示例。start_el() 方法用于输出每个菜单项的 HTML。你可以根据需要,修改这个方法,来生成你想要的 HTML 结构。

总结

wp_nav_menu()wp_get_nav_menu_items() 协同工作,完成了 WordPress 菜单的渲染。wp_nav_menu() 负责接收参数、获取菜单对象、构建容器和调用 wp_get_nav_menu_items() 获取菜单项。wp_get_nav_menu_items() 负责从数据库中查询菜单项,并将其转换为菜单项对象。最后,通过 Walker 类遍历菜单项,生成最终的 HTML 结构。

理解了 wp_nav_menu()wp_get_nav_menu_items() 的工作原理,你就可以更好地自定义 WordPress 菜单,满足你的各种需求。

希望今天的讲解能帮助你更好地理解 WordPress 菜单的渲染过程。如果你还有其他问题,欢迎提问。下次有机会,咱们再一起研究 WordPress 的其他源码。

发表回复

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