Vue.js 2.0 กับ Vuex เป็นของคู่กัน ?

Credit: StpDevelo

Vue.js & Vuex เป็นของคู่กันจริง ? เป็นคำถามค้างคาใจหลังจากผมลองเล่นมาหลายวันจากนั้นก็คิดได้ว่าใช้เถอะเพราะเค้าคิดมาแล้ว…

เริ่มด้วยการติดตั้ง vue-cli กันก่อน (ถ้าเน็ตช้าไปกินกาแฟรอได้เลย)

npm install -g vue-cli

พร้อมแล้วกับการสร้าง Project

vue init webpack play-vuex
cd play-vuex

สำหรับคำสั่งด้านบนจะอยู่ในรูปแบบ pattern นี้

vue init <template-name> <project-name>

สำหรับ template อื่นๆสามารถหาข้อมูลได้จาก vue-cli ส่วนในบทความนี้ผมเลือกใช้ webpack จากนั้นมาติดตั้ง Vuex พระเอกของเรา

npm install vuex --save
npm install

หลังจากติดตั้ง vuex และ dependencies ครบเรียบร้อยแล้วลองรัน

npm run dev

ถ๊าด๊าาาา..ได้หน้าเว็บมาเรียบร้อย หรือ เข้าผ่าน browser ที่ http://localhost:8080

ก่อนที่จะไปเล่น vuex กันขอแจ้งเวอร์ชั่นที่ใช้ในการทดสอบครั้งนี้ ถ้าเวอร์ชั่นมากกว่านี้อาจจะใช้งานไม่ได้เช่น vuex 1.x.x กับ 2.x.x เขียนต่างกันและใช้ด้วยกันไม่ได้สามารถอ่านคู่มือเพิ่มเติมได้ที่ Vuex

"vue": "2.2.2",
"vuex": "2.2.1"

Vuex

สำหรับ vuex คือ state management pattern เป็นตัวจัดการ state ในกรณีที่ Application เรามีขนาดใหญ่มีการใช้ตัวแปรร่วมกันหลาย component ซึ่งถ้าใช้การส่ง props ไปที่ component แบบปกติคงจะวุ่นวายและแก้ไขในภายหลังยุ่งยากจึงเป็นต้นกำเนิดของ vuex


https://vuex.vuejs.org/en/images/flow.png

จากรูปด้านซ้ายโดยปกติแล้วการทำงานใน vue component แบบทั่วไปจะมีการทำงานตามนี้

State เก็บข้อมูลที่มาจาก Server หรือ ตัวแปรที่เราตั้งต่างๆ
View แสดงผลโดยนำข้อมูลจาก State มา Render Html
Actions เป็นตัวตอบสนองกับผู้ใช้งาน เช่น กรอกข้อมูล แล้วเช็คค่า หรือ save

ข้อมูลจะทำงานในทิศทางเดียววนไปเรื่อยๆ แต่!!เมื่อไหร่ที่ Component และ Action หลายๆ Component มีการใช้งาน State ร่วมกันจะเกิดปัญหาส่ง props กันวุ่นวายตามที่กล่าวไป แต่เพื่อแก้ปัญหานั้นเรามาดูรูปต่อไปกันดีกว่า

https://vuex.vuejs.org/en/images/vuex.png

เอ้า งงเด้ งงเด้ !! ผมเองก็มึนไปหลายวันดูดีๆหน้าตามันเหมือนกับรูปบนเลยส่วนที่เพิ่มมาคือ Mutations ส่วน API และ Devtools เอาไว้ทีหลังครับ

ปกติ Actions จะเป็นตัวเปลี่ยน State โดยตรงแต่ในตอนนี้หน้าที่นั้นจะยกไปเป็นของ Mutations แทน

Actions จะเป็นตัวทำงาน ประมวลผล, Ajax, คำนวนต่างๆ ที่เป็น Async และ Sync เช่น เรียก API จาก Server ต้องรอข้อมูล หรือ มีการ loop, filter ต่างๆควรทำใน actions ทั้งหมด และ เรียกใช้งานด้วยคำสั่ง dispatch(actions-name)

Mutations จะเป็นตัวเปลี่ยนแปลงข้อมูลใน State ซึ่งข้อห้ามคือห้ามเขียนอะไรที่เป็น Async ในนี้เด็ดขาด(เค้าเตือนมา) ทำอะไรต้องจบในนี้เท่านั้นห้ามมีการรอคอย..เรียกใช้งานด้วยคำสั่ง commit(mutation-name)

State ตัวเก็บข้อมูล..

Getters เอาไว้ get ค่า state ออกมาสามารถแปลงค่าเช่น เพิ่ม $ ในจำนวนเงิน

เอาล่ะเกริ่นกันจนเบื่อแล้ว code ให้เมาไปเลยแล้วกัน และ Example ยอดฮิตคือ โปรแกรมบวกเลข o_C !! ผมมี Application Structure ใน Folder src/ ดังนี้

Folder src/

ให้สร้างไฟล์ตามก่อนก็ได้ครับถ้ายังไม่เห็นภาพการทำงาน

src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import Counts from './counts'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
modules: {
Counts
},
strict: debug
})

ในไฟล์นี้มีการทำงาน

  1. เรียกใช้งาน Vuex ด้วย Vue.use(Vuex)
  2. export และสร้าง Instace Vuex ขึ้นมา และ ใส่ modules ชื่อว่า Counts ที่ import จากไฟล์ counts.js ถ้าอยากเพิ่ม store สำหรับเก็บ state อื่นๆก็สามารถเอามาใส่ใน Object modules ได้เลย
  3. strict ผมยังไม่ค่อยแน่ใจแต่คิดว่าเป็นตัวเปิด mode debug ใน Devtools ผมเซตเป็น True ในขั้นตอน Develop

