Next.js Web3 社交应用前端:Lens Protocol 集成、动态 Feed 流与去中心化存储

发布时间:2026/7/24 17:42:41
Next.js Web3 社交应用前端:Lens Protocol 集成、动态 Feed 流与去中心化存储 Next.js Web3 社交应用前端Lens Protocol 集成、动态 Feed 流与去中心化存储一、Web3 社交前端不是 Web2 套个钱包Web3 社交应用的前端面临三个和传统社交完全不同的技术约束数据源不是自己的服务器来自链上 去中心化存储、用户身份不是邮箱是钱包地址和 DID、内容分发不是算法黑箱用户可以选择自己的推荐算法。这三个约束不是限制而是新架构的机会。Lens Protocol 是目前覆盖最广的链上社交图谱标准。每一条帖子、关注关系、转发记录都以 NFTProfile NFT 和 Publication NFT的形式存储在 Polygon 上。前端的工作不是去爬取和索引这些数据而是通过 Lens APIGraphQL 端点直接查询图谱并渲染。但这里有一个架构决策需要提前想清楚用户的内容帖子正文、图片、视频并不存储在链上而是通过 ContentURI 指向 IPFS 或 Arweave 上的文件。这意味着前端需要同时处理两个数据通道——链上交易发帖、评论、收藏和去中心化存储上传内容文件 元数据 JSON。两个通道的事务性不能保证如果用户签名了链上发帖交易但 IPFS 上传失败链上会有一条 ContentURI 指向不存在的内容反之如果 IPFS 上传成功但链上交易被拒绝内容文件就成了孤儿数据。Next.js 的 App Router 模式很适合这个场景——服务器组件处理数据查询客户端组件处理交互和钱包连接两者通过串行数据流从链上查到 UI 渲染自然分工。二、Lens 数据流与前端架构Lens 的数据模型是 Graph Publication 的混合结构。Profile 是图的节点Follow 和 Collect 是边而 PublicationPost、Comment、Mirror是挂载在 Profile 上的内容叶子节点。Lens API 是核心数据管道。它本质上是一个索引器——监听 Lens Hub 合约的链上事件将图数据和内容重组为 GraphQL Schema然后通过 CDN 分发查询结果。前端不需要跑自己的索引节点但需要对查询做合理的分页Cursor-based Pagination和缓存Apollo Client 或 SWR。内容存储采用两层架构元数据标题、摘要、标签、创建时间存储在 IPFS 的 JSON 文件中实质内容图片、视频、长文存储在 Arweave 的 permaweb 上。IPFS 的可寻址性CID 即内容指纹确保了元数据的完整性Arweave 的一次付费永存确保了内容不会因为 IPFS 节点退出而丢失。三、Next.js Lens 的工程实现// app/api/lens/route.ts — Lens GraphQL 代理层 import { NextRequest, NextResponse } from next/server; import { ApolloClient, InMemoryCache, gql } from apollo/client; const LENS_API https://api-v2.lens.dev/; const client new ApolloClient({ uri: LENS_API, cache: new InMemoryCache(), }); /** * 获取用户个人 Feed 的 GraphQL 查询 * * 设计决策使用 forward-pagination 而非 offset-pagination * 因为 Lens 的出版物列表会新增内容插入到列表头部 * offset 分页在新增内容后会导致重复项。Cursor 分页基于时间戳 * 天然避免了这个问题。 */ const FEED_QUERY gql query Feed($profileId: ProfileId!, $cursor: Cursor) { feed(request: { where: { for: $profileId } cursor: $cursor }) { items { id root { ... on Post { id metadata { content locale tags } stats { comments mirrors upvotes } createdAt } } electedMirror { id } } pageInfo { prev next } } } ; /** * API Route: 代理 Lens 查询并添加服务端缓存 * * 设计决策不直接从客户端调用 Lens API而是通过 Next.js API Route 代理。 * 理由有三1) 可以在服务端缓存热门查询减少 API 调用 * 2) 可以在代理层添加速率限制和滥用防护 * 3) 客户端的 API key 不会暴露。 */ export async function GET(request: NextRequest) { const { searchParams } new URL(request.url); const profileId searchParams.get(profileId); const cursor searchParams.get(cursor); if (!profileId) { return NextResponse.json({ error: profileId required }, { status: 400 }); } try { const { data } await client.query({ query: FEED_QUERY, variables: { profileId, cursor }, }); return NextResponse.json(data.feed); } catch (error) { return NextResponse.json( { error: Lens API query failed }, { status: 502 } ); } } // components/FeedList.tsx — 客户端 Feed 渲染组件 use client; import { useInfiniteQuery } from tanstack/react-query; import { useAccount } from wagmi; import { LensPublication } from /types/lens; async function fetchFeed(pageParam: string | undefined): Promise{ items: LensPublication[]; pageInfo: { next: string | null }; } { const params new URLSearchParams(); if (pageParam) params.set(cursor, pageParam); // profileId 从钱包上下文传入 const res await fetch(/api/lens?profileId0xabcd${params}); if (!res.ok) throw new Error(Feed fetch failed); return res.json(); } /** * Feed 无限滚动组件 * * 设计决策使用 tanstack/react-query 的 useInfiniteQuery 而非 * 自己管理分页状态。它的 getNextPageParam 配合 cursor 分页天然适配 * 动态更新的 Feed 场景——每次加载数据都基于上一页的末尾游标。 * SWR 也可以做类似的事情但 useInfiniteQuery 的双向缓存策略 * 对社交 Feed 的滚动体验更友好。 */ export function FeedList() { const { address } useAccount(); const { data, fetchNextPage, hasNextPage, isFetching } useInfiniteQuery({ queryKey: [feed, address], queryFn: ({ pageParam }) fetchFeed(pageParam as string | undefined), getNextPageParam: (lastPage) lastPage.pageInfo.next ?? undefined, initialPageParam: undefined, staleTime: 30_000, // 30秒内不重新获取减少 API 调用 }); return ( div classNamefeed-container {data?.pages.map((page) page.items.map((pub) ( PublicationCard key{pub.id} publication{pub} / )) )} {hasNextPage ( button onClick{() fetchNextPage()} disabled{isFetching} classNameload-more-btn {isFetching ? 加载中... : 加载更多} /button )} /div ); } // utils/storage.ts — 去中心化存储工具 import { Web3Storage } from web3.storage; const storageClient new Web3Storage({ token: process.env.WEB3_STORAGE_TOKEN! }); /** * 将发布内容上传到 IPFS * * 设计决策只上传文本内容的 JSON 到 IPFS图片和视频走 Arweave。 * IPFS 的主要优势是 CIDs 作为内容哈希的完整性保证 * Arweave 的主要优势是永久存储不需要 pinning 服务维护。 * 两层分离避免了单一存储方案的弱点。 */ export async function uploadToDecentralizedStorage( content: { text: string; tags: string[]; locale: string } ): Promisestring { const metadata JSON.stringify({ version: 2.0.0, mainContentFocus: TEXT_ONLY, metadata_id: crypto.randomUUID(), description: Lens Publication, content: content.text, locale: content.locale, tags: content.tags, appId: your-app-id, }); const blob new Blob([metadata], { type: application/json }); const file new File([blob], metadata.json); const cid await storageClient.put([file], { wrapWithDirectory: false, }); return ipfs://${cid}; }四、这个架构的边界Lens API 的依赖风险Lens API 目前由 Lens 团队维护是一个中心化的查询端点。虽然链上数据本身是去中心化的但索引和查询服务是中心化的。如果 API 下线前端需要回退到直接扫描链上事件 本地索引的方案——技术可行但工程量大。Feed 的个性化不够当前的 Lens Feed 只展示关注者的帖子按时间排序。它没有你可能感兴趣的推荐层。扩展方向是在 Feed 查询之外接入 GNN 推荐模型如第 1 篇所述将链上行为图谱的嵌入向量作为 Feed 排序的权重因子。钱包体验的摩擦Web3 社交的每次发帖、评论、转发都是一笔链上交易需要钱包签名和 Gas 确认。高频社交场景下每 30 秒弹一次签名确认是不可接受的用户体验。解决方案是使用 Session Key 无 Gas 中继器Gasless Relayer让用户在首次授权后在后台完成交易。数据主权与内容审核的矛盾去中心化存储保证了用户数据的不可篡改性和抗审查性但也意味着平台无法删除用户上传的违规内容恶意代码、非法信息、侵权材料。Lens Protocol 的做法是前端层面的内容过滤通过Momoka的 DA 层做内容校验但这实际上在应用层引入了一个中心化的审查节点。这是 Web3 社交的一个根本矛盾——绝对的去中心化数据主权与最小必要的内容审核无法兼得。目前的最佳实践是链上存储内容指针和权限链下存储可变的 moderation 标记不可见的隐藏标记让审核不意味着数据删除。五、总结Next.js Lens Protocol 的前端架构核心是把数据查询和数据写入拆到两个通道查询走 Lens API 的 GraphQL 端点服务端代理 缓存写入走钱包签名的链上交易客户端交互流。去中心化存储作为内容层IPFS 保证完整性Arweave 保证持久性。这个架构的精髓在于前端不是在做 Web2 的展示层而是在做 Web3 的状态转换层。每一个 UI 操作对应一个链上状态变化前端负责把状态变化翻译成用户能理解的交互而不是把用户困在复杂的链上操作中。