Frontend Code Standards & Engineering: ESLint + Prettier + Husky Full Workflow

前端工程

Why Your Team Needs Automated Code Standards

Two real scenarios.

Scenario A: Dev A uses 2-space indent, Dev B uses 4-space. On every code review, 80% of the diff is whitespace. Reviewer energy goes to formatting, and logic issues slip through.

Scenario B: An intern pushes code that passes npm run build, merges to main. Production breaks next day: "typo in variable name." If there had been automated checks, this commit would never have passed pre-commit hook.

Code standards aren't "arbitrary rules constraining people." They're "tools that automatically guarantee a quality baseline, freeing people to focus on decisions that actually matter." This article is based on implementing this workflow across three teams — from a messy .eslintrc to a fully automated ESLint Flat Config + Prettier + Husky pipeline.


What You Get After Setup

git add .
git commit -m "feat: add user login"

# Husky triggers pre-commit hook
# → lint-staged runs ESLint --fix + Prettier --write on staged files
# → commitlint validates commit message format
# → Everything passes → commit succeeds

If ESLint reports errors or the commit message doesn't follow Conventional Commits, the commit is rejected. This means no more inconsistent formatting or type-unsafe code entering the repository.


ESLint Flat Config: Goodbye .eslintrc

In late 2024, ESLint 9.x made Flat Config the default, and the old .eslintrc.* entered deprecation. If you're still on old config, migrate now.

Flat Config Basics

// eslint.config.mjs (note .mjs — Flat Config uses ESM by default)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from 'globals';

export default tseslint.config(
  // 1. Base recommended rules
  js.configs.recommended,

  // 2. TypeScript rules
  ...tseslint.configs.recommended,

  // 3. Global config
  {
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.node,
      },
      parserOptions: {
        project: './tsconfig.json',
      },
    },
  },

  // 4. Custom rules
  {
    rules: {
      '@typescript-eslint/no-unused-vars': ['error', {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
      }],
      '@typescript-eslint/no-explicit-any': 'warn',
      'no-console': ['warn', { allow: ['warn', 'error'] }],
      'prefer-const': 'error',
      'no-var': 'error',
      'eqeqeq': ['error', 'always'],
    },
  },

  // 5. Ignored files
  {
    ignores: [
      'dist/**',
      'node_modules/**',
      '.next/**',
      'coverage/**',
    ],
  }
);

Why Migrate

Old Format New Format Notes
.eslintrc.js / .eslintrc.json eslint.config.mjs Single unified file
extends chain inheritance tseslint.config() composition Function composition over inheritance
overrides + glob patterns files in config objects More intuitive
Scattered parserOptions Unified languageOptions Cleaner structure
Plugins by string name import pluginX from '...' Native ESM imports

Migration

npm i -D eslint@^9.0.0 @eslint/js typescript-eslint@^8.0.0 globals
npx @eslint/migrate-config .eslintrc.js

Prettier: Let Formatting Specialists Handle Formatting

ESLint handles code quality. Prettier handles code format. Don't make ESLint worry about indentation and line breaks — that's not its job, and it causes rule conflicts.

Configuration

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2,
  "arrowParens": "always",
  "endOfLine": "lf",
  "bracketSpacing": true
}

Peace Between ESLint and Prettier

// eslint.config.mjs
import prettier from 'eslint-config-prettier';

export default tseslint.config(
  // ... previous configs
  prettier,  // Last — overrides all conflicting rules
);

Husky + lint-staged: Block Problems Before Commit

Configuring ESLint and Prettier isn't enough — team members may forget to run them. Automated Git hooks are the final line of defense.

Installation

npm i -D husky lint-staged
npx husky init

lint-staged Configuration

// package.json
{
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": [
      "eslint --fix --max-warnings=0",
      "prettier --write"
    ],
    "*.{json,css,scss,md}": [
      "prettier --write"
    ]
  }
}

pre-commit Hook

# .husky/pre-commit
npx lint-staged

--max-warnings=0 means even a single warning blocks the commit. Painful early on, but guarantees long-term codebase quality.


