Chào mừng đến với Cafedev, nơi chúng tôi chia sẻ những kiến thức và kinh nghiệm về công nghệ! Trong chủ đề Vuejs with Composables hôm nay, chúng ta sẽ khám phá cách sử dụng Composition API trong Vue.js để tạo ra những đoạn mã tái sử dụng và dễ dàng quản lý. Với Composables, chúng ta có thể tổ chức và tái sử dụng logic thành phần một cách hiệu quả, giúp tăng tính linh hoạt và tái sử dụng trong ứng dụng của chúng ta. Hãy cùng đắm chìm vào thế giới của Vuejs và Composables ngay bây giờ!

tip Phần này giả định bạn có kiến thức cơ bản về Composition API. Nếu bạn chỉ học Vue với Options API, bạn có thể đặt Ưu tiên API thành Composition API (sử dụng công tắc ở đầu thanh bên trái) và đọc lại các chương Nền tảng ReactivityLifecycle Hooks.

1. “Composable” là gì?

Trong ngữ cảnh của ứng dụng Vue, một “composable” là một hàm sử dụng Composition API của Vue để đóng gói và tái sử dụng logic có trạng thái.
Khi xây dựng ứng dụng frontend, chúng ta thường cần tái sử dụng logic cho các nhiệm vụ phổ biến. Ví dụ, chúng ta có thể cần định dạng các ngày tháng ở nhiều nơi, vì vậy chúng ta trích xuất một hàm có thể tái sử dụng cho điều đó. Hàm định dạng này đóng gói logic không có trạng thái: nó nhận một số đầu vào và ngay lập tức trả về đầu ra mong đợi. Có nhiều thư viện để tái sử dụng logic không có trạng thái – ví dụ như lodashdate-fns, mà bạn có thể đã nghe qua.

Ngược lại, logic có trạng thái liên quan đến việc quản lý trạng thái thay đổi theo thời gian. Một ví dụ đơn giản có thể là theo dõi vị trí hiện tại của chuột trên trang. Trong các kịch bản thực tế, nó cũng có thể là logic phức tạp hơn như các cử chỉ chạm hoặc trạng thái kết nối đến cơ sở dữ liệu.

2. Ví dụ về Theo dõi Chuột

Nếu chúng ta muốn triển khai chức năng theo dõi chuột bằng Composition API trực tiếp trong một thành phần, nó sẽ trông như thế này:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

function update(event) {
  x.value = event.pageX
  y.value = event.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

Nhưng nếu chúng ta muốn tái sử dụng cùng một logic trong nhiều thành phần? Chúng ta có thể trích xuất logic vào một tệp ngoài, như một hàm composable:

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// by convention, composable function names start with "use"
export function useMouse() {
  // state encapsulated and managed by the composable
  const x = ref(0)
  const y = ref(0)

  // a composable can update its managed state over time.
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // a composable can also hook into its owner component's
  // lifecycle to setup and teardown side effects.
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // expose managed state as return value
  return { x, y }
}

Và đây là cách nó có thể được sử dụng trong các thành phần:

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>

Thử nó trong Play

Như chúng ta có thể thấy, logic cốt lõi vẫn giữ nguyên – tất cả những gì chúng ta cần phải làm là di chuyển nó vào một hàm bên ngoài và trả về trạng thái cần phải được tiết lộ. Giống như bên trong một thành phần, bạn có thể sử dụng toàn bộ loạt các hàm Composition API trong composables. Chức năng useMouse() giống như vậy bây giờ có thể được sử dụng trong bất kỳ thành phần nào.

Phần thú vị hơn về composables là bạn cũng có thể lồng chúng: một hàm composable có thể gọi một hoặc nhiều hàm composable khác. Điều này cho phép chúng ta kết hợp logic phức tạp bằng các đơn vị nhỏ, cô lập, tương tự như cách chúng ta xây dựng một ứng dụng toàn bộ bằng các thành phần. Trên thực tế, đây là lý do tại sao chúng tôi quyết định gọi bộ sưu tập các API làm cho mẫu này có thể Composition API.

Ví dụ, chúng ta có thể trích xuất logic thêm và xóa một trình nghe sự kiện DOM vào một composable riêng của nó:

// event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
  // if you want, you can also make this
  // support selector strings as target
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

Và bây giờ hàm composable useMouse() của chúng ta có thể được đơn giản hóa thành:


// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  useEventListener(window, 'mousemove', (event) => {
    x.value = event.pageX
    y.value = event.pageY
  })

  return { x, y }
}

