Project เล็กๆ ลืม Vuex ได้ไหม ?

Supphachoke Suntiwichaya
NECTEC
Published in
5 min readSep 6, 2020

หลังจากลองเล่นและศึกษา vue composition api มาสักพัก ทำให้พบว่ามันก็สามารถใช้แทน Vuex ได้เป็นอย่างดี เลยบันทึกเรื่องราวไว้ฟื้นฟูความจำ

ออกตัว

บทความนี้ผมเขียนตัวอย่างง่ายๆ เทียบกันระหว่าง Vue2 และ Vue3 โดยใช้ composition api จัดการ state เหมือนกันทำงานได้เหมือนกัน แต่อาจจะมีรายละเอียดเล็กน้อยต่างกันบ้าง สำหรับมือใหม่ที่กำลังปวดหัวกับ Vuex อาจจะเป็นทางเลือกหนึ่งในการสร้าง Project ขึ้นมาแบบไม่ซับซ้อนมากนัก บางอย่างอาจจะมีวิธีที่ง่ายกว่าแต่ด้วยประสบการณ์ของผมเลยเขียนมาไม่สวยมากนักผมยินดีรับฟังคำชี้แนะเพื่อแลกเปลี่ยนความรู้กันครับ

ตัวอย่างคืออะไร ?

ผมจะยกตัวอย่างสองตัวอย่างคือ

  1. ระบบจำลองการ login ง่าย
  2. ระบบ System Notification แจ้งเตือนแบบง่ายๆ

โดยผมจะให้ส่วนหนึ่งเก็บไว้ใน state ที่ผมใช้ composition api แล้ว เรียกใช้จาก components ต่างๆ โดยที่ทุก component จะเห็นค่าเหมือนกัน

Source ตัวอย่าง

Vue2

Vue3

Vue Composition API

Vue2

ให้ติดตั้งเพิ่มเติม

yarn add @vue/composition-api

และแก้ใน src/main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import VueCompositionAPI from '@vue/composition-api'
Vue.config.productionTip = false
Vue.use(VueCompositionAPI)
new Vue({
router,
render: h => h(App)
}).$mount('#app')

Vue3

สามารถใช้งานได้เลย

การสร้าง State

เราสามารถสร้าง file .js ขึ้นมาใข้งานได้เลยโดยเก็บไว้ที่ไหนก็ได้ ตัวอย่างผมเก็บไว้ใน

src/state

รายละเอียดที่แตกต่างกันเล็กน้อยระหว่าง Vue2 และ Vue3 คือ ใน state ของ Vue2 ต้องประกาศการใช้ vue composition อีกรอบผมยังหาไม่เจอว่ามันต้องทำยังไงถึงจะเขียนได้ปกติ แต่ที่เจอคือถ้ามี file state หลายๆ file เขียนไว้แค่ file เดียวก็ได้ละ ส่วน Vue3 เขียนได้แบบปกติไม่ต้องประกาศเพิ่มนะ เช่น ผมมี

  • SystemNoti.js
  • User.js

Vue2 ผมประกาศข้างล่างนี้ไว้ใน file ใด file หนึ่งก็พอ คือผมแปะไว้ใน SystemNoti.js

SystemNoti.js

ผมใช้ lib Notifierjs ผมหาใน NPM ไม่เจอเลยสั่งติดตั้งจาก github

yarn add https://github.com/jsanahuja/Notifierjs

Vue2

import Vue from 'vue'
import VueCompositionAPI, { reactive } from '@vue/composition-api'
import Notifier from '@jsanahuja/notifierjs'
Vue.use(VueCompositionAPI)class SystemNoti {
type = 'info'
msg = 'Welcome'
notifier = new Notifier({
position: 'top-right',
direction: 'top',
default_time: 3000
})
setdata({ type = 'info', msg }) {
this.type = type
this.msg = msg
this.notifier.notify(this.type, this.msg)
}
noti() {
this.notifier.notify(this.type, this.msg)
}
}
const state = reactive(new SystemNoti())
export default state

Vue3

