📚 QuantumFlow工作流自动化从入门到精通 - 第7篇
前面六篇文章我们完成了后端核心功能的开发。从本篇开始,我们将构建一个现代化的前端应用。本文将搭建基于Vite + TypeScript + TailwindCSS的工程化前端架构,为后续的可视化流程编辑器打下坚实基础。


📋 本文概览

学习目标:

  • 掌握Vite的快速开发体验和构建优化
  • 学会TypeScript在React项目中的最佳实践
  • 理解TailwindCSS的原子化CSS设计理念
  • 掌握React Router v6的路由设计模式
  • 对比Zustand和Redux,选择合适的状态管理方案
  • 实现优雅的API请求封装和缓存策略

技术栈:

  • Vite 5.0+
  • React 18+
  • TypeScript 5.0+
  • TailwindCSS 3.4+
  • React Router v6
  • Zustand 4.0+
  • Axios + React Query (TanStack Query)

预计阅读时间: 28分钟
前置知识: React基础、TypeScript基础、CSS基础


🎯 一、为什么选择这套技术栈?

1.1 技术选型对比

/**
 * 前端技术栈选型决策
 */

// 构建工具对比
const buildToolComparison = {
  "Vite": {
    pros: [
      "极速的冷启动(基于ESM)",
      "HMR热更新快如闪电",
      "开箱即用的TypeScript支持",
      "生产构建基于Rollup,体积小"
    ],
    cons: [
      "生态相对较新",
      "部分老旧浏览器需要polyfill"
    ],
    verdict: "✅ 推荐用于新项目"
  },
  "Create React App": {
    pros: [
      "官方脚手架,稳定性高",
      "配置简单,适合初学者"
    ],
    cons: [
      "启动速度慢(基于Webpack)",
      "配置不灵活(需要eject)",
      "构建速度慢"
    ],
    verdict: "❌ 已不推荐"
  },
  "Next.js": {
    pros: [
      "SSR/SSG支持",
      "文件路由系统",
      "优秀的SEO"
    ],
    cons: [
      "学习曲线陡峭",
      "对于纯SPA项目过于重量级"
    ],
    verdict: "⚠️ 适合需要SSR的项目"
  }
};

// 状态管理对比
const stateManagementComparison = {
  "Zustand": {
    size: "1.2KB (gzipped)",
    complexity: "低",
    boilerplate: "极少",
    devtools: "支持",
    typescript: "原生支持",
    verdict: "✅ 推荐用于中小型项目"
  },
  "Redux Toolkit": {
    size: "12KB (gzipped)",
    complexity: "中",
    boilerplate: "较少(相比原生Redux)",
    devtools: "强大",
    typescript: "支持",
    verdict: "⚠️ 适合大型复杂项目"
  },
  "Recoil": {
    size: "21KB (gzipped)",
    complexity: "中",
    boilerplate: "中等",
    devtools: "基础支持",
    typescript: "支持",
    verdict: "⚠️ 生态相对较新"
  }
};

1.2 性能对比数据

指标 Vite CRA Next.js
冷启动时间 ~200ms ~15s ~3s
HMR更新 <50ms ~1s ~500ms
生产构建 ~15s ~45s ~30s
包体积

🏗️ 二、Vite项目初始化

2.1 创建项目

# 使用Vite创建React + TypeScript项目
npm create vite@latest quantumflow-frontend -- --template react-ts

cd quantumflow-frontend

# 安装依赖
npm install

# 启动开发服务器
npm run dev

2.2 项目结构设计

