部落客透過精心安排的選單來向使用者宣揚自己的一切,而使用者則透過選單來了解一個網站的概要內容。在 CSS 框架盛行的年代,Wordpress 內建的選單生成函式所產生的 HTML 結構,也許已經不敷現代網頁的需求,因此客製化就顯得至關重要。
基本的 Navbar Menu 直接使用 WordPress 內建的 wp_nav_menu
函式就就可以做到,基本設定如下。
<?php wp_nav_menu(); ?>
⋯⋯嘿嘿其實可以不用傳入任何參數,Wordpress 會使用預設的模板來輸出,然而如果網站上有使用到 CSS 的 Framework,這樣子的輸出可能不太友善,因此我們需要進一步更改各個選單項目的 class、或者 HTML Tag,甚至去調整 List 結構,以符合框架的樣板,以便快速套用框架樣式。
wp_nav_menu
提供基本的選項可以讓我們做微幅的修改,其參數以及其更改的地方如下圖所示。為了專注在 HTML 結構,以下的 class 內容皆有經過簡化。
因此藉由傳入參數,例如:
<?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_el
、end_el
、start_lvl
、end_lvl
,首先來看一下它們分別的作用域,這有助於我們理解程式碼。
start_el / end_el 輸出個別選單項目
start_el
跟 end_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
變數,而不是在此函式裡的拷貝。
接著讓我們來研究一下 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_el
及 end_el
輸出完畢之後,最後走 end_lvl
結束並離開子選單。這兩個函式可用的參數有 3 個:$output
、$depth
、$args
,定義跟之前都一樣。
很顯然,start_lvl
和 end_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、或者想要進一步更改,現在我們會了 🙂