VueJS: Ví dụ tổng quan về Vuex-CLI3

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Vuex là gì?

Vuex mục đích chính là dùng để quản lý state data.

state ở đây bao gồm những thành phần như variable/property, array, object, .. Nếu ứng dụng của ta đơn giản thì ta không cần sử dụng Vuex vì ta có thể định nghĩa tất cả trong component. Ta chỉ sử dụng Vuex khi ứng dụng của ta có cỡ vừa hoặc lớn và phức tạp với nhiều component và property. Vuex sẽ xử lý bằng cách chỉ định một vị trí trung tâm nơi state data được thao tác (xem, lưu trữ, sửa đổi và truy cập).

Để dễ tiếp cận kiến thức hơn các bạn hãy theo dõi quy trình làm một project dưới đây.

Video bài viết:

Bắt đầu Project

Ta sẽ sử dụng Vue CLI 3 để tạo project.

Mở terminal và thực hiện lệnh sau:

vue create vuexstate

Bạn sẽ nhìn thấy dòng nhắc như sau:

Vue CLI v3.0.5
? Please pick a preset:
  default (babel, eslint)
> Manually select features

Ta chọn Manually select features và sau đó chọn Router và Vuex như sau:

Vue CLI v3.0.5
? Please pick a preset: Manually select features
? Check the features needed for your project:
 ( ) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
 (*) Router
>(*) Vuex
 ( ) CSS Pre-processors
 ( ) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing

Sau đó nhấn enter, sẽ hiện ra câu hỏi:

Vue CLI v3.0.5
? Please pick a preset: Manually select features
? Check the features needed for your project: Router, Vuex
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) y

Bạn nhấn phím 'y' để chọn và enter. Sau đó sẽ hiện ra câu hỏi:

Vue CLI v3.0.5
? Please pick a preset: Manually select features
? Check the features needed for your project: Router, Vuex
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.?
> In dedicated config files
  In package.json

Bạn chọn In dedicated config files rồi nhấn enter sẽ hiện ra câu hỏi:

Vue CLI v3.0.5
? Please pick a preset: Manually select features
? Check the features needed for your project: Router, Vuex
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) n

Bạn chọn 'n' nếu bạn không muốn lưu những preset này, sau đó nhấn enter => quá trình cài đặt sẽ bắt đầu.

Sau khi cài đặt xong bạn gõ lệnh sau:

> cd vuestate

Và gõ lệnh sau để chạy:

> npm run serve

Sau đó bạn mở link http://localhost:8080 trong trình duyệt để chạy thử.

Định nghĩa một property cho state

Từ code editor (Sublime Text chẳng hạn) bạn mở project ra và tìm đến file /src/store.js. Đây chính là file Vuex và là nơi ta sẽ định nghĩa state, mutations, actions, getters và một số thành phần khác.

Ta đưa vào state một property có tên title như sau:

  state: {
    title: 'V1Study'
  },

Bạn lưu lại và mở file /src/components/HelloWorld.vue rồi điều chỉnh nội dung như sau:

<template>
  <div class="hello">
    <div class="left">
      <h1>{{ title }}</h1>
    </div>
    <div class="right">

    </div>
  </div>
</template>

Tại phần script bạn điều chỉnh lại như sau:

<script>
import { mapState } from 'vuex'

export default {
  name: 'HelloWorld',
  computed: mapState([
    'title'
  ])
}
</script>

Ở đoạn code trên, trước tiên ta import mapState, helper này cho phép ta truy cập vào state, tức là ta sẽ truy cập được vào property title mà ta đã tạo ở trên.

Sau đó ta cũng tạo computed và gọi đến mapState và truyền một mảng có tên là title.

Điều này sẽ truy xuất giá trị của title và cũng cho phép ta tham chiếu đến nó qua tên của nó thông qua nôi suy trong phần <template>. Bạn lưu lại file và ra ngoài trình duyệt, kết quả bạn sẽ thấy như sau:

Bạn cũng có thể truyền một đối tượng thay vì mảng với mapState như sau:

// Template Adjustment:
  <h1>{{ custom }}</h1>

// Logic:
  computed: mapState({
    custom: 'title'
  })

Bản chất ở đây là ta đang định nghĩa một bí danh khác cho title là custom.

