Vue 3 mới có gì hay?
Vue 3.0.0 đã chính thức ra mắt cách đây không lâu (read: 21 days :v). Là một major version, Vue 3 được giới thiệu với rất nhiều ưu điểm:
- Cải thiện hiệu năng
- Bundle nhẹ hơn
- Tích hợp TypeScript tốt hơn
- API mới giúp làm việc tốt hơn với dự án quy mô lớn
- Tạo nền tảng vững chắc cho sự phát triển lâu dài của Vue
Cùng với đó là danh sách Breaking Changes khá dài.
Rất nhiều thay đổi nhãn tiền, đúng với ý nghĩa của một bản major (unlike SomeLib v17 lul).
Trong bài viết này mình xin tập trung tìm hiểu một số tính năng/API mới được giới thiệu ở v3.
Các tính năng mới nổi bật
Danh sách được team Vue điểm mặt như sau:
- Composition API
- Teleport
- Fragments
- Emits Component Option
createRenderer
API from@vue/runtime-core
- SFC Composition API Syntax Sugar
- SFC State-driven CSS Variables
- SFC
<style scope>
changes
Viết hết về những tính năng này có lẽ quá dài, mình xin được tập trung vào 4 tính năng đầu.
Nếu không có thời gian đọc hết bài viết, bạn có thể kéo xuống dưới cùng, mình sẽ để TLDR ở đó.
Composition API
Tại sao cần Composition API
Một component điển hình trong Vue có lẽ trông như sau:
export default { props: { user: { type: String } }, data() { return { repositories: [], // 1 filters: {}, // 3 searchQuery: '' // 2 } }, computed: { filteredRepositories() { // 3 ... }, repositoriesMatchingSearchQuery() { // 2 ... } }, watch: { user: 'getUserRepositories' // 1 }, methods: { getUserRepositories() { // 1 ... }, updateFilters() { // 3 ... } }, mounted() { this.getUserRepositories(); // 1 }
Component này có các luồng logic sau:
- Gọi API lấy repositories của user được truyền vào từ
props
, gọi lại API mỗi khiprops.user
thay đổi - Tìm repositories với
queryString
- Filter repositories với
filters
Chúng ta có thể hình dung ra với những component có logic phức tạp, việc mỗi option này kéo dài vài chục (read: hundreds) dòng là chuyện bình thường.
Việc này dẫn tới các phần logic có liên quan đến nhau, nhưng lại bị chia cắt ra nằm rải rác khắp nơi, khi làm việc chúng ta phải tự hình thành liên kết giữa các phần này trong đầu (mental model).
Và sớm hay muộn (read: almost certainly really soon) chúng ta sẽ quên liên kết này mà thôi, khi đó mỗi khi quay trở lại chúng ta sẽ lại thực hiện lại quy trình hình thành mental model từ đầu.
Việc phân mảnh logic này là một trong những nguyên nhân khiến chúng ta khó làm việc được với những component phức tạp trong Vue.
Bởi vậy sẽ lý tưởng hơn nếu có một cách nào đó chúng ta có thể sắp xếp các phần logic liên quan chỗ nhau vào cùng một chỗ.
Đây chính là vấn đề mà composition được tạo ra để giải quyết.
Composition API cơ bản
Component trong Vue 3 sẽ có thêm một option tên là setup
, option này sẽ được chạy trước khi component được tạo ra, sau khi props
đã được nhận. Composition API sẽ được viết trong setup
.
setup
được chạy trước khi component được tạo ra, vì thế bên trongsetup
chúng ta sẽ không thể truy cập vào bất cứ thành phần nào khác của component ngoại trừprops
setup
được viết dưới dạng 1 hàm có input là props
, bất cứ thứ gì trả về tại setup
sẽ có thể truy cập được bất cứ đâu trong component (computed
, methods
, các lifecycle hooks, template…)
setup
được thêm vào component trông như thế này:
export default { props: { user: { type: string } }, setup(props) { console.log(props); // { user: '' } return {}; } // other things }
Bây giờ hãy thử phân tách một luồng logic từ component ban đầu vào setup
.
- Gọi API lấy repositories của user được truyền vào từ
props
, gọi lại API mỗi khiprops.user
thay đổi
import { ref } from 'vue'; import { fetchUserRepositories } from '~/api/repositories'; // inside component setup (props) { const repositories = ref([]); async function getUserRepositories() { repositories.value = await fetchUserRepositories(props.user); } return { repositories, getUserRepositories } }
Chúng ta khởi tạo danh sách repositories, khai báo một hàm để lấy repositories dựa vào props.user
, sau đó trả về 2 đối tượng này.
Chúng ta có thể hình dung biến repositories
như một thành phần của data
, và getUserRepositories
là một phần của methods
như ở component thông thường trước đây.
Tuy nhiên một biến thông thường được trả về từ setup
sẽ không có tính reactive
, các thay đổi của nó sẽ không được nhận ra bởi component, bởi vậy Vue cung cấp hàm ref
, biến được khởi tạo với ref
sẽ trở nên reactive
.
Giá trị của một biến được khởi tạo bằng ref
truy cập thông qua value
property.
console.log(repositories.value); repositories.value = [...];
(BTW:
const repositories = ref([]);
Các bạn có thấy cú pháp này hơi quen quen không :v)
Như vậy phần logic tại data
và methods
đã được chuyển vào setup
, tiếp theo đó là gọi getUserRepositories
khi component mounted
import { ref, onMounted } from 'vue'; import { fetchUserRepositories } from '~/api/repositories'; setup (props) { const repositories = ref([]); async function getUserRepositories() { repositories.value = await fetchUserRepositories(props.user); } onMounted(getUserRepositories); return { repositories, getUserRepositories } }
Vue tiếp tục cung cấp cho chúng ta một API mới đó là onMounted
để sử dụng bên trong setup
.
onMounted(getUserRepositories) // is equivalent with mounted() { getUserRepositories(); }
Các lifecycle hook khác đều có thể sử dụng ở setup
, Vue cung cấp các lifecycle funciton bằng cách thêm prefix on
vào Option Api của chúng.
Chúng ta có thể thấy một pattern dần hình thành: thay vì logic được để rời rạc tại các Option Api, Vue cung cấp cho chúng ta các function và syntax để khai báo/đăng ký/trả về các thành phần
reactive
tạisetup
và sử dụng ở phần còn lại của component.
Tiếp chúng ta cần theo dõi sự thay đổi của props.users
import { ref, onMounted, watch, toRef } from 'vue'; import { fetchUserRepositories } from '~/api/repositories'; setup (props) { const { user } = toRefs(props); const repositories = ref([]); async function getUserRepositories() { repositories.value = await fetchUserRepositories(user.value); } onMounted(getUserRepositories); watch(user, getUserRepositories); return { repositories, getUserRepositories } }
Chúng ta import một function mới watch
và dùng nó để tạo một watcher phản ứng lại thay đổi của user
.
Và để thay đổi của props
được ánh xạ vào setup
chúng ta cần convert props
thành reactive
sử dụng toRefs
.
Phần Option API cuối cùng ở component ban đầu chúng ta chưa động đến đó là computed
, chắc hẳn Vue cũng sẽ có 1 function nào đó với chức năng này. (sure lul)
import { ref, onMounted, watch, toRef, computed } from 'vue'; import { fetchUserRepositories } from '~/api/repositories'; setup (props) { const { user } = toRefs(props); const repositories = ref([]); async function getUserRepositories() { repositories.value = await fetchUserRepositories(props.user); } onMounted(getUserRepositories); watch(user, getUserRepositories); const searchQuery = ref(''); const repositoriesMatchingSearchQuery = computed(() => { return repositories.value.filter( repository => repository.name.includes(searchQuery.value) ) }) return { repositories, getUserRepositories } }
Như vậy toàn bộ phần logic trước đây được phân tán ở các Option API bây giờ có thể được tập trung lại tại setup
.
Tuy nhiên nếu chỉ như vậy thì khi component trở nên phức tạp, setup
cũng sẽ phình siêu to khổng lồ và khi đó chẳng có gì đảm bảo các phần logic của chúng ta sẽ còn dễ theo dõi nữa, Composition API chỉ là mang code từ chỗ này sang chỗ khác mà thôi ???
Composition API không chỉ là di chuyển code qua lại trong component, chúng ta có thể phân tách logic liên quan tới từng nơi riêng biệt (function) và tổ hợp (compose) lại ở setup
.
Chúng ta sẽ chi các phần logic riêng biệt ở setup
trên:
// useUserRepositories.js import { fetchUserRepositories } from '~/api/repositories'; import { ref, onMounted, watch } from 'vue'; export default function useUserRepositories(user) { const repositories = ref([]); async function getUserRepositories() { repositories.value = await fetchUserRepositories(user.value); } onMounted(getUserRepositories); watch(user, getUserRepositores); return { repositories, getUserRepositories } }
// useRepositoryNameSearch.js import { ref, computed } from 'vue'; export default function useRepositoryNameSearch(repositories) { const queryString = ref(''); const repositoriesMatchingQueryString = computed(() => { return repositories.value.filter(repository => { return repository.name.includes(searchQuery.value); }); }); return { searchQuery, repositoriesMatchingQueryString } }
import useUserRepositories from './useUserRepositories'; import useRepositoryNameSearch from './userRepositoryNameSearch'; import { toRefs } from 'vue'; export default { props: { user: { type: String } }, setup(props) { const { user } = toRefs(props); const { repositories, getUserRepositores } = useUserRepositories(user); const { searchQuery, repositoriesMatchingSearchQuery } = useRepositoryNameSearch(respositories); return { repositories, getUserRepositories, searchQuery, repositoriesMatchingSearchQuery } }, // other parts }
Voilà, đây chính là Composition API.
Và nếu đến đây bạn có thấy chút thân thuộc, thì hẳn bạn cũng giống mình, IMO: Compostion API của Vue rất tương đồng với React Custom Hook.
Còn nếu bạn không thấy vậy, thì cũng không sao.
Trên đây chỉ là phần rất cơ bản về Vue Composition API.
TLDR;
- Composition API là cách thức mới để viết Vue component, bằng cách cung cấp 1 component option mới
setup
và các fuction cùng pattern để có thể:
- Gộp các thành phần logic liên quan đến nhau đang bị phân mảnh ở các Option API về chung một mối.
- Tách các phần logic liên quan này thành function riêng biệt đẻ có thể dễ dàng sử dụng lại và quản lý.
- Composition API có sự tương đồng với React Custom Hook.
Kết
Khi bắt đầu viết bài mình không nghĩ nó lại dài như thế này, đành phải để lại các tính năng còn lại ở một bài khác.