在个人理财类应用中,YNAB(You Need A Budget)的「你需要预算」方法强调给每一笔收入分配任务。将这套逻辑工程化地接入 Vue 3 项目,核心不在于画图表,而在于如何稳定拉取预算数据、将服务端模型转为前端响应式状态,以及让用户在离线时仍能查看上月规划。本文从请求层封装、数据建模、状态管理三个角度展开。

封装自治的 YNAB 请求层
YNAB 开放接口使用个人访问令牌(Personal Access Token),所有请求必须在头部携带 Authorization: Bearer <token>。如果在每个组件里直接用 fetch,令牌泄露风险和重复代码都会失控。更合理的做法是在 Vue 3 项目内建立一个独立的请求模块,基于 axios 实例统一拦截。
该模块应读取环境变量中的令牌,在请求拦截器里注入,同时在响应拦截器处理 401 与限流。YNAB 免费版每分钟限流 200 次,超限返回 429,此时应做退避重试而非直接报错。下面代码展示最小可用封装:
import axios from 'axios';
const ynabClient = axios.create({
baseURL: 'https://api.ynab.com/v1',
timeout: 10000
});
ynabClient.interceptors.request.use((config) => {
const token = import.meta.env.VITE_YNAB_TOKEN;
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
});
ynabClient.interceptors.response.use(
(res) => res,
(error) => {
if (error.response && error.response.status === 429) {
return new Promise((resolve) => {
setTimeout(() => resolve(ynabClient(error.config)), 1000);
});
}
return Promise.reject(error);
}
);
export default ynabClient;
这种封装让组件完全不感知令牌与限流,也便于在测试时替换为本地 mock。若后续要支持多用户,只需把令牌从环境量改为函数式获取,其余逻辑不动。
此外,YNAB 接口返回结构统一为 data 包裹,例如预算列表在 data.budgets。请求层可再包一层 getBudgets() 方法,直接返回业务数组,屏蔽传输细节,使上层调用更像调用本地服务。
将预算三层结构映射为 TypeScript 模型
YNAB 的数据是预算(budget)→ 账户(account)→ 分类(category)三层。预算对象含 month 字段表示月份,分类下有 budgeted 与 activity。如果不加类型约束,前端很容易把 budgeted 当可用余额,造成逻辑错误。
用接口描述能显著减少联调成本。下面给出核心类型,注意金额单位在 YNAB 中为毫分(milliunits),展示时需除以 1000:
export interface YnabCategory {
id: string;
name: string;
budgeted: number;
activity: number;
balance: number;
}
export interface YnabMonthSummary {
month: string;
categories: YnabCategory[];
}
export interface YnabBudget {
id: string;
name: string;
months: YnabMonthSummary[];
}
在组件内展示时,应写纯函数做格式转换,例如 formatAmount(milli: number) 返回带两位小数的字符串。这样模板中只调用格式化结果,不写运算,避免逻辑散落。对于「你需要预算」的核心理念,前端可突出展示未分配收入,提醒用户给每笔钱安排分类。
工程上建议把转换函数收口在 utils/ynab.ts,并补充单元测试。当 YNAB 接口字段调整时,类型报错会第一时间定位到映射层,而不是埋藏在某个.vue 文件里。
用 Pinia 管理预算状态与离线缓存
Vue 3 官方推荐 Pinia 做状态管理。预算数据拉取后存入 store,组件通过 storeToRefs 获得响应式引用。相比 provide/inject,Pinia 支持 DevTools 与时间旅行,方便排查「某月分类金额不对」类问题。
store 中除了 state,还应定义 loadMonth(budgetId, month) action,内部先查 localStorage 是否有缓存,命中则直接赋值,未命中再走请求层。这样用户在地铁里打开应用,仍能看到上月预算规划,符合「你需要预算」的随时规划习惯。
import { defineStore } from 'pinia';
import ynabClient from '@/api/ynab';
import { YnabBudget } from '@/types/ynab';
export const useBudgetStore = defineStore('budget', {
state: () => ({
current: null as YnabBudget | null,
loading: false
}),
actions: {
async loadBudget(budgetId: string) {
this.loading = true;
const cache = localStorage.getItem('ynab_' + budgetId);
if (cache) {
this.current = JSON.parse(cache);
this.loading = false;
return;
}
const res = await ynabClient.get('/budgets/' + budgetId);
this.current = res.data.data.budget;
localStorage.setItem('ynab_' + budgetId, JSON.stringify(this.current));
this.loading = false;
}
}
});
缓存策略要注意敏感数据,若设备可能公用,应加密或仅缓存非金额摘要。Pinia 的 action 也可结合 watch 做自动刷新,比如用户在前端修改了某分类预算,先乐观更新 state,再调 PATCH 接口,失败则回滚并提示。
最终页面结构可以是:左侧分类树、右侧当月预算环形图,底部输入框快速分配未指定资金。所有数据来自同一 store,任意组件改动都会即时反映,实现真正的工程化「你需要预算」体验。
Vue3YNABbudget_api修改时间:2026-08-15 13:24:15