Giao diện
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/utils | toAppError, parseTableSettings |
Vue và Nuxt (useState, useRuntimeConfig, useCookie…), không cần antdv | @tasco/composables | useApi, useTable |
antdv (Modal, message…) | @tasco/ui | useConfirm, useErrorHandler |
| Chạy khi app khởi động, route server, middleware | @tasco/nuxt-layer-base | plugin 04.error, server/utils/session.ts |
| Chỉ phục vụ tính năng AI | @tasco/ai | useAiChat |
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 ước | Chi tiết |
|---|---|
| Tên | use* 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 |
| Import | Package thư viện import tường minh từ vue, nuxt/app, h3; không dựa vào auto-import |
| Lỗi | Bậ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 API | throw new Error('[@tasco/composables] … cách sửa') |
| State dùng chung | useState('tasco:<khu>:<tên>'), không Pinia |
| Kiểu dữ liệu | interface trong types.ts, field tuỳ chọn ?:, "không có" là T | null |
Quy ước đầy đủ: Quy ước code.
Export và auto-import
- Export từ
src/index.tscủa package. - App cần dùng không cần
importthì thêm tên vào presetimportstrongpackages/nuxt-layer-base/nuxt.config.ts, và cập nhật bảng auto-import ở API của layer. - Package build bằng unbuild: dependency runtime mới của
@tasco/composableshay@tasco/aimà app phải tự cung cấp (peer) thì thêm vàoexternalstrongbuild.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.tscủacomposablesvàaialias@tasco/utilsvề source — không cần buildutilstrước khi test.- Ngưỡng coverage:
utils95%,composablesvàai90% dòng. File chỉ là glue không unit test được (vdusePermission.ts) nằm trong danh sáchexclude— 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 buildchạygen-api.mjs --strictvà fail nếu export hoặc giá trị trả về củause*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
minorcho package có export mới.
Checklist MR
- [ ] Đúng package theo bảng phụ thuộc; không import
uitừ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
- Kiến trúc nội bộ — ràng buộc phụ thuộc giữa package.
- Thêm component C* — khi cần giao diện.
- @tasco/composables — API hiện có.