Skip to content

Thêm composable & util

Logic dùng chung không phải component: composable cho app Nuxt, helper TypeScript thuần, hay composable cần antdv. Chọn đúng package trước — sai chỗ là vi phạm ràng buộc phụ thuộc.

Đặt ở đâu

Code cầnĐặt ởVí dụ
Chỉ TypeScript, không Vue, không Nuxt@tasco/utilstoAppError, parseTableSettings
Vue và Nuxt (useState, useRuntimeConfig, useCookie…), không cần antdv@tasco/composablesuseApi, useTable
antdv (Modal, message…)@tasco/uiuseConfirm, useErrorHandler
Chạy khi app khởi động, route server, middleware@tasco/nuxt-layer-baseplugin 04.error, server/utils/session.ts
Chỉ phục vụ tính năng AI@tasco/aiuseAiChat

Logic nghiệp vụ của một sản phẩm (tính giá đơn hàng, quy tắc duyệt của một phòng ban) không vào core — xem tiêu chí "core-worthy" trong RFC Process.

Lõi thuần, lớp bọc mỏng

Tách phần tính toán ra hàm thuần, composable chỉ nối hàm đó với Nuxt. Hàm thuần test trực tiếp không cần mock; lớp bọc mỏng tới mức ít thứ để hỏng.

ts
// packages/composables/src/permission.ts — lõi thuần, test trực tiếp
export function createPermissionChecker(permissions: string[], roles: string[], superRoles: string[] = []) {
  // ...
}

// packages/composables/src/usePermission.ts — lớp bọc: lấy user và cấu hình từ Nuxt
export function usePermission() {
  const { user } = useAuth()
  const superRoles = parseSuperRoles(useRuntimeConfig().public.auth)
  const checker = computed(() => createPermissionChecker(user.value?.permissions ?? [], user.value?.roles ?? [], superRoles))
  // ...
}

Lõi thuần mà package khác cũng cần (vd table-settings dùng ở cả ui lẫn composables) thì đặt ở @tasco/utils.

Viết composable

ts
import { ref } from 'vue'
import { useRequestFetch } from 'nuxt/app'
import { toAppError } from '@tasco/utils'
import type { AppError } from '@tasco/utils'

/** Tuỳ chọn của {@link useExport}. */
export interface UseExportOptions {
  /** Tên file tải về, không gồm đuôi. Mặc định `du-lieu`. */
  fileName?: string
}

/** Xuất dữ liệu ra file Excel qua BFF; lỗi ghi vào `error`, không ném ra. */
export function useExport(endpoint: string, options: UseExportOptions = {}) {
  const loading = ref(false)
  const error = ref<AppError | null>(null)
  const requestFetch = useRequestFetch()

  async function run(query: Record<string, unknown> = {}): Promise<void> {
    loading.value = true
    error.value = null
    try {
      const file = await requestFetch<Blob>(endpoint, { query, responseType: 'blob' })
      // ... tạo link tải `${options.fileName ?? 'du-lieu'}.xlsx` từ `file`
    } catch (caught) {
      error.value = toAppError(caught)
    } finally {
      loading.value = false
    }
  }

  return {
    /** Đang xuất. */
    loading,
    /** Lỗi của lần xuất gần nhất; `null` khi thành công. */
    error,
    /** Xuất theo truy vấn hiện tại. */
    run,
  }
}
Quy ướcChi tiết
Tênuse* cho composable; create* cho factory; is*/has* cho hàm kiểm tra; to* cho chuyển đổi
Tham sốTham số chính trước, options cuối cùng với mặc định = {}; kiểu tuỳ chọn tên <Tên>Options
Trả vềObject phẳng; mỗi giá trị trả về có JSDoc — thiếu là build docs fail
ImportPackage thư viện import tường minh từ vue, nuxt/app, h3; không dựa vào auto-import
LỗiBật loading → gán error.value = toAppError(e) → tắt loading trong finally; không để promise bị reject mà không ai bắt
Dùng sai APIthrow new Error('[@tasco/composables] … cách sửa')
State dùng chunguseState('tasco:<khu>:<tên>'), không Pinia
Kiểu dữ liệuinterface trong types.ts, field tuỳ chọn ?:, "không có" là T | null

Quy ước đầy đủ: Quy ước code.

Export và auto-import

  1. Export từ src/index.ts của package.
  2. App cần dùng không cần import thì thêm tên vào preset imports trong packages/nuxt-layer-base/nuxt.config.ts, và cập nhật bảng auto-import ở API của layer.
  3. Package build bằng unbuild: dependency runtime mới của @tasco/composables hay @tasco/ai mà app phải tự cung cấp (peer) thì thêm vào externals trong build.config.ts.

Không thêm vào auto-import những gì app ít dùng hoặc tên dễ trùng với code của dự án.

Test

ts
import { beforeEach, describe, expect, it, vi } from 'vitest'

// Mock Nuxt: vi.hoisted để biến mock có trước khi vi.mock chạy (vi.mock được đưa lên đầu file).
const mocks = vi.hoisted(() => ({
  runtimeConfig: { public: {} as { apiBaseURL?: string } },
  requestFetch: vi.fn(),
}))

vi.mock('nuxt/app', () => ({
  useRuntimeConfig: () => mocks.runtimeConfig,
  useRequestFetch: () => mocks.requestFetch,
}))

import { useApi } from './useApi'

describe('useApi', () => {
  beforeEach(() => {
    mocks.requestFetch.mockReset()
  })

  it('không cấu hình baseURL → gọi qua /api', async () => {
    await useApi()('/users')
    expect(mocks.requestFetch).toHaveBeenCalledWith('/users', expect.objectContaining({ baseURL: '/api' }))
  })
})
  • vitest.config.ts của composablesai alias @tasco/utils về source — không cần build utils trước khi test.
  • Ngưỡng coverage: utils 95%, composablesai 90% dòng. File chỉ là glue không unit test được (vd usePermission.ts) nằm trong danh sách exclude — thêm vào đó phải có lý do trong comment.

Chi tiết: Test & coverage.

Tài liệu

  • Trang Tra cứu sinh từ TSDoc — chỉ cần JSDoc đầy đủ. pnpm build chạy gen-api.mjs --strict và fail nếu export hoặc giá trị trả về của use* thiếu mô tả.
  • Cách dùng không hiển nhiên (phối hợp nhiều composable, bẫy về lỗi hay SSR) thì viết thêm trang hoặc mục trong khu Hướng dẫn.
  • Changeset minor cho package có export mới.

Checklist MR

  • [ ] Đúng package theo bảng phụ thuộc; không import ui từ composables, không import Vue/Nuxt từ utils.
  • [ ] Lõi thuần tách khỏi phần gọi Nuxt, có test trực tiếp.
  • [ ] JSDoc cho export, tuỳ chọn và từng giá trị trả về.
  • [ ] Export ở index.ts; auto-import nếu cần, kèm cập nhật bảng ở trang API của layer.
  • [ ] Test đạt ngưỡng coverage.
  • [ ] Changeset.

Liên quan