如果你需要一个从实时 API 拉取实时汇率的 Vue 货币转换器,这篇教程将手把手带你完成。你将使用 Composition API、TypeScript 和一个免费的汇率 API,构建一个生产就绪的 Vue 3 组件 —— 包含适当的防抖、缓存、错误处理以及兼容 Nuxt SSR 的模式。
完成后,你将拥有一个可重用的 <CurrencyConverter /> 组件、一个可放入任何 Vue 3 项目的 useCurrencyRates 组合式函数,以及对在真实用户环境中部署货币转换功能时各种取舍的清晰理解。
你将构建什么
一个具备以下功能的 Vue 3 货币转换器:
- 通过 Finexly API 提供 170+ 种货币 的实时汇率
- 用户输入时即时响应的转换
- 货币交换按钮(USD → EUR 一键变成 EUR → USD)
- 防抖的 API 调用,避免占用网络
- 内存缓存,降低延迟并保持在免费配额内
- 错误处理、加载状态和完善的 TypeScript 类型
- SSR 友好,在 Nuxt 3 中无水合不一致
最终的组件大约 80 行代码。组合式函数大约 60 行。仅此而已。
前置条件
你需要熟悉:
- Vue 3 语法(
<script setup>形式) - 基本的 TypeScript
- 用
fetch调用 REST API
你还需要一个 Finexly API 密钥。从控制台获取 —— 大约 30 秒,免费版每月提供 1,000 次请求,无需信用卡。如果你从未用过该服务,Finexly API 文档有 5 分钟的快速入门。
步骤 1:用 Vite 搭建 Vue 3 项目
如果从零开始:
npm create vite@latest finexly-converter -- --template vue-ts
cd finexly-converter
npm install
npm run devVite 提供了开箱即用的 Vue 3、TypeScript 和热模块替换。打开 src/App.vue,清空脚手架代码 —— 我们很快会用转换器替换它。
如果你将其集成到现有的 Nuxt 3 项目中,可以跳过此步。下面的组合式函数在 Nuxt 中工作方式相同,因为它使用了 vue 中标准的 ref 和 computed。
步骤 2:安全存储 API 密钥
绝不要把 Finexly API 密钥直接粘贴到组件里。把它放到 .env.local:
VITE_FINEXLY_API_KEY=your_key_hereVite 会把任何带 VITE_ 前缀的变量暴露给客户端。对于 Nuxt,使用 NUXT_PUBLIC_FINEXLY_API_KEY,并通过 useRuntimeConfig().public.finexlyApiKey 读取。
如果你担心密钥暴露在客户端打包文件中,可以通过后端路由或无服务器函数代理请求。我们将在步骤 7 中展示该模式。
步骤 3:构建 useCurrencyRates 组合式函数
组合式函数是转换器的核心。它负责请求、缓存以及加载/错误状态 —— 让组件保持纯展示。
创建 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'
// 模块级缓存:在所有调用此组合式函数的组件之间共享。
// 每条记录存活 5 分钟 —— 足够即时,又足够及时反映外汇剧烈波动。
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故意放在模块级。两个调用此组合式函数的组件共享同一个缓存,这意味着切换路由不会重复拉取相同的基准货币。rates类型化为Record<string, number>,可以直接喂给后面的<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" 货币会触发防抖的重新请求。
步骤 5:交叉汇率转换(避免浪费请求)
如果你只想从一个基准货币转换,可以跳过本节。但大多数转换器允许用户选择两边 —— 而朴素实现会在每次切换 "from" 时重新拉取。
交叉汇率数学解决了这个问题。如果你以 USD 为基准并且知道 EUR 和 JPY 相对 USD 的汇率,那么 EUR → JPY 汇率就是 (1 / EUR_rate) * JPY_rate。这正是步骤 3 中 convert 函数所做的:
return (amount / fromRate) * toRate这意味着每个会话只需请求一次。对于免费版配额来说是巨大的胜利。from 上的监听器变成了一种防御措施 —— 如果用户选择了缓存汇率表中没有的小众货币,使用该基准重新请求可保证转换继续工作。
步骤 6:加载骨架屏与错误状态
加载动画可以,但加载汇率时的布局抖动让人难受。在首次拉取时渲染占位下拉项:
<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 分开处理。配合 Finexly 每月 1,000 次免费请求和 5 分钟缓存,你需要一次真正的流量峰值才会触及上限 —— 但一个干净的重试路径会让 UI 显得稳健。
步骤 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}`
)
})然后让组合式函数指向 /api/rates?base=${base},而不是 Finexly 源。密钥永远不离开服务器。同样的模式在任何 Vue 元框架中都适用 —— Nuxt、Quasar 或普通的 Express 后端。
步骤 8:生产环境检查清单
发布前:
- 积极缓存。 外汇汇率不会每秒变化。客户端 5 分钟 TTL 加服务端 1 分钟 TTL 对几乎所有展示用例都绰绰有余。详见我们的缓存与错误处理指南。
- 正确舍入。 货币显示按其最小单位舍入(USD 2 位、JPY 0 位、KWD 3 位)。使用
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而不是onMounted中的原始fetch,这样汇率在服务器渲染期间可用,不会引发水合不一致。 - 监控配额。 添加一个小的日志器,在月度请求预算用完 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 集成指南介绍了与框架无关的模式。
常见陷阱(以及如何避免)
Nuxt 中的水合不一致。 在 onMounted 里调用 fetch 在客户端工作,但会破坏 SSR 一致性。改用 useFetch 或 useAsyncData。
长会话后的过期汇率。 上面的 5 分钟 TTL 意味着开了一整天的标签页显示的是上小时的汇率。监听 visibilitychange 来刷新:
document.addEventListener('visibilitychange', () => {
if (!document.hidden) fetchRates(from.value)
})浮点漂移。 JavaScript 数字是双精度。0.1 + 0.2 !== 0.3。对于货币金额,先乘到整数分(amount * 100),做完运算再除回去。或对任何与结账相关的内容使用 dinero.js 这样的库。
开发中的 CORS 错误。 一些货币 API 不允许直接的浏览器调用。Finexly 允许浏览器源用于客户端使用;步骤 7 中的代理可以解决其他问题。
为何 Vue 项目选择 Finexly
为前端应用挑选汇率 API时,有几件事很重要:响应时间、准确性、免费版的慷慨程度,以及干净的 JSON。Finexly 在这四点上都努力 —— sub-50ms 的 p95 延迟、每 60 秒更新一次的中间市场汇率、170+ 种货币,以及无需任何整理就能直接落入 Vue ref 的 JSON 形态。
如果你想看它与替代品的对比,我们的货币 API 对比详细讲解了取舍。或使用并排对比页。
常见问题
这篇教程能用 Vue 2 吗?
可以。组合式函数模式仅限 Vue 3,但底层逻辑 —— 拉取汇率、存到 data、计算转换 —— 在 Options API 中工作方式相同。上面的 Vue 2 例子可以直接替换。
Finexly API 对 Vue 项目免费吗?
免费版每月 1,000 次请求,对于业余项目、作品集或小型 SaaS 来说绰绰有余。配合 5 分钟缓存,大约能支撑 200 个日活用户。更高流量见价格方案。
如何避免 API 密钥暴露在客户端打包中?
按步骤 7 那样把请求代理到服务器路由。VITE_* 和 NUXT_PUBLIC_* 前缀都让变量对客户端可见。任何敏感的东西都应放在服务器函数后面。
汇率有多准确?
Finexly 聚合多个 Tier-1 流动性提供商的中间市场汇率,每 60 秒刷新一次。对于展示和大多数定价应用足够准确。对于交易执行,你需要流式 feed —— 见我们的 REST vs WebSocket 指南。
我可以转换历史金额吗?
可以 —— Finexly 的 /historical 端点接受日期参数并返回任何工作日的汇率。模式与上面的 /latest 端点相同;只需更换 URL。历史汇率 API 指南有详细介绍。
结尾
你现在拥有了一个处理真实问题的 Vue 3 货币转换器 —— 缓存、防抖、错误状态、SSR 和 API 密钥安全。组合式函数模式意味着你可以把同一个 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 →