Nuxt 3中获取用户IP及城市的首次服务端API最优调用方案
Nuxt 3 地理位置依赖应用的API调用最优方案
一、最优调用位置选择
核心原则:敏感API(如Dadata)必须放在服务端执行,避免暴露密钥;同时利用Nuxt 3的服务端能力减少客户端请求,提升首屏性能。推荐方案如下:
- 自定义服务端路由(Server Routes):把Ipify、Dadata、自有API的串联逻辑全部放在服务端路由中(如
server/api/location-content.ts),客户端仅需调用这一个内部API,减少网络请求次数。优势:保护Dadata的API密钥不暴露;服务端请求延迟更低;支持SEO预渲染首屏内容。 - 辅助缓存:首次获取城市后,用Cookie缓存用户城市信息(有效期7天左右),后续请求直接读取Cookie,跳过Ipify和Dadata的调用,大幅提升性能。同时可通过Nitro的缓存策略(如设置
Cache-Control头)缓存同一IP的地理位置查询结果,减少外部API调用量。
二、逻辑实现具体方案
1. 服务端路由实现(核心逻辑)
创建server/api/location-content.ts,处理三次API的串联调用:
export default defineEventHandler(async (event) => { // 优先从请求头获取用户IP(部署平台如Vercel/Cloudflare会自动注入) let userIp = getRequestHeader(event, 'x-forwarded-for') || getRequestHeader(event, 'remote-addr') || ''; // fallback到Ipify API获取IP if (!userIp) { const ipifyRes = await fetch('https://api.ipify.org?format=json'); const ipifyData = await ipifyRes.json(); userIp = ipifyData.ip; } // 调用Dadata API获取城市信息 const dadataApiKey = process.env.DADATA_API_KEY; const dadataSecret = process.env.DADATA_SECRET; const dadataRes = await fetch('https://cleaner.dadata.ru/api/v1/ip/geo', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Token ${dadataApiKey}`, 'X-Secret': dadataSecret }, body: JSON.stringify({ ip: userIp }) }); if (!dadataRes.ok) throw createError({ statusCode: dadataRes.status, statusMessage: '地理位置获取失败' }); const dadataData = await dadataRes.json(); const userCity = dadataData.location?.data?.city || ''; // 降级处理:无城市信息时返回默认内容 if (!userCity) { const defaultRes = await fetch(`${process.env.OWN_API_URL}/content/default`); return { city: '默认城市', content: await defaultRes.json() }; } // 调用自有API获取对应城市内容 const ownApiRes = await fetch(`${process.env.OWN_API_URL}/content?city=${encodeURIComponent(userCity)}`); if (!ownApiRes.ok) throw createError({ statusCode: ownApiRes.status, statusMessage: '内容获取失败' }); const content = await ownApiRes.json(); // 设置Cookie缓存城市,避免重复查询 setCookie(event, 'user-city', userCity, { maxAge: 60 * 60 * 24 * 7, // 缓存7天 path: '/' }); return { city: userCity, content }; });
2. 客户端调用实现
在页面/布局组件中,通过useAsyncData调用服务端路由,同时处理加载和错误状态:
<script setup> const { data: locationContent, error } = await useAsyncData('location-content', () => { return $fetch('/api/location-content'); }); // 错误处理示例 if (error.value) { console.error('加载失败:', error.value.message); } </script> <template> <div v-if="locationContent"> <h2>欢迎来到 {{ locationContent.city }}</h2> <div class="content">{{ locationContent.content }}</div> </div> <div v-else>加载中...</div> </template>
3. 复用逻辑:封装Composable
如果多个页面需要使用地理位置内容,可封装成组合式函数:
// composables/useLocationContent.ts export async function useLocationContent() { const { data, error } = await useAsyncData('location-content', () => { return $fetch('/api/location-content'); }); return { data, error }; }
页面中直接调用:
<script setup> const { data: locationContent } = await useLocationContent(); </script>
三、优化建议
- IP获取优化:优先使用平台提供的请求头(如
X-Forwarded-For)获取IP,比调用Ipify更高效,减少外部依赖。 - 缓存升级:在服务端路由中添加Nitro缓存头,缓存同一IP的查询结果:
event.res.headers.set('Cache-Control', 's-maxage=3600'); // 缓存1小时 - 用户手动切换:提供城市切换功能,覆盖自动获取失败的场景,提升用户体验。
- 错误边界:在布局中添加全局错误处理组件,统一捕获API请求失败的情况。
内容的提问来源于stack exchange,提问作者Andreas Hunter
相关产品推荐
相关产品推荐

