+27

[Vue bus] EventBus-Global Event trong VueJS

Chào các bạn, lại là mình đây. Ở tut này chúng ta sẽ cùng tìm hiểu EventBus trong VueJS. Nó là cái gì, cách khởi tạo và sử dụng nó nhé 😉 😉

Story

Chắc hẳn các bạn nếu đọc docs của Vue hoặc xem qua tut của mình thì đã biết về cách giao tiếp giữa 2 component dượng-con theo kiểu: truyền dữ liệu từ dượng vào con bằng props, con muốn thay đổi dữ liệu thì $emit 1 event lên để bảo dượng là dượng ơi dượng thay đổi dữ liệu đi 😄.

Trong quá trình phát triển đôi khi chúng ta sẽ gặp phải trường hợp ta muốn thay đổi giá trị của 1 biến nào đó ở cha gốc từ 1 component con ở mức rất sâu. Ví dụ: A là cha B, B là cha C, C là cha D. Và D muốn thay đổi 1 giá trị của biến ở A, khi đó việc $emit liên tục event từ D->C->B->A sẽ làm cho code bị dài dòng và lặp nhiều, khi thay đổi sẽ cần thay đổi ở nhiều nơi. Thật tuyệt vời vì Vue support EventBus giúp ta có thể emit event từ D lên thẳng A hoặc bất kì 1 component nào khác trong toàn ứng dụng Vue.

Cũng đã có nhiều tut hướng dẫn về cách tạo và sử dụng EventBus nhưng ở bài này mình sẽ hướng dẫn các bạn tạo EventBus theo dạng như kiểu 1 plugin (cũng là cách mình sử dụng ở các dự án thật). Vì sao thì bằng việc tách ra thành module riêng dạng plugin, nó không modify trực tiếp vào app của chúng ta, nên chúng ta có thể cài-cắm nó vào đơn giản, đồng thời bằng cách viết như thế này các bạn có thể đẩy lên registry kiểu npm hoặc github cho ae khác down về và dùng luôn.

Vue 3

Giới thiệu

Trong Vue 2, việc tạo một global event bus khá đơn giản, thường được thực hiện bằng cách sử dụng một instance của Vue. Tuy nhiên, trong Vue 3, cách tiếp cận này đã thay đổi và chúng ta cần sử dụng cách khác để tạo một event bus. Trong bài viết này, chúng ta sẽ học cách tạo một global event bus trong Vue 3 sử dụng script setup và composable functions nhé.

Tạo Event Bus

Trước tiên, chúng ta sẽ tạo một event bus sử dụng mitt, đây là một thư viện rất nhỏ nhẹ và đơn giản cho việc làm event emitter/event bus.

Ta bắt đầu thôi nhé, đây là cấu trúc folder hiện tại, mình đang dùng project Laravel + VueJS, bạn nào không dùng Laravel thì để ý tí nữa tạo file cho đúng vị trí nha:

Screenshot 2024-06-10 at 11.59.58 PM.png

  1. Cài đặt mitt:
npm install mitt
  1. Tạo file eventBus.js ở cùng level folder với app.js để cấu hình event bus:
import mitt from 'mitt';

const emitter = mitt();

export default emitter;

Cực đơn giản phải không các bạn, ta chỉ việc tạo 1 instance của mitt và lát nữa cứ thế dùng 😎

Sử dụng Event Bus trong Components

Sử dụng event bus trong Vue 3 với script setup cũng rất đơn giản. Chúng ta sẽ sử dụng event bus để truyền sự kiện giữa các components.

  1. Tạo component EventEmitter.vue để emit sự kiện:
<template>
  <div class="d-flex">
    <input class="form-control" v-model="inputMessage" placeholder="Enter a message" />
    <button class="btn btn-primary w-25 ms-3" @click="emitEvent">Send Message</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import emitter from '../eventBus';

const inputMessage = ref('');

const emitEvent = () => {
  emitter.emit('custom-event', { message: inputMessage.value });
  inputMessage.value = '';
};
</script>

Ở trên ta emit 1 event tên là custom-event, và payload bên trong nó là { message: inputMessage.value }. Chú ý là event ở đây là global, tức là tại bất kì component nào ta cũng có thể lắng nghe được, và payload ta thích để gì bên trong cũng được

  1. Tạo component EventListener.vue để lắng nghe sự kiện:
