欢迎来到 KVideo 项目!我们非常感谢你愿意为这个项目做出贡献。无论是修复 Bug、添加新功能、改进文档,还是提出建议,你的每一份贡献都将让这个项目变得更好。
为了确保协作顺畅、代码质量一致,请在提交贡献前仔细阅读本指南。
我们致力于构建一个开放、友好、包容的社区环境。请在参与项目时:
- ✅ 保持尊重和礼貌
- ✅ 欢迎不同的观点和经验
- ✅ 接受建设性的批评
- ✅ 专注于对社区最有利的事情
- ❌ 不要使用性别化的语言或图像
- ❌ 不要进行人身攻击或政治攻击
- ❌ 不要骚扰或歧视他人
详细的行为准则请参阅 CODE_OF_CONDUCT.md。
以下是一些你可以做出贡献的方式:
- 🐛 报告 Bug:发现了问题?请提交 Issue
- 💡 提出新功能:有好想法?在 Discussions 或 Issues 中分享
- 📝 改进文档:发现文档不清晰或有错误?帮助我们改进
- 🎨 优化 UI/UX:让界面更美观、更易用
- ⚡ 性能优化:让应用运行得更快
- 🔧 修复 Bug:解决现有的问题
- ✨ 添加功能:实现新的特性
如果这是你第一次为开源项目做贡献,我们推荐:
- 浏览 GitHub Issues
- 寻找标记为
good first issue的问题 - 在 Issue 中评论,表明你想要解决这个问题
- 按照本指南进行开发和提交
确保你的开发环境满足以下要求:
| 工具 | 最低版本 | 推荐版本 | 检查命令 |
|---|---|---|---|
| Node.js | 20.0.0 | 20.x LTS | node --version |
| npm | 9.0.0 | 10.x | npm --version |
| Git | 2.30.0 | 最新版本 | git --version |
点击 GitHub 页面右上角的 "Fork" 按钮,将项目 Fork 到你的账号下。
# 克隆你 Fork 的仓库
git clone https://github.com/YOUR_USERNAME/KVideo.git
cd KVideo
# 添加上游仓库
git remote add upstream https://github.com/KuekHaoYang/KVideo.gitnpm installnpm run dev访问 http://localhost:3000 查看应用。
确保以下命令都能正常运行:
# 代码检查
npm run lint
# 构建测试
npm run buildCaution
这是项目的硬性规则!所有项目文件必须保持在 150 行以内(除系统文件外)。
检查命令:
find . -type f -not -path "*/node_modules/*" -not -path "*/.next/*" -not -path "*/.git/*" -not -name "package-lock.json" -not -name "*.png" -not -name "*.md" | xargs wc -l | awk '$1 > 150 && $2 != "total" {print $2 " - " $1 "行"}'如果命令有输出,说明有文件超过 150 行,必须重构!
重构策略:
如果文件超过 150 行,请使用以下方法重构:
问题: 一个组件太长,包含太多 JSX
解决方案: 将大组件拆分为多个小组件
// ❌ 不好:一个 200 行的大组件
export function VideoPlayer() {
// 150+ 行代码
return (
<div>
{/* 大量 JSX */}
</div>
);
}
// ✅ 好:拆分为多个小组件
export function VideoPlayer() {
return (
<div>
<PlayerControls />
<ProgressBar />
<VolumeControl />
</div>
);
}
// PlayerControls.tsx (单独文件)
export function PlayerControls() { /* ... */ }
// ProgressBar.tsx (单独文件)
export function ProgressBar() { /* ... */ }
// VolumeControl.tsx (单独文件)
export function VolumeControl() { /* ... */ }问题: 组件包含大量状态逻辑
解决方案: 将逻辑提取到自定义 Hook
// ❌ 不好:组件内有大量状态逻辑
export function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
// ... 大量逻辑
const handleSearch = async () => {
// ... 50+ 行逻辑
};
return <div>{/* JSX */}</div>;
}
// ✅ 好:提取到自定义 Hook
export function SearchPage() {
const { query, results, loading, handleSearch } = useSearch();
return <div>{/* JSX */}</div>;
}
// useSearch.ts (单独文件)
export function useSearch() {
// ... 所有状态逻辑
return { query, results, loading, handleSearch };
}问题: 文件包含大量辅助函数
解决方案: 将工具函数移到 lib/utils/
// ❌ 不好:组件文件包含工具函数
export function VideoCard() {
const formatDuration = (seconds: number) => {
// ... 格式化逻辑
};
const formatDate = (date: Date) => {
// ... 格式化逻辑
};
// ... 更多工具函数
return <div>{/* JSX */}</div>;
}
// ✅ 好:提取到工具文件
import { formatDuration, formatDate } from '@/lib/utils/format-utils';
export function VideoCard() {
return <div>{/* JSX */}</div>;
}
// lib/utils/format-utils.ts
export function formatDuration(seconds: number) { /* ... */ }
export function formatDate(date: Date) { /* ... */ }问题: 单个文件处理多个相关功能
解决方案: 按功能拆分文件并使用桶文件(barrel exports)
// ❌ 不好:player-utils.ts 包含 200 行
export function parseHLS() { /* ... */ }
export function handlePlayback() { /* ... */ }
export function manageQuality() { /* ... */ }
// ... 更多函数
// ✅ 好:拆分为多个文件
// lib/utils/player/index.ts
export * from './hls-parser';
export * from './playback-manager';
export * from './quality-manager';
// lib/utils/player/hls-parser.ts
export function parseHLS() { /* ... */ }
// lib/utils/player/playback-manager.ts
export function handlePlayback() { /* ... */ }
// lib/utils/player/quality-manager.ts
export function manageQuality() { /* ... */ }类型安全
// ❌ 避免使用 any
function processData(data: any) {
return data.value;
}
// ✅ 使用具体类型
interface VideoData {
id: string;
title: string;
url: string;
}
function processData(data: VideoData) {
return data.title;
}
// ✅ 或使用 unknown(需要类型检查)
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return (data as { value: string }).value;
}
throw new Error('Invalid data');
}函数返回类型
// ❌ 缺少返回类型
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// ✅ 明确返回类型
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}接口定义
// ✅ 使用 interface 定义对象类型
interface VideoCardProps {
video: Video;
onPlay: (id: string) => void;
className?: string;
}
// ✅ 使用 type 定义联合类型
type ThemeMode = 'light' | 'dark' | 'system';函数组件
// ✅ 标准函数组件结构
interface ButtonProps {
variant?: 'primary' | 'secondary';
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ variant = 'primary', children, onClick }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}组件文件组织
// 1. 导入
import React from 'react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
// 2. 类型定义
interface ComponentProps {
// ...
}
// 3. 组件定义
export function Component({ prop1, prop2 }: ComponentProps) {
// 4. Hooks
const [state, setState] = useState();
const router = useRouter();
// 5. 事件处理函数
const handleClick = () => {
// ...
};
// 6. 渲染
return (
<div>{/* JSX */}</div>
);
}单一职责原则
// ❌ 组件做太多事情
export function VideoSection() {
// 获取数据
// 处理搜索
// 渲染列表
// 处理分页
// 处理过滤
}
// ✅ 拆分为专注的组件
export function VideoSection() {
const videos = useVideos();
return (
<div>
<SearchBar />
<FilterPanel />
<VideoList videos={videos} />
<Pagination />
</div>
);
}Tailwind CSS 优先
// ✅ 使用 Tailwind 类名
export function Card({ children }: { children: React.ReactNode }) {
return (
<div className="rounded-2xl glass p-6 hover:shadow-lg transition-shadow">
{children}
</div>
);
}遵循 Liquid Glass 设计系统
// ✅ 正确使用圆角
<div className="rounded-2xl"> {/* 容器:大圆角 */}
<div className="rounded-full"> {/* 小元素:完全圆形 */}
// ❌ 不要使用其他圆角值
<div className="rounded-lg"> {/* 错误! */}
<div className="rounded-xl"> {/* 错误! */}响应式设计
// ✅ 移动优先的响应式设计
<div className="
flex flex-col {/* 移动端:垂直布局 */}
md:flex-row {/* 平板及以上:水平布局 */}
gap-4 md:gap-6 {/* 响应式间距 */}
">文件命名
- 组件文件:
PascalCase.tsx(例如:VideoCard.tsx) - Hook 文件:
camelCase.ts(例如:useVideoPlayer.ts) - 工具文件:
kebab-case.ts(例如:format-utils.ts) - 类型文件:
kebab-case.ts(例如:video-types.ts)
变量命名
// ✅ 清晰的命名
const videoList = [...];
const isLoading = false;
const handleSubmit = () => {};
// ❌ 模糊的命名
const data = [...];
const flag = false;
const fn = () => {};常量命名
// ✅ 全大写 + 下划线
const MAX_VIDEO_DURATION = 7200;
const API_BASE_URL = 'https://api.example.com';// 1. React 和 Next.js
import React from 'react';
import { useState } from 'react';
import Link from 'next/link';
// 2. 第三方库
import { create } from 'zustand';
// 3. 项目别名导入
import { Button } from '@/components/ui/Button';
import { formatDate } from '@/lib/utils/date-utils';
// 4. 相对路径导入
import { LocalComponent } from './LocalComponent';
// 5. 类型导入
import type { Video } from '@/lib/types/video';主分支
main:稳定的生产分支,只接受 PR 合并
功能分支命名
遵循以下命名规范:
feat/功能名称:新功能(例如:feat/add-playlist)fix/问题描述:错误修复(例如:fix/search-crash)docs/文档修改:文档更新(例如:docs/update-readme)refactor/重构名称:代码重构(例如:refactor/player-controls)perf/优化内容:性能优化(例如:perf/image-loading)style/样式修改:样式调整(例如:style/button-spacing)test/测试内容:测试相关(例如:test/add-unit-tests)chore/其他修改:构建或工具变动(例如:chore/update-deps)
在开始新工作前,先同步最新的代码:
# 获取上游更新
git fetch upstream
# 切换到主分支
git checkout main
# 合并上游更新
git merge upstream/main
# 推送到你的 Fork
git push origin main# 从 main 创建新分支
git checkout -b feat/your-feature-name
# 确认当前分支
git branch在开发过程中:
- 频繁提交小的、原子性的改动
- 编写清晰的提交信息
- 定期运行
npm run lint检查代码
必须通过的检查:
# 1. 代码规范检查
npm run lint
# 2. 文件长度检查
find . -type f -not -path "*/node_modules/*" -not -path "*/.next/*" -not -path "*/.git/*" -not -name "package-lock.json" -not -name "*.png" -not -name "*.md" | xargs wc -l | awk '$1 > 150 && $2 != "total" {print $2 " - " $1 "行"}'
# 3. 构建测试
npm run build如果任何检查失败,必须先修复!
# 推送到你的 Fork
git push origin feat/your-feature-name我们使用 Conventional Commits 规范:
<type>(<scope>): <subject>
<body>
<footer>
Type 类型:
feat:新功能fix:错误修复docs:文档变更style:代码格式(不影响代码运行)refactor:重构perf:性能优化test:测试相关chore:构建过程或辅助工具的变动
示例:
# 简单提交
git commit -m "feat: 添加视频播放列表功能"
# 详细提交
git commit -m "feat(player): 添加倍速播放功能
- 支持 0.5x 到 2x 的播放速度
- 添加速度选择器 UI
- 保存用户的速度偏好
Closes #123"提交信息最佳实践:
- ✅ 使用中文或英文(保持一致)
- ✅ 使用祈使句("添加功能" 而不是 "添加了功能")
- ✅ 第一行不超过 50 个字符
- ✅ 正文每行不超过 72 个字符
- ✅ 说明 "做了什么" 和 "为什么",而不仅是 "怎么做"
- 推送分支到你的 Fork
git push origin feat/your-feature-name- 在 GitHub 上创建 PR
- 访问你的 Fork 页面
- 点击 "Compare & pull request"
- 选择目标分支:
KuekHaoYang/KVideo:main
## 📝 变更说明
简要描述这个 PR 做了什么。
## 🎯 相关 Issue
Closes #123
Fixes #456
## 📸 截图(如果是 UI 变更)
[如果有 UI 变更,添加截图或 GIF]
## ✅ 检查清单
- [ ] 代码已通过 `npm run lint`
- [ ] 所有文件都在 150 行以内
- [ ] 构建成功(`npm run build`)
- [ ] 已在本地测试所有变更
- [ ] 遵循 Liquid Glass 设计系统
- [ ] 提交信息符合规范
- [ ] 已更新相关文档
## 🧪 测试步骤
1. 第一步
2. 第二步
3. 预期结果
## 📌 额外说明
[任何其他需要 reviewer 知道的信息]- 自动检查:GitHub Actions 会自动运行检查
- 代码审查:维护者会审查你的代码
- 修改请求:如果需要修改,会留下评论
- 批准和合并:审查通过后会被合并
# 进行修改后
git add .
git commit -m "refactor: 根据审查意见调整代码"
git push origin feat/your-feature-namePR 会自动更新。
在编写 UI 代码时,必须遵循 Liquid Glass 设计系统:
Important
只使用两种圆角:rounded-2xl 和 rounded-full
// ✅ 正确
<div className="rounded-2xl"> {/* 容器、卡片、按钮、输入框 */}
<div className="rounded-full"> {/* 头像、徽章、药丸形状 */}
// ❌ 错误
<div className="rounded-lg">
<div className="rounded-xl">
<div className="rounded-md">// ✅ 使用 glass 类或 backdrop-filter
<div className="glass">
{/* 内容 */}
</div>
// 或自定义玻璃效果
<div className="
backdrop-blur-xl
backdrop-saturate-180
backdrop-brightness-110
bg-white/10
border border-white/20
">// ✅ 使用标准过渡曲线
<button className="
transition-all
duration-300
ease-out
hover:scale-105
">// ✅ 使用 CSS 变量
<div className="bg-glass text-glass-text border-glass-border">
// 或 Tailwind 的语义化颜色
<div className="bg-primary text-primary-foreground">优先复用 components/ui/ 下的基础组件:
// ✅ 好:复用基础组件
import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
export function Feature() {
return (
<Modal>
<Button variant="primary">确定</Button>
</Modal>
);
}
// ❌ 不好:重新实现基础组件
export function Feature() {
return (
<div className="modal">
<button className="btn">确定</button>
</div>
);
}在提交 PR 前,请手动测试以下内容:
- 新功能按预期工作
- 没有破坏现有功能
- 边界情况处理正确
在以下浏览器中测试:
- Chrome/Edge(最新版)
- Firefox(最新版)
- Safari(最新版)
在以下设备尺寸测试:
- 移动端(375px - 428px)
- 平板端(768px - 1024px)
- 桌面端(1280px+)
- 键盘导航正常工作
- 焦点状态清晰可见
- 屏幕阅读器友好
# 运行 ESLint
npm run lint
# 检查文件长度
find . -type f -not -path "*/node_modules/*" -not -path "*/.next/*" -not -path "*/.git/*" -not -name "package-lock.json" -not -name "*.png" -not -name "*.md" | xargs wc -l | awk '$1 > 150 && $2 != "total" {print $2 " - " $1 "行"}'A: 查看标记为 good first issue 的 Issues,这些通常比较简单,适合新手。
A: 参考 文件长度限制 部分的重构策略。关键是:
- 提取组件
- 提取 Hook
- 提取工具函数
- 模块化
注:系统文件(如 README.md、CONTRIBUTING.md 等文档)不受此限制。
- 提取组件
- 提取 Hook
- 提取工具函数
- 模块化
A: 通常在 1-3 个工作日内。如果超过一周没有回应,可以在 PR 中添加评论提醒。
A: 可以,但建议每个 PR 专注于一个功能或修复。避免在一个 PR 中做太多不相关的改动。
# 1. 同步上游
git fetch upstream
git checkout main
git merge upstream/main
# 2. 切换到功能分支并 rebase
git checkout feat/your-feature
git rebase main
# 3. 解决冲突后
git add .
git rebase --continue
# 4. 强制推送(因为 rebase 改变了历史)
git push origin feat/your-feature --force# 修改最后一次提交
git commit --amend -m "新的提交信息"
# 如果已经推送了
git push origin feat/your-feature --force- 启动开发服务器:
npm run dev - 在浏览器中手动测试功能
- 测试不同的设备尺寸
- 运行
npm run build确保生产构建成功
在 app/styles/glass.css 文件中。所有组件都应该基于这个设计系统。
如果你的 PR 包含以下内容,请更新相应文档:
- 新功能:更新 README.md
- API 变化:更新相关注释和文档
- 配置变化:更新配置说明
请查看 SECURITY.md 了解安全漏洞报告流程。不要在公开 Issue 中讨论安全问题。
如果你有任何问题:
- 查看文档:README.md 和本指南
- 搜索 Issues:可能已经有人问过相同的问题
- 提出问题:在 Discussions 或 Issues 中提问
- 联系维护者:@KuekHaoYang
感谢你花时间阅读本指南,并为 KVideo 做出贡献。每一个贡献,无论大小,都让这个项目变得更好。
我们期待看到你的 Pull Request!