[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:
- Cài đặt
mitt
:
npm install mitt
- Tạo file
eventBus.js
ở cùng level folder vớiapp.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.
- 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
- 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.vue
và EventListener.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:
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:
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ínhbus
được khởi tạo trongconstructor
với giá trị bằng việc tạo mới 1 objectVue
. 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ụngVue
. 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ònemit
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ạngplugin
, taexport
ra phương thứcinstall
với tham số là 1Vue instance
. Ở trong phương thức này, ta khởi tạo 1 đối tượngEventBus
, 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ọithis.$bus.on
haythis.$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à Foo
và Bar
. Ở 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 counter
ở Bar
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 watch
và php 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
Ở đâ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