萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 程式設計 >  >> 網路程式設計 >> PHP 程式設計

ExtJS 與 PHP 參數交互完整實戰:AJAX 請求、JSON 回應與表單驗證

ExtJS (Sencha Ext JS) 作為企業級前端框架,與 PHP 後端透過 AJAX 進行 JSON 資料交換是標準模式。本文從前端 `Ext.Ajax.request`、表單提交、Grid Store 載入,到後端 PHP 接收、驗證、回應 JSON 的完整流程,提供可直接落地的最佳實踐範本。

架構概覽

ExtJS (前端)          HTTP/JSON          PHP (後端)
┌─────────────┐      ┌─────────────┐     ┌─────────────┐
│ View/Grid   │─────▶│  API 路由   │────▶│ Controller  │
│ Form/Store  │◀────│  JSON 響應  │◀────│ Service/DAO │
└─────────────┘      └─────────────┘     └─────────────┘

後端 PHP 統一回應格式

<?php
// api.php 或 Controller 基類
function jsonResponse(bool $success, $data = null, string $message = '', int $httpCode = 200): void {
    http_response_code($httpCode);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode([
        'success' => $success,
        'data'    => $data,
        'message' => $message,
        'timestamp' => time(),
    ], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
    exit;
}

// 成功
jsonResponse(true, ['id' => 123], '建立成功');

// 驗證失敗
jsonResponse(false, ['field' => 'email', 'error' => 'Email 格式錯誤'], '驗證失敗', 422);

// 伺服器錯誤
jsonResponse(false, null, '內部伺服器錯誤', 500);

前端 ExtJS AJAX 請求封裝

Ext.define('App.util.Ajax', {
    singleton: true,
    request: function(config) {
        Ext.Ajax.request(Ext.apply({
            headers: { 'Accept': 'application/json' },
            timeout: 30000,
            success: function(response, opts) {
                var result = Ext.decode(response.responseText, true);
                if (!result) {
                    Ext.Msg.alert('錯誤', '伺服器回應格式異常');
                    if (config.failure) config.failure.call(config.scope || this, response, opts);
                    return;
                }
                if (result.success) {
                    if (config.callback) config.callback.call(config.scope || this, result.data, result.message);
                } else {
                    // 統一錯誤處理:顯示 message,若有 data.field 則標記表單欄位
                    Ext.Msg.alert('操作失敗', result.message || '未知錯誤');
                    if (config.failure) config.failure.call(config.scope || this, result.data, result.message);
                }
            },
            failure: function(response, opts) {
                var msg = '網路錯誤';
                if (response.status === 422) {
                    var r = Ext.decode(response.responseText, true);
                    msg = r?.message || '驗證失敗';
                } else if (response.status >= 500) {
                    msg = '伺服器內部錯誤';
                }
                Ext.Msg.alert('錯誤', msg);
                if (config.failure) config.failure.call(config.scope || this, null, msg);
            }
        }, config));
    }
});

實戰場景一:表單提交 (登入/註冊/編輯)

前端:Ext.form.Panel

Ext.create('Ext.form.Panel', {
    title: '用戶登入',
    renderTo: Ext.getBody(),
    width: 350,
    bodyPadding: 10,
    defaultType: 'textfield',
    items: [{
        fieldLabel: '帳號',
        name: 'username',
        allowBlank: false,
        msgTarget: 'side'
    }, {
        fieldLabel: '密碼',
        name: 'password',
        inputType: 'password',
        allowBlank: false
    }],
    buttons: [{
        text: '登入',
        handler: function(btn) {
            var form = this.up('form').getForm();
            if (form.isValid()) {
                btn.disable();
                App.util.Ajax.request({
                    url: '/api/auth/login',
                    method: 'POST',
                    params: form.getValues(),
                    callback: function(data, msg) {
                        btn.enable();
                        // 登入成功:儲存 token、導向主頁
                        localStorage.setItem('token', data.token);
                        window.location.href = '/dashboard';
                    },
                    failure: function(data, msg) {
                        btn.enable();
                        // data 可能包含欄位錯誤,可用 form.markInvalid(data)
                    }
                });
            }
        }
    }]
});

後端:登入 API

<?php
// api/auth/login.php
require_once '../bootstrap.php'; // PDO, Session, 函式庫

// 僅允許 POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    jsonResponse(false, null, 'Method Not Allowed', 405);
}

// 讀取 JSON 或表單資料
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) $input = $_POST;

$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';

// 驗證
$errors = [];
if (!$username) $errors['username'] = '請輸入帳號';
if (!$password) $errors['password'] = '請輸入密碼';
if ($errors) jsonResponse(false, $errors, '驗證失敗', 422);

