Next.js 13 App目录下如何将视图统计逻辑迁移至客户端?
问题描述
我的最终目标是基于数据库中的统计数展示浏览量最高的文章。目前仅在重建项目时生效,因为这是服务端逻辑。我希望在客户端获取视图统计数据,让UI自动展示正确的热门文章。
以下是我Next.js 13 App目录下的page.tsx文件:
// Prisma import prisma from "@/lib/prisma" // Sanity import { client } from "@/lib/sanity-client" import { groq } from "next-sanity" // Components import { Container } from "@/components/Container" import SimpleIntro from "@/components/Intros/SimpleIntro" import SimpleIntroCentred from "@/components/Intros/SimpleIntroCentred" import FeaturedList from "@/components/Posts/Lists/FeaturedList" import List from "@/components/Posts/Lists/List" async function getArticles() { const query = groq` *[_type == "article"] { ..., author->, categories[]->} ` if (!query) { throw new Error("Failed to fetch data") } const article = await client.fetch(query) return article } async function getViewCount() { const views = await prisma.view.findMany({ where: { contentType: "article" }, }) return views } export default async function ArticlePage() { const articles = await getArticles() const views = await getViewCount() const mostViewedArticles = views .sort((a: any, b: any) => Number(b.count) - Number(a.count)) .slice(0, 3) .map((view: any) => { const article = articles.find( (article: any) => article.slug.current === view.slug, ) return article }) return ( <Container className="grid gap-20 lg:gap-32 mb-40"> <div className="grid gap-10"> <SimpleIntroCentred title="Let's read" description="Read my most popular articles and perhaps discovery a new upcoming interest of yours." customClass="animate-fade-in-top" /> <SimpleIntro title="Most popular" description="Read my most popular articles and perhaps discovery a new upcoming interest of yours." customClass="animate-fade-in-bottom" /> <FeaturedList data={mostViewedArticles} contentType="article" customClass="animate-fade-in-bottom" /> </div> <div className="grid gap-10 animate-fade-in-bottom"> <SimpleIntro title="All articles" description="Find something that you find interesting and be creative." /> <List data={articles} contentType="article" /> </div> </Container> ) }
我希望将以下部分迁移至客户端:
async function getViewCount() { const views = await prisma.view.findMany({ where: { contentType: "article" }, }) return views }
在App目录结构下该如何实现?
解决方案
在Next.js 13 App Router中,Prisma无法直接在客户端使用(会暴露敏感的数据库连接信息),所以需要先通过API路由封装视图统计查询,再在客户端组件中获取数据。具体步骤如下:
1. 创建API路由
在app/api/views/articles/route.ts路径下创建接口,作为数据库和客户端的中间层:
import prisma from "@/lib/prisma"; import { NextResponse } from "next/server"; export async function GET() { try { const views = await prisma.view.findMany({ where: { contentType: "article" }, }); return NextResponse.json(views); } catch (error) { return NextResponse.json({ error: "获取浏览数据失败" }, { status: 500 }); } }
2. 改造页面为客户端组件
给原page.tsx添加客户端指令,使用React状态和副作用钩子处理异步数据获取:
'use client' // Sanity import { client } from "@/lib/sanity-client" import { groq } from "next-sanity" // Components import { Container } from "@/components/Container" import SimpleIntro from "@/components/Intros/SimpleIntro" import SimpleIntroCentred from "@/components/Intros/SimpleIntroCentred" import FeaturedList from "@/components/Posts/Lists/FeaturedList" import List from "@/components/Posts/Lists/List" import { useState, useEffect } from 'react' async function getArticles() { const query = groq` *[_type == "article"] { ..., author->, categories[]->} ` const article = await client.fetch(query) return article } async function fetchViewCount() { const res = await fetch('/api/views/articles') if (!res.ok) throw new Error('获取浏览数据失败') return res.json() } export default function ArticlePage() { const [articles, setArticles] = useState<any[]>([]) const [views, setViews] = useState<any[]>([]) const [mostViewedArticles, setMostViewedArticles] = useState<any[]>([]) const [loading, setLoading] = useState(true) useEffect(() => { async function loadData() { try { // 并行获取文章和浏览数据,提升加载效率 const [articleData, viewData] = await Promise.all([ getArticles(), fetchViewCount() ]) setArticles(articleData) setViews(viewData) // 计算热门文章 const sortedViews = viewData.sort((a: any, b: any) => Number(b.count) - Number(a.count)) const topArticles = sortedViews.slice(0,3).map(view => { return articleData.find(article => article.slug.current === view.slug) }).filter(Boolean) // 过滤掉匹配失败的文章 setMostViewedArticles(topArticles) } catch (error) { console.error('加载数据失败:', error) } finally { setLoading(false) } } loadData() }, []) if (loading) { return <div>加载中...</div> // 可替换为自定义加载组件 } return ( <Container className="grid gap-20 lg:gap-32 mb-40"> <div className="grid gap-10"> <SimpleIntroCentred title="Let's read" description="Read my most popular articles and perhaps discovery a new upcoming interest of yours." customClass="animate-fade-in-top" /> <SimpleIntro title="Most popular" description="Read my most popular articles and perhaps discovery a new upcoming interest of yours." customClass="animate-fade-in-bottom" /> <FeaturedList data={mostViewedArticles} contentType="article" customClass="animate-fade-in-bottom" /> </div> <div className="grid gap-10 animate-fade-in-bottom"> <SimpleIntro title="All articles" description="Find something that you find interesting and be creative." /> <List data={articles} contentType="article" /> </div> </Container> ) }
3. 核心说明
- API路由的必要性:Prisma依赖数据库连接字符串(敏感信息),不能在客户端直接调用,API路由作为中间层可以安全地处理数据库查询。
- 客户端组件改造:添加
'use client'指令后,组件就能使用React的useState和useEffect,实现客户端异步数据获取和状态管理。 - 数据处理逻辑:将热门文章的计算逻辑移到
useEffect中,确保只有在文章和浏览数据都就绪后才执行,避免空数据错误。
内容的提问来源于stack exchange,提问作者Mathias Riis Sorensen
相关产品推荐
相关产品推荐