src/store/counts.js

const state = {
count: 0
}
const mutations = {
INCREMENT (state, payload) {
state.count += payload
},
DE_INCREMENT (state, payload) {
state.count -= payload
}
}
const actions = {
increment: ({ commit }, amount) => commit('INCREMENT', amount),
deincrement: ({ commit }, amount) => commit('DE_INCREMENT', amount)
}
const getters = {
getCounter: state => state.count
}
export default {
state,
getters,
mutations,
actions
}

ไฟล์นี้เป็นพระเอกของงานเป็นไฟล์ที่เก็บ state และ actions ต่างๆไว้ทั้งหมดจะประกอบไปด้วยตัวแปร state, getters, mutations, actions มีการทำงานดังนี้

mutations มีฟังก์ชั่น INCREMENT, DE_INCREMENT จะดึงค่าจาก state มาบวก และ ลบค่า ตามลำดับ

actions มีฟังก์ชั่น increment, deincrement ทำหน้าที่รับค่าจากไฟล์ src/component/Inc.vue ในตัวแปร amount และ ส่งไปให้ mutations ผ่าน commit() เพื่อเปลี่ยน state (ลืมกันหรือยังว่า actions เค้าไม่ให้เปลี่ยน state นะจ๊ะ) การตั้งชื่อฟังก์ชั่นผมเลยใช้ ตัวใหญ่ใน mutations และ ตัวเล็กใน actions จะได้ไม่งงกับชื่อ

src/App.vue

<template>
<div id="app">
<img src="./assets/logo.png">
<Display></Display>
<Inc></Inc>
</div>
</template>
<script>
import Display from './components/Display'
import Inc from './components/Inc'
export default {
components: {
Display,
Inc
}
}
</script>
<style></style>

ในไฟล์นี้ไม่มีอะไรซับซ้อนเป็นการเรียก component มารวมกัน

src/components/Display.vue

<template>
<div>
<h1>Count is: {{ getCounter }}</h1>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters([
'getCounter'
])
}
}
</script>
<style></style>

ในไฟล์นี้ผมทำการเรียกใช้ mapGetters จาก vuex ซึ่งเป็น Helper Function วิธีการเขียน mapGetters จะเขียนเป็น computed ได้ดังนี้

getCounter() {
return this.$store.getters.getCounter
}

แต่แทนที่เราจะมาพิมยาวๆเราก็เอาชื่อฟังก์ชั่น getCounter ที่อยู่ในไฟล์ src/store/count.js มาใส่ mapGetters แทนแล้วเรียกใช้เหมือนตัวแปรได้เลยง่ายไปอีก..ทั้งหมดในไฟล์นี้คือการ get ค่าจาก state มาแสดงเท่านั้นครับ

src/components/Inc.vue

<template>
<div class="">
<button type="button" @click="increment(5)">Inc The Count</button>
<br>
<br>
<button type="button" @click="deincrement(10)">DeInc The Count</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
methods: {
...mapActions([
'increment',
'deincrement'
])
}
}
</script>
<style></style>

ในไฟล์นี้ผมเรียก mapActions จาก vuex การทำงานก็คล้ายกับ mapGetters แทนที่เราจะเขียน computed ยาวๆเราก็ใช้ Helper Function ที่เค้าทำมาซะเลยซึ่งทั้ง increment, deincrement ก็เป็นฟังก์ชั่นที่อยู่ใน src/store/count.js

จากนั้นใน button เมื่อกดปุ่มก็ทำการเรียกฟังก์ชั่นและส่งค่าเข้าไปแบบตัวเลข

src/main.js

import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store/'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
})

ไฟล์สุดท้ายเป็นการรวม Store เข้าไปที่ Vue ที่เป็น Instance ใหญ่ที่สุด ผมทำการ import store จากไฟล์ index.js ใน ./store/index แต่ด้วยความฉลาดของ webpack ใส่แค่ ./store พอครับ ***ห้ามใช้ S ตัวใหญ่ในตัวแปร Store เด็ดขาดเพราะมันซ้ำกับตัวแปรที่อยู่ใน vuex ผมงงหลายชั่วโมงกับเจ้า S ตัวเดียว

ในการใช้งานจริงๆผมขอยกตัวอย่าง component ที่มีการ Shared State เช่น

Component NavBar
Component Profile

นึกถึง Facebook ในหน้า profile และ Navbar จะมีรูปเราแสดงตรง Navbar และ ตรง Cover ทั้งสอง Component นี้ใช้ข้อมูลชุดเดียวกันในการ Render คำถามคือเราจะเลือก Ajax Request ไปหา Server 2 ครั้งเพราะทั้งสอง Component ใช้เหมือนกันหรือเลือกที่จะ Ajax ไปครั้งเดียวแล้วเก็บไว้ใน Vuex เป็นคำถามที่ผู้อ่านต้องตอบเองแล้วครับลองคิดดูว่าถ้ามี 10 Component ใช้ State ตัวเดียวกันดูสิครับ..

ป.ล.

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

สำหรับ Code ทั้งหมดตามลิงค์ด้านล่างเลยครับ

Show your support

Clapping shows how much you appreciated Sathaporn Thissarak’s story.