+6

Một số Vue.js composables hay dùng trong dự án

Mayfest2023

Composables là một khái niệm quan trọng trong Vue.js 3, đây là một cách tiếp cận mới để tạo và tái sử dụng logic trong các component. Composables cho phép bạn tách biệt logic từ các component giao diện người dùng và viết lại logic đó dưới dạng các hàm tái sử dụng Trong Vue.js 3, bạn có thể sử dụng bộ API Composition để tạo ra các composables. API Composition bao gồm một tập hợp các hàm như ref, reactive, computed, watch, onMounted, onUnmounted và nhiều hàm khác, giúp bạn quản lý trạng thái, hiệu suất, và thực hiện các side effect trong các composables. Sử dụng composables, bạn có thể viết lại logic liên quan đến trạng thái, xử lý sự kiện, gọi API, và nhiều nhiệm vụ khác thành các hàm tái sử dụng có thể được sử dụng trong nhiều component khác nhau. Điều này giúp tăng tính tái sử dụng, khả năng kiểm thử và khả năng bảo trì của mã nguồn. Sau đây là 5 composables hay dùng trong dự án.

useEventListener

  • Thường khi bạn thêm một bộ lắng nghe sự kiện cho các phần tử được lấy thông qua ref hoặc document.querySelector, bạn luôn cần dọn dẹp (cleanup) nó khi component bị hủy. Vì vậy, chúng ta thường phải làm điều này lặp đi lặp lại.
import { onMounted, onBeforeUnmount } from 'vue';
// in a setup function
function onEscapePressed(e) {
  if (e.key === 'Escape') {
    // Do something...
  }
}
// thêm event handler
onMounted(() => {
  window.addEventListener('keydown', onEscapePressed);
});
// xóa tránh rò rỉ bộ nhớ
onBeforeUnmount(() => {
  window.removeEventListener('keydown', onEscapePressed);
});
  • Sửa lại thành composables như sau đây:
export function useEventListener(
  // the target could be reactive ref which adds flexibility
  target: Ref<EventTarget | null> | EventTarget,
  event: string,
  handler: (e: Event) => any
) {
  // if its a reactive ref, use a watcher
  if (isRef(target)) {
    watch(target, (value, oldValue) => {
      oldValue?.removeEventListener(event, handler);
      value?.addEventListener(event, handler);
    });
  } else {
    // sử dụng trong mounted hook
    onMounted(() => {
      target.addEventListener(event, handler);
    });
  }
  // clean it up
  onBeforeUnmount(() => {
    unref(target)?.removeEventListener(event, handler);
  });
}
  • Với điều này trong tay, chúng ta có thể viết lại các component như sau:
import { useEventListener } from '@/composables';
// in a setup function
function onEscapePressed(e) {
  if (e.key === 'Escape') {
    // Do something...
  }
}
useEventListener(window, 'keydown', onEscapePressed);

useFileDialog

  • Việc nhận tệp từ người dùng bằng JavaScript thuần có thể khá phức tạp. Thông thường, điều này liên quan đến việc sử dụng một thẻ input[type="file"] có sẵn trong DOM hoặc được tạo bằng JavaScript, sau đó chỉ cần kích hoạt sự kiện click và lắng nghe sự thay đổi trên thẻ input để lấy các tệp đã chọn. Thông thường, điều này được thực hiện như sau:
// Tạo một thẻ input[type="file"]
const input = document.createElement('input');
input.type = 'file';

// Khi người dùng chọn tệp và click OK
input.addEventListener('change', (event) => {
  const selectedFiles = event.target.files;
  
  // Xử lý các tệp đã chọn ở đây
});

// Kích hoạt sự kiện click trên thẻ input
input.click();
  • Đây là một composable giúp dọn dẹp việc chọn tệp giữa các component. Bạn có thể sử dụng composable này để mở hộp thoại chọn tệp và nhận danh sách các tệp đã chọn. Các tệp được lưu trong biến files. Bạn có thể tuỳ chỉnh các tùy chọn của hộp thoại chọn tệp thông qua tham số opts khi gọi hàm openDialog.
import { ref, onMounted, onBeforeUnmount } from 'vue';

interface PickOptions {
  accept: string[];
  multiple: boolean;
}

