إذا كنت تحتاج محول عملات بـ Vue يجلب أسعار الصرف في الوقت الفعلي من API حي، فهذا الدرس يأخذك من البداية للنهاية. ستبني مكوّن Vue 3 جاهزاً للإنتاج باستخدام Composition API و TypeScript و API مجاني لأسعار الصرف — مع debouncing مناسب وتخزين مؤقت ومعالجة أخطاء وأنماط متوافقة مع SSR في Nuxt.
في النهاية ستحصل على مكوّن <CurrencyConverter /> قابل لإعادة الاستخدام، وعلى composable useCurrencyRates يمكنك إدراجه في أي مشروع Vue 3، وعلى فهم واضح للمقايضات المهمة عند تقديم تحويل العملات لمستخدمين فعليين.
ماذا ستبني
محول عملات بـ Vue 3 بهذه الميزات:
- أسعار صرف حية لأكثر من 170 عملة عبر Finexly API
- تحويل تفاعلي يتحدّث أثناء الكتابة
- زر تبديل العملات (USD → EUR يصبح EUR → USD بنقرة واحدة)
- استدعاءات API مع debounce حتى لا نرهق الشبكة
- ذاكرة تخزين مؤقت داخلية لإبقاء التأخير منخفضاً والبقاء داخل حصة الخطة المجانية
- معالجة الأخطاء وحالات التحميل وأنواع TypeScript نظيفة
- صديق لـ SSR ليعمل في Nuxt 3 دون مشاكل hydration
المكوّن النهائي حوالي 80 سطراً. الـ composable 60 سطراً إضافياً. هذا كل شيء.
المتطلبات الأساسية
ينبغي أن تكون مرتاحاً مع:
- صيغة Vue 3 (شكل
<script setup>) - TypeScript الأساسي
- استدعاء REST API بـ
fetch
ستحتاج أيضاً مفتاح Finexly API. احصل عليه من لوحة التحكم — يستغرق نحو 30 ثانية، والخطة المجانية تمنحك 1,000 طلب شهرياً بدون بطاقة ائتمان. إن لم تستخدم الخدمة من قبل، فإن توثيق Finexly API يحوي بدء سريع لـ 5 دقائق.
الخطوة 1: إنشاء مشروع Vue 3 بـ Vite
إذا كنت تبدأ من الصفر:
npm create vite@latest finexly-converter -- --template vue-ts
cd finexly-converter
npm install
npm run devVite يمنحك Vue 3 و TypeScript و hot module reload جاهزاً. افتح src/App.vue وامسح القالب الجاهز — سنستبدله بالمحول قريباً.
إذا كنت تدمجه في مشروع Nuxt 3 قائم، يمكنك تخطي هذه الخطوة. الـ composable أدناه يعمل بصورة متطابقة في Nuxt لأنه يستخدم ref و computed القياسيتين من vue.
الخطوة 2: تخزين مفتاح API بأمان
لا تلصق مفتاح Finexly API مباشرة في مكوّن. ضعه في .env.local:
VITE_FINEXLY_API_KEY=your_key_hereيكشف Vite أي متغير يبدأ بـ VITE_ للعميل. في Nuxt استخدم NUXT_PUBLIC_FINEXLY_API_KEY واقرأه من useRuntimeConfig().public.finexlyApiKey.
إن كنت قلقاً من تسرب المفتاح في حزمة العميل، مرر الطلب عبر مسار خلفي أو دالة بدون خادم. سنعرض هذا النمط في الخطوة 7.
الخطوة 3: بناء composable useCurrencyRates
الـ composable هو قلب المحول. يتولى الجلب والتخزين المؤقت وحالة التحميل/الخطأ — مما يبقي المكوّن عرضياً.
أنشئ src/composables/useCurrencyRates.ts:
import { ref, type Ref } from 'vue'
const API_KEY = import.meta.env.VITE_FINEXLY_API_KEY
const BASE_URL = 'https://api.finexly.com/v1'
// تخزين مؤقت على مستوى الوحدة: مشترك بين كل المكوّنات التي تستدعي هذا الـ composable.
// كل مدخل يعيش 5 دقائق — طويل بما يكفي ليبدو فورياً، وقصير بما يكفي ليبقى دقيقاً
// في تحركات FX المتقلبة.
const cache = new Map<string, { rates: Record<string, number>; expires: number }>()
const TTL_MS = 5 * 60 * 1000
interface RatesResponse {
base: string
date: string
rates: Record<string, number>
}
export function useCurrencyRates() {
const rates: Ref<Record<string, number>> = ref({})
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchRates(base: string) {
const cached = cache.get(base)
if (cached && cached.expires > Date.now()) {
rates.value = cached.rates
return
}
loading.value = true
error.value = null
try {
const url = `${BASE_URL}/latest?base=${base}&apikey=${API_KEY}`
const res = await fetch(url)
if (!res.ok) throw new Error(`Finexly returned ${res.status}`)
const data: RatesResponse = await res.json()
rates.value = data.rates
cache.set(base, { rates: data.rates, expires: Date.now() + TTL_MS })
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load rates'
} finally {
loading.value = false
}
}
function convert(amount: number, from: string, to: string): number {
if (from === to) return amount
const fromRate = rates.value[from]
const toRate = rates.value[to]
if (!fromRate || !toRate) return 0
return (amount / fromRate) * toRate
}
return { rates, loading, error, fetchRates, convert }
}تفاصيل تستحق الإشارة:
- يعيش
cacheعلى مستوى الوحدة عمداً. مكوّنان يستدعيان الـ composable يتشاركان نفس الكاش، ما يعني أن تبديل المسارات لن يعيد جلب نفس عملة الأساس. - النوع
Record<string, number>لـratesيسمح بتغذيته مباشرة في<select>لاحقاً. - تقوم دالة
convertبحسابات سعر متقاطع، لذا لست مضطراً لإعادة الجلب كلما تغيرت عملة "from". المزيد في الخطوة 5.
الخطوة 4: مكوّن المحول
أنشئ src/components/CurrencyConverter.vue:
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useCurrencyRates } from '@/composables/useCurrencyRates'
const { rates, loading, error, fetchRates, convert } = useCurrencyRates()
const amount = ref(100)
const from = ref('USD')
const to = ref('EUR')
const converted = computed(() =>
convert(amount.value, from.value, to.value).toFixed(2)
)
const currencies = computed(() => Object.keys(rates.value).sort())
function swap() {
;[from.value, to.value] = [to.value, from.value]
}
let timeout: ReturnType<typeof setTimeout> | null = null
watch(from, (newBase) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => fetchRates(newBase), 300)
})
onMounted(() => fetchRates(from.value))
</script>
<template>
<div class="converter">
<h2>Currency Converter</h2>
<div class="row">
<input v-model.number="amount" type="number" min="0" />
<select v-model="from">
<option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
</select>
</div>
<button @click="swap" aria-label="Swap currencies">⇅</button>
<div class="row">
<output>{{ loading ? '...' : converted }}</output>
<select v-model="to">
<option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
</select>
</div>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>ضع <CurrencyConverter /> في App.vue وستحصل على محول يعمل. تمتلئ قائمة العملات بمجرد عودة أول طلب. الكتابة في حقل المبلغ تحدّث النتيجة تفاعلياً دون لمس الشبكة. التبديل يعيد ترتيب القوائم المنسدلة. تغيير عملة "from" يفجّر إعادة جلب debounced.
الخطوة 5: التحويل بالسعر المتقاطع (لتفادي حرق الطلبات)
إذا كنت تحوّل من قاعدة واحدة فقط، يمكنك تخطي هذا القسم. لكن أغلب المحولات تترك للمستخدم اختيار الجانبين — والتنفيذ الساذج يعيد الجلب عند كل تغيير لـ "from".
تحل حسابات السعر المتقاطع هذه المشكلة. إذا كانت لديك USD كقاعدة وتعرف سعري EUR و JPY مقابل USD، فإن سعر EUR → JPY يساوي ببساطة (1 / EUR_rate) * JPY_rate. هذا بالضبط ما تفعله دالة convert من الخطوة 3:
return (amount / fromRate) * toRateهذا يعني أنك تحتاج إلى جلب واحد فقط لكل جلسة. مكسب كبير لحصص الخطة المجانية. الـ watcher على from يصبح إجراء وقائي — إذا اختار المستخدم عملة نادرة غير موجودة في جدول الأسعار المخزن، فإن إعادة الجلب بتلك القاعدة تضمن استمرار التحويل.
الخطوة 6: هياكل التحميل وحالات الخطأ
الدوّارات لا بأس بها، لكن تغيّر التخطيط أثناء تحميل الأسعار مزعج. ارسم خيارات placeholder عند أول جلب:
<select v-model="from" :disabled="loading && currencies.length === 0">
<option v-if="currencies.length === 0">Loading...</option>
<option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
</select>في مسار الخطأ، أظهر زر إعادة المحاولة بدلاً من رسالة طريق مسدود:
<div v-if="error" class="error-box">
<p>{{ error }}</p>
<button @click="fetchRates(from)">Retry</button>
</div>عامل 429 (تجاوز معدل) و 5xx بشكل منفصل إن أردت أن تكون مهذباً. مع 1,000 طلب مجاني شهرياً وذاكرة 5 دقائق، تحتاج إلى ذروة حقيقية لتصل إلى الحد — لكن مسار إعادة محاولة نظيف يجعل الواجهة تبدو متينة.
الخطوة 7: إخفاء مفتاح API خلف وكيل خادم
كل ما هو في import.meta.env.VITE_* ينتهي في حزمة العميل. لمعظم أدوات العملات للقراءة فقط هذا مقبول — أسوأ سيناريو أن يكشط أحدهم الأسعار بمفتاحك. إن أردت دفاعاً متعدد الطبقات، مرّر الطلب عبر الخادم.
مع مسار خادم Nuxt 3:
// server/api/rates.get.ts
export default defineEventHandler(async (event) => {
const { base } = getQuery(event)
const config = useRuntimeConfig()
return await $fetch(
`https://api.finexly.com/v1/latest?base=${base}&apikey=${config.finexlyApiKey}`
)
})ثم وجّه الـ composable إلى /api/rates?base=${base} بدلاً من أصل Finexly. لا يغادر المفتاح الخادم أبداً. النمط نفسه يعمل في أي إطار وسيط لـ Vue — Nuxt أو Quasar أو Express خلفي بسيط.
الخطوة 8: قائمة فحص الإنتاج
قبل النشر:
- خزّن مؤقتاً بقوة. أسعار FX لا تتغير كل ثانية. TTL 5 دقائق على العميل و 1 دقيقة على الخادم أكثر من كافٍ لمعظم حالات العرض. راجع دليل التخزين المؤقت ومعالجة الأخطاء لأنماط مختبرة في الإنتاج.
- قرّب بصورة صحيحة. عرض العملة يقرّب إلى الوحدة الأصغر للعملة (خانتان لـ USD، 0 لـ JPY، 3 لـ KWD). استخدم
Intl.NumberFormat(locale, { style: 'currency', currency: to.value })بدلاً من.toFixed(2). - صيغ بـ
Intl.new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(123.45)يعطيك "€123.45" ويحترم لغة المستخدم. - جلب آمن لـ SSR. في Nuxt فضّل
useFetchعلىfetchخام داخلonMountedلتكون الأسعار متاحة أثناء عرض الخادم وتفادي عدم تطابق hydration. - راقب حصتك. أضف مسجّلاً صغيراً ينبّه عندما تستهلك 80% من ميزانية الطلبات الشهرية. للحجم الأكبر، تبدأ خطط الأسعار من حيث تنتهي الخطة المجانية.
نسخة Vue 2 (Options API)
إن كنت على Vue 2، فإن المنطق نفسه يترجم تقريباً سطراً بسطر:
export default {
data() {
return { rates: {}, loading: false, error: null, amount: 100, from: 'USD', to: 'EUR' }
},
computed: {
converted() {
const fr = this.rates[this.from]
const tr = this.rates[this.to]
return fr && tr ? ((this.amount / fr) * tr).toFixed(2) : '0.00'
},
currencies() {
return Object.keys(this.rates).sort()
},
},
mounted() { this.fetchRates(this.from) },
watch: {
from(newBase) { this.fetchRates(newBase) },
},
methods: {
async fetchRates(base) {
this.loading = true
try {
const res = await fetch(
`https://api.finexly.com/v1/latest?base=${base}&apikey=${process.env.VUE_APP_FINEXLY_KEY}`
)
const data = await res.json()
this.rates = data.rates
} catch (e) {
this.error = e.message
} finally {
this.loading = false
}
},
},
}Composition API أنظف لمشاركة المنطق بين المكوّنات، لكن Options API يعمل بصورة جيدة لأداة محول واحدة. إن فضّل فريقك نهجاً أكثر دالّية، فإن دليل تكامل JavaScript يغطي أنماطاً مستقلة عن الإطار.
مزالق شائعة (وكيف تتجنبها)
عدم تطابق Hydration في Nuxt. استدعاء fetch في onMounted يعمل على العميل لكنه يكسر اتساق SSR. استخدم useFetch أو useAsyncData بدلاً منه.
أسعار قديمة بعد جلسة طويلة. الـ TTL 5 دقائق أعلاه يعني أن علامة تبويب مفتوحة طوال اليوم تعرض أسعار آخر ساعة. حدّث عبر visibilitychange:
document.addEventListener('visibilitychange', () => {
if (!document.hidden) fetchRates(from.value)
})انجراف الفاصلة العائمة. الأرقام في JavaScript من نوع double. 0.1 + 0.2 !== 0.3. للمبالغ المالية، اضرب لتصبح أعداداً صحيحة بالسنتات (amount * 100)، احسب، ثم اقسم. أو استخدم مكتبة مثل dinero.js لأي شيء متعلق بالدفع.
أخطاء CORS أثناء التطوير. بعض واجهات API للعملات لا تسمح بالاستدعاءات المباشرة من المتصفح. Finexly تسمح بأصول المتصفح للاستخدام من جانب العميل؛ والوكيل من الخطوة 7 يحل البقية.
لماذا Finexly لمشاريع Vue
أمور تهم عند اختيار API لأسعار صرف العملات لتطبيق واجهة أمامية: زمن الاستجابة، الدقة، سخاء الخطة المجانية، JSON نظيف. تستهدف Finexly الأربعة — تأخير p95 أقل من 50 ms، أسعار mid-market تُحدَّث كل 60 ثانية، أكثر من 170 عملة، وشكل JSON يقع مباشرة في ref لـ Vue دون معالجة.
إن أردت معرفة كيف يقارن مع البدائل، يستعرض مقارنة واجهات API للعملات المقايضات بالتفصيل. أو استخدم صفحة المقارنة جنباً إلى جنب.
الأسئلة الشائعة
هل يمكنني استخدام هذا الدرس مع Vue 2؟
نعم. نمط الـ composable حصري لـ Vue 3، لكن المنطق الأساسي — جلب الأسعار وتخزينها في data وحساب التحويل — يعمل بصورة متطابقة في Options API. مثال Vue 2 أعلاه بديل مباشر.
هل Finexly API مجاني لمشاريع Vue؟
تعطيك الخطة المجانية 1,000 طلب شهرياً، وهو أكثر من كافٍ لمشروع جانبي أو قطعة محفظة أو SaaS صغير. مع تخزين 5 دقائق، يدعم نحو 200 مستخدم نشط يومياً. راجع خطط الأسعار لأحجام أكبر.
كيف أتفادى كشف مفتاح API في حزمة العميل؟
مرّر الطلب عبر مسار خادم كما في الخطوة 7. كل من البادئتين VITE_* و NUXT_PUBLIC_* يجعلان المتغيرات مرئية للعميل. أي شيء حساس يجب أن يقبع خلف دالة خادم.
كم تبلغ دقة الأسعار؟
تجمع Finexly أسعار mid-market من عدة مزودي سيولة من الفئة الأولى وتحدّث كل 60 ثانية. دقة كافية للعرض ومعظم تطبيقات التسعير. لتنفيذ الصفقات تحتاج إلى تدفق streaming — راجع دليل REST مقابل WebSocket.
هل يمكنني تحويل مبالغ تاريخية؟
نعم — يقبل نقطة النهاية /historical في Finexly معامل تاريخ ويرجع أسعار أي يوم عمل. النمط مطابق لنقطة /latest أعلاه؛ فقط بدّل الـ URL. يغطي دليل API لأسعار الصرف التاريخية ذلك بالتفصيل.
الخلاصة
لديك الآن محول عملات بـ Vue 3 يتعامل مع هموم العالم الحقيقي — التخزين المؤقت والـ debouncing وحالات الخطأ و SSR وأمن مفتاح API. نمط الـ composable يعني أنه يمكنك إسقاط نفس useCurrencyRates في عنصر شريط تنقل أو صفحة دفع أو رسم بياني للوحة معلومات دون إعادة كتابة منطق الجلب.
جاهز لتجربته في مشروعك؟ احصل على مفتاح Finexly API المجاني — بدون بطاقة ائتمان. ابدأ بـ 1,000 طلب مجاني شهرياً ورقّ فقط حين يتجاوز ترافيكك الخطة المجانية.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →