CodeIgniter (CI) 核心輕量,無內建 Widget/元件系統,但透過 `MY_Controller` 擴展與靜態呼叫模式,可輕鬆實現「頁面區塊複用」(如導航欄、側邊欄、頁尾)。本文示範完整整合步驟與最佳實踐。
| 層級 | 檔案/類別 | 職責 |
|---|---|---|
| 基礎控制器 | `core/MY_Controller.php` | 提供 `widget()` 方法載入 Widget 類別 |
| Widget 基礎類 | `libraries/Widget.php` (或各自類別) | 封裝區塊邏輯:資料查詢 + 視圖渲染 |
| 具體 Widget | `widgets/LeftMenu.php` 等 | 特定區塊實作 |
| 視圖模板 | `views/layout/main.php` | 頁面佈局,呼叫 Widget 靜態方法 |
<?php
class MY_Controller extends CI_Controller {
protected $widgets = [];
public function __construct() {
parent::__construct();
// 載入常用資源
$this->load->library('session');
$this->load->helper(['url', 'html']);
}
/**
* 載入並實例化 Widget
* @param string $name Widget 類別名稱 (對應 widgets/ 目錄下檔名)
* @return object Widget 實例
*/
protected function widget($name) {
if (empty($name)) return null;
// 單例模式:同一請求只實例化一次
if (!isset($this->widgets[$name])) {
$file = APPPATH . "widgets/{$name}.php";
if (!file_exists($file)) {
show_error("Widget 不存在: {$name}", 500);
}
require_once $file;
$className = 'Widget_' . $name;
if (!class_exists($className)) {
show_error("Widget 類別未定義: {$className}", 500);
}
$this->widgets[$name] = new $className($this);
}
return $this->widgets[$name];
}
// 共用方法:取得登入用戶資訊
protected function getCurrentUser() {
return $this->session->userdata('user');
}
}
<?php
abstract class Widget_Base {
protected $CI;
public function __construct($ci) {
$this->CI = $ci; // 持有 CI 超級物件引用
}
// 便利方法:載入視圖
protected function view($view, $data = [], $return = false) {
return $this->CI->load->view($view, $data, $return);
}
// 便利方法:取得模型
protected function model($name) {
$this->CI->load->model($name);
return $this->CI->$name;
}
}
<?php
class Widget_LeftMenu extends Widget_Base {
public function render($params = []) {
// 1. 取得資料 (可從快取、模型、服務層)
$user = $this->CI->getCurrentUser();
$menuItems = $this->getMenuItems($user);
// 2. 渲染視圖
return $this->view('widgets/left_menu', [
'items' => $menuItems,
'active' => $params['active'] ?? '',
'user' => $user,
], true); // true = 回傳字串而非直接輸出
}
private function getMenuItems($user) {
$items = [
['label' => '儀表板', 'url' => 'dashboard', 'icon' => 'home', 'roles' => ['*']],
['label' => '文章管理', 'url' => 'articles', 'icon' => 'file', 'roles' => ['admin', 'editor']],
['label' => '用戶管理', 'url' => 'users', 'icon' => 'users', 'roles' => ['admin']],
['label' => '個人設定', 'url' => 'profile', 'icon' => 'user', 'roles' => ['*']],
];
// 依角色過濾
$role = $user['role'] ?? 'guest';
return array_filter($items, fn($item) => in_array('*', $item['roles']) || in_array($role, $item['roles']));
}
}
<nav class="sidebar">
<ul class="nav flex-column">
<{foreach $items as $item}>
<li class="nav-item">
<a class="nav-link <{if $active == $item.url}>active<{/if}>"
href="<{site_url($item.url)}>">
<i class="fas fa-<{$item.icon}>"></i> <{$item.label}>
</a>
</li>
<{/foreach}>
</ul>
<{if $user}>
<div class="sidebar-footer">
<span><{$user.name}> (<{$user.role}>)</span>
</div>
<{/if}>
</nav>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title><{$page_title}|默認標題}></title>
<link href="<{base_url('assets/css/app.css')}>" rel="stylesheet">
</head>
<body class="d-flex">
<!-- 側邊欄 Widget -->
<div class="sidebar-wrapper">
<?= $this->widget('LeftMenu')->render(['active' => $current_page ?? '']) ?>
</div>
<!-- 主內容區 -->
<main class="flex-grow-1 p-4">
<?= $content ?? '' ?>
</main>
<!-- 頁尾 Widget -->
<footer class="border-top p-3 text-center text-muted">
<?= $this->widget('Footer')->render() ?>
</footer>
</body>
</html>
<?php
class Dashboard extends MY_Controller {
public function index() {
$this->load->model('Stats_model');
$data = [
'page_title' => '儀表板',
'current_page' => 'dashboard',
'stats' => $this->Stats_model->getSummary(),
];
// 使用佈局渲染
$this->load->view('layout/main', [
'content' => $this->load->view('dashboard/index', $data, true),
]);
}
}
class Widget_ArticleList extends Widget_Base {
public function render($params = []) {
$cacheKey = 'widget_article_list_' . md5(serialize($params));
// 嘗試從快取讀取
if ($html = $this->CI->cache->get($cacheKey)) {
return $html;
}
// 參數預設值
$limit = $params['limit'] ?? 10;
$category = $params['category'] ?? null;
$template = $params['template'] ?? 'widgets/article_list_default';
// 查詢資料
$articles = $this->model('Article_model')->getLatest($limit, $category);
// 渲染
$html = $this->view($template, ['articles' => $articles], true);
// 寫入快取 (5 分鐘)
$this->CI->cache->save($cacheKey, $html, 300);
return $html;
}
}
| 特性 | Widget 模式 (本文) | HMVC (Modular Extensions) |
|---|---|---|
| 複雜度 | 低,輕量級 | 高,完整模組隔離 |
| 適用場景 | 頁面區塊複用 (導航、側邊欄、廣告位) | 功能模組獨立開發 (部落格、商城、論壇) |
| 資料存取 | 透過 CI 實例存取模型/資料庫 | 模組內含完整 MVC |
| 效能 | 單一請求上下文,輕量 | 子請求開銷較大 |
| 學習曲線 | 平緩 | 陡峭 |
CI 整合 Widget 機制的核心在於:MY_Controller 提供載入入口、Widget 類別封裝「資料+視圖」、佈局視圖靜態呼叫渲染。這套輕量架構零依賴、易理解、效能好,非常適合中小型專案快速構建可維護的頁面佈局系統。