export function useFileDialog() {
  const inputRef = ref<HTMLInputElement | null>(null);
  const files = ref<File[]>([]);

  onMounted(() => {
    const input = document.createElement('input');
    input.type = 'file';
    input.hidden = true;
    input.className = 'hidden';
    document.body.appendChild(input);
    inputRef.value = input;
  });

  // Đảm bảo loại bỏ phần tử nếu component bị hủy.
  onBeforeUnmount(() => {
    inputRef.value?.remove();
  });

  function openDialog(opts?: Partial<PickOptions>) {
    // Bỏ qua nếu input chưa được mount hoặc đã bị xóa
    if (!inputRef.value) {
      files.value = [];
      return;
    }

    if (opts?.accept) {
      inputRef.value.accept = opts.accept
        .map((ext) => `.${ext}`)
        .join(',');
    }

    inputRef.value.multiple = opts?.multiple ?? false;

    // Chuẩn bị bộ lắng nghe sự kiện
    inputRef.value.onchange = (e) => {
      const fileList = (e.target as HTMLInputElement).files;
      files.value = fileList ? Array.from(fileList) : [];

      // Xóa bộ lắng nghe sự kiện
      if (inputRef.value) {
        inputRef.value.onchange = null;
      }
    };

    inputRef.value.click();
  }

  return {
    openDialog,
    files,
  };
}
  • Bây giờ, khi chọn tệp
const { openDialog, files } = usePickFiles();
// Mở hộp thoại chọn tệp
openDialog({
  // Lọc tệp theo phần mở rộng
  accept: ['jpg', 'jpeg', 'png', 'gif'],
});
// Lấy giá trị tệp hiện tại:
files.value;

useOnClickOutside

  • Đây là một composable hữu ích để xác định việc nhấp chuột bên ngoài để thực hiện một số hành động trong các component menu và dialog. Bạn có thể sử dụng useOnClickOutside để theo dõi nhấp chuột bên ngoài phần tử gốc (rootEl) và thực thi hàm callback khi có sự nhấp chuột bên ngoài.
import { onMounted, onBeforeUnmount, Ref } from 'vue';
import { useEventListener } from './useEventListener';

export function useOnClickOutside(
  rootEl: Ref<HTMLElement | null>,
  callback: () => any
) {
  // `mousedown` hoặc `mouseup` tốt hơn `click` ở đây vì nó không lan ra như `click`
  // Nếu bạn sử dụng `click` ở đây, callback sẽ được chạy ngay lập tức.
  useEventListener(window, 'mouseup', (e: Event) => {
    const clickedEl = e.target as HTMLElement;
    // Bỏ qua nếu phần tử gốc chứa phần tử được click
    if (rootEl.value?.contains(clickedEl)) {
      return;
    }
    // Thực thi hành động
    callback();
  });
}
  • Đây là một ví dụ nhỏ về cách sử dụng composable useOnClickOutside:
import { ref } from 'vue';
import { useOnClickOutside } from './useOnClickOutside';

// Trong hàm setup
const isOpen = ref(false);
const containerRef = ref<HTMLElement | null>(null);

useOnClickOutside(containerRef, () => {
  isOpen.value = false;
});
  • Trong ví dụ này, chúng ta sử dụng composable useOnClickOutside để theo dõi sự nhấp chuột bên ngoài phần tử containerRef. Khi có sự nhấp chuột bên ngoài, chúng ta gán giá trị false cho biến isOpen, đóng menu hoặc dialog.

useHotkey

  • Đây là một composable giúp xử lý các phím tắt bàn phím.
interface HotkeyOptions {
  // Phần tử có thể là một reactive ref
  target: Ref<EventTarget> | EventTarget;
  shiftKey: boolean;
  ctrlKey: boolean;
  exact: boolean;
}

export function useHotkey(
  key: string,
  onKeyPressed: () => any,
  opts?: Partial<HotkeyOptions>
) {
  // Lấy phần tử 
  const target = opts?.target || window;
  useEventListener(target, 'keydown', (e: KeyboardEvent) => {
    const options = opts || {};
    if (e.key === key && matchesKeyScheme(options, e)) {
      e.preventDefault();
      onKeyPressed();
    }
  });
}

