[Part 5] Frontend: Scaling Vue.js EP2

[Part 5] Frontend: Scaling Vue.js EP2

Photo by Rahul Mishra / Unsplash

Vue Router

ใช้ในการจัดการการนำทางภายในเว็บไซต์ที่พัฒนาด้วย Vue.js เพื่อสร้าง Single Page Application (SPA) โดยช่วยจัดการการแสดงผลของแต่ละหน้าอย่างเป็นระบบ ไม่จำเป็นต้องโหลดหน้าเว็บใหม่

การติดตั้ง Vue Router

npm install vue-router@4

Import vue-router ใน main.js

import { createApp } from 'vue'
import App from './App.vue'
import { vuetify } from './plugins/vuetify'
import router from './router'

const app = createApp(App)
app.use(vuetify)
app.use(router)

app.mount('#app')

File ตัวอย่าง router/index.js

import { createRouter, createWebHistory } from 'vue-router';
import HomePage from '../views/HomePage.vue';
import AboutPage from '../views/AboutPage.vue';
import UserDetailPage from '@/views/UserDetailPage.vue'
import LoginPage from '@/views/LoginPage.vue'

const routes = [
  { path: '/login', component: LoginPage },
  { path: '/', component: HomePage },
  { path: '/about', component: AboutPage },
  { path: '/user/:id', component: () => UserDetailPage },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

Folder structure เมื่อมี Router

src/
├── components/
│   ├── Home.vue
│   ├── About.vue
│   └── Profile.vue
├── router/
│   └── index.js
├── views/
│   ├── HomePage.vue
│   └── AboutPage.vue
└── main.js
  • components: เก็บ component ที่สามารถนำไปใช้งานซ้ำได้
  • views: เก็บ component หลักที่ใช้แสดงผลหน้าเว็บ
  • router: เก็บการตั้งค่า router
  • <router-view> ใช้สำหรับแสดง component ที่ตรงกับ route ปัจจุบัน
<router-view></router-view>
  • <router-link> ใช้สำหรับสร้างลิงก์เพื่อเปลี่ยน route โดยไม่ต้องรีเฟรชหน้า
<router-link to="/about">About</router-link>

Example

  • ปรับ App.vue ให้รองรับ router
<script setup>
  import AppLayout from './components/layout/AppLayout.vue'
</script>

<template>
  <AppLayout>
    <router-view></router-view>
  </AppLayout>
</template>

Parameter & Query Parameter

Vue Router ยังรองรับการส่งค่า Parameter และ Query Parameter ได้อีกด้วย

Parameter

  1. แก้ไขใน router file
const routes = [
  ...
  { path: '/user/:id', component: UserDetailPage },
  ...
];
  1. เพิ่ม code ใน component
<script setup>
  import MyProfile from '@/components/MyProfile.vue'
  import { ref } from 'vue'
  import { useRoute } from 'vue-router'

  const route = useRoute();
  console.log(route.params.id);

  const firstName = ref('')
  const lastName = ref('')
</script>

<template>
  <div>
    <h1>User Detail Page</h1>
    <p>User ID: {{ route.params.id }}</p>
    <MyProfile :first-name="firstName" :last-name="lastName"/>
  </div>
</template>

Query Parameter:

เพิ่ม code ใน component:

<script setup>
  import MyProfile from '@/components/MyProfile.vue'
  import { useRoute } from 'vue-router'

  const route = useRoute();
  const firstName = route.query.firstname;
  const lastName = route.query.lastname;
</script>

<template>
  <div>
    <h1>User Detail Page</h1>
    <p>User ID: {{ route.params.id }}</p>
    <MyProfile :first-name="firstName" :last-name="lastName"/>
  </div>
</template>

ทดสอบเข้า link: http://localhost:8080/user/1?firstname=Pathompat&lastname=Sunapankhao


Lifecycle

วงจรชีวิตของ Component ใน Vue.js โดยมีช่วงหลักๆ ที่นิยมใช้งานดังนี้:

  • onMounted: เรียกใช้หลังจาก component ถูก mount
  • onUpdated: เรียกเมื่อมีการอัปเดต component
  • onUnmounted: เรียกเมื่อ component ถูกลบออกจาก DOM

Code example: AboutPage.vue

<script setup>
  import MyProfile from '@/components/MyProfile.vue'
  import { ref, onMounted, onUnmounted } from 'vue'

  const firstName = ref('Pathompat')
  const lastName = ref('Sungpankhao')

  onMounted(() => {
    console.log('Component is mounted!');
  });

  onUnmounted(() => {
    console.log('Component is unmounted!');
  });
</script>

<template>
  <MyProfile :first-name="firstName" :last-name="lastName"/>
</template>

Storage Management

เป็นการจัดเก็บข้อมูลฝั่งผู้ใช้งาน (Client-Side) ซึ่งสามารถนำมาใช้เพื่อบันทึกสถานะการใช้งาน การตั้งค่า หรือข้อมูลอื่น ๆ ที่ต้องการให้จำอยู่ระหว่างการใช้งานหรือหลังจากปิดหน้าเว็บแล้วกลับมาใหม่ โดยไม่ต้องส่งกลับไปยังเซิร์ฟเวอร์เสมอไป

Vue.js ไม่มีระบบ storage management ในตัวโดยตรง แต่สามารถใช้ API พื้นฐานของเว็บเบราว์เซอร์อย่าง Local Storage และ Session Storage ได้ง่ายภายใน component

ประเภทของ Storage

1. Local Storage

  • เก็บข้อมูลแบบ key-value
  • ข้อมูลคงอยู่แม้ปิดหน้าเว็บหรือปิดเบราว์เซอร์
  • ใช้ได้กับกรณี: การจดจำชื่อผู้ใช้, theme, language settings

2. Session Storage

  • เหมือนกับ Local Storage แต่ข้อมูลจะหายไปเมื่อปิดแท็บหรือปิดเบราว์เซอร์
  • ใช้ได้กับกรณี: การเก็บข้อมูลชั่วคราวใน session เช่น token แบบไม่ถาวร หรือค่าที่ต้อง reset ทุก session

การจัดการ Storage ใน Vue.js สามารถทำได้ผ่าน Local Storage หรือ Session Storage

ตัวอย่างการใช้งาน Storage:

// local storage
localStorage.setItem('username', 'JohnDoe');
const username = localStorage.getItem('username');
localStorage.removeItem('username');

// session storage
sessionStorage.setItem('sessionID', 'abc123');
const sessionID = sessionStorage.getItem('sessionID');
sessionStorage.removeItem('sessionID');

Example Login

  • เพิ่ม /composables/seUser.js เพื่อเก็บข้อมูล user หลัง login
import { ref, watch } from 'vue';

export function useUser() {
  const username = ref(localStorage.getItem('username') || '');

  watch(username, (newVal) => {
    localStorage.setItem('username', newVal);
  });

  return username;
}
  • สร้างหน้า login page: LoginPage.vue และเพิ่ม route
    • เมื่อ login ผ่านจะ save username ไว้ที่ local storage
<script setup>
  import { ref } from 'vue';
  import { useRouter } from 'vue-router';
  import { useUser } from '@/composables/useUser';

  const router = useRouter();
  const usernameInput = ref('');
  const password = ref('');
  const user = useUser();

  const handleLogin = () => {
    if (usernameInput.value && password.value) {
      user.value = usernameInput.value;
      router.push('/');
    } else {
      alert('กรุณากรอกข้อมูลให้ครบ');
    }
  }
</script>

<template>
  <v-container class="fill-height" fluid>
    <v-row align="center" justify="center">
      <v-col cols="12" sm="8" md="4">
        <v-card>
          <v-card-title class="text-h5">Login</v-card-title>
          <v-card-text>
            <v-text-field
              v-model="usernameInput"
              label="Username"
            />
            <v-text-field
              v-model="password"
              label="Password"
              type="password"
            />
          </v-card-text>
          <v-card-actions :style="{ justifyContent: 'right' }">
            <v-btn color="primary" @click="handleLogin">Login</v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

s

  • ปรับหน้า profile page ให้แสดงผลตาม local storage
<script setup>
  import MyProfile from '@/components/MyProfile.vue'
  import { ref } from 'vue'
  import { useUser } from '@/composables/useUser';

  const user = useUser();

  const firstName = ref(user.value)
  const lastName = ref('')
</script>

<template>
  <MyProfile :first-name="firstName" :last-name="lastName"/>
</template>

Template Ref

Template Ref ช่วยให้เราสามารถอ้างอิงถึง DOM หรือ component อื่นๆ ใน template ได้ง่ายๆ

<input ref="my-input">

Accessing Ref

<script setup>
import { useTemplateRef, onMounted } from 'vue'

// the first argument must match the ref value in the template
const input = useTemplateRef('my-input')

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="my-input" />
</template>

ตัวอย่างการใช้งานกับหน้า Login

  1. focus input เมื่อไม่ได้กรอกข้อมูล
  2. เช็คความสูงของกล่องข้อมูล
<script setup>
  import { ref, onMounted } from 'vue';
  import { useRouter } from 'vue-router';
  import { useUser } from '@/composables/useUser';

  const router = useRouter();
  const usernameInput = ref('');
  const usernameInputRef = ref(null); // Input login ref
  const divLoginRef = ref(null); // div login ref
  const password = ref('');
  const user = useUser();

  const handleLogin = () => {
    if (usernameInput.value && password.value) {
      user.value = usernameInput.value;
      router.push('/');
    } else {
      alert('กรุณากรอกข้อมูลให้ครบ');
      focusUsernameInput();
    }
  }

  const focusUsernameInput = () => {
    usernameInputRef.value.focus();
  }

  onMounted(() => {
    console.log(divLoginRef.value.offsetHeight);
  });
</script>

<template>
  <div class="login-page" ref="divLoginRef">
    <h1>Login Page</h1>
    <v-container class="fill-height" fluid>
      <v-row align="center" justify="center">
        <v-col cols="12" sm="8" md="4">
          <v-card>
            <v-card-title class="text-h5">Login</v-card-title>
            <v-card-text>
              <v-text-field
                v-model="usernameInput"
                label="Username"
                ref="usernameInputRef"
              />
              <v-text-field
                v-model="password"
                label="Password"
                type="password"
              />
            </v-card-text>
            <v-card-actions :style="{ justifyContent: 'right' }">
              <v-btn color="primary" @click="handleLogin">Login</v-btn>
            </v-card-actions>
          </v-card>
        </v-col>
      </v-row>
    </v-container>
  </div>
</template>

Assignment

  1. ใช้ code จาก Assignment หัวข้อที่แล้ว
  2. เพิ่มระบบ router เข้าไปโดยมีหน้าจอดังนี้
    1. แสดงข่าวสาร ไทย - กัมพูชา
    2. หน้า Login
    3. หน้า User Profile
  3. ให้เพิ่ม page component สำหรับ แสดงราคาหุ้นโดยให้ user input ได้ว่าต้องการดูหุ้นอะไร
    1. ใช้ API https://api.marketstack.com/v2/eod?access_key={key}&symbols={symbol} Reference: https://marketstack.com/dashboard
    2. แสดงผลข้อมูลเป็นการ์ดหรือตารางตามเหมาะสม โดยข้อมูลมีดังนี้
      1. data.open: ราคาเปิด
      2. data.close: ราคาปิด
      3. high: ราคาสูงสุด,
      4. low: ราคาต่ำสุด,
      5. name: ชื่อหุ้น,
      6. exchange_code: ตลาด
      7. asset_type: ประเภทสินทรัพย์
      8. price_currency: ค่าเงินที่ใช้
      9. symbol: ชื่อย่อหุ้น
    3. โดยที่ user เข้ามาต้องสามารถ input ชื่อย่อหุ้นเพื่อดูข้อมูล หรือเข้าผ่านลิ้งค์ที่มี query parameter ก็ได้ เช่น http://localhost:8080/market?symbol=AAPL
  4. เพิ่ม page, component สำหรับ แสดง Exchange ทั้งหมดในโลก แสดงผลเป็นการ์ดหรือ table ตามความเหมาะสม
    1. ใช้ API นี้ http://api.marketstack.com/v2/exchanges?access_key={key}
    2. ข้อมูลที่ต้องการแสดง
      1. name: ชื่อเต็ม Exchange
      2. acronym: ชื่อย่อ Exchange
      3. country_code: code ประเทศ
      4. city: เมือง
      5. website: เว็บไซต์ Exchange : ข้อมูลนี้ไม่ต้องแสดง แต่กดปุ่มแล้วให้ไปยังเว็บไซต์นั้นได้

Reference

Vue Router
The official Router for Vue.js
Vue.js
Vue.js - The Progressive JavaScript Framework