<template>
  <div class="mt-3">
    <h3>Notifications:</h3>
    <ul class="list-group">
      <li class="list-group-item" v-for="(msg, index) in messages" :key="index">{{ msg }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import emitter from '../eventBus';

const messages = ref([]);

const handleEvent = (payload) => {
  messages.value.push(payload.message);
};

onMounted(() => {
  emitter.on('custom-event', handleEvent);
});

onUnmounted(() => {
  emitter.off('custom-event', handleEvent);
});
</script>

Ở trên ta sẽ khai báo event listener khi component được mount vào DOM (dùng beforeMount cũng được, tuỳ các bạn nhé). Và ta phải nhớ là khi component unmounted thì ta phải remove event listener đi nha, nếu không là component thì destroy rồi mà vẫn thấy handleEvent chạy đó 😂

Và chú ý rằng ở đây khi on hoặc off thì ta truyền trực tiếp function vào, nó là địa chỉ (reference) đó các bạn, cùng trỏ về handleEvent, chứ nếu các bạn mà làm như sau là không được đâu nha:

onMounted(() => {
  emitter.on('custom-event', (data) => {
    console.log('custom-event', data);
  });
});

onUnmounted(() => {
  emitter.off('custom-event', (data) => {
    console.log('custom-event', data);
  });
});

Lí do là vì ở trên ta khai báo 2 function khác nhau mất rồi, ở đoạn onUnmounted nó sẽ off 1 cái function không phải là cái mà ta đã on (trông thì giống chứ chúng không như nhau đâu 😄)

Kết hợp các component lại với nhau

Chúng ta sẽ kết hợp hai components EventEmitter.vueEventListener.vue trong một component cha để hoàn thành ví dụ.

Tạo component ExampleComponent.vue:

<template>
  <div class="container mt-3">
    <EventForm />
    <EventListener />
  </div>
</template>

<script setup>
import EventForm from './EventForm.vue';
import EventListener from './EventListener.vue';
</script>

Cuối cùng là file app.js:

import './bootstrap';
import { createApp } from 'vue';
import ExampleComponent from './components/ExampleComponent.vue';

const app = createApp(ExampleComponent);

app.mount('#app');

Cuối cùng chạy lên sẽ cho ta kết quả như sau:

ezgif-7-f5f4b53061.gif

Vậy là ta đã hoàn thành việc setup Global Event Bus trong Vue 3 rồi. Với Event Bus ta có thể gửi event từ bất kì nơi nào trong app của ta, mà không phải truyền props + emit nhiều cấp lên xuống nữa.

Cấu trúc thư mục các file hiện tại của ta sau khi hoàn thành như sau:

Screenshot 2024-06-11 at 12.02.42 AM.png

Vue 2

Ở đây mình dùng dùng Vue trong project Laravel nhé các bạn (đơn giản vì máy mình cài sẵn Laravel 😃).

Trong thư mục resources/js/components các bạn tạo folder bus, chứa code cho bài này. Sau đó chúng ta sẽ cùng viết code cho EventBus, phần chính của bài này, bằng cách tạo file index.js với nội dung như sau:

import Vue from 'vue'

class EventBus {
    constructor() {
        this.bus = new Vue()
    }

    /**
     * Listen for the given event.
     *
     * @param {string} event
     * @param {function} handler
     */
    on(event, handler) {
        this.bus.$on(event, handler)
    }

    /**
     * Listen for the given event once.
     *
     * @param {string} event
     * @param {function} handler
     */
    once(event, handler) {
        this.bus.$once(event, handler)
    }

    /**
     * Remove one or more event listeners.
     *
     * @param {string} event
     * @param {function} handler
     */
    off(event, handler) {
        this.bus.$off(event, handler)
    }

    /**
     * Emit the given event.
     *
     * @param {string|object} event
     * @param {...*} args
     */
    emit(event, ...args) {
        this.bus.$emit(event, ...args)
    }
}

export default {
    install(Vue) {
        const bus = new EventBus()

        Vue.prototype.$bus = bus
    },
}

Cùng vọc đoạn code trên xem có gì nhé 😃:

  • Ở đây chúng ta có class tên EventBus. Class này chỉ có duy nhất thuộc tính bus được khởi tạo trong constructor với giá trị bằng việc tạo mới 1 object Vue. Những gì ta về cơ bản nhận được là một thành phần hoàn toàn tách rời khỏi DOM hoặc phần còn lại của ứng dụng Vue. Tất cả những gì tồn tại trên đó là các phương thức hay thuộc tính của nó, vì vậy, nó khá nhẹ (nhẹ là sướng rồi) 😄)
  • Class này có 4 phương thức. Các bạn có thể đọc phần comment của mình là có thể hiểu được từng phương thức làm gì nhé. 2 phương thức ta sẽ hay dùng nhất đó là on để lắng nghe liên tục 1 sự kiện nào đó, còn emit là để phát ra 1 event đi toàn bộ ứng dụng Vue.
  • Tiếp theo, nhìn xuống phần dưới. Để dùng EventBus như 1 dạng plugin, ta export ra phương thức install với tham số là 1 Vue instance. Ở trong phương thức này, ta khởi tạo 1 đối tượng EventBus, sau đó ta thêm 1 thuộc tính $bus vào tất cả các component Vue bằng cách sử dụng: Vue.prototype.$bus = bus, với cách làm như này, Ta sẽ có thể gọi this.$bus.on hay this.$bus.emit trong bất kì component nào ta muốn.

