1. 项目概述:为什么我们需要自建导航网站
作为一个每天要打开几十个网站的技术从业者,我受够了主流导航网站的种种问题:要么充斥着无关的商业推广,要么加载速度慢得令人发指,更别提那些强制登录才能使用的"高级功能"。三个月前,当我第N次在紧急调试时被某导航网站的登录弹窗打断后,终于下定决心要自己造轮子。
这个导航网站的核心设计目标非常明确:
- 极致的加载速度:从点击到完全呈现控制在1秒内
- 纯粹的功能体验:去掉所有非必要的功能和广告
- 完全的数据自主权:我的网站数据我做主
- 零成本运维:利用免费资源实现持续运行
经过技术选型和两周的密集开发,最终成品不仅实现了所有设计目标,还在Lighthouse测试中拿到了98分的高分。下面我就把这个项目的完整构建过程拆解给大家,特别适合有以下需求的开发者:
- 想深入学习Next.js全栈开发
- 需要快速搭建私有导航工具
- 对网站性能优化有极致追求
- 希望实践Cloudflare边缘计算方案
2. 技术架构深度解析
2.1 为什么选择Next.js + Cloudflare组合
在技术选型阶段,我对比了三种主流方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 纯静态HTML | 极致简单、加载快 | 动态功能实现困难 | 个人极简导航 |
| WordPress | 功能丰富、生态完善 | 性能较差、需要服务器 | 企业级导航门户 |
| Next.js全栈方案 | 动静结合、开发体验好 | 学习曲线略陡 | 开发者定制导航 |
最终选择Next.js 14的App Router模式,主要基于以下考量:
- 混合渲染能力:首页使用静态生成(SSG),管理后台用服务端渲染(SSR)
- API路由内置:无需额外后端服务即可实现全功能
- 图像优化组件:自动处理favicon的懒加载和响应式
- TypeScript支持:完善的类型系统避免低级错误
搭配Cloudflare Pages的三大优势:
- 全球边缘网络:用户无论在哪里都能快速访问
- 免费额度充足:足够支撑日均10万次访问
- KV存储集成:轻松实现数据持久化
2.2 核心架构设计图解
整个系统采用分层架构设计,各模块职责分明:
code复制┌─────────────────────────────┐
│ 用户浏览器 │
└─────────────┬───────────────┘
│ HTTPS
┌─────────────▼───────────────┐
│ Cloudflare CDN边缘节点 │
│ - 缓存静态资源 │
│ - 路由API请求 │
└─────────────┬───────────────┘
│
┌─────────────▼───────────────┐
│ Next.js应用服务器 │
│ - 处理页面渲染 │
│ - 响应API请求 │
└─────────────┬───────────────┘
│
┌─────────────▼───────────────┐
│ Cloudflare KV存储 │
│ - 持久化网站数据 │
│ - 缓存favicon等资源 │
└─────────────────────────────┘
2.3 关键技术栈版本选择
在具体版本选择上,我遵循"稳定为主,适度超前"的原则:
bash复制# 核心依赖
next@14.1.0 # 使用最新的App Router架构
react@18.2.0 # 兼容Next.js 14的稳定版本
typescript@5 # 带来更快的类型检查
# 样式与UI
tailwindcss@3.3.0 # 支持所有现代CSS特性
@heroicons/react@2.1.1 # 提供丰富的SVG图标
# 工具类
jose@5.2.2 # 轻量级JWT实现
lodash-es@4.17.21 # 实用的工具函数
这里特别说明几个关键选择:
- 没有使用最新的Next.js 14.2.x,因为部分插件兼容性还未完善
- 选择JOSE而不是jsonwebtoken,因为前者更小巧且支持Edge Runtime
- 使用Lodash的ES模块版本(tree-shaking友好)
3. 项目搭建全流程
3.1 初始化项目脚手架
创建项目时我采用了最精简的初始化方式:
bash复制# 创建项目目录
mkdir nav-site && cd nav-site
# 初始化package.json(-y跳过所有问答)
npm init -y
# 安装核心依赖
npm install next@14.1.0 react@18.2.0 react-dom@18.2.0
# 安装开发依赖
npm install -D typescript@5 tailwindcss@3.3.0
然后创建必要的配置文件:
next.config.js 关键配置
javascript复制/** @type {import('next').NextConfig} */
const nextConfig = {
trailingSlash: true, // 统一URL结尾斜杠
images: {
unoptimized: true, // 禁用Next.js图片优化(用Cloudflare的)
formats: ['image/webp'], // 优先使用webp格式
},
compress: true, // 启用Gzip压缩
swcMinify: true // 使用SWC替代Babel
}
module.exports = nextConfig
Tailwind配置技巧
javascript复制// tailwind.config.ts
import type { Config } from 'tailwindcss'
const config: Config = {
darkMode: 'class', // 手动切换暗黑模式
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#3b82f6',
dark: '#2563eb' // 暗色模式下的主色
}
},
},
},
}
export default config
关键提示:在content配置中使用
./src/**/*比单独列出每个目录更可靠,能确保所有文件都被扫描到
3.2 项目目录结构设计
经过多次迭代,最终形成的目录结构如下:
code复制src/
├── app/
│ ├── (public)/ # 公开路由
│ │ ├── page.tsx # 首页
│ ├── (admin)/ # 管理后台路由组
│ │ ├── login/page.tsx
│ │ └── dashboard/page.tsx
│ ├── api/ # API路由
│ │ ├── sites/route.ts
│ │ └── categories/route.ts
│ └── layout.tsx # 根布局
├── components/
│ ├── ui/ # 通用UI组件
│ └── SiteCard.tsx # 业务组件
├── lib/
│ ├── auth.ts # 认证相关
│ └── storage.ts # 数据存储抽象层
└── styles/
└── globals.css # 全局样式
这种结构的主要优势:
- 使用路由组
(public)和(admin)清晰划分权限边界 - API路由与页面路由并列,符合Next.js 14最佳实践
- 组件按通用性分级,方便维护
4. 核心功能实现细节
4.1 数据模型设计
采用TypeScript定义了两个核心接口:
typescript复制// src/types/index.ts
export interface Site {
id: string // 使用时间戳生成唯一ID
name: string // 网站名称(必填)
url: string // 完整URL(包含协议)
description: string // 简短描述(120字内)
icon: string // favicon URL
category: string // 所属分类ID
order: number // 排序权重(默认为0)
createdAt: string // ISO格式创建时间
}
export interface Category {
id: string
name: string
order: number
createdAt: string
}
在设计时特别注意了以下几点:
- 所有字段都用明确的类型约束
- 使用
order字段而非数组索引实现灵活排序 createdAt采用ISO格式便于序列化和排序
4.2 首页动态渲染实现
首页采用增量静态再生(ISR)策略,关键代码如下:
tsx复制// src/app/(public)/page.tsx
export const revalidate = 3600 // 每小时重新验证数据
export default async function Home() {
const [sites, categories] = await Promise.all([
fetchSites(),
fetchCategories()
])
// 按分类分组
const groupedSites = categories
.sort((a, b) => a.order - b.order)
.map(category => ({
category,
sites: sites
.filter(site => site.category === category.id)
.sort((a, b) => a.order - b.order)
}))
.filter(group => group.sites.length > 0)
return (
<div className="container mx-auto px-4">
<SearchBar />
{groupedSites.map(group => (
<CategorySection
key={group.category.id}
category={group.category}
sites={group.sites}
/>
))}
</div>
)
}
性能优化点:
- 使用
Promise.all并行获取数据 - 在服务端完成数据分组和排序
- 设置合理的revalidate时间
4.3 自动获取favicon的三种方案
实现自动获取网站图标时,我对比了三种技术方案:
-
Google Favicon API
typescript复制`https://www.google.com/s2/favicons?domain=${domain}&sz=64`- 优点:稳定可靠
- 缺点:需要联网
-
DuckDuckGo API
typescript复制`https://icons.duckduckgo.com/ip3/${domain}.ico`- 优点:隐私友好
- 缺点:图标质量参差不齐
-
本地解析
typescript复制async function fetchSiteIcon(url: string) { try { const res = await fetch(`https://${domain}/favicon.ico`) return res.ok ? res.url : '' } catch { return '' } }- 优点:最准确
- 缺点:成功率低
最终采用混合方案:优先尝试Google API,失败后回退到默认图标,并添加KV缓存:
typescript复制// src/lib/favicon.ts
const CACHE_TTL = 60 * 60 * 24 * 7 // 1周
export async function getFavicon(url: string) {
const domain = extractDomain(url)
if (!domain) return DEFAULT_ICON
const cacheKey = `favicon:${domain}`
const cached = await KV.get(cacheKey)
if (cached) return cached
try {
const iconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=64`
await KV.put(cacheKey, iconUrl, { expirationTtl: CACHE_TTL })
return iconUrl
} catch {
return DEFAULT_ICON
}
}
5. 管理后台安全实现
5.1 JWT认证流程设计
采用标准的JWT认证方案,特别注意了以下几点安全措施:
- 使用HS256算法而非RS256,简化密钥管理
- 设置7天有效期,平衡安全与用户体验
- 在内存中维护注销令牌列表(生产环境应改用Redis)
核心实现代码:
typescript复制// src/lib/auth.ts
import { SignJWT, jwtVerify } from 'jose'
const SECRET_KEY = process.env.JWT_SECRET || 'default-secret'
export async function createAuthToken(username: string) {
return await new SignJWT({ username })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d')
.sign(new TextEncoder().encode(SECRET_KEY))
}
export async function verifyAuthToken(token: string) {
try {
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(SECRET_KEY)
)
return payload.username === process.env.ADMIN_USERNAME
} catch {
return false
}
}
5.2 受保护路由实现
使用Next.js的中间件实现路由保护:
typescript复制// src/middleware.ts
import { NextResponse } from 'next/server'
import { verifyAuthToken } from './lib/auth'
export async function middleware(request) {
const pathname = request.nextUrl.pathname
// 只保护/admin路径(除了/login)
if (pathname.startsWith('/admin') && !pathname.includes('/login')) {
const token = request.cookies.get('auth_token')?.value
if (!token || !(await verifyAuthToken(token))) {
return NextResponse.redirect(new URL('/admin/login', request.url))
}
}
return NextResponse.next()
}
配置说明:
- 中间件只对
/admin/*路径生效 - 显式排除
/admin/login路径 - 从HTTP Only的cookie中获取token
6. 部署到Cloudflare的实战技巧
6.1 KV存储配置要点
在wrangler.toml中配置KV时需要注意:
toml复制name = "nav-site"
compatibility_date = "2024-01-01"
[[kv_namespaces]]
binding = "KV" # 代码中使用的变量名
id = "xxxxxx" # 生产环境Namespace ID
preview_id = "yyyyyy" # 预览环境ID
关键步骤:
- 通过
wrangler kv:namespace create创建命名空间 - 将生成的ID填入配置文件
- 为生产环境和预览环境分别创建命名空间
6.2 环境变量管理
Cloudflare Pages支持三种环境变量设置方式:
- 项目级变量:所有部署共享
- 环境特定变量:区分生产/预览环境
- 秘密变量:加密存储的敏感信息
最佳实践:
- 将JWT_SECRET设为秘密变量
- API基础URL等设为环境特定变量
- 功能开关等设为项目级变量
6.3 部署优化技巧
通过调整构建命令显著提升部署速度:
bash复制# 原生命令(耗时约3分钟)
npm run build
# 优化后的命令(缩短到1分钟内)
NEXT_TELEMETRY_DISABLED=1 npm_config_engine_strict=true npm run build
优化原理:
- 禁用Next.js遥测减少网络请求
- 强制使用与Cloudflare兼容的引擎版本
- 利用缓存加速依赖安装
7. 性能优化实战记录
7.1 Lighthouse评分提升路径
从初始的72分到最终的98分,主要优化措施:
-
图片优化
- 使用
loading="lazy"延迟加载非首屏图片 - 为
<img>添加明确的width/height属性避免布局偏移 - 实现srcset支持响应式图片
- 使用
-
字体优化
- 使用
next/font本地托管字体 - 设置
font-display: swap避免FOIT - 预加载关键字体
- 使用
-
JavaScript优化
- 动态导入非关键组件
- 使用
React.memo减少不必要的重渲染 - 实现虚拟滚动长列表
7.2 边缘缓存策略
通过Cloudflare的缓存规则实现毫秒级响应:
javascript复制// 在API路由中设置缓存头
export async function GET() {
const data = await getSites()
return new Response(JSON.stringify(data), {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120',
'CDN-Cache-Control': 'public, s-maxage=300'
}
})
}
缓存策略说明:
- 浏览器缓存1分钟
- CDN边缘节点缓存5分钟
- 允许在验证期间使用过期内容(stale-while-revalidate)
8. 开发中的经验教训
8.1 静态导出遇到的坑
最初尝试使用output: 'export'时遇到的主要问题:
- API路由失效:静态导出后所有API端点不可用
- 动态路由缺失:
/category/[id]等路由无法预生成 - 重定向问题:鉴权逻辑无法正常工作
解决方案:
- 移除
output: 'export'配置 - 为动态路由添加
export const dynamic = 'force-dynamic' - 使用Cloudflare Functions处理重定向
8.2 数据存储方案演进
数据持久化方案经历了三个阶段迭代:
-
开发阶段:使用内存存储
typescript复制// src/lib/memory-store.ts const sites: Site[] = [] export function getSites() { return sites } -
过渡阶段:使用本地JSON文件
typescript复制// src/lib/file-store.ts import fs from 'fs/promises' const DB_FILE = './data.json' export async function getSites() { const data = await fs.readFile(DB_FILE, 'utf-8') return JSON.parse(data).sites } -
生产环境:迁移到Cloudflare KV
typescript复制// src/lib/kv-store.ts export async function getSites() { return await KV.get('sites', 'json') || [] }
关键经验:通过抽象存储接口,可以无缝切换实现方案:
typescript复制interface Storage {
getSites(): Promise<Site[]>
addSite(site: Site): Promise<void>
}
let storage: Storage
if (process.env.NODE_ENV === 'production') {
storage = new KVStorage()
} else {
storage = new MemoryStorage()
}
9. 项目扩展方向
9.1 功能增强计划
-
用户系统
- 多用户支持
- 个人收藏夹
- 自定义分类
-
数据分析
- 访问统计
- 热门网站排行
- 使用时长分析
-
高级管理
- 批量导入/导出
- 操作日志
- 数据备份
9.2 架构优化思路
- 迁移到D1数据库:Cloudflare新推出的关系型数据库
- 实现边缘计算:使用Cloudflare Workers处理部分逻辑
- 添加WebSocket支持:实时同步数据变更
- 引入全文搜索:基于WebAssembly实现客户端搜索
10. 项目资源与参考
完整项目代码已开源:
- GitHub仓库:
https://github.com/example/nav-site - 在线演示:
https://nav-site.pages.dev
推荐学习资源:
这个项目从构思到上线历时两周,期间经历了三次大的架构调整。最大的收获是:在技术选型时,不仅要考虑功能实现,更要关注部署和运维成本。Cloudflare的全栈解决方案让我能够专注于业务逻辑,而不用操心服务器维护等问题。