返回博客

如何用 Vue.js 和实时汇率 API 构建货币转换器(2026 指南)

V
Vlado Grigirov
April 28, 2026
Currency API Vue.js Tutorial Exchange Rates JavaScript Finexly Composition API

如果你需要一个从实时 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 dev

Vite 提供了开箱即用的 Vue 3、TypeScript 和热模块替换。打开 src/App.vue,清空脚手架代码 —— 我们很快会用转换器替换它。

如果你将其集成到现有的 Nuxt 3 项目中,可以跳过此步。下面的组合式函数在 Nuxt 中工作方式相同,因为它使用了 vue 中标准的 refcomputed

步骤 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:构建 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 一致性。改用 useFetchuseAsyncData

长会话后的过期汇率。 上面的 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 次免费请求开始,等流量超出免费版再升级。

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 →