commitlint: Unify Commit Message Format

"fix bug," "update," "modified some stuff" — commit messages like these will make you hate your past self six months later. Conventional Commits enables auto-generated changelogs and automatic version bumping.

npm i -D @commitlint/cli @commitlint/config-conventional
// commitlint.config.mjs
export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',     // New feature
        'fix',      // Bug fix
        'docs',     // Documentation
        'style',    // Formatting (no logic change)
        'refactor', // Code restructuring
        'perf',     // Performance
        'test',     // Tests
        'chore',    // Build/tooling
        'ci',       // CI config
        'revert',   // Rollback
      ],
    ],
    'subject-case': [0],
  },
};
# .husky/commit-msg
npx --no -- commitlint --edit $1

Correct commit format:

git commit -m "feat: add user authentication module"
git commit -m "fix: resolve token refresh race condition"
git commit -m "perf: reduce bundle size by tree-shaking unused utils"

VS Code Team Configuration Sync

.vscode/settings.json should NOT be in .gitignore — it belongs in the repository:

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.useFlatConfig": true,
  "typescript.tsdk": "node_modules/typescript/lib",
  "files.eol": "\n",
  "files.trimTrailingWhitespace": true,
  "files.insertFinalNewline": true
}
// .vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "bradlc.vscode-tailwindcss"
  ]
}

New team members clone → VS Code prompts to install recommended extensions → config is synced automatically. Zero communication overhead.


CI Pipeline Integration

Local hooks can be bypassed with --no-verify. Add a second layer in CI:

# .github/workflows/lint.yml
name: Lint & Format Check

on:
  pull_request:
    branches: [main, develop]

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Install dependencies
        run: npm ci
      - name: Run ESLint
        run: npx eslint . --max-warnings=0
      - name: Check formatting
        run: npx prettier --check "src/**/*.{ts,tsx,js,jsx,json,css}"
      - name: Type check
        run: npx tsc --noEmit

Real Migration Experience

Step 1: Format First, Rules Later

Don't enable all ESLint rules at once — your project will explode with hundreds of errors.

# 1. Format entire project with Prettier first
npx prettier --write "src/**/*.{ts,tsx}"

# 2. Commit formatting changes (one big but safe diff)
git add .
git commit -m "style: format all files with Prettier"

# 3. Gradually enable ESLint rules
# Start with only errors, keep warnings off

Step 2: Progressive Tightening

// Phase 1: Only error-level rules
{
  rules: {
    '@typescript-eslint/no-explicit-any': 'warn',    // warn first
    'no-console': 'off',                              // off first
    '@typescript-eslint/no-unused-vars': 'error',     // direct error
  },
}

// Phase 2: Promote warnings to errors
{
  rules: {
    '@typescript-eslint/no-explicit-any': 'error',
    'no-console': ['warn', { allow: ['warn', 'error'] }],
  },
}

Step 3: Handle Legacy Code

// Option A: File-level ignore (not recommended)
/* eslint-disable */

// Option B: Line-level ignore with explanation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const legacyData: any = window.__LEGACY_DATA__;

// Option C: Directory-level ignore
{
  ignores: ['src/legacy/**'],
}

Common Pitfalls

Pitfall 1: ESLint Flat Config + VS Code Plugin Not Working

Symptom: No lint errors in editor, but CLI eslint works fine.

Fix: Ensure VS Code ESLint plugin version ≥ 3.0:

{
  "eslint.useFlatConfig": true,
  "eslint.options": {
    "overrideConfigFile": "eslint.config.mjs"
  }
}

Pitfall 2: lint-staged Type-Check Too Slow

Move type-check to CI only, skip in pre-commit:

{
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": ["eslint --fix --max-warnings=0"]
  }
}

Pitfall 3: Windows vs macOS Line Ending Hell

.prettierrc set "endOfLine": "lf" and add .gitattributes:

* text=auto eol=lf

Try these browser-local tools — no sign-up required →

#ESLint#Prettier#代码规范#工程化#前端#教程