Cùng nhau học VueX (Phần 1)

Xin chào các bạn, lại đến dịp được update profile cho Viblo rồi =)). Cũng hơi cạn kiệt đề tài nên sẽ dùng một cái gì đó không mới lắm, nhưng mình cũng không thấy nhiều hướng dẫn khi tìm trên google lắm. Seri này chúng ta sẽ dùng VueX để xây dựng một công việc quá quen thuộc (thậm chí là nhàm chán) với các lập trình viên là CRUD. Tuy nhiên bài này mục đích của mình không phải là CRUD, mà là sử dụng VueX. Và trong bài này mình chỉ giới hạn ở mức giới thiệu VueX, bài sau chúng ta sẽ build ứng dụng dựa trên nền tảng này.

Giới thiệu

Hiện giờ có 3 FramworkJS đang rất thịnh hành là AngularJS, React và VueJS. Nhìn tổng thể thì Angular được xây dựng rất hoàn chỉnh và mạnh mẽ, tuy nhiên bạn sẽ cần phải thời gian rất lâu để nắm bắt được công nghệ này, và đôi khi ứng dụng của bạn sẽ không tận dụng hết sức mạnh của nó, khiến cho việc dùng đến Angular là một sự lãng phí. React là một cái tên hot trong thời gian rất dài và thực sự rất tuyệt vời. Cả 2 nền tảng trên đều có ông lớn đứng sau là Google và Facebook, và khi sử dụng bạn có thể sẽ bị ràng buộc bởi điều này. Chính vì thế bạn thử lựa chọn VueJS cho hướng đi mới trong năm nay xem thế nào. Và tất nhiên lựa chọn VueJS thì cũng không nên bỏ qua VueX, bạn sẽ cần cân nhắc nên sử dụng VueX trong ứng dụng của bạn hay không? Với một ứng dụng lớn thì việc sử dụng nó gần như là cần thiết để dễ dàng quản lý dữ liệu của ứng dụng.

Nội dung

Thế VueX là cái gì.

Đại khái nếu ai đã làm với React thì có lẽ cũng không lạ lẫm gì đến Redux, VueJS là một FramworkJS mới và đang ngày càng được đánh giá rất cao, chính vì thế việc VueJS cũng cần một phương pháp để quản lý dữ liệu như Redux của React, và đó chính là VueX. VueX là thư viện giúp quản lý trạng thái các component trong VueJS, đây là nơi lưu trữ tập trung dữ liệu cho tất cả các component trong một ứng dụng, với nguyên tắc trạng thái chỉ có thể được thay đổi theo kiểu có thể dự đoán.

Tại sao cần phải dùng VueX

Với những ứng dụng nhỏ, nguyên lý của Vue rất đơn giản như sau, các bạn hãy nhìn hình vẽ sau:

Trên là mô hình Luồng dữ liệu một chiều thường được sử dụng trong các ứng dụng đơn thuần trước đây. Trong một ứng dụng khép kín có các thành phần như sau: State – Trạng thái, là nơi khởi nguồn để thực hiện ứng dụng, View sẽ đọc dữ liệu từ đây để render ra cho người dùng. View – Là nơi người dùng nhìn thấy, tương tác được, là các khai báo ánh xạ với trạng thái, nơi bắt các event của người dùng để tạo ra Action. Action – Hành động, là những cách thức làm trạng thái thay đổi phản ứng lại các tương tác của người dùng từ View.

Đó là với ứng dụng nhỏ và khép kín. Khi ứng dụng ngày càng lớn lên, khi đó sẽ có các component con, và các component này lại phụ thuộc vào component cha. Như thế vòng tròng khép kín kia bị phá vỡ, phải chia sẻ State cho component khác, phải nhận Action từ component khác. Và còn trường hợp View phải nhận State từ component cha của component cha của component cha... Ứng dụng như thế rất khó quản lý State và khó khăn cho việc tiếp tục mở rộng. Chính vì thế VueX cần được sử dụng để quản lý State, Action của toàn bộ ứng dụng, nơi bạn làm bất kì biến đổi State nào đều được ghi lại và có thể undo một cách dễ dàng. Các bạn có thể nhìn hình vẽ dưới đây để hiểu hơn về VueX

Cấu trúc của VueX

State – trạng thái

Với tư tưởng dữ liệu tập trung, thì VueX sẽ sử dụng một cây trạng thái duy nhất, và nó sẽ chứa tất cả trạng thái của ứng dụng. Để sử dụng trạng thái trong Vue component, chúng ta sẽ lấy chúng qua thuộc tính computed của component

const Counter = {
    template: '<div> {{ count }} </div>',
    computed: {
        count() {
            return store.state.count
        }
    }
}

Mỗi khi store.state.count thay đổi, thì thuộc tính computed sẽ được tính toán lại và một trigger được tạo ra cho các DOM liên quan. Trong ứng dụng thiết kế dạng module, cần import store ở những nơi component sử dụng trạng thái. Vuex cung cấp cơ chế giúp sử dụng store ở tất cả các component con từ component gốc với tùy chọn store. Các component con có thể truye xuất thông qua this.$store

