首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

ExtJS 與 PHP 參數交互實戰:AJAX 請求、表單提交與 JSON 回應處理

     編輯:PHP 相關知識

架構概覽

ExtJS(前端 MVC/MVVM 框架)與 PHP(後端 API)透過 Ext.Ajax.request、Ext.form.Panel、Store/Proxy 進行資料交換,核心均為 HTTP + JSON。

基礎:Ext.Ajax.request 完整範例

前端:登入請求

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 登入處理 (api/login.php)

<?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.form.Panel 表單提交

前端:使用者編輯表單

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);
}

資料網格:Store + JsonProxy 完整串接

前端:使用者列表 Store

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'
});

後端:分頁列表 API

<?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 處理特定邏輯
}

PHP 統一回應助手類別

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 6+ 現代工具組:ViewModel + ViewController + Binding

Ext.define('App.view.user.List', {
    extend: 'Ext.grid.Panel',
    xtype: 'user-list',
    
    requires: ['App.store.Users'],
    
    viewModel: {
        stores: {
            users: {
                type: 'users',
                autoLoad: true
            }
        }
    },
    
    bind: {
        store: '{users}'
    },
    
    columns: [
        { text: 'ID', dataIndex: 'id', width: 60 },
        { text: '帳號', dataIndex: 'username', flex: 1 },
        { text: 'Email', dataIndex: 'email', flex: 1 },
        { text: '角色', dataIndex: 'roles', renderer: function(v) { return Array.isArray(v) ? v.join(', ') : v; } },
        { text: '狀態', dataIndex: 'status', width: 80, renderer: function(v) { return v ? '啟用' : '停用'; } },
        { text: '建立時間', dataIndex: 'created_at', width: 160, renderer: Ext.util.Format.dateRenderer('Y-m-d H:i') }
    ],
    
    dockedItems: [{
        xtype: 'pagingtoolbar',
        dock: 'bottom',
        bind: {
            store: '{users}'
        },
        displayInfo: true
    }],
    
    tbar: [{
        text: '新增',
        handler: 'onAddUser'
    }, {
        text: '刷新',
        handler: 'onRefresh'
    }],
    
    controller: {
        onAddUser: function() {
            Ext.create('App.view.user.EditWindow').show();
        },
        onRefresh: function() {
            this.getViewModel().getStore('users').reload();
        }
    }
});

ExtJS 與 PHP 串接的關鍵在於 統一 JSON 契約:請求參數格式、回應結構、錯誤代碼、分頁欄位、日期格式。建立 ApiResponse 統一回應類別、BaseController 統一參數解析、BaseModel 統一欄位定義,即可構建穩健、可維護的企業級應用架構。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
現象可能原因解決
ExtJS 顯示 "successProperty" 錯誤PHP 回應缺少 success 欄位確保所有 API 回應包含 success: true/false
表單驗證錯誤不顯示errors 格式不符、fieldName 不匹配errors 鍵名必須與 Model field name 完全一致
Store 分頁不正確totalProperty 回傳錯誤檢查 total 為整數、對應 totalProperty 設定
日期欄位顯示 NaNdateFormat 不匹配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