Trường hợp bạn muốn sử dụng thực hiện nhiều điều hơn trong computed, khi đó mapState() sẽ cần phải được sử dụng theo hình thức gọi là Object Spread Operator (dịch nôm na là: Toán tử Lan truyền Đối tượng). Cụ thể như sau:

export default {
  name: 'HelloWorld',
  computed: {
    ...mapState([
      'title'
    ]),
    // Other properties
  }
}

Bây giờ ta sẽ thêm một property nữa vào state của store là links như sau:

  state: {
    title: 'My Custom Title',
    links: [
      'http://v1study.com',
      'https://www.facebook.com/v1study',
      'https://www.youtube.com/c/V1studyAll'
    ]
  },

Bây giờ ta sẽ truy cập vào property links thông qua mapState và hiển thị ra template như sau:

    <div class="left">
      <h1>{{ title }}</h1>

      <ul>
        <li v-for="(link, index) in links" v-bind:key="index">
          {{ link }}
        </li>
      </ul>
    </div>

Và trong phần script ta thêm như sau:

    ...mapState([
      'title',
      'links'
    ]),

Kết quả ta được:

Vuex getters

Như vậy là ta đã sử dụng helper mapState để truy cập trực tiếp dữ liệu chứa trong state. Điều này sẽ rất phù hợp nếu như ta không có sự tính toán trên dữ liệu, nhưng nếu có và đặc biệt là khi ta làm việc với nhiều component thì ta nên nghĩ tới getters. Ví dụ như ta muốn đếm số link có trong property links chẳng hạn, khi đó ta làm như sau:

Sửa /src/store.js thành:

  state: {
    // Code removed for brevity
  },
  getters: {
    countLinks: state => {
      return state.links.length
    }
  },
  mutations: {},
  actions: {}

Ở đoạn code trên ta tạo một getter tên countLinks, getter này có nhiệm vụ trả về độ dài của links (đếm số link trong mảng links) trong state.

Quay lại file /src/components/HelloWorld.vue, ta có thể dễ dàng truy cập vào getter countLinks từ bên trong component này, tuy nhiên ở đây ta sẽ tạo một component khác và từ component này ta truy cập getter. Mục đích chính của Vuex là cho phép nhiều component cùng sử dụng chung state, đây là lý do tôi muốn tạo thêm component.

Bây giờ ta sẽ điều chỉnh file HelloWorld.vue lại bằng việc thêm những dòng màu đỏ như sau:

<template>
  <div class="hello">
    <div class="left">
      <!-- tạm ẩn để cho dễ nhìn -->
    </div>
    <div class="right">
      <stats />
    </div>
  </div>
</template>

<script>
import Stats from '@/components/Stats.vue'
import { mapState } from 'vuex'

export default {
  name: 'HelloWorld',
  components: {
    Stats
  },
  computed: {
    // tạm ẩn để cho dễ nhìn
  }
}
</script>

Tiếp theo, ta tạo file /src/component/Stats.vue và copy toàn bộ nội dung của file HelloWorld.vue và paste vào Stats.vue, sau đó ta điều chỉnh lại nội dung như sau:

<template>
  <div class="stats">
    <h1>A different component</h1>
    <p>There are currently {{ countLinks }} links</p>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'Stats',
  computed: {
    ...mapGetters([
      'countLinks'
    ]),
  }
}
</script>

Ở đoạn code trên bạn thấy, thay vì ta import mapState, ta lại import mapGetters để ta có thể truy cập được vào các getters.

Ở đây ta gọi mapGetters() và truyền đi tên của getter ta muốn truy cập và hiển thị nó thông qua nội suy (trong cặp {{}}) ở template. Kết quả:

Chỉnh sửa giao diện

Giao diện hiển thị như trên nhìn sẽ không đẹp mắt. Ta sẽ cần chỉnh sửa giao diện một chút cho đẹp hơn.

Tại phần <style> của file /src/components/HelloWorld.vue ta chỉnh sửa CSS như sau:

