圖解 WordPress 的 wp_nav_menu 及利用 Walker 客製化 Navbar Menu

Wordpress 內建的 wp_nav_menu 選單生成函式所產生的 HTML 結構,也許已經不敷現代網頁框架的需求,因此利用客製化及學會 Walker 就顯得至關重要。

部落客透過精心安排的選單來向使用者宣揚自己的一切,而使用者則透過選單來了解一個網站的概要內容。在 CSS 框架盛行的年代,Wordpress 內建的選單生成函式所產生的 HTML 結構,也許已經不敷現代網頁的需求,因此客製化就顯得至關重要。

wp_nav_menu 基本用法

基本的 Navbar Menu 直接使用 WordPress 內建的 wp_nav_menu 函式就就可以做到,基本設定如下。

<?php wp_nav_menu(); ?>

⋯⋯嘿嘿其實可以不用傳入任何參數,Wordpress 會使用預設的模板來輸出,然而如果網站上有使用到 CSS 的 Framework,這樣子的輸出可能不太友善,因此我們需要進一步更改各個選單項目的 class、或者 HTML Tag,甚至去調整 List 結構,以符合框架的樣板,以便快速套用框架樣式。

wp_nav_menu 提供基本的選項可以讓我們做微幅的修改,其參數以及其更改的地方如下圖所示。為了專注在 HTML 結構,以下的 class 內容皆有經過簡化。

Wordpress wp_nav_menu

因此藉由傳入參數,例如:

<?php wp_nav_menu(
  array(
    // parameters related to appearance.
    'container'  => 'div', // if set to '', there is no container
    'container_class' => 'c-class',
    'container_id'  => 'c-id', 
    'menu_class'   => 'm-class',  
    'menu_id'   => 'm-id', 
    'before' => '<span>',
    'after'  => '</span',
    'link_before'  => '- ',  
    'link_after'  => ' -',
    'items_wrap'  => '<ul id="%1$s" class="items-wrap %2$s">%3$s</ul>',

    // other goes here
  )
); ?>

前端頁面原始碼就會變成:

<div id="c-id" class="c-class">
  <ul id="m-id" class="items-wrap m-class">
    <li class="menu-item">
      <span><a href="https://wp.site/tag/">- Tags -</a></span>
    </li>
    <li class="menu-item menu-item-has-children">
      <span><a href="https://wp.site/category/">- Categories -</a></span>
      <ul class="sub-menu">
        <li class="menu-item">
          <span><a href="https://wp.site/category/music/">- Music -</a></span>
        </li>
        <li class="menu-item">
          <span><a href="https://wp.site/category/music/">- Sport -</a></span>
        </li>
      </ul>
    </li>
    <li class="menu-item">
      <span><a href="https://wp.site/about/">- About -</a></span>
    </li>
  </ul>
</div>

不過我們發現能修改的地方其實只有最上層的 <ul> 標籤和 container,以及其選單內部的各項 <a>,內部的 <li><ul> 完全改不了,因此我們就要使用 Walker,來歷遍整個選單的項目,藉由取得逐個項目的屬性、或根據所在階層,來改變當前項目的細部輸出格式。

尤其當我們想要客製化多層的選單結構時,就一定得利用 Walker 來做修改,讓我們先來暸解一下這是什麼 。

Walker

Johnnie Walker 顧名思義,就是「約翰走路」,這個類(Class)所做的事就是把所有選單項目都走一遍,而在選單的部分,Wordpress 核心預設就是利用從 Walker 繼承而來的類 Walker_Nav_Menu。這個類裡面有 4 個跟 HTML 輸出有關的方法(Method),分別是 start_elend_elstart_lvlend_lvl,首先來看一下它們分別的作用域,這有助於我們理解程式碼。

Wordpress walker

start_el / end_el 輸出個別選單項目

start_elend_el 處理的是選單中每一個項目的輸出,可用參數有 4 個:$output、item$depth、和 $args,其中只有 $depth 是傳值(Pass by value),其餘都是傳址(Pass by reference)。

讓我們從簡單的 end_el 開始看起,比較容易說明,可以從 WordPress 官方找到線上原始碼對照著看,簡化版下:

public function end_el( &$output, $item, $depth = 0, $args = array() ) {
    /* 這邊只是在處理原始碼的空白及跳行,故省略。
     * Ex. $n = '\n';
     */
    $output .= "</li>{$n}";
}

就這麼簡單。

$output 會隨著 Walker 的前進,持續的把需要的 HTML 黏上去,而這裡只要把結尾的 </li> 標籤加入就結束了,整個 Walker 走完就會把這個 $output 輸出到前端,特別注意在函式輸入參數的地方,$output 前面有個 &,表示以傳址方式傳送,這樣才能真正修改到傳進來的 $output 變數,而不是在此函式裡的拷貝。

在 PHP 裡,Array 跟 Object 只能以傳址的方式傳遞,故不需要另外加上 &。

接著讓我們來研究一下 start_el,一樣可以找到線上原始碼,簡化版如下:

public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
    /*
     * 以下省略部分程式碼,並給各變數舉個例子
     */
    $indent = '\t'; // 處理原始碼的跳行,若$depth==1

    /*
     * 有空來寫一篇apply_filters的用法
     * 這邊可以先假裝此函式直接回傳第2個參數,所以
     */
    $args = $args;

    $classes     = array('menu-item');     // class陣列
    $class_names = ' class="menu-item"';   // 從class陣列變來的HTML輸出用String
    $id          = ' id="menu-item-12345"';// ID輸出用的String,前面範例中我都把這個刪掉了

    // 將輸出黏到$output輸出,Wordpress也沒什麼了不起,如此土法煉鋼
    $output .= $indent . '<li' . $id . $class_names . '>';
 
    /*
     * 接下來這裡看起來很複雜,其實只是想輸出<a>裡面需要的各項attribute
     */
    // 首先$atts可能會長這樣
    $atts = array(
        'title'  => 'Tags',
        'target' => '',
        'rel'    => '',
        'href'   => 'https://wp.site/tag/',
    );
    // 然後$attributes就會變成這樣
    $attributes = ' title="Tags" href="https://wp.site/tag/"';
 
    /*
     * 一樣只要遇到apply_filters都先假裝直接回傳第2個參數。 
     * 所以,
     */
    $title = $item->title;
 
    // 最後,把該黏的黏一黏
    $item_output  = $args->before;
    $item_output .= '<a' . $attributes . '>';
    $item_output .= $args->link_before . $title . $args->link_after;
    $item_output .= '</a>';
    $item_output .= $args->after;
 
    // 加進$output才會被傳出去
    $output .= $item_output; // 遇到apply_filters就先假裝直接回傳第2個參數。
}

$item 代表目前走到的項目物件,類型是 WP_Post,舉一個可能的例子:

$item = array(
    // 僅列出部分
    'classes' => ['', 'menu-item'],
    'object'  => 'category',
    'object_id' => '378', // 可以直接與get_the_ID()比對來判斷是否為目前的頁面
    'type'    => 'taxonomy',
    'title'   => 'Music'  // 項目名稱
    'url'     => 'https://wp.site/category/music/'
);

$depth 指的就是目前的所在階層數,第一層為 0,每進入一層子選單就會加 1。

$args 基本上就是一開始我們傳給 wp_nav_menu 的那個參數陣列,而在進入函式後有一個好用的屬性,可以用來判斷當前項目是否包含子選單。

$args->walker->has_children // =true if has sub-menu, or =false

另外,如果是包含子選單的項目物件,在該 Class 陣列 $item->classes 中還會多一個 menu-item-has-children,所以也可以用這個來判斷。

start_lvl / end_lvl 輸出子選單的 Wrapper

每當 Walker 碰到一個子選單時,就會先走 start_lvl 函式給它一個 Wrapper,待裡面的項目都流經 start_elend_el 輸出完畢之後,最後走 end_lvl 結束並離開子選單。這兩個函式可用的參數有 3 個:$output$depth$args,定義跟之前都一樣。

很顯然,start_lvlend_lvl 做的事也一樣,就只是把一些 HTML 黏到 $output上。

public function start_lvl( &$output, $depth = 0, $args = array() ) {
    $n = '\n';      // 原始碼跳行
    $indent = '\t'; // 原始碼跳空格
 
    $classes = array( 'sub-menu' );     // 預設,所以你知道sub-menu是怎麼來的了
    $class_names = ' class="sub-menu"'; // 根據$classes變來的輸出用String
 
    $output .= "{$n}{$indent}<ul$class_names>{$n}";
}

public function end_lvl( &$output, $depth = 0, $args = array() ) {
    $n = '\n';      // 原始碼跳行
    $indent = '\t'; // 原始碼跳空格

    $output .= "$indent</ul>{$n}";
}

不能再更簡單。

使用自定義的 Walker

了解 WordPress 核心內建的 Walker_Nav_Menu 後,想要客製化就只要以它為延伸新增一個類就可以了。 為了方便管理,可以選擇在佈景主題根目錄新增一個檔案,例如命名為 navwalker.php,然後在裡面加入以下內容。

<?php
class custom_navwalker extends Walker_Nav_Menu
{
    public function start_lvl(&$output, $depth, $args = array())
    {
        $output .= '<ul>';
    {
}

需要客製化的部分就直接寫同名的方法來覆蓋,其餘則會從原有的繼承。然而現在這個檔案還不會被 WordPress 認到,所以需要在佈景主題的 function.php 裡面補上這一行。

require get_template_directory() . '/navwalker.php';

最後,在一開始的 wp_nav_menu 函式裡,加入參數告訴它我們要使用自定義 Walker。

<?php wp_nav_menu(
  array(
    // parameters related to appearance.
    // ...
    // other goes here
    'walker' => new custom_navwalker(),
  )
); ?>

現在重新整理頁面應該會發現,一開始的 sub-menu 這個 class 不見了,就只剩下我們在 start_lvl 函式裡的自訂的 <ul> 標籤。其餘的客製化以此類推,建議可以使用官方的原始碼來做修改,使用 Bootstrap 的話在 Github 上有範例:WP Bootstrap Navwalker

當出現 Bug、或者想要進一步更改,現在我們會了 🙂

需要注意的是,開始跟結束 HTML 標籤必須成對出現。雖然錯誤的語法瀏覽器或許也能解,但請務必記住預設的結束標籤是否相符,否則需要在相應的 end_el 或 end_lvl 中做修改。

Reference

  1. WordPress/Walker_nav_menu