somsakul.com

ทดลองทำสิ่งต่างๆ ทั่วไป

Quick-Win PHP Apps setup

- Posted in บันเทิง by

ต้องการสร้าง Minimalistic High-Performance Web Application บนระบบปฏิบัติการ Windows โดยใช้ PocketBase เป็น Backend, Caddy เป็น Web Server + Reverse Proxy + HTTPS, หน้าบ้านใช้ Pico CSS + HTMX + Lucide Icons + Alpine.js โดยไม่พึ่งพา Node.js Build Tools ใดๆ 🚀

📁 1. โครงสร้างโฟลเดอร์โครงการ (Project File Structure)

จัดโครงสร้างให้คลีน 🧼 และแยกสัดส่วนชัดเจนตามสไตล์ Minimalist ดังนี้:

my-app/
├── caddy.exe             # 🌐 Caddy Server Binary
├── Caddyfile             # ⚙️ ไฟล์ตั้งค่า Web Server & Reverse Proxy
├── pocketbase.exe        # 📦 PocketBase Binary
├── pb_data/              # 💾 โฟลเดอร์เก็บข้อมูล SQLite & Settings (สร้างให้อัตโนมัติ)
├── pb_migrations/        # 🔄 โฟลเดอร์เก็บ Migration ของ Schema (ถ้ามี)
└── public/               # 📂 Root Directory สำหรับไฟล์ Static Web
    ├── index.html        # 🏠 หน้าหลัก Web Application
    ├── css/              # 🎨 (Optional) Custom CSS เพิ่มเติมจาก Pico CSS
    ├── js/               # 📜 (Optional) Custom Helper JS
    └── includes/         # 🧩 Partial HTML fragments สำหรับ HTMX Render
        └── item-row.html

⚙️ 2. การ Setup และตัวอย่างโค้ด (Boilerplate)

📄 2.1. ไฟล์ Caddyfile

กำหนดการทำ Reverse Proxy ✨ ส่งข้อมูลไปยัง PocketBase พร้อมตั้งค่าการป้องกันความปลอดภัยล็อกการเข้าถึงไฟล์ระบบอย่าง /pb_data หรือ /pb_migrations 🛡️