quantumflow-frontend/
├── public/                  # 静态资源
│   └── logo.svg
├── src/
│   ├── api/                # API请求封装
│   │   ├── client.ts       # Axios实例配置
│   │   ├── workflows.ts    # 工作流API
│   │   └── auth.ts         # 认证API
│   ├── components/         # 公共组件
│   │   ├── ui/             # UI基础组件
│   │   │   ├── Button.tsx
│   │   │   ├── Input.tsx
│   │   │   └── Modal.tsx
│   │   ├── layout/         # 布局组件
│   │   │   ├── Header.tsx
│   │   │   ├── Sidebar.tsx
│   │   │   └── Layout.tsx
│   │   └── common/         # 通用业务组件
│   │       ├── LoadingSpinner.tsx
│   │       └── ErrorBoundary.tsx
│   ├── features/           # 功能模块(按业务划分)
│   │   ├── workflows/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── types/
│   │   │   └── utils/
│   │   ├── auth/
│   │   └── dashboard/
│   ├── hooks/              # 全局自定义Hooks
│   │   ├── useAuth.ts
│   │   └── useDebounce.ts
│   ├── routes/             # 路由配置
│   │   ├── index.tsx
│   │   └── ProtectedRoute.tsx
│   ├── store/              # 状态管理
│   │   ├── authStore.ts
│   │   └── workflowStore.ts
│   ├── types/              # TypeScript类型定义
│   │   ├── api.ts
│   │   ├── workflow.ts
│   │   └── user.ts
│   ├── utils/              # 工具函数
│   │   ├── format.ts
│   │   └── validation.ts
│   ├── App.tsx             # 根组件
│   ├── main.tsx            # 入口文件
│   └── index.css           # 全局样式
├── .eslintrc.cjs           # ESLint配置
├── .prettierrc             # Prettier配置
├── tailwind.config.js      # TailwindCSS配置
├── tsconfig.json           # TypeScript配置
├── vite.config.ts          # Vite配置
└── package.json

2.3 Vite配置优化

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [
    react({
      // 启用React Fast Refresh
      fastRefresh: true,
      // Babel配置
      babel: {
        plugins: [
          // 支持装饰器语法(可选)
          ['@babel/plugin-proposal-decorators', { legacy: true }]
        ]
      }
    })
  ],
  
  // 路径别名
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@api': path.resolve(__dirname, './src/api'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
      '@store': path.resolve(__dirname, './src/store'),
      '@types': path.resolve(__dirname, './src/types'),
      '@utils': path.resolve(__dirname, './src/utils')
    }
  },
  
  // 开发服务器配置
  server: {
    port: 3000,
    open: true, // 自动打开浏览器
    proxy: {
      // API代理(开发环境)
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  // 构建配置
  build: {
    outDir: 'dist',
    sourcemap: false, // 生产环境不生成sourcemap
    rollupOptions: {
      output: {
        // 代码分割
        manualChunks: {
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          'ui-vendor': ['@headlessui/react', 'lucide-react'],
          'data-vendor': ['axios', '@tanstack/react-query', 'zustand']
        }
      }
    },
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true, // 移除console
        drop_debugger: true
      }
    }
  },
  
  // 优化依赖预构建
  optimizeDeps: {
    include: ['react', 'react-dom', 'react-router-dom']
  }
});

📘 三、TypeScript配置优化

3.1 严格模式配置

