搜索全站
主题 15 分钟阅读

register_nav_menus 注册菜单:后台位置与前台调用对应

后台"外观 → 菜单"里的位置从哪来?讲清 register_nav_menus 注册、后台指派、wp_nav_menu 前台调用的完整链路,附 Walker 自定义与常见失效排查。

U
UFO_PF 2020-05-19 · 阅读 15 分钟
re 主题
阅读提示
本文约 5,741 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:菜单做好了,前台一片空白

「外观 → 菜单」里建好了导航,勾了”主导航”位置,保存成功。前台却什么都不显示,或者显示的是一串默认页面列表。翻主题代码发现 header.php 里写的是 wp_nav_menu( array( 'theme_location' => 'menu-1' ) ),而你注册的位置叫 primary。

菜单这条链路有三段:注册位置、后台建菜单并指派到位置、前台按位置名调用。任意一段对不上,就会静默失败——不报错,只是不显示。

原理分析:三张表与一个映射关系

菜单数据存在数据库里,涉及三张表:

数据 存储位置 说明
菜单本身(容器) wp_terms(taxonomy = nav_menu) 一个菜单就是一个 term
菜单项 wp_posts(post_type = nav_menu_item) 每项是一篇”菜单项文章”
菜单与项的关联 wp_term_relationships 多对多

而”菜单位置”是另一回事,它不进数据库,而是运行时注册的。核心用两个概念区分:

① 位置(theme_location):主题用 register_nav_menus() 注册的"插槽",如 primary / footer
② 菜单(menu):用户在后台创建的菜单实体,如"主菜单""页脚菜单"
③ 指派关系:存在 theme_mods_{stylesheet} 这个 option 的 nav_menu_locations 数组里

wp_nav_menu() 的查找顺序是:

theme_location 有值 → 查 nav_menu_locations[位置] 有没有指派菜单
  ├─ 有 → 渲染该菜单
  └─ 无 → 退回按 menu / menu_id 参数找 → 再退回 wp_page_menu() 输出页面列表

这解释了那个经典现象:位置没指派菜单时,前台会显示一份”所有页面列表”——这是 wp_page_menu() 的兜底输出,不是 bug。

保姆级实操

第 1 步:注册菜单位置

add_action( 'after_setup_theme', function () {
    register_nav_menus( array(
        'primary'   => '主导航(页头)',
        'footer'    => '页脚导航',
        'mobile'    => '移动端抽屉菜单',
        'social'    => '社交链接(页脚右侧)',
    ) );
} );

数组键是位置标识(英文、小写、连字符),值是后台显示的名称(可中文)。位置标识一旦定下来就不要改,改名会让已指派的关系丢失——因为映射存在 theme_mods 里,键就是位置标识。

第 2 步:前台调用

// header.php
wp_nav_menu( array(
    'theme_location' => 'primary',
    'container'      => 'nav',
    'container_class'=> 'main-navigation',
    'container_id'   => 'site-navigation',
    'menu_class'     => 'primary-menu',
    'fallback_cb'    => false,          // 关掉页面列表兜底,便于排查
    'depth'          => 2,              // 支持二级
    'item_spacing'   => 'discard',      // 去掉 li 之间的换行空白,便于 inline-block 布局
) );

fallback_cb 默认值为 wp_page_menu,把它设为 false 后,”没指派菜单”就会什么都不输出,一眼能看出问题所在。

第 3 步:加判断与兜底

<?php if ( has_nav_menu( 'primary' ) ) : ?>
    <?php
    wp_nav_menu( array(
        'theme_location' => 'primary',
        'container'      => 'nav',
        'container_class'=> 'main-navigation',
        'menu_class'     => 'primary-menu',
        'fallback_cb'    => false,
    ) );
    ?>
<?php elseif ( current_user_can( 'manage_options' ) ) : ?>
    <p class="menu-hint">
        <a href="<?php echo esc_url( admin_url( 'nav-menus.php' ) ); ?>">
            <?php esc_html_e( '请到后台指派主导航菜单。', 'mimangfei' ); ?>
        </a>
    </p>
<?php endif; ?>

只对管理员显示提示,普通访客看不到任何异常。has_nav_menu( 'primary' ) 内部查的就是 get_nav_menu_locations() 里该位置是否已指派且菜单存在。

第 4 步:自定义菜单 HTML 结构

默认的 <ul><li><a> 结构不满足需求(比如要加箭头图标、描述文字)时,用 Walker_Nav_Menu 子类:

class MMF_Walker_Nav_Menu extends Walker_Nav_Menu {
    // 在每个 <li> 的开始处插入箭头,用于二级菜单
    public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
        $classes   = empty( $item->classes ) ? array() : (array) $item->classes;
        $has_child = in_array( 'menu-item-has-children', $classes, true );

        $output .= sprintf(
            '<li id="menu-item-%1$d" class="%2$s">',
            esc_attr( $item->ID ),
            esc_attr( implode( ' ', $classes ) )
        );

        $title = apply_filters( 'the_title', $item->title, $item->ID );

        $output .= sprintf(
            '<a href="%1$s"%2$s>%3$s</a>',
            esc_url( $item->url ),
            ! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) . '"' : '',
            esc_html( $title )
        );

        if ( $has_child ) {
            $output .= '<button class="submenu-toggle" aria-expanded="false"><span class="screen-reader-text">展开子菜单</span></button>';
        }
    }
}

调用时传 walker:

wp_nav_menu( array(
    'theme_location' => 'primary',
    'walker'         => new MMF_Walker_Nav_Menu(),
    'fallback_cb'    => false,
) );

改造菜单项本身(加”高亮””新窗口打开”等字段)可另辟蹊径,参考主题选项面板实现方式对比里关于自定义器控件的做法。

第 5 步:用过滤器微调,不必写整个 Walker

只想在 <a> 上加属性时,用 nav_menu_link_attributes 就够了:

add_filter( 'nav_menu_link_attributes', function ( $atts, $item, $args ) {
    if ( 'primary' === $args->theme_location ) {
        $atts['class'] = 'nav-link';
        if ( $item->target ) {
            $atts['rel'] = 'noopener noreferrer';
        }
    }
    return $atts;
}, 10, 3 );

// 给当前菜单项加 class(核心默认已有 current-menu-item)
add_filter( 'nav_menu_css_class', function ( $classes, $item, $args ) {
    if ( in_array( 'current-menu-item', $classes, true ) ) {
        $classes[] = 'is-active';
    }
    return $classes;
}, 10, 3 );

第 6 步:命令行核对指派关系

wp menu list
wp menu location list
wp menu location assign primary 5   # 把 ID 为 5 的菜单指派到 primary 位置

wp menu location list 的输出能直接确认”位置 → 菜单”是否对上,比在后台点来点去快。

常见踩坑

  1. 位置标识与调用时写的字符串不一致。 注册 primary,调用写 menu-1,前台走兜底。
  2. 改了位置标识导致菜单丢失。 映射存在 theme_mods_{stylesheet} 的 nav_menu_locations 里,键名一变,指派关系就断了。
  3. register_nav_menus 挂在 init 上太晚。 虽然多数情况仍能工作,但后台菜单页的”显示位置”可能不出现,统一挂 after_setup_theme。
  4. 子主题重复注册同名位置。 会覆盖父主题的注册,名称显示子主题的版本,通常无害但容易困惑。
  5. 菜单项太多导致后台保存失败。 这是 PHP 的 max_input_vars(默认 1000)限制,菜单项超过一定数量后保存被截断,需要在 php.ini 里调大到 3000 或用 .htaccess 的 php_value max_input_vars 3000。表现为”勾了位置但保存后没生效”。
  6. 案例: 某站页脚菜单在中文站正常,英文站空白。查 wp menu location list 发现两个语言的 theme_mods 是同一份(用了同一主题且未启用多语言插件的位置隔离),指派的是中文菜单 ID,英文站查不到对应项。最后改成在代码里按语言常量切换 theme_location,才彻底解决。这类”多语言下的菜单指派”问题是菜单排查里的高频场景。
  7. depth 限制了层级但 CSS 没跟上。 勾了三级菜单,depth 默认 0(无限),但主题 CSS 只写了二级的 .sub-menu 样式,三级菜单叠在一起显示。移动端抽屉菜单还需要一段初始化脚本,注入位置见wp_head 与 wp_footer 的作用。

总结

菜单排查的固定顺序是:wp menu location list 确认位置与菜单的指派关系 → 确认模板里 theme_location 的字符串与注册键名完全一致 → 把 fallback_cb 设为 false 排除兜底干扰。日常开发里,位置标识一旦上线就不要改;前台调用一律配 has_nav_menu() 做保护;只想改一两个属性时用 nav_menu_link_attributes 这类过滤器,需要改结构时才上 Walker。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。