# เปลี่ยน yourdomain.com เป็น Domain จริง หรือ localhost สำหรับ Dev Environment
localhost {
    # 📌 Reverse Proxy ไปยัง PocketBase TCP Port 8090
    reverse_proxy 127.0.0.1:8090

    # 🛡️ บล็อกการเข้าถึงโฟลเดอร์ภายในผ่าน Web Request เพื่อความปลอดภัย
    respond /pb_data/* "403 Access Denied" 403
    respond /pb_migrations/* "403 Access Denied" 403

    # ⚡ เปิดการใช้งาน Compression เพื่อเพิ่มความเร็วในการโหลด
    encode gzip zstd
}

📄 2.2. ไฟล์ public/index.html

ประกอบร่าง CDN ทั้งหมด 🧩 พร้อมตัวอย่างการใช้ HTMX ดึงข้อมูล (hx-get) และส่งข้อมูล (hx-post) ไปยัง PocketBase โดยตรงแบบไม่ต้องเขียน JavaScript สักบรรทัด! 🔥

<!DOCTYPE html>
<html lang="th" data-theme="light">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Minimal High-Performance App 🚀</title>

       <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">

       <script src="https://unpkg.com/[email protected]"></script>
       <script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>

       <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

       <script src="https://unpkg.com/lucide@latest"></script>
</head>
<body>
    <main class="container">
               <header style="display: flex; justify-content: space-between; align-items: center;">
            <h2><i data-lucide="zap"></i> App Dashboard</h2>
            <p>Powered by <strong>PocketBase + HTMX + Pico CSS</strong> ⚡</p>
        </header>

        <hr />

                      <section x-data="{ open: true }">
            <details open x-bind:open="open">
                <summary role="button" class="outline">➕ เพิ่มรายการใหม่ (Add New Item)</summary>
                <form hx-post="/api/collections/tasks/records"
                      hx-ext="json-enc"
                      hx-target="#item-list"
                      hx-swap="beforeend"
                      hx-on::after-request="this.reset(); Lucide.createIcons();">

                    <div class="grid">
                        <label for="title">
                            ชื่อรายการ 📝
                            <input type="text" id="title" name="title" placeholder="เช่น ซื้อกาแฟสด ☕" required>
                        </label>

                        <label for="status">
                            สถานะ 📌
                            <select id="status" name="status">
                                <option value="pending">⏳ รอดำเนินการ</option>
                                <option value="completed">✅ เสร็จสิ้น</option>
                            </select>
                        </label>
                    </div>

                    <button type="submit" class="contrast">
                        <i data-lucide="save"></i> บันทึกข้อมูล
                    </button>
                </form>
            </details>
        </section>

               <section style="margin-top: 2rem;">
            <h3>📋 รายการข้อมูลทั้งหมด</h3>

                       <button hx-get="/api/collections/tasks/records"
                    hx-target="#item-list"
                    hx-transform="response"
                    class="secondary outline"
                    style="width: auto; margin-bottom: 1rem;">
                <i data-lucide="refresh-cw"></i> โหลดข้อมูลใหม่
            </button>

            <figure>
                <table>
                    <thead>
                        <tr>
                            <th scope="col">ID</th>
                            <th scope="col">ชื่อรายการ</th>
                            <th scope="col">สถานะ</th>
                            <th scope="col">วันที่สร้าง</th>
                        </tr>
                    </thead>
                    <tbody id="item-list"
                           hx-get="/api/collections/tasks/records"
                           hx-trigger="load"
                           hx-transform="response">
                                           </tbody>
                </table>
            </figure>
        </section>
    </main>

       <script>
        lucide.createIcons();

        // 🛠️ Helper Script: แปลง JSON Response จาก PocketBase ให้เป็น HTML Row สำหรับ HTMX
        document.body.addEventListener('htmx:beforeSwap', function(evt) {
            if (evt.detail.xhr.status === 200) {
                try {
                    const response = JSON.parse(evt.detail.xhr.responseText);
                    // รองรับทั้งแบบ Get List (items) และ Create Single Record
                    const items = response.items ? response.items : [response];

                    let htmlRows = '';
                    items.forEach(item => {
                        htmlRows += `
                            <tr>
                                <td><code>${item.id}</code></td>
                                <td>${item.title || '-'}</td>
                                <td><mark>${item.status || 'pending'}</mark></td>
                                <td>${new Date(item.created).toLocaleString('th-TH')}</td>
                            </tr>
                        `;
                    });

                    evt.detail.serverResponse = htmlRows;
                } catch (e) {
                    // หากไม่ใช่ JSON ให้ใช้ Response เดิม
                }
            }
        });
    </script>
</body>
</html>

🚀 3. คำแนะนำและวิธีการเปิดใช้งานบน Windows

คุณสามารถสั่งการทำงานของระบบได้สะดวกผ่าน PowerShell หรือ Command Prompt (cmd) บน Windows ครับ:

3.1. รันระบบสำหรับสภาพแวดล้อม Development

เปิด PowerShell 2 หน้าต่างในโฟลเดอร์โครงการ: หน้าต่างที่ 1 (PocketBase):

.\pocketbase.exe serve

(ระบบจะสร้างโฟลเดอร์ pb_data และเปิด Admin UI ให้เข้าใช้งานที่ http://127.0.0.1:8090/_/) ⚙️ หน้าต่างที่ 2 (Caddy Server):

.\caddy.exe run

(Caddy จะอ่านไฟล์ Caddyfile และเริ่มทำหน้าที่ Reverse Proxy ทันที) 🌐

3.2. การวิเคราะห์เชิงลึก: ผลกระทบ โอกาส และแนวทางรับมือ 💡

💥 ผลกระทบ (Impact):

ประสิทธิภาพสูงสุด (Ultra High Performance): ใช้ Resource RAM/CPU ต่ำมาก (PocketBase ใช้ RAM เพียงหลักสิบ MB) เหมาะแก่การรันบน Windows Server ขนาดเล็ก ลดปัญหา Dependency Hell: ไม่ต้องคอยอัปเดต node_modules หรือเจอปัญหา Build Tools พังในอนาคต 🛠️

🌟 โอกาส (Opportunity):

การพัฒนาที่รวดเร็ว (Rapid Prototyping): สามารถส่งมอบงานได้เร็วกว่าสถาปัตยกรรม SPA (React/Vue) ถึง 2-3 เท่า เพราะเน้นส่ง HTML ตรงจาก Server Real-time Capabilities: PocketBase มี SSE (Server-Sent Events) ในตัว เมื่อนำมาใช้ร่วมกับ HTMX (hx-ext="sse") จะทำระบบ Real-time Dashboard ได้ง่ายดาย 📡

🛡️ แนวทางการรับมือ (Mitigation & Best Practices):

การ Backup ข้อมูล: เนื่องจาก SQLite เก็บอยู่ในไฟล์เดียวภายใน /pb_data/data.db ให้ตั้งค่าการ Backup โฟลเดอร์ pb_data เป็นประจำ 💾 CORS & Safety: ตั้งค่า API Rules ใน PocketBase Admin UI ให้รัดกุมเสมอ โดยกำหนดสิทธิ์การ Read/Write ตาม Role ของผู้ใช้งานเพื่อป้องกันไม่ให้ผู้ไม่หวังดีส่ง Request โดยตรงผ่าน REST API 🔒