const Counter = {
    template: `<div>{{ count }}</div>`,
    computed: {
        count () {
            // Lấy state từ store
            return this.$store.state.count
        }
    }
}

VueX cung cấp cho chúng ta mapState Helper để làm ngắn gọn hơn trong trường hợp chúng ta cần sử dụng nhiều dữ liệu trong Store tổng.

import {mapState} from 'vuex`

export default {
    // ...
    computed: mapState([
          // map this.count to store.state.count
        'count'
    ])
}

Getters - Lọc trạng thái

Đây là sự khác biệt nhỏ với Redux, trước khi trả về State ta có điều chỉnh dữ liệu trả về theo ý. Tuy nhiên tính năng này thực sự không cần thiết, nhưng đuợc bổ sung vào thì ta có thể tận dụng tùy theo mục đích, hoặc có thể cũng không cần quan tâm đến điều này.

// Khai báo Store
const store = new Vuex.Store({
    state: {
        todos: [
           { id: 1, text: '...', done: true },
           { id: 2, text: '...', done: false }
        ]
     },
     getters: {
         doneTodos: state => {
             return state.todos.filter(todo => todo.done)
         }
     }
})

// Sử dụng tại Component
const Counter = {
    // ...
    computed: {
        doneTodos () {
            return this.$store.getters.doneTodos
        }
    }
}

Mutations - thay đổi trạng thái

Store là nơi component chỉ có thể đọc dữ liệu và không thể thay đổi trạng thái một cách trực tiếp. Để thay đổi trạng thái thì Mutations sẽ đảm nhiệm chức năng này, đây là nơi duy nhất có thể thay đổi trạng thái. Mutations sẽ thực hiện thay đổi thông qua commit. Tại component cũng có thể thực hiện được commit đến mutations, tuy nhiên điều này không khuyến khích để đảm bảo Flow chuẩn mình sẽ nói ở dưới.

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // mutate state
      state.count++
    }
  }
})

Để gọi đến mutation, ta gọi commit như sau

store.commit('increment')

Tuy nhiên commit này sẽ được gọi tại Action

Action - Hành động

Actions gần giống như mutations, tuy nhiên nó khác nhau ở vài điểm:

  • Nó không được phép thay đổi trạng thái, mà cần commit một mutation để thực hiện điều này.
  • Nó có thể chứa các hoạt động bất đồng bộ, như lời gọi api để fetch dữ liệu, khi trả về thì commit dữ liệu tới mutations

Các bạn chút ý là về mặt coding các hoạt động bất đồng bộ có thể được sử dụng ở cả mutationsactions, tuy nhiên để đảm bảo flow thì bạn cần thực hiện các hoạt động này tại actions, mutations chỉ được chứa các hoạt động đồng bộ. Để gọi actions ta dùng dispatch khác với gọi đến mutations là dùng commit

/// Khai báo Store
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

// Sử dụng action tại component
const Counter = {
    template: `<div @click="handleIncrement">{{ count }}</div>`,
    computed: {
        count () {
            return this.$store.state.count
        }
    },
    methods: {
        handleIncrement() {
            this.$store.dispatch('increment') // chú ý đây là lời gọi đến action
        }
    }
}

Module

Đây là cách tổ chức trạng thái thành module để chia nhỏ trạng thái để tiện cho quá trình phát triển và mở rộng. Tại mỗi module ta có thể khai báo đầy đủ các thành phần của Store ở trên là state, getters, mutations và actions. Phần này mình chỉ cho các bạn xem qua cấu trúc module, phần sau khi viết ứng dụng mình sẽ áp dụng điều này để các bạn dễ hình hình dung hơn.

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

Trên là ví dụ nhỏ về cấu trúc module của Store.

Data flow

Giả sử bạn làm một component đếm số lần người dùng click vào button.

/// Khai báo Store
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

// Sử dụng action tại component
const Counter = {
    template: `<buttuon @click="handleIncrement">{{ count }}</button>`,
    computed: {
        count () {
            return this.$store.state.count
        }
    },
    methods: {
        handleIncrement() {
            this.$store.dispatch('increment') // chú ý đây là lời gọi đến action
        }
    }
}

Ở trên ta có một Store lưu trữ count (số lượt người dùng click), tại View, mỗi khi người dùng click vào thì component sẽ gọi đến method handleIncrement, tại đây ta sẽ sử lý dispatch một action đến store, và action này là increment. Trong Store khi nhận được action này sẽ commit đến mutations increment và tại đây thực hiện thay đổi state. Khi State thay đổi, tại component ta sử dụng computed nên nó sẽ trigger sự thay đổi này và lấy giá trị này trong State và thực hiện render lại dữ liệu ra View.

Kết luận

Bài viết này mình đã giới thiệu về VueX, tuy nhiên có thể bạn đọc nó vẫn còn khá mơ hồ, bài viết tiếp theo mình sẽ viết ứng dụng và sẽ giải thích chi tiết bước làm, hy vọng sẽ giúp bạn có thể hiểu sâu hơn về VueX. Số lượng người sử dụng ViewJS đang ngày càng nhiều nên việc nắm bắt các công nghệ liên quan đến nó như VueX là một điều nên làm. Hy vọng bài viết này có thể cho bạn hứng thú làm việc với VueJS + VueX.