// tsconfig.json
{
  "compilerOptions": {
    // 基础配置
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    // 模块解析
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    // 严格类型检查
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noUncheckedIndexedAccess": true,

    // 路径映射(与vite.config.ts保持一致)
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"],
      "@api/*": ["./src/api/*"],
      "@hooks/*": ["./src/hooks/*"],
      "@store/*": ["./src/store/*"],
      "@types/*": ["./src/types/*"],
      "@utils/*": ["./src/utils/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

3.2 类型定义示例

// src/types/workflow.ts
/**
 * 工作流相关类型定义
 */

export interface Workflow {
  id: string;
  name: string;
  description: string;
  nodes: WorkflowNode[];
  edges: WorkflowEdge[];
  created_at: string;
  updated_at: string;
  owner_id: string;
  is_active: boolean;
}

export interface WorkflowNode {
  id: string;
  type: NodeType;
  position: Position;
  data: NodeData;
}

export type NodeType = 
  | 'trigger'
  | 'action'
  | 'condition'
  | 'loop'
  | 'transform';

export interface Position {
  x: number;
  y: number;
}

export interface NodeData {
  label: string;
  config: Record<string, unknown>;
  connector?: string;
}

export interface WorkflowEdge {
  id: string;
  source: string;
  target: string;
  type?: 'default' | 'conditional';
  label?: string;
}

// API响应类型
export interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
  error?: string;
}

export interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  page_size: number;
  total_pages: number;
}

// 表单类型
export interface CreateWorkflowInput {
  name: string;
  description?: string;
}

export interface UpdateWorkflowInput extends Partial<CreateWorkflowInput> {
  is_active?: boolean;
}

🎨 四、TailwindCSS配置与主题定制

4.1 安装与配置

# 安装TailwindCSS及依赖
npm install -D tailwindcss postcss autoprefixer

# 生成配置文件
npx tailwindcss init -p

4.2 自定义主题配置

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      // 自定义颜色
      colors: {
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          200: '#bfdbfe',
          300: '#93c5fd',
          400: '#60a5fa',
          500: '#3b82f6', // 主色
          600: '#2563eb',
          700: '#1d4ed8',
          800: '#1e40af',
          900: '#1e3a8a',
          950: '#172554',
        },
        success: {
          light: '#d1fae5',
          DEFAULT: '#10b981',
          dark: '#065f46',
        },
        warning: {
          light: '#fef3c7',
          DEFAULT: '#f59e0b',
          dark: '#92400e',
        },
        error: {
          light: '#fee2e2',
          DEFAULT: '#ef4444',
          dark: '#991b1b',
        },
        // 工作流编辑器专用颜色
        node: {
          trigger: '#8b5cf6',
          action: '#3b82f6',
          condition: '#f59e0b',
          loop: '#10b981',
          transform: '#ec4899',
        }
      },
      
      // 自定义字体
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
        mono: ['Fira Code', 'monospace'],
      },
      
      // 自定义间距
      spacing: {
        '128': '32rem',
        '144': '36rem',
      },
      
      // 自定义圆角
      borderRadius: {
        '4xl': '2rem',
      },
      
      // 自定义阴影
      boxShadow: {
        'inner-lg': 'inset 0 2px 4px 0 rgb(0 0 0 / 0.1)',
        'glow': '0 0 20px rgba(59, 130, 246, 0.5)',
      },
      
      // 自定义动画
      animation: {
        'spin-slow': 'spin 3s linear infinite',
        'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
        'bounce-slow': 'bounce 2s infinite',
      },
      
      // 自定义关键帧
      keyframes: {
        'slide-in': {
          '0%': { transform: 'translateX(-100%)' },
          '100%': { transform: 'translateX(0)' },
        },
        'fade-in': {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        }
      }
    },
  },
  plugins: [
    // 表单插件
    require('@tailwindcss/forms'),
    // 排版插件
    require('@tailwindcss/typography'),
    // 自定义插件
    function({ addUtilities }) {
      const newUtilities = {
        '.scrollbar-hide': {
          /* IE and Edge */
          '-ms-overflow-style': 'none',
          /* Firefox */
          'scrollbar-width': 'none',
          /* Safari and Chrome */
          '&::-webkit-scrollbar': {
            display: 'none'
          }
        }
      }
      addUtilities(newUtilities)
    }
  ],
}

4.3 全局样式

/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* 自定义基础样式 */
@layer base {
  * {
    @apply border-border;
  }
  
  body {
    @apply bg-gray-50 text-gray-900 antialiased;
  }
  
  h1, h2, h3, h4, h5, h6 {
    @apply font-semibold;
  }
  
  h1 {
    @apply text-4xl;
  }
  
  h2 {
    @apply text-3xl;
  }
  
  h3 {
    @apply text-2xl;
  }
}