// 查詢用戶
$stmt = $pdo->prepare('SELECT id, username, password_hash, role FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch();

if (!$user || !password_verify($password, $user['password_hash'])) {
    jsonResponse(false, null, '帳號或密碼錯誤', 401);
}

// 產生 JWT 或 Session
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['role'] = $user['role'];

// 回應
jsonResponse(true, [
    'user'  => ['id' => $user['id'], 'username' => $user['username'], 'role' => $user['role']],
    'token' => bin2hex(random_bytes(32)), // 簡易 token 示意
], '登入成功');

實戰場景二:Grid Store 分頁/排序/篩選

前端:Ext.data.Store + Proxy

Ext.define('App.store.Users', {
    extend: 'Ext.data.Store',
    alias: 'store.users',
    fields: ['id', 'username', 'email', 'role', 'created_at', 'status'],
    pageSize: 20,
    proxy: {
        type: 'ajax',
        url: '/api/users',
        reader: {
            type: 'json',
            rootProperty: 'data',
            totalProperty: 'total',
            successProperty: 'success',
            messageProperty: 'message'
        },
        // 將分頁/排序/篩選參數轉為查詢字串
        simpleSortMode: true,
        filterParam: 'filters',
        sortParam: 'sort',
        pageParam: 'page',
        limitParam: 'limit',
        startParam: 'start',
    },
    autoLoad: true,
    listeners: {
        load: function(store, records, successful) {
            if (!successful) Ext.Msg.alert('錯誤', store.getProxy().getReader().rawData.message);
        }
    }
});

// Grid 使用
Ext.create('Ext.grid.Panel', {
    title: '用戶管理',
    store: { type: 'users' },
    columns: [
        { text: 'ID', dataIndex: 'id', width: 60 },
        { text: '帳號', dataIndex: 'username', flex: 1 },
        { text: 'Email', dataIndex: 'email', flex: 1 },
        { text: '角色', dataIndex: 'role', width: 100 },
        { text: '狀態', dataIndex: 'status', width: 80, renderer: v => v ? '啟用' : '停用' },
    ],
    dockedItems: [{
        xtype: 'pagingtoolbar',
        dock: 'bottom',
        displayInfo: true,
        store: 'users'
    }],
    renderTo: Ext.getBody(),
    width: 800,
    height: 400
});

後端:分頁查詢 API

<?php
// api/users.php
require_once '../bootstrap.php';

$page   = max(1, (int)($_GET['page'] ?? 1));
$limit  = min(100, max(1, (int)($_GET['limit'] ?? 20)));
$start  = ($page - 1) * $limit;
$sort   = $_GET['sort'] ?? 'id';
$dir    = strtoupper($_GET['dir'] ?? 'ASC');
$filters = json_decode($_GET['filters'] ?? '[]', true);

// 白名單排序欄位
$allowedSort = ['id', 'username', 'email', 'created_at'];
$sort = in_array($sort, $allowedSort) ? $sort : 'id';
$dir  = in_array($dir, ['ASC', 'DESC']) ? $dir : 'ASC';

// 建構 WHERE
$where = ['1=1'];
$params = [];
if (!empty($filters)) {
    foreach ($filters as $f) {
        $field = $f['property'] ?? '';
        $value = $f['value'] ?? '';
        if (in_array($field, ['username', 'email', 'role'])) {
            $where[] = "$field LIKE ?";
            $params[] = "%$value%";
        }
    }
}
$whereSql = implode(' AND ', $where);

// 總筆數
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM users WHERE $whereSql");
$countStmt->execute($params);
$total = $countStmt->fetchColumn();

// 分頁資料
$dataStmt = $pdo->prepare("SELECT id, username, email, role, created_at, status FROM users WHERE $whereSql ORDER BY $sort $dir LIMIT ? OFFSET ?");
$params[] = $limit;
$params[] = $start;
$dataStmt->execute($params);
$rows = $dataStmt->fetchAll(PDO::FETCH_ASSOC);

jsonResponse(true, [
    'total' => (int)$total,
    'data'  => $rows,
    'page'  => $page,
    'limit' => $limit,
]);

實戰場景三:檔案上傳 (ExtJS 4/5/6/7 通用)

// 前端:Form submit (標準 multipart/form-data)
form.submit({
    url: '/api/upload',
    waitMsg: '上傳中...',
    success: function(form, action) {
        jsonResponse(true, action.result.data, '上傳成功');
    },
    failure: function(form, action) {
        jsonResponse(false, null, action.result?.message || '上傳失敗');
    }
});

// 後端:處理 $_FILES
$file = $_FILES['file'] ?? null;
// ... 驗證、移動、儲存資料庫 ...
jsonResponse(true, ['path' => '/uploads/abc123.jpg', 'url' => 'https://cdn.example.com/abc123.jpg']);

常見坑點與解決

問題原因解決
PHP 收不到 `$_POST` 資料Content-Type 為 application/json`json_decode(file_get_contents('php://input'), true)`
ExtJS Store 不發送分頁參數Proxy 設定錯誤或 `remoteSort`/`remoteFilter` 未開啟檢查 proxy 參數對應
日期格式不一致PHP `date('c')` vs ExtJS `dateFormat`統一用 ISO 8601 (`Y-m-d\TH:i:sP`),Model 指定 `dateFormat: 'c'`
大數據量 Grid 卡頓一次性渲染千行 DOM啟用 `bufferedrenderer`、分頁、虛擬滾動
CSRF Token 驗證失敗ExtJS 未自動發送 Token在 `Ext.Ajax.defaultHeaders` 設定 `X-CSRF-Token` 或表單隱藏欄位

安全加固清單

  1. 所有 API 強制 HTTPS
  2. JWT/Session 驗證中介軟體保護需登入端點
  3. RBAC 權限檢查 (Controller/Service 層)
  4. 輸入驗證白名單 (欄位、排序、篩選條件)
  5. SQL 參數化查詢 (PDO prepare)
  6. 輸出 JSON 編碼安全旗標 (`JSON_HEX_TAG` 等)
  7. 錯誤訊息不洩露堆疊/資料庫結構

遵循「統一 JSON 回應格式、前端統一錯誤處理、後端參數化查詢與情境驗證」三大原則,ExtJS + PHP 專案即可建立穩健、可維護的前後端協作架構。

copyright © 萬盛學電腦網 all rights reserved