架构总览
相关源文件
本页面内容基于以下源文件生成:
- src/app/layout.tsx
- src/lib/config.ts
- src/lib/db.ts
- src/lib/types.ts
- src/middleware.ts
- next.config.js
- src/lib/time.ts
- src/lib/auth.ts
- src/lib/live.ts
- src/lib/utils.ts
- Dockerfile
- package.json
- start.js
- .eslintrc.js
- jest.setup.js
- .prettierrc.js
- jest.config.js
- proxy.worker.js
- postcss.config.js
- tailwind.config.ts
LunaTV 是一个基于 Next.js 构建的影视聚合平台,采用现代化的前端技术栈与灵活的后端存储策略,支持多源视频搜索、直播流解析、用户认证与数据持久化等核心功能。该项目通过中间件实现请求拦截与认证,利用适配器模式支持多种存储后端(Redis、Upstash、Kvrocks),并通过运行时配置注入实现环境无关的部署能力。
技术栈与框架基础
Next.js 应用配置与 PWA 支持
项目基于 Next.js 框架构建,采用 standalone 输出模式以支持 Docker 容器化部署。核心配置在 next.config.js:1-79 中定义,包含以下关键特性:
- 输出模式:
output: 'standalone'用于生成独立的部署包,减少镜像体积 - PWA 集成:通过
next-pwa插件实现渐进式 Web 应用支持,配置了 Service Worker 注册与预缓存 - 图片策略:设置
unoptimized: true禁用 Next.js 图片优化,配合remotePatterns允许任意 HTTP/HTTPS 源图片加载 - Webpack 扩展:自定义 SVG 处理规则,使用
@svgr/webpack将 SVG 转换为 React 组件,同时保留?url查询参数的文件加载能力
javascript1// next.config.js 核心配置片段 2const nextConfig = { 3 output: 'standalone', 4 reactStrictMode: false, 5 swcMinify: false, 6 experimental: { 7 instrumentationHook: process.env.NODE_ENV === 'production', 8 }, 9 images: { 10 unoptimized: true, 11 remotePatterns: [ 12 { protocol: 'https', hostname: '**' }, 13 { protocol: 'http', hostname: '**' }, 14 ], 15 }, 16};
React 组件化架构与 UI 框架集成
UI 层采用 Tailwind CSS 作为样式解决方案,配置文件 tailwind.config.ts:1-91 定义了完整的设计系统:
- 主题色系:定义了
primary色板(50-900 共 10 个层级),基于 Sky 色系构建品牌色 - 响应式断点:新增
mobile-landscape断点处理横屏移动设备 - 动画系统:内置
fade-in、slide-up、slide-down、slide-in-from-right四种过渡动画 - 插件集成:使用
@tailwindcss/forms插件统一表单样式
PostCSS 配置在 postcss.config.js:1-6 中定义,仅包含 tailwindcss 和 autoprefixer 两个必要插件,保持构建流程简洁。
代码质量工具链配置
项目配置了完整的代码质量保障体系:
- ESLint:.eslintrc.js:1-45 集成了 TypeScript、React、Next.js、Prettier 规则,并配置了
simple-import-sort和unused-imports插件自动优化导入语句 - Prettier:.prettierrc.js:1-7 定义了统一的代码格式化规则(单引号、箭头函数括号、2 空格缩进)
- Jest:jest.config.js:1-30 配置了测试环境,支持路径别名(
@/映射到src/)和 SVG 模块 Mock
核心应用架构与生命周期
Root Layout 与全局状态管理
应用入口组件 src/app/layout.tsx:25-130 实现了多层 Provider 嵌套的全局状态管理架构:
正在加载图表渲染器...
关键实现细节:
- 环境变量读取:根据
NEXT_PUBLIC_STORAGE_TYPE环境变量决定配置来源,localstorage模式直接使用环境变量,其他模式从数据库读取配置(src/app/layout.tsx:42-74) - 运行时配置注入:通过
dangerouslySetInnerHTML将服务端配置注入到window.RUNTIME_CONFIG全局变量,使客户端组件能访问服务端配置(src/app/layout.tsx:106-111) - 主题系统:使用
next-themes库实现深色/浅色模式切换,配置disableTransitionOnChange避免页面加载时的主题闪烁
服务端配置注入机制
配置管理系统是应用的核心基础设施,src/lib/config.ts:61-331 实现了多层级配置合并与缓存策略:
配置合并流程:
- 文件配置解析:尝试解析
ConfigFileJSON 字符串,失败时使用空对象(src/lib/config.ts:62-67) - 源站点合并:将文件中的
api_site配置与数据库中的SourceConfig合并,已存在的源只更新关键字段,新源标记为from: 'config'(src/lib/config.ts:70-94) - 来源标记:不在文件配置中的源标记为
from: 'custom',用于区分用户自定义源(src/lib/config.ts:96-100)
缓存策略:
typescript1// src/lib/config.ts:293-315 2export async function getConfig(): Promise<AdminConfig> { 3 // 直接使用内存缓存 4 if (cachedConfig) { 5 return cachedConfig; 6 } 7 8 // 读 db 9 let adminConfig: AdminConfig | null = null; 10 try { 11 adminConfig = await db.getAdminConfig(); 12 } catch (e) { 13 console.error('获取管理员配置失败:', e); 14 } 15 16 // db 中无配置,执行一次初始化 17 if (!adminConfig) { 18 adminConfig = await getInitConfig(""); 19 } 20 adminConfig = configSelfCheck(adminConfig); 21 cachedConfig = adminConfig; 22 db.saveAdminConfig(cachedConfig); 23 return cachedConfig; 24}
配置自检机制:configSelfCheck 函数确保所有必要属性存在且类型正确,防止配置缺失导致的运行时错误(src/lib/config.ts:317-331)
应用启动流程与定时任务
Docker 环境下的应用启动由 start.js:14-70 控制,实现了健康检查与定时任务调度:
正在加载图表渲染器...
启动流程关键点:
- Manifest 生成:启动前执行
generate-manifest.js脚本生成 PWA 清单文件(start.js:14-22) - 健康检查:通过轮询
/login端点检测服务就绪状态,收到 2xx 响应后停止轮询(start.js:29-57) - Cron 调度:服务就绪后立即执行一次
/api/cron任务,随后每小时重复执行(start.js:42-50)
数据持久化与存储策略
多后端存储适配器模式
存储层采用抽象工厂模式,src/lib/db.ts:19-83 定义了统一的存储接口与多种后端实现:
正在加载图表渲染器...
存储后端选择逻辑(src/lib/db.ts:19-31):
| 环境变量值 | 存储实现 | 适用场景 |
|---|---|---|
redis | RedisStorage | 传统服务器部署 |
upstash | UpstashRedisStorage | Serverless/边缘环境 |
kvrocks | KvrocksStorage | 高性能持久化场景 |
localstorage | null | 单机演示/开发环境 |
数据迁移机制:DbManager 构造函数自动触发数据迁移,先执行结构迁移再执行密码哈希迁移,迁移过程异步执行不阻塞启动(src/lib/db.ts:53-66)
核心数据模型定义
src/lib/types.ts:4-69 定义了业务核心数据结构与存储契约:
播放记录模型(src/lib/types.ts:4-15):
typescript1export interface PlayRecord { 2 title: string; // 视频标题 3 source_name: string; // 来源站点名称 4 cover: string; // 封面图片 URL 5 year: string; // 发行年份 6 index: number; // 当前集数 7 total_episodes: number; // 总集数 8 play_time: number; // 播放进度(秒) 9 total_time: number; // 总时长(秒) 10 save_time: number; // 保存时间戳 11 search_title: string; // 搜索时使用的标题 12}
收藏模型(src/lib/types.ts:17-27):
typescript1export interface Favorite { 2 source_name: string; 3 total_episodes: number; 4 title: string; 5 year: string; 6 cover: string; 7 save_time: number; 8 search_title: string; 9 origin?: 'vod' | 'live'; // 内容来源类型 10}
IStorage 接口(src/lib/types.ts:29-69)定义了 20+ 个方法,覆盖播放记录、收藏、用户管理、搜索历史、管理员配置等完整业务场景,确保所有存储后端行为一致。
安全认证与请求处理
中间件认证拦截流程
src/middleware.ts:1-138 实现了完整的请求拦截与认证逻辑:
正在加载图表渲染器...
路径白名单(src/middleware.ts:117-131):
typescript1function shouldSkipAuth(pathname: string): boolean { 2 const skipPaths = [ 3 '/login', 4 '/warning', 5 '/api/login', 6 '/api/register', 7 '/api/logout', 8 '/api/cron', 9 '/api/server-config', 10 '/_next', 11 '/favicon.ico', 12 '/robots.txt', 13 '/manifest.json', 14 '/icons/', 15 '/logo.png', 16 '/screenshot.png', 17 ]; 18 return skipPaths.some((path) => pathname.startsWith(path)); 19}
Matcher 配置(src/middleware.ts:134-138):使用负向先行断言排除静态资源与认证相关路径,确保中间件只处理需要认证的页面请求。
Cookie 与签名验证机制
src/lib/auth.ts:4-65 提供了认证信息的解析与验证能力:
服务端 Cookie 解析(src/lib/auth.ts:4-23):
typescript1export function getAuthInfoFromCookie(request: NextRequest): { 2 password?: string; 3 username?: string; 4 signature?: string; 5 timestamp?: number; 6} | null { 7 const authCookie = request.cookies.get('auth'); 8 if (!authCookie) return null; 9 10 try { 11 const decoded = decodeURIComponent(authCookie.value); 12 const authData = JSON.parse(decoded); 13 return authData; 14 } catch (error) { 15 return null; 16 } 17}
客户端 Cookie 解析(src/lib/auth.ts:25-65):
- 手动解析
document.cookie字符串 - 处理双重 URL 编码情况(检测解码后是否仍包含
%) - 返回包含
role字段的扩展类型(owner|admin|user)
多模式认证策略:
| 存储模式 | Cookie 内容 | 验证方式 |
|---|---|---|
localstorage | password 明文 | 直接比对环境变量 PASSWORD |
| 其他模式 | username + signature | 签名验证(需确认签名算法实现位置) |
业务服务与工具库
直播流解析与 EPG 集成
src/lib/live.ts:29-89 实现了直播频道的获取、解析与缓存:
频道获取流程:
- 缓存检查:优先返回内存中的
cachedLiveChannels[key] - 配置查找:从
LiveConfig数组中查找匹配的直播源配置 - 频道刷新:调用
refreshLiveChannels获取并解析 M3U 播放列表 - EPG 解析:根据 M3U 中的
tvg-url或配置中的epgURL 获取电子节目单 - 配置更新:将频道数量写回配置并持久化
M3U 解析(src/lib/live.ts:51-81):
typescript1export async function refreshLiveChannels(liveInfo: { 2 key: string; 3 name: string; 4 url: string; 5 ua?: string; 6 epg?: string; 7 from: 'config' | 'custom'; 8 channelNumber?: number; 9 disabled?: boolean; 10}): Promise<number> { 11 // 清除旧缓存 12 if (cachedLiveChannels[liveInfo.key]) { 13 delete cachedLiveChannels[liveInfo.key]; 14 } 15 16 // 获取 M3U 内容 17 const ua = liveInfo.ua || defaultUA; 18 const response = await fetch(liveInfo.url, { 19 headers: { 'User-Agent': ua }, 20 }); 21 const data = await response.text(); 22 23 // 解析 M3U 并获取 EPG 24 const result = parseM3U(liveInfo.key, data); 25 const epgUrl = liveInfo.epg || result.tvgUrl; 26 const epgs = await parseEpg(epgUrl, ua, 27 result.channels.map(c => c.tvgId).filter(Boolean)); 28 29 // 缓存结果 30 cachedLiveChannels[liveInfo.key] = { 31 channelNumber: result.channels.length, 32 channels: result.channels, 33 epgUrl: epgUrl, 34 epgs: epgs, 35 }; 36 37 return result.channels.length; 38}
图片代理与资源优化
src/lib/utils.ts:4-107 提供了图片处理与视频质量检测工具:
豆瓣图片代理策略(src/lib/utils.ts:5-61):
| 代理类型 | 处理方式 | 适用场景 |
|---|---|---|
server | 通过 /api/image-proxy 服务端代理 | 需要隐藏源地址 |
cmliussss-cdn-tencent | 替换域名为 img.doubanio.cmliussss.net | 使用腾讯 CDN 加速 |
cmliussss-cdn-ali | 替换域名为 img.doubanio.cmliussss.com | 使用阿里 CDN 加速 |
custom | 拼接自定义代理 URL | 自建代理服务 |
视频质量检测(src/lib/utils.ts:63-107):
- 使用 HLS.js 加载 M3U8 播放列表
- 通过
HEAD请求测量网络延迟(ping 时间) - 从视频元数据提取分辨率信息
- 设置 4 秒超时防止长时间等待
时间解析工具
src/lib/time.ts:5-56 提供了自定义时间格式的兼容解析:
支持的格式:
- ISO 8601:包含
T或-的标准格式 - 自定义格式:
YYYYMMDDHHMMSS +ZZZZ(如20250824000000 +0800)
解析逻辑(src/lib/time.ts:5-25):
typescript1export function parseCustomTimeFormat(timeStr: string): Date { 2 // 标准格式直接返回 3 if (timeStr.includes('T') || timeStr.includes('-')) { 4 return new Date(timeStr); 5 } 6 7 // 处理 "20250824000000 +0800" 格式 8 const match = timeStr.match( 9 /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\s*([+-]\d{4})$/ 10 ); 11 12 if (match) { 13 const [, year, month, day, hour, minute, second, timezone] = match; 14 const isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}${timezone}`; 15 return new Date(isoString); 16 } 17 18 return new Date(timeStr); 19}
核心设计决策与取舍
架构模式选择
-
适配器模式用于存储层:通过
IStorage接口抽象存储操作,支持 Redis、Upstash、Kvrocks 等多种后端,使业务逻辑与存储实现解耦。代价是增加了间接层,但换来了部署灵活性(src/lib/db.ts:19-31) -
中间件认证而非 API 路由守卫:在 Edge Middleware 层统一处理认证,避免每个 API 路由重复验证逻辑。限制是无法访问完整 Node.js API,但 Next.js 的
NextRequest/NextResponse足够满足需求(src/middleware.ts:7-56) -
运行时配置注入而非构建时环境变量:通过
window.RUNTIME_CONFIG将服务端配置传递给客户端,支持同一构建产物在不同环境部署。代价是首屏渲染前需要等待配置加载(src/app/layout.tsx:106-111) -
内存缓存 + 数据库持久化:配置数据在内存中缓存,减少数据库访问频率,同时通过
configSelfCheck确保数据完整性。风险是重启后缓存丢失,但通过启动时自动加载解决(src/lib/config.ts:293-315) -
Standalone 输出模式:Next.js 配置为
output: 'standalone',生成包含必要依赖的独立目录,显著减小 Docker 镜像体积。代价是无法使用某些依赖动态require的特性(next.config.js:4-5)
技术选型理由
| 技术 | 用途 | 选型理由 | 替代方案 |
|---|---|---|---|
| Next.js | 全栈框架 | SSR/SSG 支持、API 路由、Middleware 能力 | Nuxt.js、SvelteKit |
| Tailwind CSS | 样式方案 | 原子化 CSS、设计系统一致性、构建时优化 | styled-components、CSS Modules |
| next-pwa | PWA 支持 | 与 Next.js 深度集成、零配置 Service Worker | workbox-webpack-plugin |
| next-themes | 主题切换 | 无闪烁切换、系统主题跟随、SSR 友好 | 自定义 Context 实现 |
| Redis/Kvrocks | 数据存储 | 高性能键值存储、支持过期时间、持久化 | PostgreSQL、MongoDB |
| HLS.js | 视频播放 | 浏览器端 HLS 支持、质量检测能力 | Video.js、Plyr |
已知限制与边界条件
-
localstorage 模式无持久化:当
STORAGE_TYPE=localstorage时,存储层返回null,用户数据仅保存在浏览器本地(src/lib/db.ts:27-29) -
图片优化禁用:
images.unoptimized: true导致无法使用 Next.js 图片优化 CDN,所有图片原样输出(next.config.js:18-20) -
SWC 压缩禁用:
swcMinify: false回退到 Terser 压缩,可能影响构建速度(next.config.js:11) -
Cron 依赖内部 HTTP 调用:定时任务通过
http.get调用/api/cron,在无外网访问的容器环境可能失败(start.js:60-70) -
双重 Cookie 编码兼容:客户端解析需要处理双重 URL 编码情况,增加了复杂度(src/lib/auth.ts:59-65)
模块依赖关系
正在加载图表渲染器...
依赖方向说明:
- 应用层 → 服务层:
RootLayout依赖ConfigService获取站点配置,Middleware依赖AuthService解析认证信息 - 服务层 → 基础设施层:
ConfigService和LiveService都依赖DbManager进行数据持久化 - 跨层依赖:所有模块都依赖
Types模块定义的接口与数据结构
关键配置与启动流程
环境变量配置
| 变量名 | 用途 | 默认值 |
|---|---|---|
NEXT_PUBLIC_STORAGE_TYPE | 存储后端类型 | localstorage |
NEXT_PUBLIC_SITE_NAME | 站点名称 | MoonTV |
PASSWORD | 访问密码(必填) | 无 |
NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索最大页数 | 5 |
NEXT_PUBLIC_DOUBAN_PROXY_TYPE | 豆瓣代理类型 | cmliussss-cdn-tencent |
NEXT_PUBLIC_FLUID_SEARCH | 流式搜索开关 | true |
启动命令序列
bash1# Docker 环境启动流程 21. node start.js 32. 执行 generate-manifest.js 生成 PWA 清单 43. 启动 Next.js standalone server 54. 轮询 /login 检测服务就绪 65. 执行首次 Cron 任务 76. 进入每小时 Cron 循环
配置初始化流程
src/lib/config.ts:185-292 中的 getInitConfig 函数定义了配置初始化逻辑:
- 解析传入的配置文件 JSON
- 从环境变量读取站点配置(站点名称、公告、搜索页数等)
- 从环境变量读取代理配置(豆瓣代理类型、URL)
- 初始化用户配置(空用户列表)
- 合并文件中的源站点配置
- 初始化自定义分类与直播配置