tip Mỗi phiên bản thành phần gọi useMouse() sẽ tạo ra các bản sao riêng của trạng thái xy để chúng không giao nhau với nhau. Nếu bạn muốn quản lý trạng thái được chia sẻ giữa các thành phần, hãy đọc chương Quản lý Trạng thái.

3. Ví dụ Trạng thái Asynchronous

Hàm composable useMouse() không nhận bất kỳ đối số nào, vì vậy hãy xem một ví dụ khác sử dụng một đối số. Khi thực hiện lấy dữ liệu async, chúng ta thường cần xử lý các trạng thái khác nhau: đang tải, thành công và lỗi:

<script setup>
import { ref } from 'vue'

const data = ref(null)
const error = ref(null)

fetch('...')
  .then((res) => res.json())
  .then((json) => (data.value = json))
  .catch((err) => (error.value = err))
</script>

<template>
  <div v-if="error">Oops! Error encountered: {{ error.message }}</div>
  <div v-else-if="data">
    Data loaded:
    <pre>{{ data }}</pre>
  </div>
  <div v-else>Loading...</div>
</template>

Sẽ rất tẻ nhạt nếu phải lặp lại mẫu này trong mỗi thành phần cần phải lấy dữ liệu. Hãy trích xuất nó vào một composable:

// fetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  fetch(url)
    .then((res) => res.json())
    .then((json) => (data.value = json))
    .catch((err) => (error.value = err))

  return { data, error }
}

Bây giờ trong thành phần của chúng ta, chúng ta chỉ cần làm:

<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('...')
</script>

3.1 Chấp nhận Trạng thái Phản ứng

useFetch() nhận một chuỗi URL tĩnh làm đầu vào – vì vậy nó thực hiện việc fetch chỉ một lần và sau đó kết thúc. Nhưng nếu chúng ta muốn nó thực hiện lại fetch mỗi khi URL thay đổi thì sao? Để đạt được điều này, chúng ta cần truyền trạng thái phản ứng vào hàm composable, và để hàm composable tạo ra các watchers thực hiện hành động bằng trạng thái đã truyền vào.
Ví dụ, useFetch() nên có thể chấp nhận một ref:

const url = ref('/initial-url')

const { data, error } = useFetch(url)

// this should trigger a re-fetch
url.value = '/new-url'

Hoặc, chấp nhận một hàm getter:

// re-fetch when props.id changes
const { data, error } = useFetch(() => `/posts/${props.id}`)

Chúng ta có thể tái cấu trúc triển khai hiện tại của mình với các API watchEffect()toValue():


// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    // reset state before fetching..
    data.value = null
    error.value = null

    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  watchEffect(() => {
    fetchData()
  })

  return { data, error }
}

toValue() là một API được thêm vào từ phiên bản 3.3. Nó được thiết kế để chuẩn hóa refs hoặc getters thành giá trị. Nếu đối số là một ref, nó sẽ trả về giá trị của ref; nếu đối số là một hàm, nó sẽ gọi hàm đó và trả về giá trị trả về của nó. Nếu không, nó sẽ trả về đối số như là nó. Nó hoạt động tương tự như unref(), nhưng có xử lý đặc biệt cho hàm.
Lưu ý rằng toValue(url) được gọi bên trong callback của watchEffect. Điều này đảm bảo rằng bất kỳ phụ thuộc phản ứng nào được truy cập trong quá trình chuẩn hóa toValue() sẽ được theo dõi bởi watcher.

Phiên bản này của useFetch() bây giờ chấp nhận chuỗi URL tĩnh, refs, và getters, làm cho nó linh hoạt hơn nhiều. Hiệu ứng watcher sẽ chạy ngay lập tức, và sẽ theo dõi bất kỳ phụ thuộc nào được truy cập trong toValue(url). Nếu không có phụ thuộc nào được theo dõi (ví dụ: url đã là một chuỗi), hiệu ứng sẽ chỉ chạy một lần; nếu không, nó sẽ chạy lại mỗi khi một phụ thuộc được theo dõi thay đổi.

4. Quy ước và Thực hành Tốt nhất

4.1 Đặt tên

Là một quy ước để đặt tên các hàm composable với các tên camelCase bắt đầu bằng “use”.

4.2 Đối số Đầu vào

