Skip to content

16.5 Wave 3 — Frontend migration to Nuxt 3

Wave 3 ย้าย frontend ทั้งสองตัว (vtrc-web และ vtrc-rc-backoffice) จาก React 16 + Webpack 4 + Apollo v2 ไป Nuxt 3 (Vue 3, Vite, Feature-Sliced Design)

เป้าหมายของ Wave 3

  • frontend ทั้งสองตัวรันบน Nuxt 3
  • ใช้ Feature-Sliced Design (FSD) เป็นโครงสร้าง
  • TypeScript เป็น default
  • มี visual regression test
  • Effort: 15 เดือน (30 sprint) — parallel กับ Wave 2
  • ทีม: 2-3 frontend engineer

ทำไมต้อง Nuxt 3

เหตุผลอธิบาย
Vue 3 มี Composition APIcode อ่านง่ายกว่า, reusability ดีกว่า
ViteHMR เร็วกว่า Webpack 10 เท่า, build เร็วกว่า
Nuxt 3 มี auto-importcode สั้นลง, boilerplate น้อย
SSR/SSG ในตัวโหลดเร็วขึ้น, SEO ดีขึ้น
ชุมชนดีในไทยหานักพัฒนา Vue ในไทยง่ายกว่า React

หมายเหตุ: ทีมสามารถเลือก React/Next.js แทนได้ แต่ต้องปรับ structure ตาม (หลักการ FSD ใช้ได้กับทุก framework)

โครงสร้าง Feature-Sliced Design

src/
├── app/                    # config แอป (Nuxt config, plugin, middleware)
│   ├── plugins/
│   ├── middleware/
│   └── nuxt.config.ts
├── processes/              # เฉพาะ multi-step (เช่น onboarding)
├── pages/                  # route page (Nuxt file-based routing)
│   ├── index.vue
│   ├── leave/
│   │   └── [id].vue
│   └── payroll/
├── widgets/                # block ใหญ่ในหน้า (LeaveRequestForm)
├── features/               # feature เล็ก (LeaveBalanceBadge)
├── entities/               # domain model (User, Leave, Payslip)
│   ├── user/
│   │   ├── api/
│   │   ├── model/
│   │   ├── ui/
│   │   └── index.ts
│   └── leave/
└── shared/                 # ใช้ร่วม (UI kit, util, config)
    ├── ui/
    ├── api/
    └── config/

กฎ FSD:

  • layer บน import layer ล่างได้ (pages → widgets → features → entities → shared)
  • layer ล่างห้าม import layer บน
  • แต่ละ layer มี slice (กลุ่มตาม domain) และ segment (api, model, ui, lib)

ลำดับการ migrate

migration ทำทีละ page ไม่ใช่ทั้งหมดในครั้งเดียว (ตามหลัก strangler)

Sprint 57-58: สร้าง foundation
Sprint 59-60: หน้า login + หน้าหลัก (high-value, low-risk)
Sprint 61-64: หน้า leave + payroll
Sprint 65-68: หน้า welfare + provident-fund
Sprint 69-72: หน้า admin/backoffice
Sprint 73-76: หน้า report + analytics
Sprint 77-80: ทดสอบ, cutover, decommission React

รายละเอียด Sprint แรก (foundation)

Task 3.1 · สร้าง repo ใหม่

vtrc-web-next/
├── nuxt.config.ts
├── package.json
├── tsconfig.json
├── app.vue
└── src/                # FSD structure ข้างบน

หรือใช้ vtrc-web เดิม แล้วสร้าง branch next — ขึ้นกับทีม

Task 3.2 · ตั้งค่า

  • Nuxt 3 + Vite + TypeScript
  • @nuxtjs/apollo (สำหรับ GraphQL)
  • @nuxtjs/tailwind (สำหรับ styling)
  • pinia (สำหรับ state)
  • vitest (สำหรับ test)
  • playwright (สำหรับ e2e test)

Task 3.3 · auth flow

ย้าย token refresh logic จาก vtrc-web/src/config/apollo.js มา Nuxt plugin:

typescript
// src/app/plugins/apollo.ts
export default defineApolloAuth((nuxtApp) => ({
  httpEndpoint: useRuntimeConfig().public.graphqlEndpoint,
  tokenName: 'token',
  authType: 'Bearer',
  onCachedError(error) {
    if (error.message === 'TOKEN_EXPIRED') {
      navigateTo('/login');
    }
  }
}));

Task 3.4 · UI kit

เลือกอย่างเดียว — ห้ามใช้หลาย library (debt ปัจจุบันใช้ AntD + MUI + styled-components):

แนะนำ:

  • UI Bakery / Nuxt UI (Vue 3 native) — แนะนำ
  • PrimeVue — component เยอะ
  • Ant Design Vue — คล้าย AntD เดิม (ทีมเคยชิน)

Task 3.5 · theme + design token

ย้าย design token ไป CSS variable:

css
:root {
  --color-primary: #d8232a; /* แดงกาชาด */
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  /* ... */
}

