EduCore & FinFlow Workspace ตอนที่ 2: การปฏิวัติหน้าบ้านด้วย Vue 3.5 V2 Architecture, ระบบจัดการสถานะ และ UI ยุคใหม่
เจาะลึกส่วนติดต่อผู้ใช้ (Frontend) ของ EduCore & FinFlow Workspace ในการอัปเกรดเทคโนโลยีไปสู่ V2 Architecture ด้วย Vue 3.5 Composition API, TypeScript, Pinia, TanStack Query และการทำงานแบบสอดคล้องตามอุปกรณ์
Problem
ระบบหน้าบ้านแบบเดิม (Legacy V1) ใช้ Options API ร่วมกับ Vuex และการสร้าง HTTP client ชั้นนอกโดยไม่มีสเปคประเภทข้อมูล (Type Safety) ส่งผลให้พบปัญหาการเรนเดอร์ UI ผิดพลาดเมื่อมีการเปลี่ยนแปลงข้อมูลโครงสร้างงบประมาณจากเซิร์ฟเวอร์ และมีความล่าช้าในการพัฒนาฟีเจอร์จัดซื้อ
Solution
ออกแบบและจัดวางโครงสร้างหน้าบ้านใหม่ภายใต้โฟลเดอร์ /src/v2 ทั้งหมด โดยการพัฒนาบนฐาน Vue 3.5 + TS ควบคุม Client-side State ด้วย Pinia และจัดการ Server-side State ด้วย TanStack Query เพื่อการทำ Caching ข้อมูล พร้อมใช้ PrimeVue 4 สร้างความสวยงาม
Impact
หน้าจอมีความลื่นไหลเป็นธรรมชาติ การเรียกข้อมูลลดลงกว่า 60% ผ่านกลไก Cache อัจฉริยะ โค้ดของทีมผ่านการตรวจจับข้อผิดพลาดระดับ Compile-time และรองรับการแสดงผลตารางงบประมาณแบบ Responsive Pagination 3/5/10 รายการ
1. บริบทการย้ายระบบและปรับปรุงส่วนติดต่อผู้ใช้ใน Phase 2
ในการเข้ามารับงานต่อใน Phase 2 สำหรับส่วนติดต่อผู้ใช้ (Frontend) ของระบบ EduCore & FinFlow Workspace ทีมงานเผชิญกับโครงสร้างหน้าบ้านเดิมที่เป็นระบบ Legacy V1:
- โค้ดเขียนแบบ Options API (JavaScript): นำไปสู่การจัดวางสปาเกตตีโค้ดที่ขยายและทำความเข้าใจตรรกะได้ยากเมื่อไฟล์มีขนาดใหญ่ขึ้น
- การจัดการข้อมูลผ่าน Vuex 4: ข้อมูลของแต่ละฟีเจอร์เกิดความทับซ้อนและมีโอกาสเกิดสภาวะแย่งชิงข้อมูล (Race Condition) ได้ง่ายเมื่อเปิดหลายแท็บหน้าจอ
- ไม่มี Type Safety: ระบบมักจะทำงานล้มเหลว (Runtime Crashes) เมื่อข้อมูลตอบกลับจาก API ส่งค่าที่เป็น
nullหรือฟิลด์ที่ไม่ได้คาดการณ์ไว้
ในการอัปเกรดเพื่อรองรับความคงทนและประสิทธิภาพที่สูงขึ้น ทีมพัฒนาได้กำหนดและรังสรรค์ V2 Architecture ขึ้นใหม่ภายใต้พาธ /src/v2 โดยบังคับใช้ TypeScript, Vue 3.5 และคอมโพเนนต์การเรนเดอร์ประสิทธิภาพสูงจาก PrimeVue 4
2. โครงสร้างและเลเยอร์ของคอมโพเนนต์หน้าบ้าน (V2 Component Architecture)
เพื่อแยกหน้าที่การทำงานของโค้ดให้สั้นและกระชับ สถาปัตยกรรม V2 แบ่งแยกความรับผิดชอบออกเป็นชั้นย่อย:
graph LR
subgraph UI_Layer [UI Presentation]
View[View: BudgetEstimationList.vue]
Comp[Dialog: BudgetEstimationDialog.vue]
end
subgraph State_Layer [Logic & Reactivity]
FormComp[Form Composable: useBudgetEstimationForm]
QueryComp[Query Composable: useBudgetEstimationQuery]
end
subgraph Storage_Network [Storage & API Services]
PiniaStore[Client Store: Pinia]
AxiosClient[Axios V2 client]
end
View --> FormComp
View --> Comp
Comp --> FormComp
FormComp --> QueryComp
FormComp --> PiniaStore
QueryComp --> AxiosClient
AxiosClient --> API_V2[Backend API /api/v2]
หน้าที่แต่ละส่วน
- View (Page Owner): ดูแลเรื่อง Layout และการผูก Composables เข้ากับองค์ประกอบเมนูนำทางหลัก
- Composables: แยกตรรกะการประมวลผล ฟอร์ม และ State การโหลดข้อมูลออกจากคอมโพเนนต์หน้าจอหลัก
- Components: UI ชิ้นย่อยที่ทำงานและรับค่าผ่านพารามิเตอร์ Props และสื่อสารกลับขึ้นไปผ่านการ Emit Events เท่านั้น
- Services / Client: จัดการติดต่อเครือข่ายและแนบ Token ตรวจสอบสิทธิ์กลางของสถาบัน (Centralized OAuth)
3. โครงสร้างโฟลเดอร์ของ Frontend V2
ซอร์สโค้ด V2 ทั้งหมดจะอยู่ในกลุ่มโฟลเดอร์ต่อไปนี้:
src/v2/views/และsrc/v2/components/สำหรับหน้าจอหลักและชิ้นส่วนควบคุมsrc/v2/composables/บรรจุ Logic ต่างๆ ในลักษณะ Hookssrc/v2/services/และsrc/v2/stores/สำหรับจัดการฝั่ง Network API และเก็บค่า Client State เช่น โทเค็นล็อกอินsrc/v2/types/สำหรับเก็บอินเทอร์เฟซไทป์กลาง (TypeScript Interfaces)
4. เจาะลึกซอร์สโค้ดตัวอย่างโมดูลประมาณการงบประมาณ (Budget Estimation)
นี่คือโครงสร้างจริงของโมดูล ประมาณการงบประมาณรายจ่าย (Budget Estimation) ที่ประยุกต์ใช้ V2 Stack:
4.1 ประกาศโครงสร้างประเภทข้อมูล: types/budget.ts
ช่วยควบคุมสเปคโครงสร้างข้อมูลตั้งแต่ฝั่ง IDE:
export interface BudgetEstimation {
id: string;
year: number;
projectName: string;
amount: number;
status: "draft" | "pending_approval" | "approved" | "rejected";
}
export interface CreateBudgetInput {
year: number;
projectName: string;
amount: number;
}
4.2 ตรรกะควบคุมและผูกข้อมูล (Composable Layer)
ใช้ประโยชน์จาก TanStack Vue Query และตรวจสอบฟอร์ม (Validation) ก่อนยิงข้อมูลเข้าสู่ API:
import { ref } from "vue";
import { useMutation, useQueryClient } from "@tanstack/vue-query";
import { CreateBudgetInput } from "../types/budget";
const budgetService = {
create: async (data: CreateBudgetInput) => {
// แนบ JWT และ POST ไปยัง /api/v2/budget-estimation
return { id: "mock-id", ...data, status: "draft" };
},
};
export function useBudgetEstimationForm(onSuccessCallback?: () => void) {
const queryClient = useQueryClient();
const errors = ref<Record<string, string>>({});
const mutation = useMutation({
mutationFn: budgetService.create,
onSuccess: () => {
// เคลียร์ Cache เพื่อสั่งอัปเดตหน้ารายการทันที
queryClient.invalidateQueries({ queryKey: ["budget-estimations"] });
if (onSuccessCallback) onSuccessCallback();
},
});
const validateAndSubmit = async (formData: CreateBudgetInput) => {
errors.value = {};
if (!formData.projectName || formData.projectName.length < 5) {
errors.value.projectName = "กรอกชื่อโครงการให้ถูกต้อง";
return;
}
await mutation.mutateAsync(formData);
};
return { errors, validateAndSubmit, isSubmitting: mutation.isPending };
}
4.3 คอมโพเนนต์แสดงผลและระบบ Responsive Pagination
ตารางรายการนำความสามารถของ PrimeVue 4 มาจัดกลุ่มตารางข้อมูล โดยมีการเฝ้าระวังขนาดของหน้าจอ (Resize Event Listener) เพื่อปรับจำนวนแถวข้อมูลที่ดึงขึ้นมาให้พอดีกับขนาดหน้าจอผู้ใช้งานโดยอัตโนมัติ:
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { useQuery } from "@tanstack/vue-query";
import DataTable from "primevue/datatable";
import Column from "primevue/column";
const currentPage = ref(1);
const limitPerPage = ref(10); // ค่าเริ่มต้นจอ Desktop
const updateLimitByScreenSize = () => {
const width = window.innerWidth;
if (width < 768)
limitPerPage.value = 3; // จอมือถือแสดง 3 แถว
else if (width < 1024)
limitPerPage.value = 5; // จอแท็บเล็ตแสดง 5 แถว
else limitPerPage.value = 10; // จอขนาดใหญ่แสดง 10 แถว
};
onMounted(() => {
updateLimitByScreenSize();
window.addEventListener("resize", updateLimitByScreenSize);
});
onUnmounted(() => window.removeEventListener("resize", updateLimitByScreenSize));
// คอนฟิกการคิวรีข้อมูลอัจฉริยะร่วมกับ TanStack Query
const { data, isLoading } = useQuery({
queryKey: ["budget-estimations", currentPage, limitPerPage],
queryFn: () =>
fetch(`/api/v2/budget-estimation?page=${currentPage.value}&limit=${limitPerPage.value}`).then(
(res) => res.json()
),
});
</script>
<template>
<div class="card p-4">
<DataTable :value="data?.data" :loading="isLoading">
<Column field="year" header="ปีงบประมาณ" />
<Column field="projectName" header="ชื่อโครงการ" />
<Column field="amount" header="งบประมาณ (บาท)" />
</DataTable>
<div class="mt-2 text-sm text-gray-500">
แสดงผล {{ limitPerPage }} แถวต่อหน้า (Responsive Pagination)
</div>
</div>
</template>
5. สรุปผลลัพธ์การย้ายสู่หน้าบ้านยุคใหม่ (Frontend V2)
การบูรณาการเทคโนโลยี V2 ในส่วนติดต่อผู้ใช้ส่งมอบผลลัพธ์ที่ดีขึ้นเป็นทวีคูณ:
- หมดปัญหา Runtime Crash: การกรองข้อมูลด้วย TypeScript ในระดับ Compile-time และ Zod ในระดับฟอร์ม ป้องกันไม่ให้แอปพลิเคชันค้าง
- Caching ประสิทธิภาพสูง: TanStack Query ลดการเรียกเน็ตเวิร์กซ้ำซ้อน
- Responsive Layout: ตัวคัดกรอง Pagination อัตโนมัติช่วยให้การทำรายการผ่านโทรศัพท์มือถือลื่นไหลและดูได้ง่ายสอดคล้องตามอุปกรณ์