import { reactive } from 'vue'
import Notifier from '@jsanahuja/notifierjs'
class SystemNoti {
type = 'info'
msg = 'Welcome'
notifier = new Notifier({
position: 'top-right',
direction: 'top',
default_time: 3000
})
setdata({ type = 'info', msg }) {
this.type = type
this.msg = msg
this.notifier.notify(this.type, this.msg)
}
noti() {
this.notifier.notify(this.type, this.msg)
}
}
const state = reactive(new SystemNoti())
export default state

ข้อแตกต่างอีกอย่างคือเวลาเรา import ของจาก vue composition api ถ้า Vue2 เรา import จาก ‘@vue/composition-api’ ส่วน Vue3 เรา import จาก ‘vue’ ได้โดยตรงเลย

ตัวอย่างนี้ผมสร้าง class ง่ายๆ ไว้จัดการ notification แล้วจริงๆ noti ไม่จำเป็นต้องจำ state แบบนี้ก็ได้เนอะฮาๆ แค่ยกตัวอย่างให้ดูละกัน

จุดสำคัญคือ ผมสร้าง object reactive ขึ้นมาจากการ new SystemNoti() แล้ว export ออกไปซึ่งตอนน object ดังกล่าวนี้ก็ทำตัวเป็น state เรียบร้อย ง่ายไหมครับ เวลาผมจัดการเปลี่ยนค่าต่างๆ ใน object นี้ส่วนต่างๆ ที่รับ object นี้ไปทำงานด้วยก็จะเห็นการเปลี่ยนแปลงเหมือนกันหมด

User.js

อีก file หนึ่งสร้างมาเพื่อทำระบ login หลอกๆดังนี้

Vue2

import { reactive } from '@vue/composition-api'
import SystemNoti from './SystemNoti'
const users = [
{
username: 'user1',
password: 'abc',
fullname: 'MrChoke'
},
{
username: 'user2',
password: 'abc',
fullname: 'Guest'
}
]
const auth = reactive({
user: {},
status: false
})

const login = (username, password) => {
const user = users.find(u => u.username === username && u.password === password)
if (user) {
auth.user = user
auth.status = true
SystemNoti.setdata({ type: 'success', msg: `Welcome ${user.fullname}` })
} else {
SystemNoti.setdata({ type: 'error', msg: `User ${username} not found or Password incorrect` })
return false
}
return true
}
const logout = () => {
const olduser = Object.assign({}, auth.user)
SystemNoti.setdata({ type: 'success', msg: `See ya ${olduser.fullname}` })
auth.user = {}
auth.status = false
}
export { login, logout, auth }

Vue3

import { reactive } from 'vue'
import SystemNoti from './SystemNoti'
const users = [
{
username: 'user1',
password: 'abc',
fullname: 'MrChoke'
},
{
username: 'user2',
password: 'abc',
fullname: 'Guest'
}
]
const auth = reactive({
user: {},
status: false
})

const login = (username, password) => {
const user = users.find(u => u.username === username && u.password === password)
if (user) {
auth.user = user
auth.status = true
SystemNoti.setdata({ type: 'success', msg: `Welcome ${user.fullname}` })
} else {
SystemNoti.setdata({ type: 'error', msg: `User ${username} not found or Password incorrect` })
return false
}
return true
}
const logout = () => {
const olduser = Object.assign({}, auth.user)
SystemNoti.setdata({ type: 'success', msg: `See ya ${olduser.fullname}` })
auth.user = {}
auth.status = false
}
export { login, logout, auth }

ซึ่งผมสร้าง object และ function ต่างๆ ไว้ และ export ออกไป จริงๆ จะสร้างเป็น class เหมือนก่อนหน้าก็ง่ายดีนะเวลา export ออกไปจะไม่วุ่นวาย

จุดสำคัญก็เป็น object auth ที่ผมสร้างเป็น reactive ไว้ซึ่งถ้ามีการเปลี่ยนแปลงค่า ก็จะรับรู้ค่าที่เหมือนกันในทุกๆ ที่

การใช้งาน

Login.vue

template

<template>
<div v-if="!auth.status">
<div>
<label>Username:</label>
<input type="text" v-model="username" />
</div>
<div>
<label>Password:</label>
<input type="password" v-model="password" @keyup.enter="preLogin" />
</div>
<div>
<button @click="preLogin" :disabled="!canLogin()">Sign In</button>
</div>
</div>
<div v-else>
<button @click="logout()">Logout</button>
</div>
</template>

