Nuxt 数据获取:SSR 直连 vs. 代理 API
2025-01-10 22:59:16
Nuxt 数据获取:直接访问 vs. 代理访问
在 Nuxt 应用中使用服务端渲染(SSR)进行数据获取时,会遇到客户端与服务器端数据交互的抉择问题。特别是在使用类似 Supabase 这样的后端服务时,如何合理组织数据获取逻辑,避免重复代码并确保应用性能,是需要考虑的重点。这篇文章分析直接访问后端服务 API 和通过 Nuxt Nitro 服务器代理访问 API 的差异,并给出最佳实践。
问题:客户端数据获取与 SSR 的冲突
通常,客户端单页面应用 (SPA) 会直接通过客户端代码中的 supabase-js
库从 Supabase API 获取数据。这种方式在客户端渲染(CSR)环境中工作良好,因为所有的逻辑都运行在浏览器端。当引入 SSR 后,需要在服务器端获取数据进行预渲染,这时直接使用 supabase-js
就无法实现,因为它在 Node.js 环境中无法正常运行。
为了解决这个问题,通常采用以下策略:
- 使用 Nuxt 的
$fetch
,useFetch
或者useAsyncData
这些 composable 函数从服务端获取数据。 - 在 Nuxt Nitro 服务器 API 中代理后端服务请求。
- 服务器代理后端请求的思路是:先由服务器获取数据,并将数据返回给客户端,客户端再通过
useFetch
等函数来访问 Nuxt 服务器,这种做法可以在一定程度上隐藏后端服务 API,降低安全风险,并可以缓存响应数据提升性能。
//客户端 SPA (仅限客户端渲染)
<script setup lang="ts">
await supabase.from('recipes').select(...)
</script>
// 使用 Nuxt SSR,引入 nitro api server (简化示例)
<script setup lang="ts">
useFetch("$supabase-server/api/recipes")
</script>
// Nitro 服务器 API (/server/api/recipes.ts)
export default defineEventHandler(async (event) => {
const data = await supabase.from('recipes').select(...)
return data
});
看起来问题好像得到了解决,但 SSR 初次渲染之后,当客户端进行数据刷新、用户进行页面互动操作时,如何进行客户端的数据请求?这时又将面对新的抉择:
- 客户端是否需要重复访问 Nitro 服务器?
- 直接访问后端服务 API 是否会更好,是否可以避免重复代码,并降低服务器的性能损耗?
解决方案与分析
上述抉择核心在于服务端渲染后的客户端数据同步,以及代码复用和维护性考虑,并没有“银弹”一样的标准答案。一般要根据实际应用情况进行决策,通常有两种方案可供选择:
方案一:服务端代理请求 (红线方案)
-
原理: 客户端始终通过 Nitro 服务器 API 进行数据请求。在初始的 SSR 渲染和客户端交互更新时,均通过代理层进行。
-
实现步骤:
- 在 Nuxt 服务器的
/server/api/
目录下创建 API 路由(例如recipes.ts
),该 API 路由处理向后端服务(如 Supabase)发送请求的逻辑。 - 在客户端组件中,使用
useFetch
等函数向此 Nitro API 路由发送请求,获取数据。 - 后续数据刷新操作,也仍然通过此 API 请求。
// /server/api/recipes.ts export default defineEventHandler(async (event) => { const supabase = useSupabase(); // 假定有一个useSupabase的composable返回一个Supabase客户端实例 return await supabase.from('recipes').select('*') }) // Home.vue <script setup lang="ts"> const {data: recipes } = await useAsyncData('recipes', () => $fetch('/api/recipes') ) </script>
- 在 Nuxt 服务器的
-
优点:
- 统一管理请求逻辑,服务端拥有后端服务 API 的访问权限。
- 客户端请求不直接暴露后端服务 API。
- Nitro API 提供了缓存机制(可以利用Nuxt caching 或是 http header 的 cache-control)。
- 方便添加服务端授权/鉴权检查。
-
缺点:
- 增加了额外的请求跳转(客户端 => Nitro 服务器 => Supabase)。
- 当大量数据被访问的时候,需要大量的内存处理,消耗更多的服务器资源。
方案二:客户端直接请求后端服务 API (橙线方案)
-
原理: 客户端在初始的 SSR 渲染时通过 Nitro 服务器 API 获取数据,但客户端交互的数据请求则直接访问 Supabase 的 API 。
-
实现步骤 :
- 在 Nuxt 服务器端 API 中保留向 Supabase 获取数据的逻辑,仅用于初始页面渲染。
- 在客户端定义可复用的函数或服务 (例如,使用
composables/supabase-client.ts
)封装客户端数据获取的逻辑,以便在组件中直接调用 Supabase API。
// /server/api/recipes.ts export default defineEventHandler(async (event) => { const supabase = useSupabase(); return await supabase.from('recipes').select('*'); }); // composables/supabase-client.ts export function useSupabaseClient (){ const config = useRuntimeConfig(); const supabaseUrl = config.supabaseUrl const supabaseKey = config.supabaseKey; const supabase = createClient(supabaseUrl, supabaseKey) return { supabase, } } // Home.vue <script setup lang="ts"> const {data: recipes} = await useAsyncData('recipes', () => $fetch('/api/recipes') ); // 在客户端操作时进行数据刷新: const { supabase} = useSupabaseClient(); const onRefresh = async ()=> { const res = await supabase.from("recipes").select("*") recipes.value=res.data; } </script>
-
优点:
- 客户端直接连接到后端服务,减少一次网络跳转,请求更快。
- 对于客户端频繁数据更新操作,性能较高,且不需要消耗服务端性能。
- 通过封装
supabase-client
,复用客户端的数据获取逻辑。
-
缺点:
- 客户端需要管理后端服务的访问凭证,存在一定的安全风险。可以使用环境变量的方式解决。
- 如果对安全有极高的要求,不希望在客户端暴露 api key等敏感信息,此方法慎用。
- 部分复杂的授权、鉴权需要在客户端自行处理,可能造成代码逻辑不清晰。
最佳实践
最佳实践通常取决于项目实际情况,在两个方案之间做出合理取舍。
如果安全性是首要考虑,并要求统一管理 API 访问控制,推荐方案一:使用服务端代理 API 请求。此时应谨慎处理好请求的并发量和缓存,以防止过大的服务器负载。
如果性能是首要考虑,特别是那些客户端有频繁数据更新的应用,可以考虑方案二:让客户端直接访问后端服务 API。 此时注意安全控制, 使用 composables
函数和 .env
管理 key。 此外还需考虑对代码的可维护性和后期升级改造。
通常,较小的、内部使用的应用倾向于直接连接后端服务以提高响应速度。而大型、公用的、对安全性要求较高的应用更适合使用代理服务进行数据交互。 可以考虑方案的混合模式:将所有安全相关的请求,如用户认证等,由服务器 API 进行代理;其他数据相关的请求直接由客户端发出。