Một composable có thể chấp nhận đối số ref hoặc getter ngay cả khi nó không phụ thuộc vào chúng để có tính phản ứng. Nếu bạn đang viết một composable có thể được sử dụng bởi các nhà phát triển khác, nên xem xét việc xử lý trường hợp các đối số đầu vào là refs hoặc getters thay vì là các giá trị nguyên thủy. Hàm tiện ích toValue() sẽ hữu ích cho mục đích này:

import { toValue } from 'vue'

function useFeature(maybeRefOrGetter) {
  // If maybeRefOrGetter is a ref or a getter,
  // its normalized value will be returned.
  // Otherwise, it is returned as-is.
  const value = toValue(maybeRefOrGetter)
}

Nếu composable của bạn tạo ra các hiệu ứng phản ứng khi đầu vào là một ref hoặc một getter, hãy chắc chắn rằng bạn đã theo dõi rõ ràng ref / getter với watch(), hoặc gọi toValue() bên trong một watchEffect() để nó được theo dõi đúng cách.
Thực hiện useFetch() đã thảo luận trước đó cung cấp một ví dụ cụ thể về một composable chấp nhận refs, getters và các giá trị nguyên thủy làm đối số đầu vào.

4.3 Giá trị Trả về

Bạn có thể đã nhận thấy rằng chúng ta luôn sử dụng ref() thay vì reactive() trong composables. Quy ước khuyến nghị là cho composables luôn trả về một đối tượng phẳng, không phản ứng chứa nhiều refs. Điều này cho phép nó được giải nén trong các thành phần trong khi vẫn giữ tính phản ứng:

// x and y are refs
const { x, y } = useMouse()

Trả về một đối tượng phản ứng từ một composable sẽ làm cho những việc giải nén như vậy mất kết nối phản ứng với trạng thái bên trong composable, trong khi các refs sẽ giữ lại kết nối đó.
Nếu bạn muốn sử dụng trạng thái được trả về từ composables như các thuộc tính của đối tượng, bạn có thể bọc đối tượng được trả về bằng reactive() để các refs được giải bọc. Ví dụ:

const mouse = reactive(useMouse())
// mouse.x is linked to original ref
console.log(mouse.x)
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}

4.4 Hiệu ứng Phụ

Việc thực hiện các hiệu ứng phụ (ví dụ: thêm trình nghe sự kiện DOM hoặc lấy dữ liệu) trong composables là hoàn toàn được chấp nhận, nhưng hãy chú ý đến các quy tắc sau:”

  • Nếu bạn đang làm việc trên một ứng dụng sử dụng Server-Side Rendering (SSR), hãy đảm bảo thực hiện các hiệu ứng phụ cụ thể cho DOM trong các hooks vòng đời sau khi mount, ví dụ như onMounted(). Các hooks này chỉ được gọi trong trình duyệt, vì vậy bạn có thể chắc chắn rằng mã bên trong chúng có quyền truy cập vào DOM.
  • Hãy nhớ làm sạch các hiệu ứng phụ trong onUnmounted(). Ví dụ, nếu một composable thiết lập một trình nghe sự kiện DOM, nó nên gỡ bỏ trình nghe đó trong onUnmounted() như chúng ta đã thấy trong ví dụ useMouse(). Điều này có thể là một ý tưởng tốt là sử dụng một composable tự động thực hiện điều này cho bạn, giống như ví dụ useEventListener().

4.5 Hạn chế Sử dụng

Những hạn chế này quan trọng vì đây là các ngữ cảnh mà Vue có thể xác định được phiên bản của thành phần hiện đang hoạt động. Việc truy cập vào một phiên bản thành phần hoạt động là cần thiết để:
1. Hooks vòng đời có thể được đăng ký vào đó.
2. Các tính năng tính toán và watchers có thể được liên kết với nó, để chúng có thể bị hủy bỏ khi phiên bản được gỡ bỏ để ngăn rò rỉ bộ nhớ.

MẸO <script setup> là nơi duy nhất bạn có thể gọi composables sau khi sử dụng await. Trình biên dịch sẽ tự động khôi phục bối cảnh của thể hiện hoạt động cho bạn sau khi thực hiện bất đồng bộ.

5. Trích xuất Composables cho Tổ chức Mã

Composables có thể được trích xuất không chỉ để tái sử dụng, mà còn để tổ chức mã. Khi sự phức tạp của các thành phần của bạn tăng lên, bạn có thể kết thúc với các thành phần quá lớn để điều hướng và suy luận. Composition API cho phép bạn linh hoạt tổ chức mã thành phần của mình thành các hàm nhỏ dựa trên mục tiêu logic:

