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:

  1. Gọi API lấy repositories của user được truyền vào từ props, gọi lại API mỗi khi props.user thay đổi
  2. Tìm repositories với queryString
  3. 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 trong setup 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 (computedmethods, 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.

  1. Gọi API lấy repositories của user được truyền vào từ props, gọi lại API mỗi khi props.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ại setup 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ể:
  1. 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.
  2. 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.

Add a Comment

Scroll Up