function matchesKeyScheme(
  opts: Pick<Partial<HotkeyOptions>, 'shiftKey' | 'ctrlKey' | 'exact'>,
  evt: KeyboardEvent
) {
  const ctrlKey = opts.ctrlKey ?? false;
  const shiftKey = opts.shiftKey ?? false;
  if (opts.exact) {
    return ctrlKey === evt.ctrlKey && shiftKey == evt.shiftKey;
  }
  const satisfiedKeys: boolean[] = [];
  satisfiedKeys.push(ctrlKey === evt.ctrlKey);
  satisfiedKeys.push(shiftKey === evt.shiftKey);
  return satisfiedKeys.every((key) => key);
}
  • Đây là một composable để xử lý các phím tắt (hotkey). Bạn có thể sử dụng useHotkey để theo dõi sự kiện nhấn phím và thực thi hàm onKeyPressed khi phím tương ứng được nhấn, tuân thủ các tùy chọn trong opts. Bạn có thể cung cấp một key (phím tắt), hàm onKeyPressed để xử lý và các tùy chọn opts (bao gồm target, shiftKey, ctrlKey, exact) cho phím tắt.
  • Sử dụng
import { useHotkey } from './useHotkey';
useHotkey(
  'Enter',
  () => {
    console.log('Enter Pressed!');
  },
  {
    exact: true,
  }
);
useHotkey(
  'Enter',
  () => {
    console.log('Cmd Enter pressed!');
  },
  {
    exact: true,
    meta: true,
  }
);

useMedia

  • Thông thường, hầu hết logic liên quan đến @media (ví dụ: kích thước màn hình, dark mode, tùy chọn chuyển động) được thể hiện dưới dạng các truy vấn phương tiện CSS như sau:
@supports (display: flex) {
  @media screen and (min-width: 900px) {
    article {
      display: flex;
    }
  }
}

-Tuy nhiên, đôi khi bạn cần sử dụng thông tin đó để điều khiển logic JavaScript. Ví dụ, bạn có thể có một số hiệu ứng JavaScript đang chạy, nhưng để tăng tính bao quát, bạn cần biết người dùng có ưu tiên hiệu ứng giảm hay không. Để làm điều đó trong JavaScript, thường bạn sử dụng window.matchMedia để kiểm tra các truy vấn phương tiện mà bạn cần kiểm tra:

const motionMatchMedia = window.matchMedia(
  '(prefers-reduced-motion)'
);
  • Đây là một trong những hooks dễ triển khai, đặc biệt nếu chúng ta đã sử dụng useEventListener mà chúng ta đã có:
export function useMedia(query: Ref<string> | string) {
  const mediaQuery = window.matchMedia(query);
  const matches = ref(mediaQuery.matches);
  useEventListener(mediaQuery, 'change', (e) => {
    matches.value = event.matches;
  });
  return matches;
}
  • Bây giờ bạn có thể thực hiện một số media queries và có chúng như các giá trị reactive trong thành phần Vue:
import { useMedia } from './useMedia';
const isReducedMotion = useMedia('(prefers-reduced-motion)');
const isDark = useMedia('(prefers-color-scheme: dark)');
const isLight = useMedia('(prefers-color-scheme: light)');
const isTablet = useMedia('(min-width: 640px)');
  • Và thực tế, hàm composable useMedia như một cơ sở cho các media query thông thường khác trong ứng dụng. Chẳng hạn, truy vấn kích thước màn hình như sau:
export function useScreenSize() {
  const isMobile = useMedia('(min-width: 640px)');
  const isLaptop = useMedia('(min-width: 1024px)');
  const isDesktop = useMedia('(min-width:1280px)');
  return computed(() => {
    if (isDesktop.value) {
      return 'desktop';
    }
    if (isLaptop.value) {
      return 'laptop';
    }
    if (isMobile.value) {
      return 'mobile';
    }
    return 'unknown';
  });
}

** TRÊN ĐÂY LÀ NHỮNG CHIA SẺ CỦA MÌNH, CẢM ƠN CÁC BẠN ĐÃ ĐỌC BÀI VIẾT .**😘 Nguồn: https://logaretm.com/blog/my-favorite-5-vuejs-composables/


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í