/* 自定义组件样式 */
@layer components {
  .btn {
    @apply px-4 py-2 rounded-lg font-medium transition-colors duration-200;
  }
  
  .btn-primary {
    @apply bg-primary-500 text-white hover:bg-primary-600 active:bg-primary-700;
  }
  
  .btn-secondary {
    @apply bg-gray-200 text-gray-900 hover:bg-gray-300 active:bg-gray-400;
  }
  
  .card {
    @apply bg-white rounded-lg shadow-md p-6;
  }
  
  .input {
    @apply w-full px-4 py-2 border border-gray-300 rounded-lg 
           focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
  }
}

/* 自定义工具类 */
@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}

🛣️ 五、React Router v6路由设计

5.1 路由配置

// src/routes/index.tsx
import { createBrowserRouter, Navigate } from 'react-router-dom';
import { lazy, Suspense } from 'react';

// 布局组件
import Layout from '@components/layout/Layout';
import LoadingSpinner from '@components/common/LoadingSpinner';

// 懒加载页面组件
const Dashboard = lazy(() => import('@/features/dashboard/Dashboard'));
const WorkflowList = lazy(() => import('@/features/workflows/WorkflowList'));
const WorkflowEditor = lazy(() => import('@/features/workflows/WorkflowEditor'));
const WorkflowExecution = lazy(() => import('@/features/workflows/WorkflowExecution'));
const Login = lazy(() => import('@/features/auth/Login'));
const Register = lazy(() => import('@/features/auth/Register'));
const NotFound = lazy(() => import('@/features/common/NotFound'));

// 受保护路由组件
import ProtectedRoute from './ProtectedRoute';

// 路由配置
export const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Navigate to="/dashboard" replace />
      },
      {
        path: 'dashboard',
        element: (
          <ProtectedRoute>
            <Suspense fallback={<LoadingSpinner />}>
              <Dashboard />
            </Suspense>
          </ProtectedRoute>
        )
      },
      {
        path: 'workflows',
        children: [
          {
            index: true,
            element: (
              <ProtectedRoute>
                <Suspense fallback={<LoadingSpinner />}>
                  <WorkflowList />
                </Suspense>
              </ProtectedRoute>
            )
          },
          {
            path: 'new',
            element: (
              <ProtectedRoute>
                <Suspense fallback={<LoadingSpinner />}>
                  <WorkflowEditor />
                </Suspense>
              </ProtectedRoute>
            )
          },
          {
            path: ':workflowId',
            element: (
              <ProtectedRoute>
                <Suspense fallback={<LoadingSpinner />}>
                  <WorkflowEditor />
                </Suspense>
              </ProtectedRoute>
            )
          },
          {
            path: ':workflowId/executions/:executionId',
            element: (
              <ProtectedRoute>
                <Suspense fallback={<LoadingSpinner />}>
                  <WorkflowExecution />
                </Suspense>
              </ProtectedRoute>
            )
          }
        ]
      }
    ]
  },
  {
    path: '/login',
    element: (
      <Suspense fallback={<LoadingSpinner />}>
        <Login />
      </Suspense>
    )
  },
  {
    path: '/register',
    element: (
      <Suspense fallback={<LoadingSpinner />}>
        <Register />
      </Suspense>
    )
  },
  {
    path: '*',
    element: (
      <Suspense fallback={<LoadingSpinner />}>
        <NotFound />
      </Suspense>
    )
  }
]);

5.2 受保护路由实现

// src/routes/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '@store/authStore';

interface ProtectedRouteProps {
  children: React.ReactNode;
}