Ổn rồi đó anh em. Giờ ta cùng import plugin này vào app Vue của chúng ta nhé. Ở file app.js ta thêm vào như sau:

import Bus from './components/bus'
Vue.use(Bus)

Chỉ đơn giản như vậy thôi, bây giờ EventBus đã có sẵn ở toàn bộ app của ta rồi đó 😉.

Tiếp theo vẫn ở folder bus ta tạo các component lần lượt như sau: App.vue

<template>
    <div>
        <div>
            <Foo></Foo>
        </div>
        <hr>
        <div>
            <Bar></Bar>
        </div>
    </div>
</template>

<script>
    import Foo from './Foo.vue'
    import Bar from './Bar.vue'
    export default {   
        components: {
            Foo,
            Bar
        }
    }
</script>

Foo.vue

<template>
    <div>
        <h1>Counter: {{ counter }}</h1>
        <button @click="increaseCounter">Increase</button>
    </div>
</template>

<script>
    export default {
        data () {
            return {
                counter: 0
            }
        },
        methods: {
            increaseCounter () {
                this.counter++
                this.$bus.emit('increaseCounter', this.counter)
            }
        }
    }
</script>

Bar.vue

<template>
    <div>
        <h1>Counter from Foo: {{ counter }}</h1>
    </div>
</template>

<script>
    export default {
        data () {
            return {
                counter: 0
            }
        },
        created () {
            this.$bus.on('increaseCounter', value => {
                this.counter = value
            })
        }
    }
</script>

Giải thích một chút nhé. Ở trên ta có component App ở đó ta có 2 component con là FooBar. Ở Foo ta có biến counter và 1 button để tăng giá trị counter mỗi khi click. Đồng thời mỗi khi click ta sẽ phát đi 1 event cho toàn bộ app với tên là increaseCounter (đặt tên tuỳ ý nhé các bạn, mình có thói quen hay đặt trùng với tên hàm), cùng đi với event đó là giá trị của counter vừa được tăng lên.

Ở bên component Bar mình sẽ lắng nghe sự kiện ở trên, khi nào thấy có sự kiện thì cũng tăng giá trị của counterBar bằng với giá trị của được gửi kèm trong sự kiện increaseCounter

Ok tiếp theo ta thêm component App vào file app.js nhé: app.js

...
Vue.component('app', require('./components/bus/App.vue').default);
...

Ở file welcome.blade.php ta sửa lại như sau nhé:

//...Other codes
<body>
        <div id="app">
            <app></app>
        </div>
        <script src="/js/app.js"></script>
</body>

Cuối cùng ta chạy npm run watchphp artisan serve( với các bạn dùng Laravel) để khởi động app nhé. Mở trình duyệt xem kết quả nhé. Mở cả Vue devtool để xem kĩ hơn nhé các bạn

EventBus

Ở đây mỗi khi click vào button ở Foo sẽ tương ứng phát đi 1 event, Bar lắng nghe và update giá trị của mình theo giá trị của Foo vừa thay đổi được gửi kèm sự kiện. Do đó ta không cần emit 1 event từ Foo lên App sau đó truyền 1 props từ App xuống Bar để có được sự thay đổi này nữa.

NOTE:

  • Để gửi nhiều hơn 1 biến kèm sự kiện, các bạn đơn giản viết như sau:
//in Foo
this.$bus.emit('increaseCouter', count1, count2, count3)

//in Bar
this.$bus.on('increaseCouter', (count1, count2, count3) => {
    ....
})
  • Vì sự kiện khi được emit sẽ được truyền đi toàn ứng dụng, nên nếu nhớ cẩn thận đặt tên của event sao cho không bị lặp nhé các bạn.

The end

Trong Vue 3, chúng ta có thể dễ dàng tạo một global event bus sử dụng thư viện mitt. Với script setup, việc sử dụng event bus để truyền và lắng nghe sự kiện giữa các components trở nên đơn giản và hiệu quả hơn.

Còn với Vue 2 thì các bạn nào chưa hiểu về global event trong Vue có thể sử dụng EventBus, đồng thời biết cách để viết 1 plugin trong VueJS như thế nào nhé (ở các dự án thật mình hay viết kiểu này vì khá tiện và chỉnh sửa dễ dàng).

Nhìn chung EventBus có cách sử dụng khá giống với this.$emit để emit 1 event từ con lên cha như ta vẫn dùng, chỉ khác là giờ event sẽ được truyền đi toàn ứng dụng và bất cứ đâu cũng có thể lắng nghe.

Cám ơn các bạn đã theo dõi từng bài của mình. Hẹn gặp lại các bạn ở các bài sau. 😃. Nếu có gì thắc mắc các bạn để lại dưới comment cho mình được biết nhé 😉


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí