專案中多頁面共用「側邊欄導航」、「用戶資訊卡」、「最新公告」等區塊。若每個控制器重複查詢資料並載入視圖,違反 DRY 原則且難以維護。Widget 模式將「資料獲取 + 視圖渲染」封裝為獨立類別,視圖中直接呼叫,實現區塊級復用。
application/
├── core/
│ └── MY_Controller.php # 擴展基礎控制器,提供 widget() 方法
├── libraries/
│ └── Widget.php # Widget 基礎類 (可選)
├── widgets/ # Widget 類別目錄 (自定義)
│ ├── LeftSidebar.php
│ ├── UserCard.php
│ └── Announcement.php
└── views/
├── partials/
│ ├── left_sidebar.php
│ ├── user_card.php
│ └── announcement.php
└── layouts/
└── main.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class MY_Controller extends CI_Controller {
protected $widget_namespace = 'App\\Widgets\\'; // 命名空間前綴
public function __construct() {
parent::__construct();
}
/**
* 載入並實例化 Widget
* @param string $name Widget 類別名 (不含命名空間)
* @param array $params 傳遞給 Widget 建構子的參數
* @return object Widget 實例
*/
public function widget(string $name, array $params = []) {
$class = $this->widget_namespace . ucfirst($name);
if (!class_exists($class)) {
// 相容無命名空間寫法
$class = 'Widget_' . ucfirst($name);
if (!class_exists($class)) {
show_error("Widget [$name] not found: $class");
}
}
return new $class($this, ...$params); // 注入 CI 實例
}
// 靜態代理方法 (供視圖中直接呼叫 Widget::left())
public static function run_widget(string $name, array $params = []) {
$ci =& get_instance();
return $ci->widget($name, $params)->render();
}
}
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
abstract class Widget {
protected $ci;
protected $data = [];
public function __construct(CI_Controller $ci) {
$this->ci = $ci;
}
protected function load_view(string $view, array $data = [], bool $return = false) {
return $this->ci->load->view($view, array_merge($this->data, $data), $return);
}
abstract public function render(): string;
}
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
namespace App\Widgets; // 需 PHP 5.6+ 且 composer.json 設定 psr-4 自動載入
// 或類名 Widget_LeftSidebar 放在 libraries/widgets/
class LeftSidebar extends Widget {
public function render(): string {
// 1. 獲取資料 (可快取)
$cache_key = 'widget:left_sidebar:v1';
$menu = $this->ci->cache->get($cache_key);
if ($menu === false) {
$menu = $this->ci->db->select('id, title, url, icon, sort')
->where('status', 1)
->order_by('sort', 'asc')
->get('nav_menu')
->result_array();
$this->ci->cache->save($cache_key, $menu, 3600);
}
// 2. 獲取用戶資訊 (若登入)
$user = $this->ci->session->userdata('user');
// 3. 組裝資料
$this->data = [
'menu_items' => $menu,
'current_user' => $user,
'active_url' => $this->ci->uri->uri_string(),
];
// 4. 渲染視圖並回傳字串
return $this->load_view('partials/left_sidebar', [], true);
}
}
application/views/partials/left_sidebar.php
<aside class="sidebar">
<div class="user-panel">
<?php if ($current_user): ?>
<img src="<?= base_url($current_user['avatar'] ?? 'assets/img/default_avatar.png') ?>" alt="Avatar">
<span><?= html_escape($current_user['username']) ?></span>
<?php else: ?>
<a href="<?= site_url('login') ?>">登入</a> / <a href="<?= site_url('register') ?>">註冊</a>
<?php endif; ?>
</div>
<nav class="menu">
<ul>
<?php foreach ($menu_items as $item): ?>
<li class="<?= ($active_url === $item['url']) ? 'active' : '' ?>">
<a href="<?= site_url($item['url']) ?>">
<?php if ($item['icon']): ?><i class="<?= $item['icon'] ?>"></i><?php endif; ?>
<span><?= html_escape($item['title']) ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</nav>
</aside>
<?php
// 控制器
class Home extends MY_Controller {
public function index() {
// 預載入 widget 實例到視圖變數
$this->data['left_sidebar'] = $this->widget('LeftSidebar')->render();
$this->load->view('layouts/main', $this->data);
}
}
<?php
// 佈局 views/layouts/main.php
<div class="wrapper">
<div class="sidebar-col">
<?= $left_sidebar ?> <!-- 直接輸出預渲染字串 -->
</div>
<div class="content-col">
<?php $this->load->view($content_view); ?>
</div>
</div>
<?php
// 佈局 views/layouts/main.php
<div class="wrapper">
<div class="sidebar-col">
<?= MY_Controller::run_widget('LeftSidebar') ?>
</div>
<div class="content-col">
<?php $this->load->view($content_view); ?>
</div>
</div>
// 控制器傳參
$this->widget('Announcement', ['limit' => 5, 'category' => 'news'])
// Widget 內部使用參數
class Announcement extends Widget {
private $limit;
private $category;
public function __construct(CI_Controller $ci, array $params = []) {
parent::__construct($ci);
$this->limit = $params['limit'] ?? 3;
$this->category = $params['category'] ?? 'all';
}
public function render(): string {
$cache_key = "widget:announcement:{$this->category}:{$this->limit}";
// ... 同前
}
}
$this->ci->cache->save($key, $html, 300),避免重複查詢與渲染。SELECT *。| 特性 | Widget 模式 | HMVC (modules::run) |
|---|---|---|
| 架構複雜度 | 低 (單一類別) | 高 (完整 MVC 模組) |
| 資料隔離 | 共享主請求 CI 實例 | 獨立請求上下文 |
| 效能 | 極高 (無額外路由分發) | 較低 (模擬內部請求) |
| 適用場景 | UI 區塊復用 (側邊欄、導航、卡片) | 業務模組復用 (完整功能頁面) |
| 學習成本 | 低 | 中 |
CodeIgniter 3 無內建 Widget 機制,但透過 **MY_Controller 擴展 + 獨立 Widget 類別 + 視圖部分檔** 輕鬆實現區塊級復用。核心優勢:零侵入、效能高、符合單一職責、易於單元測試。建議採用「控制器預載入 + 視圖輸出字串」模式作為預設,複雜參數化場景再使用靜態呼叫。配合檔案/Redis 快取,可支撐高併發下的頁面組裝需求。