<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'

const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>

Đến một mức độ nào đó, bạn có thể nghĩ về các composables được trích xuất này như là các dịch vụ phạm vi thành phần có thể trò chuyện với nhau.

6. Sử dụng Composables trong Options API

Nếu bạn đang sử dụng Options API, composables phải được gọi bên trong setup(), và các liên kết được trả về phải được trả về từ setup() để chúng được tiết lộ cho this và mẫu:

import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'

export default {
  setup() {
    const { x, y } = useMouse()
    const { data, error } = useFetch('...')
    return { x, y, data, error }
  },
  mounted() {
    // setup() exposed properties can be accessed on `this`
    console.log(this.x)
  }
  // ...other options
}

7. So sánh với Các Kỹ thuật Khác

7.1 So sánh với Mixins

Người dùng chuyển từ Vue 2 có thể quen với tùy chọn mixins, cũng cho phép chúng ta trích xuất logic thành phần thành các đơn vị có thể tái sử dụng. Có ba hạn chế chính của mixins:
1. Nguồn không rõ ràng của các thuộc tính: khi sử dụng nhiều mixins, trở nên không rõ ràng thuộc tính của phiên bản nào được chèn bởi mixin nào, làm cho việc theo dõi cài đặt và hiểu hành vi của thành phần trở nên khó khăn. Đây cũng là lý do tại sao chúng tôi khuyến nghị sử dụng mẫu refs + destructure cho composables: nó làm cho nguồn thuộc tính rõ ràng trong các thành phần tiêu thụ.
2. Xung đột Namespace: nhiều mixins từ các tác giả khác nhau có thể đăng ký các khóa thuộc tính giống nhau, gây ra xung đột Namespace. Với composables, bạn có thể đổi tên biến được giải nén nếu có các khóa xung đột từ các composables khác nhau.

3.Giao tiếp chéo-mixin ngầm định: nhiều mixins cần tương tác với nhau phải phụ thuộc vào các khóa thuộc tính chia sẻ, khiến chúng ngầm kết nối. Với composables, các giá trị trả về từ một composable có thể được chuyển vào một composable khác dưới dạng đối số, giống như các hàm bình thường.

Vì các lý do trên, chúng tôi không còn khuyến nghị sử dụng mixins trong Vue 3. Tính năng này chỉ được giữ lại vì lý do di chuyển và quen thuộc.

7.2 So sánh với Các Thành phần không render

Trong chương về các khe cắm thành phần, chúng tôi đã thảo luận về mẫu Thành phần không render dựa trên các khe cắm phạm vi. Chúng tôi thậm chí đã triển khai cùng một demo theo dõi chuột bằng cách sử dụng các thành phần không render.
Ưu điểm chính của composables so với các thành phần không render là composables không gây ra chi phí thêm cho các phiên bản thành phần. Khi được sử dụng trên toàn bộ ứng dụng, số lượng các phiên bản thành phần thêm do mẫu thành phần không render có thể trở thành một chi phí hiệu suất có thể nhận thấy.

Khuyến nghị là sử dụng composables khi tái sử dụng logic thuần túy, và sử dụng các thành phần khi tái sử dụng cả logic và bố cục trực quan.

7.3 So sánh với React Hooks

Nếu bạn có kinh nghiệm với React, bạn có thể nhận thấy rằng điều này trông rất giống với các hook React tùy chỉnh. Composition API được một phần lấy cảm hứng từ React hooks, và composables Vue thực sự tương tự như React hooks về khả năng hợp thành logic. Tuy nhiên, composables Vue dựa trên hệ thống phản ứng tinh tế của Vue, đó là một điểm khác biệt cơ bản so với mô hình thực thi của React hooks. Điều này được thảo luận chi tiết hơn trong Composition API FAQ.

Cảm ơn bạn đã tham gia cùng chúng tôi trên Cafedev để tìm hiểu về Vuejs with Composables. Hy vọng rằng thông qua bài viết này, bạn đã có cái nhìn sâu sắc hơn về cách sử dụng Composition API trong Vue.js để tận dụng sức mạnh của Composables trong phát triển ứng dụng web. Với sự linh hoạt và tiện lợi mà Composables mang lại, chúng ta có thể xây dựng các ứng dụng mạnh mẽ và dễ bảo trì hơn. Hãy tiếp tục theo dõi Cafedev để cập nhật thêm nhiều thông tin hữu ích khác nhé!

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!