<style>
  html, #app, .home {
    height: 100%;
  }
  body {
    background-color: #F4F4F4;
    margin: 0;
    height: 100%;
  }

  .hello {
    display: grid;
    grid-template-columns: repeat(2, 50%);
    grid-template-rows: 100%;
    grid-template-areas: "left right";
    height: 100%;
  }

  .left, .right {
    padding: 30px;
  }

  ul {
    list-style-type: none;
    padding: 0;
  }
  ul li {
    padding: 20px;
    background: white;
    margin-bottom: 8px;
  }

  .right {
    grid-area: right;
    background-color: #E9E9E9;
  }

</style>

Sửa phần file /src/App.vue thành như sau:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<style>
  #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
  }
</style>

Sửa phần <template> của file /src/views/Home.vue thành như sau:

<template>
  <div class="home">
    <HelloWorld />
  </div>
</template>

Lưu lại và ta sẽ thấy kết quả như thế này:

Vuex mutations

Mutations đóng vai trò quan trọng khi ta muốn thao tác thay đổi dữ liệu trong state data.

Bạn mở /src/store.js và thêm mutations như sau:

  mutations: {
    ADD_LINK: (state, link) => {
      state.links.push(link)
    }
  },

Ở đây tên của mutation là ADD_LINK, các bạn lưu ý là tên của mutation thường được đặt với tất cả các ký tự đều in hoa, mỗi từ cách nhau bằng dấu gạch dưới. mutation sẽ truyền data ở đây là link cho state, link là giá trị mà người dùng sẽ điền vào. Hàm push() ở đây là hàm của JavaScript có nhiệm vụ thêm phần tử có giá trị link vào cuối mảng links.

Trong file /src/HelloWorld.vue ta sẽ sửa như sau ở phần <template>:

<h1>...</h1>

<!-- Thêm form để người dùng nhập link -->

<form @submit.prevent="addLink">
   <input class="link-input" type="text" placeholder="Add a Link" v-model="newLink" />
</form>

<ul>...</ul>

Sau khi điền xong link người dùng nhấn enter thì sự kiện submit sẽ được kích hoạt và phương thức addLink sẽ được thực thi, dữ liệu điền vào sẽ được lưu vào data newLink.

Bây giờ ta sẽ sửa phần <script> của component này như sau:

<script>
import Stats from '@/components/Stats.vue'
import { mapState, mapMutations } from 'vuex'

export default {
  name: 'HelloWorld',
  data() {
    return {
      newLink: ''
    }
  },
  components:{
    Stats
  },
  computed: mapState([
    'title',
    'links'
  ]),
  methods: {
    ...mapMutations([
      'ADD_LINK'
    ]),
    addLink: function() {
      this.ADD_LINK(this.newLink)
      this.newLink = ''
    }
  }
}
</script>

Ở đây, thành phần ta cần tập trung là methods: { }. Ta sử dụng helper mapMutations để import mutation ADD_LINK, còn trong phương thức addLink() ta gọi mutation này để truyền dữ liệu this.newLink.

Bây giờ ta thêm một chút CSS cho input để có giao diện đẹp hơn một chút.

  input {
    border: none;
    padding: 20px;
    width: calc(100% - 40px);
    box-shadow: 0 5px 5px lightgrey;
    margin-bottom: 50px;
    outline: none;
  }

Kết quả:

Tiếp theo ta sẽ thêm mutation, ta sẽ sử dụng actions thực thi các mutations.

Vuex actions

Lời gọi các mutations trong component là dành cho các sự kiện đồng bộ, còn đối với các hàm/phương thức không đồng bộ thì ta sử dụng các actions.

Ở đây ta sẽ áp dụng action để gọi mutation xóa link.

Trong file /src/store.js ta thêm mutation và action có màu đỏ như sau:

  mutations: {
    ADD_LINK: (state, link) => {
      state.links.push(link)
    },
    REMOVE_LINK: (state, link) => {
      state.links.splice(link, 1)
    }
  },
  actions: {
    removeLink: (context, link) => {
      context.commit("REMOVE_LINK", link)
    }
  }

Không có gì đặc biệt xảy ra ở REMOVE_LINK nhưng ở actions thì ta tạo một action tên removeLink trong đó ngữ cảnh (ngữ cảnh đơn giản là cung cấp cho ta những phương thức và thuộc tính tương tự trên đối tượng store), và tải trọng link được truyền.