export default function ProtectedRoute({ children }: ProtectedRouteProps) {
  const { isAuthenticated } = useAuthStore();
  const location = useLocation();
  
  if (!isAuthenticated) {
    // 重定向到登录页,并保存当前路径
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  return <>{children}</>;
}

🗄️ 六、Zustand状态管理

6.1 认证状态管理

// src/store/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

interface User {
  id: string;
  username: string;
  email: string;
  full_name?: string;
}

interface AuthState {
  // 状态
  user: User | null;
  accessToken: string | null;
  refreshToken: string | null;
  isAuthenticated: boolean;
  
  // 操作
  login: (user: User, accessToken: string, refreshToken: string) => void;
  logout: () => void;
  updateUser: (user: Partial<User>) => void;
  setTokens: (accessToken: string, refreshToken: string) => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      // 初始状态
      user: null,
      accessToken: null,
      refreshToken: null,
      isAuthenticated: false,
      
      // 登录
      login: (user, accessToken, refreshToken) => set({
        user,
        accessToken,
        refreshToken,
        isAuthenticated: true
      }),
      
      // 登出
      logout: () => set({
        user: null,
        accessToken: null,
        refreshToken: null,
        isAuthenticated: false
      }),
      
      // 更新用户信息
      updateUser: (userData) => set((state) => ({
        user: state.user ? { ...state.user, ...userData } : null
      })),
      
      // 更新Token
      setTokens: (accessToken, refreshToken) => set({
        accessToken,
        refreshToken
      })
    }),
    {
      name: 'auth-storage', // localStorage key
      storage: createJSONStorage(() => localStorage),
      // 只持久化部分字段
      partialize: (state) => ({
        user: state.user,
        accessToken: state.accessToken,
        refreshToken: state.refreshToken,
        isAuthenticated: state.isAuthenticated
      })
    }
  )
);

6.2 工作流状态管理

// src/store/workflowStore.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import type { Workflow, WorkflowNode, WorkflowEdge } from '@types/workflow';

interface WorkflowState {
  // 当前编辑的工作流
  currentWorkflow: Workflow | null;
  nodes: WorkflowNode[];
  edges: WorkflowEdge[];
  
  // 选中状态
  selectedNodeId: string | null;
  selectedEdgeId: string | null;
  
  // 操作
  setCurrentWorkflow: (workflow: Workflow) => void;
  addNode: (node: WorkflowNode) => void;
  updateNode: (nodeId: string, data: Partial<WorkflowNode>) => void;
  deleteNode: (nodeId: string) => void;
  addEdge: (edge: WorkflowEdge) => void;
  deleteEdge: (edgeId: string) => void;
  selectNode: (nodeId: string | null) => void;
  selectEdge: (edgeId: string | null) => void;
  clearWorkflow: () => void;
}

export const useWorkflowStore = create<WorkflowState>()(
  devtools(
    (set) => ({
      // 初始状态
      currentWorkflow: null,
      nodes: [],
      edges: [],
      selectedNodeId: null,
      selectedEdgeId: null,
      
      // 设置当前工作流
      setCurrentWorkflow: (workflow) => set({
        currentWorkflow: workflow,
        nodes: workflow.nodes,
        edges: workflow.edges
      }),
      
      // 添加节点
      addNode: (node) => set((state) => ({
        nodes: [...state.nodes, node]
      })),
      
      // 更新节点
      updateNode: (nodeId, data) => set((state) => ({
        nodes: state.nodes.map(node =>
          node.id === nodeId ? { ...node, ...data } : node
        )
      })),
      
      // 删除节点
      deleteNode: (nodeId) => set((state) => ({
        nodes: state.nodes.filter(node => node.id !== nodeId),
        edges: state.edges.filter(edge => 
          edge.source !== nodeId && edge.target !== nodeId
        ),
        selectedNodeId: state.selectedNodeId === nodeId ? null : state.selectedNodeId
      })),
      
      // 添加边
      addEdge: (edge) => set((state) => ({
        edges: [...state.edges, edge]
      })),
      
      // 删除边
      deleteEdge: (edgeId) => set((state) => ({
        edges: state.edges.filter(edge => edge.id !== edgeId),
        selectedEdgeId: state.selectedEdgeId === edgeId ? null : state.selectedEdgeId
      })),
      
      // 选中节点
      selectNode: (nodeId) => set({
        selectedNodeId: nodeId,
        selectedEdgeId: null
      }),
      
      // 选中边
      selectEdge: (edgeId) => set({
        selectedEdgeId: edgeId,
        selectedNodeId: null
      }),
      
      // 清空工作流
      clearWorkflow: () => set({
        currentWorkflow: null,
        nodes: [],
        edges: [],
        selectedNodeId: null,
        selectedEdgeId: null
      })
    }),
    { name: 'WorkflowStore' }
  )
);

