Skip to content
⚠️ This article was written in 2022. Some content may be outdated.

Frontend Infrastructure Design: Building a Team Toolchain from Scratch

As the team's tech lead, one of the most important tasks this year was building frontend infrastructure. Not writing business code, but making those who write it more efficient. This article covers our design approach and implementation process.

Infrastructure Overview ​

前端基础设施
├── 代码规范
│   ├── ESLint 共享配置
│   ├── Prettier 配置
│   ├── Stylelint 配置
│   └── Commitlint
├── 构建工具
│   ├── Vite 预设配置
│   ├── Babel 预设
│   └── Webpack 预设(兼容旧项目)
├── 组件库
│   ├── 基础组件
│   ├── 业务组件
│   └── 文档站
├── 工具包
│   ├── 请求封装
│   ├── 路由封装
│   └── 通用工具函数
├── 脚手架
│   ├── 项目创建 CLI
│   └── 代码生成器
└── CI/CD
    ├── GitHub Actions 模板
    └── 部署脚本

ESLint Shared Config ​

bash
packages/
└── eslint-config/
    ├── package.json
    ├── index.js          # 基础配置
    ├── react.js          # React 项目配置
    ├── vue.js            # Vue 项目配置
    └── node.js           # Node.js 项目配置
javascript
// packages/eslint-config/index.js
module.exports = {
  env: {
    browser: true,
    node: true,
    es2022: true,
  },
  parser: '@typescript-eslint/parser',
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:import/recommended',
    'plugin:import/typescript',
    'prettier',
  ],
  plugins: ['@typescript-eslint', 'import'],
  rules: {
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/explicit-function-return-type': 'off',
    'import/order': [
      'error',
      {
        groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
        pathGroups: [
          { pattern: '@/**', group: 'internal', position: 'after' },
        ],
        'newlines-between': 'always',
      },
    ],
  },
};
javascript
// packages/eslint-config/react.js
module.exports = {
  extends: [
    './index.js',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/recommended',
  ],
  plugins: ['react', 'react-hooks', 'jsx-a11y'],
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
  },
  settings: {
    react: { version: 'detect' },
  },
};
json
// packages/eslint-config/package.json
{
  "name": "@mono/eslint-config",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "@typescript-eslint/eslint-plugin": "^5.30.0",
    "@typescript-eslint/parser": "^5.30.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-react": "^7.30.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-jsx-a11y": "^6.6.0"
  },
  "peerDependencies": {
    "eslint": ">=8"
  }
}

Vite Preset Config ​

typescript
// packages/vite-config/index.ts
import { defineConfig, type UserConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

interface PresetOptions {
  port?: number;
  proxy?: Record<string, string>;
  outDir?: string;
}

export function createViteConfig(opts: PresetOptions = {}): UserConfig {
  return defineConfig({
    plugins: [react()],
    resolve: {
      alias: { '@': path.resolve('src') },
    },
    server: {
      port: opts.port || 3000,
      proxy: opts.proxy
        ? Object.fromEntries(
            Object.entries(opts.proxy).map(([key, target]) => [
              key,
              { target, changeOrigin: true },
            ])
          )
        : undefined,
    },
    build: {
      outDir: opts.outDir || 'dist',
      target: 'es2020',
      rollupOptions: {
        output: {
          manualChunks: {
            'vendor-react': ['react', 'react-dom'],
            'vendor-router': ['react-router-dom'],
          },
        },
      },
    },
    css: {
      modules: {
        localsConvention: 'camelCaseOnly',
      },
    },
  });
}

Scaffolding CLI ​

typescript
// packages/create-app/src/index.ts
import { select, input, confirm } from '@inquirer/prompts';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';

async function main() {
  const projectName = await input({
    message: '项目名称:',
    default: 'my-app',
  });

  const framework = await select({
    message: '选择框架:',
    choices: [
      { name: 'React + Vite', value: 'react-vite' },
      { name: 'Vue + Vite', value: 'vue-vite' },
      { name: 'Next.js', value: 'nextjs' },
    ],
  });

  const withTypeScript = await confirm({
    message: '使用 TypeScript?',
    default: true,
  });

  // 从模板创建
  const templateDir = path.join(__dirname, '../templates', framework);
  const targetDir = path.resolve(projectName);

  fs.cpSync(templateDir, targetDir, { recursive: true });

  // 安装依赖
  execSync('pnpm install', { cwd: targetDir, stdio: 'inherit' });

  // 初始化 git
  execSync('git init', { cwd: targetDir });

  console.log(`
  项目创建成功!

    cd ${projectName}
    pnpm dev
  `);
}

main();

CI/CD Templates ​

yaml
# templates/.github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-test-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - uses: pnpm/action-setup@v2
        with:
          version: 7

      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm lint

      - name: Type Check
        run: pnpm typecheck

      - name: Test
        run: pnpm test

      - name: Build
        run: pnpm build

Infrastructure Evolution Strategy ​

typescript
// 我们的原则:

// 1. 约定优于配置
// 提供默认值,用户可以覆盖
function createViteConfig(overrides?: Partial<UserConfig>) {
  return { ...defaults, ...overrides };
}

// 2. 渐进式采用
// 不强制所有项目迁移,新项目用新配置
// 老项目在下次大版本升级时迁移

// 3. 版本锁定策略
// 基础设施包用 caret ( ^1.0.0 )
// 配合 lockfile 保证一致性
// 定期用 pnpm update -i 升级

Summary ​

The core value of frontend infrastructure is freeing the team from reinventing the wheel so they can focus on business logic. ESLint configs, Vite presets, scaffolding CLI — each one is small on its own, but together they deliver a significant efficiency boost. The key is to keep it lightweight and avoid over-engineering.

MIT Licensed