在 Vue 3 项目里接入 Gusto 的薪资与 HR 接口,本质上是把第三方 SaaS 能力通过前端工程化手段缝合进业务系统。Gusto 提供了基于 OAuth 2.0 的授权体系和一套覆盖员工、薪酬、工时、福利的 REST API,前端并不直连数据库,而是作为用户代理去换取访问令牌并调用资源接口。工程化重点不是写几个请求,而是把授权状态、令牌生命周期、错误重试、环境切换做成可复用的模块。
授权流程与令牌管理的工程化封装
Gusto 的 OAuth 流程要求前端把用户重定向到授权页,回调后拿授权码换 access_token 与 refresh_token。在 Vue 3 中,我们通常用组合式函数(composable)把这层逻辑收口,避免在每个页面里散落重定向代码。令牌不能明文存 localStorage 的长期风险较高,但纯前端工程里常见做法是加密后存,或借助后端代理把 refresh 动作放在服务端。无论如何,前端需要维护一个响应式的 token 状态,供请求层读取。
下面这段 composable 展示了如何用 ref 保存令牌,并在应用启动时尝试从会话存储恢复。注意 Gusto 的 token 过期时间较短,工程上必须配合静默刷新。我们把刷新逻辑抽成独立函数,在 axios 拦截器里捕获 401 后调用,避免业务组件感知授权细节。
import { ref } from 'vue';
const accessToken = ref('');
const refreshToken = ref('');
export function useGustoAuth() {
function loadFromSession() {
const t = sessionStorage.getItem('gusto_at');
const r = sessionStorage.getItem('gusto_rt');
if (t) accessToken.value = t;
if (r) refreshToken.value = r;
}
function saveTokens(at, rt) {
accessToken.value = at;
if (rt) refreshToken.value = rt;
sessionStorage.setItem('gusto_at', at);
if (rt) sessionStorage.setItem('gusto_rt', rt);
}
return { accessToken, refreshToken, loadFromSession, saveTokens };
}
这种封装让授权成为基础设施而非业务负担。当后端代理模式介入时,只需把 saveTokens 改为只存后端种下的 cookie,前端拿不到 refresh_token 也能安全工作。工程化 Gusto 集成的第一步,就是确定令牌归属与存储边界,这直接决定后续组件写法的复杂度。
基于 axios 的请求层与错误重试策略
Vue 3 工程普遍用 axios 做 HTTP 客户端,接入 Gusto 时要统一拼接 baseURL、注入令牌、处理 401 与速率限制。Gusto API 根地址分沙箱与生产,工程里用环境变量切换即可。请求拦截器读取上面 composable 的 accessToken,响应拦截器监听 401 并触发刷新,刷新失败则跳转重新授权。这一层写稳了,上层组件才能专注业务。
速率限制是容易被忽视的点。Gusto 免费层有每分钟请求数上限,前端批量同步员工时若不分页并发,会触发 429。我们在拦截器里对 429 做指数退避重试,并用队列控制并发。下方代码演示了最小可用的拦截器骨架,实际工程可在此基础上加取消重复请求、错误上报等。
import axios from 'axios';
import { useGustoAuth } from './auth.js';
const { accessToken, refreshToken, saveTokens } = useGustoAuth();
const client = axios.create({
baseURL: import.meta.env.VITE_GUSTO_API_BASE,
});
client.interceptors.request.use((config) => {
if (accessToken.value) {
config.headers.Authorization = 'Bearer ' + accessToken.value;
}
return config;
});
client.interceptors.response.use(
(res) => res,
async (error) => {
const status = error.response && error.response.status;
if (status === 401) {
// 调用后端刷新接口或 Gusto refresh 接口
const newAt = await refreshTokenFn(refreshToken.value);
saveTokens(newAt, refreshToken.value);
error.config.headers.Authorization = 'Bearer ' + newAt;
return client.request(error.config);
}
if (status === 429) {
await new Promise((r) => setTimeout(r, 1000));
return client.request(error.config);
}
return Promise.reject(error);
}
);
async function refreshTokenFn(rt) {
const resp = await axios.post('/api/gusto/refresh', { rt });
return resp.data.access_token;
}
export default client;
把重试和限流放在请求层,业务组件发请求时就不用写 try catch 套娃。工程化思维体现在这种横向切面的统一处理上。Gusto 的字段命名多为蛇形,而 Vue 组件倾向驼峰,建议在响应拦截器里做一次字段映射,或在前端建一个轻量适配器,避免模板里到处写 employee.first_name 这种别扭写法。
员工与薪资数据的组件化展示与同步
拿到令牌和请求层后,真正给 HR 用的界面才开始。一个典型场景是公司管理员在 Vue 页面查看 Gusto 同步过来的员工列表,并手动触发薪资核算周期拉取。我们用 <script setup> 写组件,通过 onMounted 调用封装好的 API 函数,把数据塞进 ref 数组,模板用 v-for 渲染。Gusto 的员工对象包含雇佣状态、部门、薪酬频率等,前端要做空值与权限降级。
同步操作不能让用户干等。我们借助 async/await 配合加载态,把同步按钮禁用直到接口返回。下面的组件片段演示了如何拉取员工并展示基本信息,同时用 computed 过滤在职人员。工程里还应加上错误提示,比如令牌失效引导重新绑定。
<template>
<div>
<button :disabled="loading" @click="syncEmployees">同步员工</button>
<ul>
<li v-for="e in activeEmployees" :key="e.id">
{{ e.first_name }} {{ e.last_name }} - {{ e.department }}
</li>
</ul>
<p v-if="errorMsg">{{ errorMsg }}</p>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import client from './gustoClient.js';
const employees = ref([]);
const loading = ref(false);
const errorMsg = ref('');
const activeEmployees = computed(() =>
employees.value.filter((e) => e.employment_status === 'active')
);
async function syncEmployees() {
loading.value = true;
errorMsg.value = '';
try {
const res = await client.get('/v1/companies/123/employees');
employees.value = res.data.employees;
} catch (e) {
errorMsg.value = '同步失败,请检查 Gusto 授权';
} finally {
loading.value = false;
}
}
onMounted(syncEmployees);
</script>
薪资工具部分更敏感,前端只应做展示与发起动作,具体计算与税表由 Gusto 服务端完成。工程化时要对薪资路由加角色守卫,并用 Vue Router 的 meta 字段限制非 HR 访问。Gusto 的 Webhook 可用于被动刷新,我们在后端验签后向前端发事件,前端用 EventSource 或轮询更新界面。这样整套 Vue 3 加 Gusto 的 HR 工具既合规又顺手。