本文档为前端开发者提供完整的 GitHub 工作流指南,涵盖仓库管理、CI/CD、自动化测试、性能监控、代码质量保障等核心环节,帮助前端团队建立高效规范的开发流程。
- 前端项目 GitHub 仓库结构
- 前端项目 CI/CD
- 静态网站部署方案
- 前端自动化测试
- Lighthouse CI 自动化性能测试
- 前端代码质量工具
- 依赖管理与安全
- Storybook 与组件库管理
- 前端 Monorepo 管理
- npm/pnpm 包发布流程
- 前端国际化(i18n)协作
- 设计稿与代码协作
- 国内前端团队 GitHub 使用经验
一个规范的前端项目仓库应该具备清晰的目录结构,便于团队协作和自动化工具集成。以下是一个典型的 React 项目结构:
my-frontend-app/
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # 持续集成流水线
│ │ ├── deploy.yml # 部署流水线
│ │ ├── lighthouse.yml # 性能检测流水线
│ │ └── release.yml # 发布流水线
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md # Bug 报告模板
│ │ └── feature_request.md # 功能需求模板
│ ├── PULL_REQUEST_TEMPLATE.md # PR 模板
│ ├── CODEOWNERS # 代码所有者配置
│ └── dependabot.yml # 依赖更新配置
├── src/
│ ├── components/ # 通用组件
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.test.tsx
│ │ │ ├── Button.stories.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── pages/ # 页面组件
│ ├── hooks/ # 自定义 Hook
│ ├── utils/ # 工具函数
│ ├── services/ # API 服务层
│ ├── stores/ # 状态管理
│ ├── styles/ # 全局样式
│ ├── types/ # TypeScript 类型定义
│ ├── locales/ # 国际化资源
│ └── App.tsx
├── public/ # 静态资源
├── tests/ # 测试配置和工具
│ ├── setup.ts
│ └── mocks/
├── scripts/ # 构建和部署脚本
├── docs/ # 项目文档
├── .eslintrc.js # ESLint 配置
├── .prettierrc # Prettier 配置
├── .stylelintrc.js # Stylelint 配置
├── .lintstagedrc.js # lint-staged 配置
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 构建配置
├── package.json
├── pnpm-lock.yaml
└── README.md
.github 目录是 GitHub 项目的核心配置目录,包含工作流、模板、代码审查规则等关键配置。
CODEOWNERS 文件示例:
# 全局代码所有者
* @frontend-team
# 组件库由特定成员维护
/src/components/ @zhangsan @lisi
# 构建配置变更需要架构师审批
/vite.config.ts @architect-wang
/tsconfig.json @architect-wang
/.github/ @devops-team
# 国际化文件由翻译团队维护
/src/locales/ @i18n-team
Issue 模板配置(.github/ISSUE_TEMPLATE/config.yml):
blank_issues_enabled: false
contact_links:
- name: 功能讨论
url: https://github.com/orgs/your-org/discussions
about: 请在 Discussions 中讨论新功能想法
- name: 查看文档
url: https://your-docs-site.com
about: 查看项目文档获取帮助前端项目推荐采用 Git Flow 或 Trunk-Based Development:
| 策略 | 适用场景 | 分支模型 | 优点 | 缺点 |
|---|---|---|---|---|
| Git Flow | 大型项目、版本发布 | main/develop/feature/release/hotfix | 规范清晰、适合多版本并行 | 流程复杂、合并冲突多 |
| Trunk-Based | 持续部署、小团队 | main/feature-short-lived | 速度快、冲突少 | 需要完善的自动化测试 |
| GitHub Flow | 简单项目、快速迭代 | main/feature | 简单易学 | 不适合复杂发布流程 |
Git Flow 示例:
# 创建功能分支
git checkout develop
git pull origin develop
git checkout -b feature/user-login
# 开发完成后推送
git push origin feature/user-login
# 在 GitHub 上创建 PR,目标分支为 develop
# Code Review 通过后合并
# 发布流程
git checkout -b release/v1.2.0 develop
# 修复发布相关问题后
git checkout main
git merge release/v1.2.0
git tag -a v1.2.0 -m "Release v1.2.0"
git push origin main --tags前端项目应严格遵循 Conventional Commits 规范,便于生成 CHANGELOG 和自动化版本管理:
<type>(<scope>): <subject>
[body]
[footer]
常用 type 说明:
| type | 说明 | 示例 |
|---|---|---|
| feat | 新功能 | feat(login): 添加微信扫码登录 |
| fix | 修复 Bug | fix(cart): 修复数量选择器溢出问题 |
| docs | 文档更新 | docs: 更新 API 接口文档 |
| style | 代码格式调整 | style: 统一缩进为 2 空格 |
| refactor | 代码重构 | refactor(hooks): 抽取 useRequest 通用逻辑 |
| perf | 性能优化 | perf(list): 虚拟列表优化大数据渲染 |
| test | 测试相关 | test(login): 补充登录模块单元测试 |
| chore | 构建/工具变更 | chore: 升级 vite 至 5.0 |
| ci | CI 配置变更 | ci: 添加 Lighthouse CI 检测 |
| revert | 回滚 | revert: 回滚 feat(login) 微信登录 |
使用 commitlint 自动校验:
pnpm add -D @commitlint/cli @commitlint/config-conventional husky// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'chore', 'ci', 'revert'
]],
'scope-enum': [2, 'always', [
'login', 'cart', 'product', 'user', 'order',
'payment', 'common', 'config', 'deps'
]],
'subject-max-length': [2, 'always', 100],
'body-max-line-length': [1, 'always', 200],
}
};完整的 React + TypeScript 项目 CI 流水线:
# .github/workflows/ci.yml
name: React App CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
env:
NODE_VERSION: '20'
PNPM_VERSION: '9'
jobs:
lint:
name: 代码检查
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: 安装依赖
run: pnpm install --frozen-lockfile
- name: ESLint 检查
run: pnpm eslint . --ext .ts,.tsx --format json --output-file eslint-report.json
- name: Prettier 检查
run: pnpm prettier --check "src/**/*.{ts,tsx,css,scss}"
- name: Stylelint 检查
run: pnpm stylelint "src/**/*.{css,scss}"
- name: TypeScript 类型检查
run: pnpm tsc --noEmit
- name: 上传检查报告
if: always()
uses: actions/upload-artifact@v4
with:
name: lint-reports
path: eslint-report.json
test:
name: 自动化测试
runs-on: ubuntu-latest
needs: lint
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: 安装依赖
run: pnpm install --frozen-lockfile
- name: 运行单元测试
run: pnpm test -- --coverage --reporters=default --reporters=jest-junit
env:
JEST_JUNIT_OUTPUT_DIR: ./test-results
- name: 上传测试结果
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results/
- name: 上传覆盖率报告
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
- name: 检查覆盖率阈值
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "当前代码覆盖率: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "::warning::代码覆盖率 ${COVERAGE}% 低于阈值 80%"
fi
build:
name: 构建验证
runs-on: ubuntu-latest
needs: test
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 设置 Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: 安装依赖
run: pnpm install --frozen-lockfile
- name: 构建项目
run: pnpm build
env:
NODE_ENV: production
- name: 分析构建产物大小
run: |
echo "## 构建产物分析" >> $GITHUB_STEP_SUMMARY
echo "| 文件类型 | 大小 | 文件数量 |" >> $GITHUB_STEP_SUMMARY
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
for ext in js css html json png jpg svg woff2; do
SIZE=$(find dist -name "*.${ext}" -exec du -ch {} + 2>/dev/null | tail -1 | cut -f1)
COUNT=$(find dist -name "*.${ext}" | wc -l)
if [ "$COUNT" -gt 0 ]; then
echo "| .${ext} | ${SIZE:-0} | ${COUNT} |" >> $GITHUB_STEP_SUMMARY
fi
done
- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.node-version }}
path: dist/
retention-days: 7# .github/workflows/vue-ci.yml
name: Vue 3 App CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Vue 类型检查 (vue-tsc)
run: pnpm vue-tsc --noEmit
- name: ESLint
run: pnpm eslint . --ext .vue,.ts,.tsx
- name: 单元测试 (Vitest)
run: pnpm vitest run --coverage
env:
VITE_API_BASE_URL: https://test-api.example.com
- name: 构建
run: pnpm build
- name: 部署预览(PR 环境)
if: github.event_name == 'pull_request'
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
working-directory: ./# .github/workflows/angular-ci.yml
name: Angular App CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Lint
run: npx ng lint
- name: 单元测试 (Karma)
run: npx ng test --watch=false --code-coverage --browsers=ChromeHeadless
- name: E2E 测试 (Playwright)
run: npx playwright install --with-deps chromium && npx ng e2e
- name: 构建
run: npx ng build --configuration=production
- name: 上传覆盖率到 Codecov
uses: codecov/codecov-action@v4
with:
directory: ./coverage
token: ${{ secrets.CODECOV_TOKEN }}# .github/workflows/sveltekit-ci.yml
name: SvelteKit CI
on:
push:
branches: [main]
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Svelte Check
run: pnpm svelte-kit sync && pnpm svelte-check
- name: Lint
run: pnpm lint
- name: Test (Vitest)
run: pnpm test -- --coverage
- name: Build
run: pnpm build
- name: Adapter 验证
run: ls -la build/一个成熟的前端项目 CI/CD 流水线应包含以下阶段:
代码提交 → 代码检查 → 单元测试 → 构建验证 → E2E测试 → 预览部署 → 生产部署 → 监控
各阶段职责:
| 阶段 | 工具 | 耗时 | 失败处理 |
|---|---|---|---|
| 代码检查 | ESLint/Prettier/Stylelint | 1-2 分钟 | 阻止合并 |
| 单元测试 | Jest/Vitest | 2-5 分钟 | 阻止合并 |
| 构建验证 | Vite/Webpack | 2-4 分钟 | 阻止合并 |
| E2E 测试 | Playwright/Cypress | 5-15 分钟 | 阻止合并(可选) |
| 预览部署 | Vercel/Netlify | 1-3 分钟 | 告警通知 |
| 生产部署 | GitHub Actions | 3-10 分钟 | 自动回滚 |
| 监控 | Sentry/Lighthouse | 持续 | 告警通知 |
GitHub Pages 是最简单的免费静态网站托管方案,适合个人项目、文档站点和小型应用。
基础部署配置:
# .github/workflows/deploy-pages.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: 配置 GitHub Pages
uses: actions/configure-pages@v4
- name: 上传构建产物
uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: 部署到 GitHub Pages
id: deployment
uses: actions/deploy-pages@v4SPA 路由处理(Vue Router / React Router):
由于 GitHub Pages 不支持服务端路由,SPA 应用需要特殊处理:
# 在构建后创建 404.html 处理 SPA 路由
- name: SPA 路由处理
run: |
cp dist/index.html dist/404.html或者使用 spa-github-pages 方案,在 index.html 中添加重定向脚本。
自定义域名配置:
# 在仓库 Settings > Pages 中配置自定义域名
# 或在构建步骤中自动配置
- name: 配置自定义域名
if: github.ref == 'refs/heads/main'
run: |
echo "www.example.com" > dist/CNAMEVercel 是前端项目最流行的部署平台之一,提供极佳的开发者体验。
自动部署配置:
// vercel.json
{
"buildCommand": "pnpm build",
"outputDirectory": "dist",
"framework": "vite",
"rewrites": [
{ "source": "/api/(.*)", "destination": "https://api.example.com/$1" }
],
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}GitHub Actions 与 Vercel 集成:
# .github/workflows/vercel-deploy.yml
name: Vercel Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 安装 Vercel CLI
run: npm install -g vercel
- name: 拉取 Vercel 环境配置
run: vercel pull --yes --environment=${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }}
- name: 构建
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- name: 部署
id: deploy
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
url=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }})
else
url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
fi
echo "url=$url" >> $GITHUB_OUTPUT
- name: 评论 PR 部署链接
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 预览部署: ${{ steps.deploy.outputs.url }}`
})# .github/workflows/netlify-deploy.yml
name: Netlify Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile && pnpm build
- name: 部署到 Netlify
uses: nwtgck/actions-netlify@v3
with:
publish-dir: ./dist
production-deploy: ${{ github.ref == 'refs/heads/main' }}
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions - ${{ github.sha }}"
netlify-config-path: ./netlify.toml
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}Netlify 配置文件:
# netlify.toml
[build]
command = "pnpm build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[plugins]]
package = "@netlify/plugin-lighthouse"Cloudflare Pages 提供全球 CDN 和 Workers 支持,适合对性能要求高的项目。
# .github/workflows/cloudflare-pages.yml
name: Cloudflare Pages Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile && pnpm build
- name: 部署到 Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-app| 特性 | GitHub Pages | Vercel | Netlify | Cloudflare Pages |
|---|---|---|---|---|
| 免费额度 | 100GB/月 | 100GB/月 | 100GB/月 | 无限带宽 |
| 构建时间 | 需自建 CI | 6000分钟/月 | 300分钟/月 | 500次/月 |
| 自定义域名 | 支持 | 支持 | 支持 | 支持 |
| SSL 证书 | 自动 | 自动 | 自动 | 自动 |
| Serverless | 不支持 | 支持 | 支持 | Workers |
| 预览部署 | 不支持 | 支持 | 支持 | 支持 |
| 国内访问 | 较慢 | 较慢 | 较慢 | 较快 |
| 适合场景 | 文档/博客 | 全栈应用 | 静态站点 | 全球化应用 |
Jest 是 React 生态中最流行的测试框架,也广泛用于 Vue 和 Angular 项目。
Jest 配置示例:
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterSetup: ['<rootDir>/tests/setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/tests/__mocks__/fileMock.js',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
'!src/main.tsx',
],
coverageThresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};React 组件测试示例:
// src/components/UserCard/UserCard.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi } from 'vitest';
import { UserCard } from './UserCard';
import { fetchUser } from '@/services/user';
vi.mock('@/services/user');
const mockUser = {
id: '1',
name: '张三',
email: 'zhangsan@example.com',
avatar: 'https://example.com/avatar.jpg',
};
describe('UserCard', () => {
beforeEach(() => {
vi.mocked(fetchUser).mockResolvedValue(mockUser);
});
afterEach(() => {
vi.clearAllMocks();
});
it('应该正确渲染用户信息', async () => {
render(<UserCard userId="1" />);
await waitFor(() => {
expect(screen.getByText('张三')).toBeInTheDocument();
expect(screen.getByText('zhangsan@example.com')).toBeInTheDocument();
});
});
it('应该显示加载状态', () => {
render(<UserCard userId="1" />);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
it('应该处理加载失败', async () => {
vi.mocked(fetchUser).mockRejectedValue(new Error('网络错误'));
render(<UserCard userId="1" />);
await waitFor(() => {
expect(screen.getByText('加载失败')).toBeInTheDocument();
});
});
it('点击按钮应该触发回调', async () => {
const onEdit = vi.fn();
render(<UserCard userId="1" onEdit={onEdit} />);
await waitFor(() => {
expect(screen.getByText('张三')).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: '编辑' }));
expect(onEdit).toHaveBeenCalledWith(mockUser);
});
});// cypress/e2e/login.cy.ts
describe('登录功能', () => {
beforeEach(() => {
cy.visit('/login');
});
it('应该显示登录表单', () => {
cy.get('[data-testid="login-form"]').should('be.visible');
cy.get('input[name="username"]').should('exist');
cy.get('input[name="password"]').should('exist');
cy.get('button[type="submit"]').should('contain', '登录');
});
it('空表单提交应该显示验证错误', () => {
cy.get('button[type="submit"]').click();
cy.get('.error-message').should('have.length', 2);
cy.get('.error-message').first().should('contain', '请输入用户名');
});
it('应该成功登录并跳转', () => {
cy.intercept('POST', '/api/auth/login', {
statusCode: 200,
body: { token: 'fake-jwt-token', user: { name: '测试用户' } },
}).as('loginRequest');
cy.get('input[name="username"]').type('testuser');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.wait('@loginRequest');
cy.url().should('include', '/dashboard');
cy.get('.user-name').should('contain', '测试用户');
});
it('登录失败应该显示错误提示', () => {
cy.intercept('POST', '/api/auth/login', {
statusCode: 401,
body: { message: '用户名或密码错误' },
});
cy.get('input[name="username"]').type('wronguser');
cy.get('input[name="password"]').type('wrongpass');
cy.get('button[type="submit"]').click();
cy.get('.alert-error').should('contain', '用户名或密码错误');
});
});Playwright 是新一代 E2E 测试工具,支持多浏览器、自动等待和更好的调试体验。
// tests/e2e/shopping.spec.ts
import { test, expect } from '@playwright/test';
test.describe('购物流程', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('浏览商品并添加到购物车', async ({ page }) => {
// 浏览商品列表
await page.getByRole('link', { name: '商品列表' }).click();
await expect(page.getByTestId('product-list')).toBeVisible();
// 点击第一个商品
const firstProduct = page.getByTestId('product-card').first();
const productName = await firstProduct.getByTestId('product-name').textContent();
await firstProduct.click();
// 添加到购物车
await page.getByRole('button', { name: '加入购物车' }).click();
// 验证购物车数量
await expect(page.getByTestId('cart-count')).toHaveText('1');
// 验证购物车内容
await page.getByTestId('cart-icon').click();
await expect(page.getByTestId('cart-item')).toContainText(productName!);
});
test('完整的下单流程', async ({ page }) => {
// 登录
await page.goto('/login');
await page.getByLabel('用户名').fill('testuser');
await page.getByLabel('密码').fill('password123');
await page.getByRole('button', { name: '登录' }).click();
await expect(page.getByText('欢迎回来')).toBeVisible();
// 添加商品
await page.goto('/products/1');
await page.getByRole('button', { name: '加入购物车' }).click();
// 结算
await page.getByRole('button', { name: '去结算' }).click();
await expect(page).toHaveURL('/checkout');
// 填写收货地址
await page.getByLabel('收货人').fill('张三');
await page.getByLabel('手机号').fill('13800138000');
await page.getByLabel('详细地址').fill('北京市朝阳区xxx路xxx号');
// 提交订单
await page.getByRole('button', { name: '提交订单' }).click();
await expect(page.getByText('订单提交成功')).toBeVisible();
});
test('页面性能验证', async ({ page }) => {
const startTime = Date.now();
await page.goto('/');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000);
// 验证核心 Web 指标
const metrics = await page.evaluate(() => {
return new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
resolve(entries.map((e) => ({ name: e.name, value: e.value })));
}).observe({ type: 'largest-contentful-paint', buffered: true });
});
});
console.log('性能指标:', metrics);
});
});Playwright 配置文件:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'test-results/e2e-results.xml' }],
],
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});# .github/workflows/lighthouse.yml
name: Lighthouse CI
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile && pnpm build
- name: 启动本地服务器
run: pnpm preview &
env:
PORT: 4173
- name: 等待服务器就绪
run: npx wait-on http://localhost:4173 --timeout 30000
- name: 运行 Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
urls: |
http://localhost:4173/
http://localhost:4173/login
http://localhost:4173/products
configPath: .lighthouserc.json
uploadArtifacts: true
temporaryPublicStorage: trueLighthouse CI 配置文件:
// .lighthouserc.json
{
"ci": {
"collect": {
"numberOfRuns": 3,
"settings": {
"preset": "desktop",
"chromeFlags": "--no-sandbox --headless"
}
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.8 }],
"categories:accessibility": ["warn", { "minScore": 0.9 }],
"categories:best-practices": ["warn", { "minScore": 0.9 }],
"categories:seo": ["warn", { "minScore": 0.9 }],
"first-contentful-paint": ["warn", { "maxNumericValue": 2000 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"total-blocking-time": ["warn", { "maxNumericValue": 300 }]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}- name: 运行 Lighthouse CI 并评论
uses: treosh/lighthouse-ci-action@v12
id: lighthouse
with:
urls: |
http://localhost:4173/
uploadArtifacts: true
- name: 生成 Lighthouse 报告评论
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const results = JSON.parse(fs.readFileSync('${{ steps.lighthouse.outputs.manifest }}', 'utf8'));
let comment = '## 🔦 Lighthouse 性能报告\n\n';
comment += '| 页面 | 性能 | 可访问性 | 最佳实践 | SEO |\n';
comment += '|------|------|---------|---------|----|\n';
for (const result of results) {
const summary = result.summary;
const url = result.url;
const perf = (summary.performance * 100).toFixed(0);
const a11y = (summary.accessibility * 100).toFixed(0);
const bp = (summary['best-practices'] * 100).toFixed(0);
const seo = (summary.seo * 100).toFixed(0);
const score = (s) => s >= 90 ? '🟢' : s >= 50 ? '🟠' : '🔴';
comment += `| ${url} | ${score(summary.performance)} ${perf} | ${score(summary.accessibility)} ${a11y} | ${score(summary['best-practices'])} ${bp} | ${score(summary.seo)} ${seo} |\n`;
}
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});现代前端项目推荐使用 ESLint Flat Config 格式:
// eslint.config.js
import js from '@eslint/js';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import importPlugin from 'eslint-plugin-import';
import prettierConfig from 'eslint-config-prettier';
export default [
js.configs.recommended,
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
},
plugins: {
'@typescript-eslint': tsPlugin,
'react': reactPlugin,
'react-hooks': reactHooksPlugin,
'import': importPlugin,
},
rules: {
// TypeScript 规则
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
// React 规则
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Import 规则
'import/order': ['error', {
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
}],
// 通用规则
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'no-var': 'error',
},
settings: {
react: { version: 'detect' },
},
},
prettierConfig,
];// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.md",
"options": { "printWidth": 80 }
}
]
}// .stylelintrc.js
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-standard-scss',
'stylelint-config-prettier',
],
rules: {
'selector-class-pattern': '^[a-z][a-zA-Z0-9]+$',
'no-empty-source': null,
'scss/operator-no-newline-after': null,
'declaration-block-no-redundant-longhand-properties': null,
},
};// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss}": [
"stylelint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}# .github/workflows/quality.yml
name: Code Quality
on: [pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: 检查代码格式
run: |
pnpm eslint . --format json --output-file eslint.json || true
pnpm prettier --check "src/**/*" || true
- name: 代码质量报告
uses: actions/github-script@v7
if: always()
with:
script: |
const fs = require('fs');
try {
const eslintResults = JSON.parse(fs.readFileSync('eslint.json', 'utf8'));
const errors = eslintResults.reduce((sum, f) => sum + f.errorCount, 0);
const warnings = eslintResults.reduce((sum, f) => sum + f.warningCount, 0);
let report = `## 📊 代码质量报告\n\n`;
report += `- ❌ 错误: ${errors}\n`;
report += `- ⚠️ 警告: ${warnings}\n`;
report += `- 📁 检查文件数: ${eslintResults.length}\n`;
if (errors > 0) {
report += `\n### 错误详情\n`;
for (const file of eslintResults.filter(f => f.errorCount > 0)) {
report += `\n**${file.filePath}**\n`;
for (const msg of file.messages.filter(m => m.severity === 2)) {
report += `- L${msg.line}: ${msg.message} (${msg.ruleId})\n`;
}
}
}
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: report
});
} catch (e) {
console.log('无法生成报告:', e.message);
}# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "Asia/Shanghai"
open-pull-requests-limit: 10
reviewers:
- "frontend-team"
labels:
- "dependencies"
- "automated"
groups:
react:
patterns:
- "react*"
- "@types/react*"
testing:
patterns:
- "jest*"
- "@testing-library/*"
- "vitest*"
- "playwright*"
build:
patterns:
- "vite*"
- "esbuild*"
- "rollup*"
lint:
patterns:
- "eslint*"
- "prettier*"
- "stylelint*"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "ci"
- "dependencies"Renovate 是 Dependabot 的替代方案,提供更多自定义选项:
// renovate.json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices",
":semanticCommits"
],
"schedule": ["before 9am on monday"],
"timezone": "Asia/Shanghai",
"labels": ["dependencies"],
"reviewers": ["team:frontend"],
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true,
"automergeType": "pr"
},
{
"matchPackagePatterns": ["^@testing-library"],
"groupName": "testing libraries",
"automerge": true
},
{
"matchPackagePatterns": ["^react"],
"groupName": "React"
},
{
"matchDepTypes": ["devDependencies"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"]
}
}# .github/workflows/security.yml
name: Dependency Security
on:
schedule:
- cron: '0 9 * * 1' # 每周一早上9点
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: 安全审计
run: pnpm audit --json > audit-report.json || true
- name: 检查许可证
run: |
pnpm add -D license-checker
npx license-checker --json --out licenses.json
# 检查是否有 GPL 或 AGPL 许可证
node -e "
const licenses = require('./licenses.json');
const problematic = Object.entries(licenses)
.filter(([_, info]) => /GPL|AGPL/.test(info.licenses));
if (problematic.length > 0) {
console.error('发现受限许可证:');
problematic.forEach(([pkg, info]) => {
console.error(' ' + pkg + ': ' + info.licenses);
});
process.exit(1);
}
"
- name: 创建 Issue(发现漏洞时)
if: failure()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('audit-report.json', 'utf8'));
let body = '## 🔒 依赖安全漏洞报告\n\n';
body += `发现 ${report.metadata.vulnerabilities.total} 个漏洞\n\n`;
body += '| 严重程度 | 数量 |\n|---------|------|\n';
const vulns = report.metadata.vulnerabilities;
body += `| 🔴 严重 | ${vulns.critical} |\n`;
body += `| 🟠 高 | ${vulns.high} |\n`;
body += `| 🟡 中 | ${vulns.moderate} |\n`;
body += `| 🟢 低 | ${vulns.low} |\n`;
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '🔒 发现依赖安全漏洞',
body: body,
labels: ['security', 'dependencies']
});# .github/workflows/storybook.yml
name: Storybook
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-storybook:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: 构建 Storybook
run: pnpm build-storybook
- name: 部署 Storybook 到 Chromatic
if: github.event_name == 'push'
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
exitOnceUploaded: true
- name: PR 预览部署
if: github.event_name == 'pull_request'
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
exitZeroOnChanges: true// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline', 'ghost', 'danger'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
isLoading: { control: 'boolean' },
isDisabled: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: '主要按钮',
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
children: '次要按钮',
variant: 'secondary',
},
};
export const Loading: Story = {
args: {
children: '加载中...',
variant: 'primary',
isLoading: true,
},
};
export const AllSizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<Button size="sm">小按钮</Button>
<Button size="md">中按钮</Button>
<Button size="lg">大按钮</Button>
</div>
),
};# .github/workflows/visual-test.yml
name: Visual Regression Test
on: [pull_request]
jobs:
visual-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: 运行视觉回归测试
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
onlyChanged: true
diagnostics: true# .github/workflows/monorepo-ci.yml
name: Monorepo CI
on:
push:
branches: [main]
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Turborepo 需要完整 git 历史
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: 构建(仅变更的包)
run: pnpm turbo build --filter=...[origin/main]
- name: 测试(仅变更的包)
run: pnpm turbo test --filter=...[origin/main]
- name: Lint(仅变更的包)
run: pnpm turbo lint --filter=...[origin/main]
- name: 变更检测报告
run: |
echo "## 📦 变更包分析" >> $GITHUB_STEP_SUMMARY
CHANGED=$(pnpm turbo --filter=...[origin/main] --dry=json 2>/dev/null | jq -r '.packages[]' || echo "无法检测")
echo "变更的包: $CHANGED" >> $GITHUB_STEP_SUMMARYTurborepo 配置文件:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"inputs": ["src/**", "tests/**"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}# .github/workflows/nx-ci.yml
name: Nx Monorepo CI
on:
push:
branches: [main]
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Nx 受影响的项目
run: npx nx affected -t lint test build --base=origin/main --head=HEAD
- name: Nx Cloud 分布式缓存
run: npx nx run-many -t build --parallel=3
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}# .github/workflows/monorepo-release.yml
name: Monorepo Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- name: 创建 Release PR
uses: changesets/action@v1
with:
publish: pnpm release
version: pnpm changeset version
title: "chore: 版本发布"
commit: "chore: 版本发布"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}# .github/workflows/publish.yml
name: Publish Package
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- name: 运行测试
run: pnpm test
- name: 构建
run: pnpm build
- name: 发布到 npm
run: pnpm publish --no-git-checks --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: 发布到 GitHub Packages
run: pnpm publish --no-git-checks --access public --registry https://npm.pkg.github.com
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: 安装 semantic-release
run: npm install -g semantic-release @semantic-release/changelog @semantic-release/git
- name: 执行发布
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}semantic-release 配置:
// .releaserc.js
module.exports = {
branches: ['main'],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
'@semantic-release/changelog',
'@semantic-release/npm',
'@semantic-release/github',
['@semantic-release/git', {
assets: ['CHANGELOG.md', 'package.json'],
message: 'chore(release): ${nextRelease.version} [skip ci]',
}],
],
};# .github/workflows/i18n.yml
name: i18n Management
on:
push:
branches: [main]
paths:
- 'src/locales/**'
jobs:
validate-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 检查翻译完整性
run: |
node scripts/check-translations.js
- name: 检查翻译格式
run: |
pnpm i18next-parser src/**/*.{ts,tsx} --config i18next-parser.config.js
git diff --exit-code src/locales/ || (echo "翻译文件有未处理的键值" && exit 1)
sync-translations:
runs-on: ubuntu-latest
needs: validate-translations
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: 同步翻译到 Crowdin
uses: crowdin/github-action@v1
with:
upload_sources: true
upload_translations: false
download_translations: true
create_pull_request: true
pull_request_title: 'chore(i18n): 更新翻译文件'
pull_request_labels: 'i18n'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}// scripts/check-translations.js
const fs = require('fs');
const path = require('path');
const LOCALES_DIR = path.join(__dirname, '../src/locales');
const BASE_LANG = 'zh-CN';
const TARGET_LANGS = ['en-US', 'ja-JP'];
function loadTranslations(lang) {
const filePath = path.join(LOCALES_DIR, `${lang}.json`);
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function getKeys(obj, prefix = '') {
let keys = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null) {
keys = keys.concat(getKeys(value, fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}
const baseTranslations = loadTranslations(BASE_LANG);
const baseKeys = getKeys(baseTranslations).sort();
let hasError = false;
for (const lang of TARGET_LANGS) {
const translations = loadTranslations(lang);
const langKeys = getKeys(translations).sort();
const missing = baseKeys.filter((k) => !langKeys.includes(k));
const extra = langKeys.filter((k) => !baseKeys.includes(k));
if (missing.length > 0) {
console.error(`❌ ${lang} 缺少以下翻译键:`);
missing.forEach((k) => console.error(` - ${k}`));
hasError = true;
}
if (extra.length > 0) {
console.warn(`⚠️ ${lang} 有多余的翻译键:`);
extra.forEach((k) => console.warn(` - ${k}`));
}
console.log(`✅ ${lang}: ${langKeys.length}/${baseKeys.length} 个翻译键`);
}
if (hasError) {
process.exit(1);
}
console.log('\n🎉 所有翻译文件完整!');# .github/workflows/design-sync.yml
name: Design Sync
on:
issues:
types: [labeled]
jobs:
design-review:
if: contains(github.event.label.name, 'design-review')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 提取 Figma 链接
id: figma
uses: actions/github-script@v7
with:
script: |
const body = context.payload.issue.body;
const figmaRegex = /https:\/\/www\.figma\.com\/file\/[^\s]+/g;
const links = body.match(figmaRegex) || [];
return links[0] || '';
- name: 同步设计 Token
run: |
# 从 Figma 导出设计 Token
npx style-dictionary build --config design-tokens.config.js
- name: 生成设计规范 PR
if: steps.figma.outputs.result != ''
uses: peter-evans/create-pull-request@v5
with:
title: 'design: 同步设计 Token'
body: |
自动从 Figma 设计稿同步的设计 Token 变更。
Figma 链接: ${{ steps.figma.outputs.result }}
branch: design/token-sync
labels: design, automated// design-tokens.config.js
module.exports = {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'src/styles/tokens/',
files: [
{
destination: '_variables.css',
format: 'css/variables',
options: {
outputReferences: true,
},
},
],
},
scss: {
transformGroup: 'scss',
buildPath: 'src/styles/tokens/',
files: [
{
destination: '_variables.scss',
format: 'scss/variables',
},
],
},
js: {
transformGroup: 'js',
buildPath: 'src/styles/tokens/',
files: [
{
destination: 'tokens.js',
format: 'javascript/es6',
},
],
},
},
};国内访问 GitHub 经常遇到速度慢的问题,以下是几种解决方案:
方案一:使用 GitHub Actions 镜像加速
# 使用国内 npm 镜像
- name: 设置 npm 镜像
run: |
pnpm config set registry https://registry.npmmirror.com
# 使用国内 pip 镜像(如果有 Python 工具)
- name: 设置 pip 镜像
run: |
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple方案二:使用 Gitee 镜像同步
# .github/workflows/mirror.yml
name: Mirror to Gitee
on:
push:
branches: [main]
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 同步到 Gitee
uses: wearerequired/git-mirror-action@master
env:
SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
with:
source-repo: git@github.com:your-org/your-repo.git
destination-repo: git@gitee.com:your-org/your-repo.git方案三:GitHub Proxy 加速
# 在 CI 中使用代理
- name: 设置 Git 代理
run: |
git config --global http.proxy http://your-proxy:port
git config --global https.proxy http://your-proxy:port# .github/workflows/deploy-china.yml
name: Deploy to China
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
registry-url: 'https://registry.npmmirror.com'
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: 上传到阿里云 OSS
uses: manyuanrong/setup-ossutil@v3.0
with:
access-key-id: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
access-key-secret: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket: your-bucket-name
- name: 部署到 OSS
run: |
ossutil cp -r dist/ oss://your-bucket-name/ --update
ossutil set-meta oss://your-bucket-name/ --update \
--header "Cache-Control:public,max-age=31536000" \
--include "*.js" --include "*.css"
- name: 刷新 CDN 缓存
run: |
# 使用阿里云 CDN API 刷新缓存
aliyun cdn RefreshObjectCaches \
--ObjectPath "https://www.example.com/" \
--ObjectType "Directory"
deploy-to-tencent:
runs-on: ubuntu-latest
needs: build
steps:
- name: 部署到腾讯云 COS
uses: zkqiang/tencent-cos-action@v1.0.0
with:
args: delete -r -f / && upload -r dist/ /
secret_id: ${{ secrets.TENCENT_SECRET_ID }}
secret_key: ${{ secrets.TENCENT_SECRET_KEY }}
bucket: your-bucket-name
region: ap-guangzhou
- name: 刷新腾讯云 CDN
run: |
pip install coscmd
coscmd refresh-cdn --url "https://www.example.com/*"1. 代码规范统一
// .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false2. 团队协作规范文档模板
# 前端团队 GitHub 协作规范
## 分支命名规范
- 功能分支: `feature/需求编号-简短描述`
- 修复分支: `fix/Bug编号-简短描述`
- 热修复: `hotfix/问题描述`
## PR 规范
1. 标题格式: `type(scope): 描述`
2. 必须关联 Issue
3. 必须通过 CI 检查
4. 至少 2 人 Code Review
5. 使用 PR 模板填写变更说明
## Code Review 关注点
- 代码风格是否符合规范
- 是否有潜在的性能问题
- 是否有安全风险
- 测试是否充分
- 文档是否更新3. 国内 CI/CD 优化
| 优化项 | 方案 | 效果 |
|---|---|---|
| npm 安装速度 | 使用 npmmirror 镜像 | 提速 5-10 倍 |
| Docker 镜像 | 使用阿里云镜像仓库 | 提速 3-5 倍 |
| 构建缓利 | GitHub Actions Cache | 减少 50% 构建时间 |
| 并行测试 | 分片测试 + 并行执行 | 减少 60% 测试时间 |
| 增量构建 | Turborepo/Nx 缓存 | 减少 70% 构建时间 |
问题 1:GitHub Actions 下载依赖超时
# 解决方案:增加超时时间和重试机制
- name: 安装依赖
run: |
for i in 1 2 3; do
pnpm install --frozen-lockfile && break
echo "第 $i 次尝试失败,等待 10 秒后重试..."
sleep 10
done
timeout-minutes: 15问题 2:Git LFS 大文件传输慢
# 解决方案:使用 CDN 托管静态资源
- name: 上传静态资源到 CDN
run: |
# 将大文件上传到 CDN,Git 仓库只存储引用
curl -T dist/assets/vendor.js \
-H "Authorization: ${{ secrets.CDN_TOKEN }}" \
https://cdn.example.com/upload/问题 3:多团队协作冲突频繁
# 解决方案:使用 CODEOWNERS + 必须的 Review
# .github/CODEOWNERS
/src/components/ @component-team
/src/pages/ @page-team
/src/services/ @backend-team
/.github/ @devops-team| 项目类型 | 包管理器 | 构建工具 | 测试框架 | 部署平台 |
|---|---|---|---|---|
| React SPA | pnpm | Vite | Vitest + Playwright | Vercel |
| Vue 3 项目 | pnpm | Vite | Vitest + Cypress | Netlify |
| Next.js 项目 | pnpm | Next.js | Jest + Playwright | Vercel |
| Nuxt 项目 | pnpm | Nuxt | Vitest + Playwright | Cloudflare Pages |
| 组件库 | pnpm | Vite/Storybook | Vitest + Chromatic | npm + Storybook |
| Monorepo | pnpm | Turborepo | Vitest | 各平台独立部署 |
| 文档站点 | pnpm | VitePress | - | GitHub Pages |
本文档涵盖了前端开发者在 GitHub 上进行高效协作的完整工作流。从仓库结构设计、CI/CD 流水线搭建、自动化测试、性能监控到代码质量保障,每个环节都提供了详细的配置示例和最佳实践。
核心要点:
- 自动化优先:将代码检查、测试、构建、部署全部自动化,减少人工操作
- 质量门禁:通过 CI 流水线设置质量门禁,确保代码质量
- 安全第一:使用 Dependabot/Renovate 管理依赖安全
- 团队协作:通过 CODEOWNERS、PR 模板、Commit 规范提升协作效率
- 国内优化:使用镜像加速、CDN 部署等方案优化国内使用体验
通过合理运用这些工具和流程,前端团队可以显著提升开发效率和代码质量,实现快速迭代和稳定发布。