این راهنما یک پروژه واقعی برای اتصال Vue به وبسرویس و API سرویکس است؛ نه یک آموزش عمومی که فقط اسم موجودیتها را عوض کرده باشد. پروژه تستشده در docs/examples/vue-pinia از قرارداد واقعی GET /api/v1/assets?codes=...، احراز هویت با X-API-Key، نام فیلدهای سرویکس، رفتار خطا، retry محدود و کش مشترک بکاند استفاده میکند و از یک checkout تمیز قابل اجراست.
مرورگر مستقیم سراغ endpoint احرازشده سرویکس نمیرود. Vue فقط route هممبدأ /api/quotes را صدا میزند. proxy مبتنی بر Node، مقدار SERVIX_API_KEY را از environment سرور میخواند، کد نمادها را با allowlist محدود میکند و پاسخ احرازشده را به همان فیلدهایی تبدیل میکند که این اپلیکیشن لازم دارد.
اول پروژه کامل را اجرا کنید
Node.js 22 یا جدیدتر لازم است. repository را clone کنید و دقیقاً همین دستورهای تستشده را اجرا کنید:
cd docs/examples/vue-pinia
npm ci
cp .env.example .env
# Set SERVIX_API_KEY only in .env on your server.
npm test
npm run build
npm run dev
نشانی http://127.0.0.1:5175 را باز کنید. launcher، Vite و API proxy هممبدأ را با هم بالا میآورد. lockfile نصب را تکرارپذیر میکند. تستها typed client، تغییر state در Pinia، allowlist، header احراز هویت، اعتبارسنجی پاسخ، کش، رفتار 429، retry خطاهای منتخب 5xx، headerهای امنیتی و خطاهای امن را پوشش میدهند.
مرز امنیتی را درست بچینید
| لایه | مسئولیت | چیزی که نباید داشته باشد |
|---|---|---|
| کامپوننت Vue | نمایش داده انتخابی و حالتهای قابلفهم loading، empty و error. | API Key یا پاسخ نامحدود سرویکس. |
| store در Pinia | نگهداری کدهای انتخابی، چرخه درخواست، داده اعتبارسنجیشده و request ID امن. | environment سرور یا credential. |
| client هممبدأ | فقط /api/quotes?codes=... را صدا میزند و DTO اپلیکیشن را بررسی میکند. | URL مستقیم API احرازشده مشتری. |
| proxy در Node | اعمال allowlist، افزودن X-API-Key، اعتبارسنجی، کش و تبدیل خطا. | کلید در URL، body، log یا پاسخ مرورگر. |
این secret را با پیشوند VITE_ نسازید. Vite متغیرهای دارای این پیشوند را عمداً وارد کد قابلدسترسی مرورگر میکند. در production از secret manager استفاده کنید و SERVIX_API_KEY را فقط به process بکاند بدهید.
Pinia را در ورودی Vue نصب کنید
فایل واقعی src/main.ts پیش از mount شدن اپ، یک instance از Pinia میسازد:
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
یک client تایپشده و هممبدأ بسازید
src/api/marketQuotes.ts فقط کدهای انتخابی و مجاز را serialize میکند، credential را در همان origin نگه میدارد، فیلدهای پایدار خطا را map میکند و پاسخ موفقِ خراب را نمیپذیرد:
export async function fetchMarketQuotes(
codes: readonly AssetCode[],
signal?: AbortSignal,
): Promise<MarketQuoteResponse> {
const query = new URLSearchParams({ codes: codes.join(',') })
const response = await fetch('/api/quotes?' + query, {
headers: { Accept: 'application/json' },
credentials: 'same-origin',
signal,
})
const payload: unknown = await response.json().catch(() => null)
if (!response.ok) {
const problem = readProblem(payload)
throw new MarketQuotesError(
problem.message ?? 'Servix data is temporarily unavailable.',
problem.code ?? 'REQUEST_FAILED',
response.status,
problem.requestId,
)
}
return parseQuoteResponse(payload)
}
فایل کامل، code، label، quoteUnit، عدد متناهی value و businessTime دارای timezone را بررسی میکند. status برابر 200 بهتنهایی دلیل قابلاعتماد بودن JSON نیست.
چرخه درخواست را با setup store در Pinia مدیریت کنید
این store با primitiveهای Composition API نوشته شده است. refresh قبلی cancel میشود، انتخاب خالی درخواست اضافه نمیفرستد و در state خطا فقط پیام امن و request ID قرار میگیرد:
export const useMarketQuotesStore = defineStore('marketQuotes', () => {
const selectedCodes = ref<AssetCode[]>(['USD_RLS', 'EUR_RLS'])
const quotes = ref<MarketQuote[]>([])
const status = ref<QuoteLoadStatus>('idle')
const errorMessage = ref<string | null>(null)
const requestId = ref<string | null>(null)
const loadedAt = ref<string | null>(null)
let activeRequest: AbortController | null = null
const isLoading = computed(() => status.value === 'loading')
const hasSelection = computed(() => selectedCodes.value.length > 0)
function toggleCode(code: AssetCode): void {
selectedCodes.value = selectedCodes.value.includes(code)
? selectedCodes.value.filter((selected) => selected !== code)
: ASSET_CODES.filter((candidate) => [...selectedCodes.value, code].includes(candidate))
}
async function load(): Promise<void> {
activeRequest?.abort()
if (!hasSelection.value) {
quotes.value = []
status.value = 'empty'
return
}
const request = new AbortController()
activeRequest = request
status.value = 'loading'
errorMessage.value = null
requestId.value = null
try {
const response = await fetchMarketQuotes(selectedCodes.value, request.signal)
quotes.value = response.quotes
loadedAt.value = new Date().toISOString()
status.value = response.quotes.length > 0 ? 'ready' : 'empty'
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') return
quotes.value = []
status.value = 'error'
errorMessage.value =
error instanceof Error ? error.message : 'Servix data is temporarily unavailable.'
requestId.value = error instanceof MarketQuotesError ? (error.requestId ?? null) : null
} finally {
if (activeRequest === request) activeRequest = null
}
}
function cancel(): void {
activeRequest?.abort()
activeRequest = null
}
return {
selectedCodes,
quotes,
status,
errorMessage,
requestId,
loadedAt,
isLoading,
hasSelection,
toggleCode,
load,
cancel,
}
})
رفتار lifecycle را در یک composable جمع کنید
این composable کد setup کامپوننت را کوتاه نگه میدارد، با storeToRefs reactivity را حفظ میکند، هنگام mount داده را میگیرد و موقع dispose درخواست باز را میبندد:
export function useMarketQuotes() {
const store = useMarketQuotesStore()
const state = storeToRefs(store)
onMounted(() => void store.load())
onScopeDispose(() => store.cancel())
return {
...state,
toggleCode: store.toggleCode,
refresh: store.load,
}
}
App.vue از همین composable استفاده میکند و checkbox نمادها، دکمه refresh دستی، اعلان loading و error، کارت داده، واحد روشن و عنصر معنایی time را نشان میدهد. برای داده گمشده هیچوقت مقدار صفر جایگزین نمیشود.
قرارداد واقعی سرویکس را در بکاند صدا بزنید
proxy تستشده در Node، URL قیمتهای انتخابی را میسازد، API Key را فقط در header میفرستد، timeout دارد، فقط خطای شبکه و 5xxهای منتخب را retry میکند، روی اتمام سهمیه همانجا متوقف میشود و پیش از کش کردن پاسخ آن را اعتبارسنجی میکند:
async function requestQuotes(codes) {
const url = new URL('/api/v1/assets', normalizedBaseUrl)
url.searchParams.set('codes', codes.join(','))
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
let response
try {
response = await fetchImpl(url, {
headers: {
Accept: 'application/json',
'X-API-Key': normalizedApiKey,
},
redirect: 'error',
signal: AbortSignal.timeout(timeoutMs),
})
} catch (error) {
if (attempt === maximumAttempts) {
throw new ServixProxyError('Servix could not be reached.', {
code: 'SERVIX_NETWORK_ERROR',
status: 503,
cause: error,
})
}
await sleep(retryDelay(attempt))
continue
}
const requestId = response.headers.get('x-request-id')
if (response.ok) {
const payload = await response.json().catch(() => null)
return validateQuotes(payload, codes, requestId)
}
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maximumAttempts) {
await sleep(retryDelay(attempt))
continue
}
throw mapUpstreamError(response.status, requestId)
}
throw new ServixProxyError('Servix could not be reached.')
}
server کامل، کلید دارای فاصله را رد میکند، بهجز تست loopback فقط HTTPS را میپذیرد، دسترسی را به USD_RLS، EUR_RLS و GOLD_18_RLS محدود میکند، ترتیب درخواست را نگه میدارد و برای مرورگر Cache-Control: no-store میفرستد. کش موفق در حافظه همان process بهصورت پیشفرض 30 ثانیه میان درخواستها مشترک است.
پاسخ احرازشده را دقیق نمایش دهید
این صفحه عمومی بهجای نرخ واقعی از placeholder استفاده میکند:
{
"code": "USD_RLS",
"label": "US dollar / Iranian rial",
"quoteUnit": "RLS",
"value": "<protected-market-value>",
"businessTime": "<source-timestamp>"
}
quoteUnitبخشی از قرارداد است؛ واحد ریال، دلار یا شاخص را از روی label حدس نزنید.businessTimeزمان مشاهده بازار است، نه زمان refresh شدن مرورگر.- status برابر 429 یعنی سهمیه روزانه تمام شده است. refresh خودکار را متوقف کنید و retry loop نسازید.
- allowlist را به همان چند نمادی محدود کنید که محصول شما واقعاً نشان میدهد. proxy را به mirror نامحدود API تبدیل نکنید.
به snippet اعتماد نکنید؛ اتصال را تست کنید
npm test تستهای قطعی را با داده ساختگی و بدون credential واقعی اجرا میکند. بعد از تنظیم یک API Key تستی در سمت سرور، smoke test احرازشده را جداگانه اجرا کنید:
npm run smoke:real
این فرمان همین proxy را روی یک port موقت loopback بالا میآورد و از مسیر /api/quotes به سرویکس میرسد. عددی بودن فیلد محافظتشده بررسی میشود، اما خروجی فقط کد نماد، واحد، business time و نتیجه boolean را نشان میدهد؛ نه API Key و نه نرخ بازار.
همین مرز را در production نگه دارید
- در CI دستورهای
npm ci،npm testوnpm run buildرا اجرا کنید. SERVIX_API_KEYرا از secret manager به service مبتنی بر Node تزریق کنید.- مرورگر و proxy را روی یک origin امن HTTPS نگه دارید؛ اگر معماری شما origin جدا میخواهد، CORS را دقیق و محدود تنظیم کنید.
- اگر چند instance از Node دارید، برای استفاده بهینه از سهمیه یک کش خارجی مشترک در نظر بگیرید.
- کد خطای امن و request ID را پایش کنید؛ credential، response body یا نرخ بازار را وارد log قابلدیدن کاربر نکنید.
احراز هویت با API Key، endpointهای قیمت جاری، نمادهای پشتیبانیشده و خطا و سهمیه را هم ببینید. نمونههای Python، Spring Boot و PHP برای مقایسه در دسترساند.
ساخت حساب و فعالسازی دسترسی API · مقایسه پلنها و سهمیه روزانه