ตัวอย่าง migration — หน้า Leave Request

เดิม (React)

jsx
// vtrc-web/src/pages/LeaveRequest.jsx (ประมาณ)
import { useQuery, useMutation } from '@apollo/react-hooks';
import { Form, Input, Button, DatePicker } from 'antd';
import styled from 'styled-components';

const Wrapper = styled.div`padding: 16px;`;

const LeaveRequest = () => {
  const { data, loading } = useQuery(LEAVE_BALANCE);
  const [submitLeave] = useMutation(SUBMIT_LEAVE);

  const onSubmit = (values) => {
    submitLeave({ variables: values });
  };

  return (
    <Wrapper>
      <Form onFinish={onSubmit}>
        <Form.Item name="startDate" label="วันเริ่ม">
          <DatePicker />
        </Form.Item>
        {/* ... */}
      </Form>
    </Wrapper>
  );
};

ใหม่ (Vue 3 + FSD)

src/
├── entities/
│   └── leave/
│       ├── api/
│       │   ├── queries.ts      # LEAVE_BALANCE, SUBMIT_LEAVE
│       │   └── types.ts
│       ├── model/
│       │   └── useLeaveBalance.ts
│       └── index.ts
├── features/
│   └── submit-leave/
│       ├── ui/
│       │   └── SubmitLeaveForm.vue
│       ├── model/
│       │   └── useSubmitLeave.ts
│       └── index.ts
├── widgets/
│   └── leave-request/
│       └── ui/
│           └── LeaveRequestWidget.vue
└── pages/
    └── leave/
        └── request.vue
vue
<!-- src/features/submit-leave/ui/SubmitLeaveForm.vue -->
<script setup lang="ts">
import { useLeaveBalance } from '~/entities/leave/model/useLeaveBalance';
import { useSubmitLeave } from '../model/useSubmitLeave';

const { balance } = useLeaveBalance();
const { submit, loading } = useSubmitLeave();
</script>

<template>
  <UForm @submit="submit">
    <UFormField label="วันเริ่ม" name="startDate">
      <UInputDate v-model="form.startDate" />
    </UFormField>
    <!-- ... -->
  </UForm>
</template>
vue
<!-- src/pages/leave/request.vue -->
<script setup lang="ts">
import { LeaveRequestWidget } from '~/widgets/leave-request';
</script>

<template>
  <LeaveRequestWidget />
</template>

cutover strategy

            ┌──────────────────────────┐
            │       Nginx              │
            └──────────┬───────────────┘

            ┌──────────┴───────────────┐
            │                          │
       เส้นทางเดิม              เส้นทางใหม่
            │                          │
            ▼                          ▼
   ┌────────────────┐         ┌────────────────┐
   │ vtrc-web       │         │ vtrc-web-next  │
   │ (React)        │         │ (Nuxt 3)       │
   └────────────────┘         └────────────────┘

Nginx route ตาม path:

nginx
location /leave {
  proxy_pass http://vtrc-web-next:3000;  # ใหม่
}

location / {
  proxy_pass http://vtrc-web:3000;        # เดิม
}

ค่อย ๆ เพิ่ม path ที่ไป vtrc-web-next จนครบ แล้ว decommission vtrc-web

เกณฑ์การตัดสินใจว่า Wave 3 เสร็จ

  • [ ] frontend ทั้งสองตัวรันบน Nuxt 3
  • [ ] Visual regression test ครอบคลุมทุกหน้าหลัก
  • [ ] Lighthouse score ≥ 80 (Performance, Accessibility)
  • [ ] bundle size ลดลง ≥ 30%
  • [ ] ไม่มี React/AntD v3/Apollo v2 ใน codebase

ความเสี่ยงของ Wave 3

ความเสี่ยงผลกระทบMitigation
ทีมไม่เคยใช้ Vuemigration ช้าtraining + spike ก่อนเริ่ม
Design ไม่เหมือนเดิมผู้ใช้ complaintvisual regression test + screenshot compare
Apollo Client v3 → Nuxt Apollodata flow เปลี่ยนใช้ ent gql หรือเขียน wrapper
Migration ยาวนานทีมเบื่อmigrate ทีละ page แล้ว decommission เลย

สรุป Wave 3

Wave 3 ใช้เวลา 15 เดือน (parallel กับ Wave 2) ย้าย frontend ไป Nuxt 3 ที่ทันสมัยและ maintain ได้ง่ายกว่า

Timeline รวม Wave 0-3

เดือน    1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18
        │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │
Wave 0  ████│││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││
        │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │
Wave 1      ████████████████████│││││││││││││││││││││││││││││││││││││││││││
        │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │
Wave 2                  ████████████████████████████████████│││││││││││││││││
        │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │
Wave 3                          ████████████████████████████████████████████

รวม 18-24 เดือนสำหรับ modernization เต็มรูปแบบ

บทถัดไปจะเป็น playbook สำหรับ bounded context extraction (ใช้ได้กับทุก context ใน Wave 2)