
本文讲解如何在 Next.js App Router 中正确分离客户端交互逻辑与服务端静态文件读取,解决 use client 组件中无法直接使用 glob 等 Node.js 服务端 API 的问题,并提供基于 Route Handler + use Hook 的标准实践。
本文讲解如何在 next.js app router 中正确分离客户端交互逻辑与服务端静态文件读取,解决 `use client` 组件中无法直接使用 `glob` 等 node.js 服务端 api 的问题,并提供基于 route handler + `use` hook 的标准实践。
在 Next.js 13+ 的 App Router 架构下,"use client" 指令标记的组件运行在浏览器环境中(客户端组件),无法访问 Node.js 内置模块(如 fs, glob, crypto)或执行服务端文件系统操作。你遇到的错误(ReferenceError: glob is not defined 或 Cannot use 'glob' in client component)正是这一限制的典型表现——app/ui/Gallery.tsx 虽被声明为 Server Component(无 "use client"),但它被嵌套在 Home 这个客户端组件中渲染,且其内部调用 glob("public/images/*") 会在构建/请求时尝试在客户端执行,必然失败。
✅ 正确解法:职责分离 + 客户端数据消费
应将静态资源枚举(glob)严格保留在服务端环境(如 Route Handler 或 Server Component),客户端仅负责发起请求、响应状态变化并渲染结果。
✅ 推荐实现:Route Handler + use Hook(React 18.3+)
1. 创建服务端 API 端点(app/api/route.ts)
// app/api/route.ts
import { glob } from 'glob';
import path from 'path';
export async function GET() {
try {
// 注意:路径需相对于项目根目录,且 `glob` 默认不支持 `public/` 下的相对路径
// 推荐写法:使用绝对路径 + `path.join(process.cwd(), 'public', 'images', '*')`
const images = await glob(
path.join(process.cwd(), 'public', 'images', '*.{jpg,jpeg,png,gif}')
);
// 返回相对于 `/` 的 URL 路径(Next.js 静态资源通过 `/images/xxx.png` 访问)
const publicPaths = images.map((file) =>
file.replace(process.cwd(), '').replace(/\/g, '/').replace('/public', '')
);
return Response.json(publicPaths);
} catch (error) {
console.error('Failed to fetch gallery images:', error);
return Response.json({ error: 'Failed to load images' }, { status: 500 });
}
}⚠️ 注意事项:
dmap-jsapi-skill下载DMap GL地图JSAPI(dmapgl)开发指南。用于编写、审查、调试 DMap 地图 API 代码,涵盖地图初始化、覆盖物渲染、图层管理、事件处理、控件交互等任务。当用户提及 dmap、dmapgl、jsapi‑gl 或相关地图开发需求时自动触发。
glob必须在服务端运行,因此只能放在app/api/、Server Components 或generateStaticParams中;process.cwd()在 Vercel 环境中指向部署根目录,本地开发也一致,是安全的;- 使用
*.{jpg,jpeg,png,gif}显式指定扩展名,避免匹配隐藏文件或非图像资源。
2. 客户端 Gallery 组件(app/ui/Gallery.tsx)
// app/ui/Gallery.tsx
'use client';
import { use } from 'react';
import Image from 'next/image';
// 在客户端组件顶层预取数据(自动 suspense,无需 useEffect)
const fetchImages = async () => {
const res = await fetch('/api');
if (!res.ok) throw new Error('Failed to load gallery');
return res.json();
};
const Gallery = ({ isSearching }: { isSearching: boolean }) => {
const images = use(fetchImages); // ✅ React 18.3+ `use` Hook,专为 Suspense 数据流设计
return (
<div className={`flex flex-wrap gap-4 transition-opacity duration-300 ${isSearching ? 'opacity-50' : ''}`}>
{images.map((src: string) => (
<div key={src} className="relative w-48 h-48 overflow-hidden rounded-lg shadow-sm">
<Image
src={src}
alt={`Gallery item: ${src}`}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 48rem"
/>
</div>
))}
</div>
);
};
export default Gallery;✅ 关键优势:
use(fetchImages)自动触发 Suspense,配合<suspense fallback="{...}"></suspense>可优雅处理加载态;- 无
useState/useEffect,零副作用,符合服务端渲染(SSR)友好原则;isSearching状态由父组件控制,Gallery 纯响应式,符合 React 单向数据流。
3. 主页面(app/page.tsx)保持简洁
// app/page.tsx —— 无需 'use client',除非有交互逻辑
import Gallery from './ui/Gallery';
import SearchBar from './ui/SearchBar';
import { useState } from 'react';
export default function Home() {
const [isSearching, setIsSearching] = useState(false);
return (
<div className="max-w-5xl mx-auto mt-10 px-4">
<SearchBar setIsSearching={setIsSearching} />
<Gallery isSearching={isSearching} />
</div>
);
}4. 增强版 SearchBar(推荐添加 onBlur 复位)
// app/ui/SearchBar.tsx
'use client';
import { Dispatch, SetStateAction } from 'react';
export default function SearchBar({
setIsSearching,
}: {
setIsSearching: Dispatch<SetStateAction<boolean>>;
}) {
return (
<div className="mb-8">
<input
type="text"
placeholder="Search gallery..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-base"
onFocus={() => setIsSearching(true)}
onBlur={() => setIsSearching(false)} // ✅ 用户失焦时恢复透明度
/>
</div>
);
}? 总结与最佳实践
- ❌ 禁止:在 Client Component 中
import { glob } from 'glob'或调用任何 Node.js 文件系统 API; - ✅ 必须:将
glob、fs.readdir等操作封装在app/api/Route Handler 或 Server Component 中; - ✅ 推荐:客户端使用
use(asyncFn)(React 18.3+)替代useEffect + useState获取初始数据,更简洁、可中断、SSR 友好; - ✅ 增强体验:为
opacity添加transition-opacity duration-300实现平滑渐变; - ? 生产注意:Vercel 环境中
glob可用,但确保public/images/目录已静态部署(即文件在构建时存在,非运行时生成)。
该方案完全遵循 Next.js 官方渲染模型(Rendering Docs),兼顾性能、可维护性与平台约束,是现代 Next.js 图库类交互的标准解法。


















