ExtJS(前端 MVC/MVVM 框架)與 PHP(後端 API)透過 Ext.Ajax.request、Ext.form.Panel、Store/Proxy 進行資料交換,核心均為 HTTP + JSON。
Ext.onReady(function() {
Ext.Ajax.request({
url: 'api/login.php', // PHP 端點
method: 'POST', // HTTP 方法
params: { // 將隨請求發送的參數
username: 'admin',
password: 'secret123',
_token: Ext.util.Cookies.get('csrf_token') // CSRF Token
},
timeout: 30000, // 30 秒超時
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
success: function(response, opts) {
var result = Ext.decode(response.responseText);
if (result.success) {
Ext.Msg.alert('成功', '歡迎回來,' + result.data.name);
// 導向主頁或儲存 Token
localStorage.setItem('auth_token', result.data.token);
window.location.href = '/dashboard';
} else {
Ext.Msg.alert('失敗', result.message || '登入失敗');
}
},
failure: function(response, opts) {
var msg = '網路錯誤';
if (response.responseText) {
var result = Ext.decode(response.responseText);
msg = result.message || msg;
}
Ext.Msg.alert('錯誤', msg + ' (HTTP ' + response.status + ')');
}
});
});
<?php
// 統一回應標頭
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
// 僅允許 POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method Not Allowed']);
exit;
}
// 讀取 JSON 或表單資料
$input = json_decode(file_get_contents('php://input'), true);
if ($input === null) {
$input = $_POST; // 相容 application/x-www-form-urlencoded
}
// 取得參數
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
$token = $input['_token'] ?? '';
// CSRF 驗證(簡化示範)
if (!validateCsrfToken($token)) {
http_response_code(419);
echo json_encode(['success' => false, 'message' => 'CSRF Token 無效']);
exit;
}
// 業務邏輯
$user = authenticateUser($username, $password);
if ($user) {
// 產生 JWT 或 Session
$authToken = generateAuthToken($user);
http_response_code(200);
echo json_encode([
'success' => true,
'message' => '登入成功',
'data' => [
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
'roles' => $user['roles'],
'token' => $authToken,
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} else {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => '帳號或密碼錯誤',
], JSON_UNESCAPED_UNICODE);
}
// 輔助函數
function validateCsrfToken($token) {
// 實作:比對 Session、檢查過期、一次性使用等
return hash_equals($_SESSION['csrf_token'] ?? '', $token);
}
function authenticateUser($username, $password) {
// 實作:資料庫查詢、密碼驗證(password_verify)
// 返回使用者陣列或 false
return false; // 示範
}
function generateAuthToken($user) {
// 實作:JWT、隨機 Token 存 Redis 等
return bin2hex(random_bytes(32));
}
Ext.define('App.view.user.EditForm', {
extend: 'Ext.form.Panel',
xtype: 'user-edit-form',
title: '編輯使用者',
frame: true,
bodyPadding: 15,
width: 480,
defaultType: 'textfield',
fieldDefaults: {
labelWidth: 100,
anchor: '100%',
msgTarget: 'side'
},
items: [{
xtype: 'hiddenfield',
name: 'id'
}, {
fieldLabel: '帳號',
name: 'username',
allowBlank: false,
minLength: 3,
maxLength: 20
}, {
fieldLabel: 'Email',
name: 'email',
vtype: 'email',
allowBlank: false
}, {
fieldLabel: '角色',
name: 'roles',
xtype: 'tagfield', // 多選標籤
store: ['admin', 'editor', 'viewer'],
displayField: 'name',
valueField: 'value'
}],
buttons: [{
text: '取消',
handler: 'onCancel'
}, {
text: '儲存',
formBind: true, // 表單驗證通過才啟用
disabled: true,
handler: 'onSave'
}],
// Controller 方法
controller: {
onSave: function(btn) {
var form = this.getView();
var values = form.getValues(); // 取得所有欄位值
form.submit({
url: 'api/user/save.php',
method: 'POST',
waitMsg: '儲存中...',
success: function(form, action) {
Ext.Msg.alert('成功', action.result.message);
// 關閉視窗、重新載入 Grid
this.getView().up('window').close();
Ext.getStore('Users').reload();
},
failure: function(form, action) {
var msg = action.result?.message || '儲存失敗';
Ext.Msg.alert('錯誤', msg);
}
});
},
onCancel: function(btn) {
btn.up('window').close();
}
}
});
<?php
// api/user/save.php
header('Content-Type: application/json; charset=utf-8');
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$id = (int)($input['id'] ?? 0);
$username = trim($input['username'] ?? '');
$email = trim($input['email'] ?? '');
$roles = $input['roles'] ?? [];
// 驗證
$errors = [];
if (empty($username)) $errors['username'] = '帳號不可為空';
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) $errors['email'] = 'Email 格式錯誤';
if (empty($roles)) $errors['roles'] = '至少選擇一個角色';
if ($errors) {
http_response_code(422);
echo json_encode([
'success' => false,
'message' => '驗證失敗',
'errors' => $errors, // ExtJS 表單會自動對應顯示
], JSON_UNESCAPED_UNICODE);
exit;
}
// 業務邏輯:新增或更新
if ($id > 0) {
$result = updateUser($id, $username, $email, $roles);
$msg = '更新成功';
} else {
$result = createUser($username, $email, $roles);
$msg = '建立成功';
}
if ($result) {
echo json_encode([
'success' => true,
'message' => $msg,
'data' => $result,
], JSON_UNESCAPED_UNICODE);
} else {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => '資料庫操作失敗',
], JSON_UNESCAPED_UNICODE);
}
Ext.define('App.store.Users', {
extend: 'Ext.data.Store',
alias: 'store.users',
model: 'App.model.User',
autoLoad: true,
pageSize: 25,
remoteSort: true,
remoteFilter: true,
proxy: {
type: 'ajax',
url: 'api/user/list.php',
reader: {
type: 'json',
rootProperty: 'data', // 資料陣列位置
totalProperty: 'total', // 總筆數位置
successProperty: 'success',
messageProperty: 'message'
},
writer: {
type: 'json',
writeAllFields: true,
encode: true,
rootProperty: 'data'
},
listeners: {
exception: function(proxy, response, operation) {
var result = Ext.decode(response.responseText);
Ext.Msg.alert('伺服器錯誤', result.message || '載入失敗');
}
}
},
listeners: {
beforeload: function(store, operation) {
// 可動態帶入額外參數
var params = operation.getParams();
params._token = Ext.util.Cookies.get('csrf_token');
}
}
});
// Model 定義
Ext.define('App.model.User', {
extend: 'Ext.data.Model',
fields: [
{ name: 'id', type: 'int' },
{ name: 'username', type: 'string' },
{ name: 'email', type: 'string' },
{ name: 'roles', type: 'auto' }, // 陣列
{ name: 'status', type: 'bool' },
{ name: 'created_at', type: 'date', dateFormat: 'c' } // ISO 8601
],
idProperty: 'id'
});
<?php
// api/user/list.php
header('Content-Type: application/json; charset=utf-8');
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = min(100, max(1, (int)($_GET['limit'] ?? 25)));
$sort = $_GET['sort'] ?? 'id';
$dir = strtoupper($_GET['dir'] ?? 'DESC');
$filters = json_decode($_GET['filter'] ?? '[]', true); // ExtJS 遠端過濾
// 建構查詢
$where = [];
$params = [];
if (!empty($filters)) {
foreach ($filters as $filter) {
$property = $filter['property'] ?? '';
$value = $filter['value'] ?? '';
$operator = $filter['operator'] ?? '=';
if ($property && $value !== '') {
$where[] = "`$property` $operator ?";
$params[] = $value;
}
}
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$orderSql = "ORDER BY `$sort` $dir";
$limitSql = "LIMIT " . (($page - 1) * $limit) . ", $limit";
// 總筆數
$total = (int)fetchOne("SELECT COUNT(*) FROM users $whereSql", $params);
// 資料
$rows = fetchAll("SELECT * FROM users $whereSql $orderSql $limitSql", $params);
// 回應
echo json_encode([
'success' => true,
'total' => $total,
'data' => $rows,
'page' => $page,
'limit' => $limit,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// 簡易 PDO 封裝
function fetchAll($sql, $params = []) {
$pdo = getPdo();
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function fetchOne($sql, $params = []) {
$pdo = getPdo();
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchColumn();
}
function getPdo() {
static $pdo = null;
if ($pdo === null) {
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return $pdo;
}
// 成功
{
"success": true,
"message": "操作成功",
"data": { ... }, // 物件或陣列
"meta": { ... } // 額外資訊(分頁、時間戳等)
}
// 失敗(驗證錯誤 422)
{
"success": false,
"message": "驗證失敗",
"errors": { // 欄位級錯誤,供 Ext.form.Panel 自動顯示
"fieldName": "錯誤訊息"
}
}
// 失敗(業務/系統錯誤 400/500)
{
"success": false,
"message": "錯誤描述",
"code": "ERROR_CODE" // 可選:前端依 code 處理特定邏輯
}
class ApiResponse {
public static function success($data = null, $message = '成功', $meta = []) {
http_response_code(200);
self::output([
'success' => true,
'message' => $message,
'data' => $data,
'meta' => $meta,
]);
}
public static function error($message = '失敗', $code = 400, $errors = null, $errorCode = null) {
http_response_code($code);
self::output([
'success' => false,
'message' => $message,
'errors' => $errors,
'code' => $errorCode,
]);
}
public static function validationError($errors, $message = '驗證失敗') {
self::error($message, 422, $errors, 'VALIDATION_ERROR');
}
private static function output($payload) {
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
}
// 使用範例
ApiResponse::success($user, '登入成功', ['token' => $token]);
ApiResponse::validationError(['email' => 'Email 已存在']);
ApiResponse::error('權限不足', 403, null, 'FORBIDDEN');
| 現象 | 可能原因 | 解決 |
|---|---|---|
| ExtJS 顯示 "successProperty" 錯誤 | PHP 回應缺少 success 欄位 | 確保所有 API 回應包含 success: true/false |
| 表單驗證錯誤不顯示 | errors 格式不符、fieldName 不匹配 | errors 鍵名必須與 Model field name 完全一致 |
| Store 分頁不正確 | totalProperty 回傳錯誤 | 檢查 total 為整數、對應 totalProperty 設定 |
| 日期欄位顯示 NaN | dateFormat 不匹配 | PHP 回傳 ISO 8601 (c),Model 設定 dateFormat: 'c' |
| 中文亂碼 | 編碼不一致 | PHP header charset=utf-8、JSON_UNESCAPED_UNICODE、資料庫 utf8mb4 |
| CSRF Token 失效 | Session 過期、Token 未正確傳遞 | 檢查 Cookie 設定、AJAX 標頭攜帶、Ext.Ajax.defaultHeaders |