构建工具与工程化
代码质量工具
1. ESLint 的核心原理是什么?
工作流程:
typescript
// 1. 解析代码为 AST
const code = 'const a = 1'
const ast = parse(code)
// 2. 遍历 AST 节点
traverse(ast, {
VariableDeclaration(node) {
// 检查规则
if (node.kind === 'const' && !isUpperCase(node.id.name)) {
report('常量应该大写')
}
}
})
// 3. 报告问题
// 4. 自动修复(如果可能)配置文件:
javascript
// .eslintrc.js
module.exports = {
// 环境
env: {
browser: true,
es2021: true,
node: true,
},
// 继承配置
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier', // 必须放在最后
],
// 解析器
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
project: './tsconfig.json',
},
// 插件
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
// 规则
rules: {
'no-console': 'warn',
'@typescript-eslint/no-unused-vars': 'error',
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
// 覆盖特定文件
overrides: [
{
files: ['*.test.ts', '*.test.tsx'],
rules: {
'no-console': 'off',
},
},
],
}2. 如何编写自定义 ESLint 规则?
javascript
// eslint-plugin-custom/rules/no-console-log.js
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: '禁止使用 console.log',
category: 'Best Practices',
},
fixable: 'code',
schema: [],
},
create(context) {
return {
// 访问 CallExpression 节点
CallExpression(node) {
// 检查是否是 console.log
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'console' &&
node.callee.property.name === 'log'
) {
context.report({
node,
message: '不要使用 console.log,请使用 logger',
fix(fixer) {
// 自动修复
return fixer.replaceText(
node.callee,
'logger.info'
)
},
})
}
},
}
},
}
// 使用
// .eslintrc.js
module.exports = {
plugins: ['custom'],
rules: {
'custom/no-console-log': 'error',
},
}3. Prettier 与 ESLint 如何配合使用?
javascript
// .prettierrc.js
module.exports = {
// 基础配置
semi: false,
singleQuote: true,
trailingComma: 'es5',
printWidth: 100,
tabWidth: 2,
useTabs: false,
// JSX
jsxSingleQuote: false,
jsxBracketSameLine: false,
// 其他
arrowParens: 'always',
endOfLine: 'lf',
}
// .prettierignore
dist
node_modules
*.min.js与 ESLint 集成:
bash
# 安装
pnpm add -D eslint-config-prettier eslint-plugin-prettier
# .eslintrc.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier', // 关闭 ESLint 中与 Prettier 冲突的规则
],
plugins: ['prettier'],
rules: {
'prettier/prettier': 'error', // Prettier 规则作为 ESLint 规则
},
}VS Code 配置:
json
// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}4. Husky 和 lint-staged 如何配置?
bash
# 安装
pnpm add -D husky lint-staged
# 初始化 husky
pnpm exec husky initjson
// package.json
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
]
}
}bash
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm lint-staged
# .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm commitlint --edit $1Commitlint 配置:
javascript
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复
'docs', // 文档
'style', // 格式
'refactor', // 重构
'perf', // 性能
'test', // 测试
'chore', // 构建/工具
'revert', // 回退
],
],
'subject-case': [0],
},
}CI/CD
5. GitHub Actions 的基础配置?
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/6. 如何配置自动部署?
yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
env:
VITE_API_URL: ${{ secrets.API_URL }}
# 部署到 Vercel
- name: Deploy to Vercel
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 }}
vercel-args: '--prod'
# 或部署到 Netlify
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v2
with:
publish-dir: './dist'
production-branch: main
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: 'Deploy from GitHub Actions'
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
# 或部署到 GitHub Pages
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist7. 如何优化 CI/CD 性能?
1. 缓存依赖:
yaml
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
# 或手动缓存
- uses: actions/cache@v3
with:
path: |
~/.pnpm-store
node_modules
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-2. 并行执行:
yaml
jobs:
lint:
runs-on: ubuntu-latest
# 并行运行
test:
runs-on: ubuntu-latest
# 并行运行
build:
runs-on: ubuntu-latest
needs: [lint, test] # 等待前面的任务完成3. 矩阵构建:
yaml
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [16, 18, 20]
steps:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}4. 条件执行:
yaml
jobs:
deploy:
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy
run: pnpm deploy
test:
if: github.event_name == 'pull_request'
steps:
- name: Test
run: pnpm test包管理器
8. npm、yarn、pnpm 的区别?
| 特性 | npm | yarn | pnpm |
|---|---|---|---|
| 安装速度 | 慢 | 快 | 最快 |
| 磁盘空间 | 大 | 大 | 小(硬链接) |
| lock 文件 | package-lock.json | yarn.lock | pnpm-lock.yaml |
| workspace | 支持 | 支持 | 支持 |
| 严格模式 | 否 | 否 | 是 |
pnpm 的优势:
bash
# 1. 节省磁盘空间
# npm/yarn: 每个项目都有完整的 node_modules
project-a/node_modules/lodash (1MB)
project-b/node_modules/lodash (1MB)
# 总共 2MB
# pnpm: 使用硬链接
~/.pnpm-store/lodash (1MB)
project-a/node_modules/lodash → 硬链接
project-b/node_modules/lodash → 硬链接
# 总共 1MB
# 2. 严格的依赖管理
# 只能访问 package.json 中声明的依赖
# 防止幽灵依赖
# 3. 更快的安装速度
# 并行安装 + 硬链接9. 如何处理依赖版本锁定?
json
// package.json
{
"dependencies": {
// 精确版本
"react": "18.2.0",
// 兼容版本(推荐)
"vue": "^3.3.0", // 3.3.0 <= version < 4.0.0
// 次要版本
"lodash": "~4.17.0", // 4.17.0 <= version < 4.18.0
// 最新版本(不推荐)
"axios": "*",
},
// 覆盖依赖版本
"overrides": {
"lodash": "4.17.21"
},
// pnpm 特有
"pnpm": {
"overrides": {
"react": "^18.2.0"
}
}
}锁文件的作用:
bash
# package-lock.json / pnpm-lock.yaml
# 1. 锁定确切版本
# 2. 确保团队使用相同版本
# 3. 加速安装
# 安装时使用锁文件
pnpm install --frozen-lockfile # CI 环境推荐
pnpm install # 更新锁文件环境变量管理
10. 如何管理不同环境的配置?
bash
# .env
VITE_APP_TITLE=My App
VITE_API_URL=http://localhost:3000
# .env.development
VITE_API_URL=http://localhost:8080
VITE_DEBUG=true
# .env.production
VITE_API_URL=https://api.production.com
VITE_DEBUG=false
# .env.local (不提交到 git)
VITE_SECRET_KEY=xxxtypescript
// 使用环境变量
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.VITE_API_URL)
// TypeScript 类型定义
// env.d.ts
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_URL: string
readonly VITE_DEBUG: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}dotenv-cli 使用:
bash
# 安装
pnpm add -D dotenv-cli
# package.json
{
"scripts": {
"dev": "dotenv -e .env.development vite",
"build:staging": "dotenv -e .env.staging vite build",
"build:prod": "dotenv -e .env.production vite build"
}
}性能监控
11. 如何监控构建性能?
javascript
// vite.config.ts
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html',
}),
],
build: {
// 输出分析
reportCompressedSize: true,
// chunk 大小警告
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
})webpack-bundle-analyzer:
javascript
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html',
}),
],
}12. 如何进行代码质量检查?
bash
# 安装工具
pnpm add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
pnpm add -D eslint-plugin-sonarjs
pnpm add -D eslint-plugin-securityjavascript
// .eslintrc.js
module.exports = {
extends: [
'plugin:sonarjs/recommended', // 代码质量
'plugin:security/recommended', // 安全检查
],
plugins: ['sonarjs', 'security'],
rules: {
'sonarjs/cognitive-complexity': ['error', 15],
'sonarjs/no-duplicate-string': 'error',
'security/detect-object-injection': 'warn',
},
}SonarQube 集成:
yaml
# .github/workflows/sonarqube.yml
name: SonarQube
on:
push:
branches: [main]
jobs:
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}测试工具
13. Vitest 的配置和使用?
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
],
},
},
})
// src/test/setup.ts
import { expect, afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
import matchers from '@testing-library/jest-dom/matchers'
expect.extend(matchers)
afterEach(() => {
cleanup()
})测试示例:
typescript
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { Button } from './Button'
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const onClick = vi.fn()
render(<Button onClick={onClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(onClick).toHaveBeenCalledTimes(1)
})
})14. E2E 测试如何配置?
Playwright 配置:
typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
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'] },
},
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})测试示例:
typescript
// e2e/example.spec.ts
import { test, expect } from '@playwright/test'
test('homepage has title', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/My App/)
})
test('login flow', async ({ page }) => {
await page.goto('/login')
await page.fill('input[name="email"]', 'user@example.com')
await page.fill('input[name="password"]', 'password')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
})文档工具
15. 如何搭建项目文档?
VitePress 配置:
typescript
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
export default defineConfig({
title: 'My Project',
description: 'Project documentation',
themeConfig: {
nav: [
{ text: 'Guide', link: '/guide/' },
{ text: 'API', link: '/api/' },
],
sidebar: {
'/guide/': [
{
text: 'Introduction',
items: [
{ text: 'Getting Started', link: '/guide/getting-started' },
{ text: 'Installation', link: '/guide/installation' },
],
},
],
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/user/repo' },
],
},
})Storybook 配置:
typescript
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite'
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
}
export default config
// 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'],
}
export default meta
type Story = StoryObj<typeof Button>
export const Primary: Story = {
args: {
children: 'Button',
variant: 'primary',
},
}最佳实践
16. 前端工程化的完整配置清单?
bash
# 项目初始化
pnpm init
# 包管理器
pnpm add -D pnpm
# 构建工具
pnpm add -D vite @vitejs/plugin-react
# TypeScript
pnpm add -D typescript @types/node @types/react @types/react-dom
# 代码质量
pnpm add -D eslint prettier
pnpm add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
pnpm add -D eslint-config-prettier eslint-plugin-prettier
# Git Hooks
pnpm add -D husky lint-staged @commitlint/cli @commitlint/config-conventional
# 测试
pnpm add -D vitest @testing-library/react @testing-library/jest-dom
pnpm add -D @playwright/test
# 文档
pnpm add -D vitepress
# 工具
pnpm add -D cross-env dotenv-cli配置文件:
json
// package.json
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:e2e": "playwright test",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit",
"prepare": "husky install"
}
}