script

<script>
import { defineComponent, ref } from '@vue/composition-api'
import router from '@/router'
import SystemNoti from '@/state/SystemNoti'
import { login, logout, auth } from '@/state/User'
export default defineComponent({
name: 'Login',
setup() {
const username = ref('')
const password = ref('')
const canLogin = () => {
return username.value.length > 0 && password.value.length > 0
}
const preLogin = () => {
if (!canLogin()) {
SystemNoti.setdata({ type: 'error', msg: 'Username or Password incorrect' })
} else {
if (login(username.value, password.value)) {
username.value = ''
password.value = ''
router.push({ name: 'Home' })
}
}
}
return {
username,
password,
preLogin,
canLogin,
logout,
auth
}
}
})
</script>

Vue2 และ Vue3 สามารถเขียนได้เหมือนกันเลยแค่ import ไม่เหมือนกันเท่านั้นในตัวอย่าง script ด้านบนจะเป็น Vue2 ถ้า Vue3 ก็เปลี่ยน import เป็น ‘vue’

ซึ่งในตัวอย่างก็รับค่า username และ password มาจาก form ใน template แล้วตรวจสอบเบื้องต้นง่ายๆ ว่ามีค่าไหมถ้าไม่มีก็ให้ Notify ข้อความขึ้นมา ถ้ามีครบก็ให้ส่งไปยัง login ใน src/state/User.js

ซึ่งถ้า login สำหรับตัว object auth ใน User.js ก็จะถูกแทนที่ด้วย Object ของ user และ ค่า status ก็จะเปลี่ยนเป็น true

ถ้าอยากให้ component อื่นรับรู้ด้วยต้องทำอย่างไร ?

ตัวอย่างใน App.vue

App.vue ถือเป็น component หลักการขึ้น status ต่างๆ ทำไว้ที่นี่ก็จะง่ายสุดเช่น แสดงปุ่ม login / logout

script

<script>
import { defineComponent } from '@vue/composition-api'
import { auth, logout } from '@/state/User'
export default defineComponent({
name: 'App',
setup() {
return {
auth,
logout

}
}
})
</script>

template

<router-link :to="{ name: 'Login' }" v-if="!auth.status">Login</router-link><a href="#" @click="logout" v-else>Logout</a>

หรือจะแสดง Welcome พร้อมกับชื่อผู้ใช้

<div v-if="auth.status">Welcome: {{ auth.user.fullname }}</div>

ถ้าให้ Router รู้ละว่าตอนนี้ login อยู่หรือเปล่าต้องทำไง ?

Vue Router 3

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/Login.vue'
import { auth } from '../state/User'
Vue.use(VueRouter)const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/about',
name: 'About',
meta: {
auth: true
},

component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
router.beforeEach((to, from, next) => {
if (auth.status || to.name === 'Login' || !to.meta.auth) {
next()
} else {
next({ name: 'Login' })
}
})

export default router

Vue Router 4

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/Login.vue'
import { auth } from '../state/User'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/about',
name: 'About',
meta: {
auth: true
},


component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
router.beforeEach((to, from, next) => {
if (auth.status || to.name === 'Login' || !to.meta.auth) {
next()
} else {
next({ name: 'Login' })
}
})

export default router

จากตัวอย่างก็ง่ายๆ เหมือนเรา check กับ Vuex import auth state มาแล้วตรวจสอบดูว่า login อยู่ไหมถ้าไม่ให้ไปหน้า login ซึ่งหน้าไหนบ้างที่ต้อง login ก็แปะ meta ไว้จากตัวอย่างผมแปะ

meta: {
auth: true
}

ไม่รู้ว่าอ่านเข้าใจกันไหมครับ จากประสบการณ์อันน้อยนิดของผมเมื่อเทียบกับ Vuex แล้วมันง่ายกว่ากันมากไม่ต้องพรรณาอะไรมากมายแค่ import เข้ามาก็ใช้ได้เลย

Demo

--

--