Bây giờ ta gọi context.commit, nó sẽ gọi mutation REMOVE_LINK và truyền link đi theo lời gọi. Điều này có vẻ không cần thiết, nhưng thực ra nó cần thiết cho các hoạt động không đồng bộ.

Tiếp theo ta quay lại HelloWorld.vue và thêm đoạn sau vào <template>

<ul>
  <li v-for="(link, index) in links" v-bind:key="index">
    {{ link }}
    <button @click="removeLinks(index)" class="rm">Remove</button>
  </li>
</ul>

Thêm mapActions:

import { mapState, mapMutations, mapActions } from 'vuex'

Sửa lại phần methods:

  methods: {
    ...mapMutations([
      'ADD_LINK'
    ]),
    ...mapActions([
      'removeLink'
    ]),
    addLink() {
      this.ADD_LINK(this.newLink)
      this.newLink = '';
    },
    removeLinks(link) {
      this.removeLink(link)
    }
  }
}

Như bạn thấy action được cài đặt để làm việc theo cách tương tự như lời gọi đến các mutations.

Bây giờ ta sẽ thêm CSS cho nút Remove:

.rm {
    float: right;
    text-transform: uppercase;
    font-size: .8em;
    background: #8ac007;
    border: none;
    padding: 5px;
    color: #fff;
    cursor:pointer;
    border-radius: 5px;
  }

Kết quả:

Tuy vậy đây không phải điều thú vị, điều ta cần thấy ở đây là khả năng actions giải quyết các hoạt động bất đồng bộ. Dưới đây là sẽ thực nghiệm điều này.

Tại file /src/store.js, ta tạo một mutation mới và một action mới có nhiệm vụ xóa tất cả các link khi nhấn vào nút Remove all links:

  mutations: {
    // Others removed for brevity,
    REMOVE_ALL: (state) => {
      state.links = []
    }
  },
  actions: {
    removeLink: (context, link) => {
      context.commit("REMOVE_LINK", link)
    },
    removeAll ({commit}) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          commit('REMOVE_ALL')
          resolve()
        }, 1500)
      })
    }
  }

Như vậy ta đã có mutation REMOVE_ALL để empty mảng, ta cũng đã có action removeAll, và ở đây ta sử dụng đối số argument destructuring để truyền trong commit để làm cho code trở lên đơn giản hơn.

Ta tạo một Promise và mô phỏng hoạt động gọi REMOVE_ALL sau 1.5 giây.

Ta sẽ đặt nút Remove all links trong file /src/components/Stats.vue:

<template>
  <div class="stats">
    <h1>A different component</h1>
    <p>There are currently {{ countLinks }} links</p>

    <button @click="removeAllLinks">Remove all links</button>
    <p>{{ msg }}</p>
  </div>
</template>

Trong phần <script>:

<script>
import { mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  name: 'Stats',
  data() {
    return {
      msg: ''
    }
  },
  computed: {
    ...mapGetters([
      'countLinks'
    ]),
  },
  methods: {
    ...mapMutations(['REMOVE_ALL']),
    ...mapActions(['removeAll']),
    removeAllLinks(){
        if(confirm('Are you sure?'))
          this.removeAll().then(()=>{
            this.msg='They have been removed';
          })
      }
  }
}
</script>

Phần quan trọng ở đây là this.removeAll().then() => msg sẽ được hiển thị khi action được thực thi.

Bây giờ ta cũng viết một đoạn CSS cho nút <button> trong component như sau:

button {
    padding: 10px;
    margin-top: 30px;
    width: 100%;
    background: none;
    border: 1px solid lightgray;
    outline: 0;
    cursor: pointer;
}

Try it out in the browser. Click Remove all links and in 1.5 seconds, the links will be removed and our message displayed!

Bạn cũng có thể tích hợp async await vào trong action và gọi ở các actions khác, ta sẽ tìm hiểu về vấn đề này ở bài viết sau.

Hy vọng bài viết này có thể giúp ích ít nhiều cho bạn về quản lý ứng dụng Vue với Vuex. Hẹn gặp lại.

» Tiếp: Plugin vue-meta và cách sử dụng
« Trước: Ví dụ cơ bản với CLI3, Vuex và Axios
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!