🌐 七、API请求封装

7.1 Axios实例配置

// src/api/client.ts
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { useAuthStore } from '@store/authStore';

// 创建Axios实例
export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器
apiClient.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    // 添加认证Token
    const { accessToken } = useAuthStore.getState();
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    
    return config;
  },
  (error: AxiosError) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
apiClient.interceptors.response.use(
  (response) => {
    return response.data;
  },
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
    
    // Token过期,尝试刷新
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        const { refreshToken, setTokens } = useAuthStore.getState();
        
        if (!refreshToken) {
          throw new Error('No refresh token');
        }
        
        // 刷新Token
        const response = await axios.post(
          `${import.meta.env.VITE_API_BASE_URL}/auth/refresh`,
          { refresh_token: refreshToken }
        );
        
        const { access_token, refresh_token } = response.data;
        
        // 更新Token
        setTokens(access_token, refresh_token);
        
        // 重试原请求
        originalRequest.headers.Authorization = `Bearer ${access_token}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        // 刷新失败,登出用户
        useAuthStore.getState().logout();
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);

7.2 React Query集成

// src/api/workflows.ts
import { apiClient } from './client';
import type { Workflow, CreateWorkflowInput, UpdateWorkflowInput, PaginatedResponse } from '@types/workflow';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// API函数
export const workflowApi = {
  // 获取工作流列表
  getWorkflows: async (page = 1, pageSize = 20) => {
    return apiClient.get<PaginatedResponse<Workflow>>('/workflows', {
      params: { page, page_size: pageSize }
    });
  },
  
  // 获取单个工作流
  getWorkflow: async (workflowId: string) => {
    return apiClient.get<Workflow>(`/workflows/${workflowId}`);
  },
  
  // 创建工作流
  createWorkflow: async (data: CreateWorkflowInput) => {
    return apiClient.post<Workflow>('/workflows', data);
  },
  
  // 更新工作流
  updateWorkflow: async (workflowId: string, data: UpdateWorkflowInput) => {
    return apiClient.put<Workflow>(`/workflows/${workflowId}`, data);
  },
  
  // 删除工作流
  deleteWorkflow: async (workflowId: string) => {
    return apiClient.delete(`/workflows/${workflowId}`);
  }
};

// React Query Hooks
export function useWorkflows(page = 1, pageSize = 20) {
  return useQuery({
    queryKey: ['workflows', page, pageSize],
    queryFn: () => workflowApi.getWorkflows(page, pageSize),
    staleTime: 5 * 60 * 1000, // 5分钟
    gcTime: 10 * 60 * 1000 // 10分钟
  });
}

export function useWorkflow(workflowId: string) {
  return useQuery({
    queryKey: ['workflow', workflowId],
    queryFn: () => workflowApi.getWorkflow(workflowId),
    enabled: !!workflowId
  });
}

export function useCreateWorkflow() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: workflowApi.createWorkflow,
    onSuccess: () => {
      // 创建成功后,使工作流列表缓存失效
      queryClient.invalidateQueries({ queryKey: ['workflows'] });
    }
  });
}

export function useUpdateWorkflow() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: ({ workflowId, data }: { workflowId: string; data: UpdateWorkflowInput }) =>
      workflowApi.updateWorkflow(workflowId, data),
    onSuccess: (_, variables) => {
      // 更新成功后,使相关缓存失效
      queryClient.invalidateQueries({ queryKey: ['workflow', variables.workflowId] });
      queryClient.invalidateQueries({ queryKey: ['workflows'] });
    }
  });
}

export function useDeleteWorkflow() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: workflowApi.deleteWorkflow,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['workflows'] });
    }
  });
}

🎨 八、UI组件示例

8.1 按钮组件

// src/components/ui/Button.tsx
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { cn } from '@utils/cn';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant = 'primary', size = 'md', isLoading, children, disabled, ...props }, ref) => {
    const baseStyles = 'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
    
    const variants = {
      primary: 'bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500',
      secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
      outline: 'border-2 border-primary-500 text-primary-500 hover:bg-primary-50 focus:ring-primary-500',
      ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
      danger: 'bg-error-500 text-white hover:bg-error-600 focus:ring-error-500'
    };
    
    const sizes = {
      sm: 'px-3 py-1.5 text-sm',
      md: 'px-4 py-2 text-base',
      lg: 'px-6 py-3 text-lg'
    };
    
    return (
      <button
        ref={ref}
        className={cn(baseStyles, variants[variant], sizes[size], className)}
        disabled={disabled || isLoading}
        {...props}
      >
        {isLoading && (
          <svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
            <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
            <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
          </svg>
        )}
        {children}
      </button>
    );
  }
);

Button.displayName = 'Button';

export default Button;

// 工具函数
// src/utils/cn.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

💡 九、代码质量工具配置

9.1 ESLint配置

// .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime'
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh'],
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    '@typescript-eslint/no-explicit-any': 'warn',
    'react/prop-types': 'off'
  },
  settings: {
    react: {
      version: 'detect'
    }
  }
}

9.2 Prettier配置

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

💡 本文小结

核心要点回顾:

  1. Vite优势

    • 极速的开发体验(ESM + esbuild)
    • 开箱即用的TypeScript支持
    • 高效的生产构建(Rollup)
  2. TypeScript最佳实践

    • 启用严格模式
    • 合理的类型定义
    • 路径别名配置
  3. TailwindCSS原子化CSS

    • 快速开发UI
    • 主题定制
    • 响应式设计
  4. React Router v6

    • 声明式路由配置
    • 懒加载优化
    • 受保护路由
  5. Zustand状态管理

    • 轻量级(1.2KB)
    • 简单易用
    • 持久化支持
  6. React Query数据管理

    • 自动缓存
    • 后台更新
    • 乐观更新

性能优化建议:

// 性能优化清单
const PERFORMANCE_CHECKLIST = {
  "代码分割": [
    "路由级别懒加载",
    "组件级别动态导入",
    "第三方库按需加载"
  ],
  "资源优化": [
    "图片懒加载",
    "使用WebP格式",
    "CDN加速"
  ],
  "渲染优化": [
    "使用React.memo避免不必要的重渲染",
    "使用useMemo/useCallback缓存计算结果",
    "虚拟滚动处理长列表"
  ],
  "网络优化": [
    "API请求去重",
    "数据预取",
    "离线缓存"
  ]
};

📦 本文资源

项目脚手架:

# 克隆前端项目模板
git clone https://github.com/quantumflow/frontend-template.git
cd frontend-template
npm install
npm run dev

配置文件:

  • vite.config.ts - Vite配置
  • tsconfig.json - TypeScript配置
  • tailwind.config.js - TailwindCSS配置
  • .eslintrc.cjs - ESLint配置
  • .prettierrc - Prettier配置

VSCode推荐插件:

{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "bradlc.vscode-tailwindcss",
    "ms-vscode.vscode-typescript-next",
    "usernamehw.errorlens",
    "eamodio.gitlens"
  ]
}

🤔 思考题

  1. 构建工具选择:在什么场景下应该选择Next.js而不是Vite?

  2. 状态管理:如何决定哪些状态应该放在Zustand,哪些应该用React Query?

  3. 性能优化:如何监控和优化前端应用的性能?

  4. 类型安全:如何确保前后端API类型的一致性?

  5. 代码分割:如何设计合理的代码分割策略?

欢迎在评论区分享你的思考和经验!


作者:DREAMVFIA
专注于企业级应用架构设计与工作流自动化技术

版权声明:
本文为QuantumFlow专栏原创内容,遵循MIT开源协议。欢迎转载,请注明出处。


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