Skip to content

Framer Motion企业级实践指南 ​

动画系统设计 ​

1. 动画主题配置 ​

创建统一的动画配置系统,确保整个应用的动画风格一致:

tsx
// src/theme/animations.ts
export const animationTokens = {
  duration: {
    fast: 0.2,
    normal: 0.3,
    slow: 0.5
  },
  ease: {
    default: [0.4, 0, 0.2, 1],
    in: [0.4, 0, 1, 1],
    out: [0, 0, 0.2, 1],
    inOut: [0.4, 0, 0.2, 1]
  },
  transition: {
    default: {
      duration: 0.3,
      ease: [0.4, 0, 0.2, 1]
    },
    spring: {
      type: "spring",
      stiffness: 300,
      damping: 30
    }
  }
} as const;

// 动画变体预设
export const animationPresets = {
  fadeIn: {
    initial: { opacity: 0 },
    animate: { opacity: 1 },
    exit: { opacity: 0 },
    transition: animationTokens.transition.default
  },
  slideInRight: {
    initial: { x: 20, opacity: 0 },
    animate: { x: 0, opacity: 1 },
    exit: { x: -20, opacity: 0 },
    transition: animationTokens.transition.default
  }
} as const;

2. 动画Provider组件 ​

tsx
// src/components/AnimationProvider.tsx
interface AnimationProviderProps {
  children: React.ReactNode;
  reducedMotion?: boolean;
}

export const AnimationProvider: React.FC<AnimationProviderProps> = ({
  children,
  reducedMotion = false
}) => {
  return (
    <MotionConfig
      reducedMotion={reducedMotion ? "always" : "never"}
      transition={animationTokens.transition.default}
    >
      {children}
    </MotionConfig>
  );
};

企业级组件实现 ​

1. 数据表格动画 ​

tsx
interface AnimatedTableProps<T> {
  data: T[];
  columns: {
    key: keyof T;
    title: string;
    render?: (value: T[keyof T], record: T) => React.ReactNode;
  }[];
  loading?: boolean;
  onRowClick?: (record: T) => void;
}

export function AnimatedTable<T extends { id: string | number }>({
  data,
  columns,
  loading,
  onRowClick
}: AnimatedTableProps<T>) {
  return (
    <div style={{ overflow: 'hidden' }}>
      <motion.table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          <tr>
            {columns.map(column => (
              <th key={String(column.key)} style={{ padding: '12px' }}>
                {column.title}
              </th>
            ))}
          </tr>
        </thead>
        <AnimatePresence mode="wait">
          {loading ? (
            <motion.tbody
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            >
              <tr>
                <td colSpan={columns.length} style={{ textAlign: 'center', padding: '20px' }}>
                  加载中...
                </td>
              </tr>
            </motion.tbody>
          ) : (
            <motion.tbody>
              <AnimatedList
                items={data}
                keyExtractor={item => String(item.id)}
                staggerDelay={0.05}
                renderItem={(record) => (
                  <motion.tr
                    whileHover={{ backgroundColor: '#f5f5f5' }}
                    onClick={() => onRowClick?.(record)}
                    style={{ cursor: onRowClick ? 'pointer' : 'default' }}
                  >
                    {columns.map(column => (
                      <td key={String(column.key)} style={{ padding: '12px' }}>
                        {column.render
                          ? column.render(record[column.key], record)
                          : String(record[column.key])}
                      </td>
                    ))}
                  </motion.tr>
                )}
              />
            </motion.tbody>
          )}
        </AnimatePresence>
      </motion.table>
    </div>
  );
}

2. 数据可视化动画 ​

tsx
interface AnimatedChartProps {
  data: { value: number; label: string }[];
  height?: number;
  color?: string;
}

export const AnimatedChart: React.FC<AnimatedChartProps> = ({
  data,
  height = 200,
  color = '#1a73e8'
}) => {
  const maxValue = Math.max(...data.map(d => d.value));
  
  return (
    <div style={{ height, display: 'flex', alignItems: 'flex-end', gap: 8 }}>
      <AnimatePresence>
        {data.map((item, index) => (
          <motion.div
            key={item.label}
            initial={{ height: 0 }}
            animate={{ height: `${(item.value / maxValue) * 100}%` }}
            exit={{ height: 0 }}
            transition={{
              duration: 0.5,
              delay: index * 0.1,
              ease: "easeOut"
            }}
            style={{
              flex: 1,
              backgroundColor: color,
              borderRadius: '4px 4px 0 0',
              position: 'relative'
            }}
          >
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              style={{
                position: 'absolute',
                top: -24,
                left: '50%',
                transform: 'translateX(-50%)',
                fontSize: 12
              }}
            >
              {item.value}
            </motion.div>
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              style={{
                position: 'absolute',
                bottom: -24,
                left: '50%',
                transform: 'translateX(-50%)',
                fontSize: 12
              }}
            >
              {item.label}
            </motion.div>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
};

3. 路由转场动画 ​

  1. 使用BrowserRouter和Routes:在react-router-dom v6中,使用BrowserRouter和Routes来定义路由。
  2. 使用AnimatePresence:AnimatePresence用于管理组件的进入和退出动画。使用mode="wait"来确保退出动画完成后再开始进入动画。
  3. 定义动画:使用motion.div来定义页面的动画效果。
jsx
// src/App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AnimatePresence } from 'framer-motion';
import Home from './Home';
import About from './About';

function App() {
  return (
    <Router>
      <AnimatePresence mode="wait">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </AnimatePresence>
    </Router>
  );
}

export default App;

// src/Home.jsx
import React from 'react';
import { motion } from 'framer-motion';

const pageVariants = {
  initial: {
    opacity: 0,
    x: "-100vw"
  },
  in: {
    opacity: 1,
    x: 0
  },
  out: {
    opacity: 0,
    x: "100vw"
  }
};

const pageTransition = {
  type: "tween",
  ease: "anticipate",
  duration: 0.5
};

function Home() {
  return (
    <motion.div
      initial="initial"
      animate="in"
      exit="out"
      variants={pageVariants}
      transition={pageTransition}
    >
      <h1>Home Page</h1>
    </motion.div>
  );
}

export default Home;

// src/About.jsx
import React from 'react';
import { motion } from 'framer-motion';

const pageVariants = {
  initial: {
    opacity: 0,
    x: "-100vw"
  },
  in: {
    opacity: 1,
    x: 0
  },
  out: {
    opacity: 0,
    x: "100vw"
  }
};

const pageTransition = {
  type: "tween",
  ease: "anticipate",
  duration: 0.5
};

function About() {
  return (
    <motion.div
      initial="initial"
      animate="in"
      exit="out"
      variants={pageVariants}
      transition={pageTransition}
    >
      <h1>About Page</h1>
    </motion.div>
  );
}

export default About;

性能监控与优化 ​

1. 性能监控HOC ​

tsx
// src/hocs/withAnimationMetrics.tsx
export function withAnimationMetrics<P extends object>(
  WrappedComponent: React.ComponentType<P>,
  componentName: string
) {
  return function WithAnimationMetrics(props: P) {
    useEffect(() => {
      const startTime = performance.now();
      
      return () => {
        const duration = performance.now() - startTime;
        console.log(`[Animation Metrics] ${componentName} rendered in ${duration}ms`);
        
        // 可以发送到性能监控系统
        if (duration > 16) { // 超过一帧的时间
          console.warn(`[Animation Performance Warning] ${componentName} animation took too long`);
        }
      };
    }, []);

    return <WrappedComponent {...props} />;
  };
}

2. 自定义动画Hook ​

tsx
// src/hooks/useAnimationControl.ts
interface AnimationControl {
  start: () => Promise<void>;
  stop: () => void;
  reset: () => void;
  isAnimating: boolean;
}

export function useAnimationControl(
  controls: AnimationControls,
  animation: TargetAndTransition
): AnimationControl {
  const [isAnimating, setIsAnimating] = useState(false);

  const start = async () => {
    setIsAnimating(true);
    await controls.start(animation);
    setIsAnimating(false);
  };

  const stop = () => {
    controls.stop();
    setIsAnimating(false);
  };

  const reset = () => {
    controls.set(animation);
    setIsAnimating(false);
  };

  return {
    start,
    stop,
    reset,
    isAnimating
  };
}

实战案例:企业级数据大屏 ​

tsx
interface DataScreenProps {
  data: {
    charts: { id: string; data: any[] }[];
    tables: { id: string; data: any[] }[];
  };
}

export const DataScreen: React.FC<DataScreenProps> = ({ data }) => {
  return (
    <div style={{ padding: 24 }}>
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(3, 1fr)',
          gap: 24
        }}
      >
        {data.charts.map((chart, index) => (
          <motion.div
            key={chart.id}
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ delay: index * 0.1 }}
          >
            <AnimatedChart data={chart.data} />
          </motion.div>
        ))}
        
        {data.tables.map((table, index) => (
          <motion.div
            key={table.id}
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ delay: index * 0.1 }}
          >
            <AnimatedTable data={table.data} />
          </motion.div>
        ))}
      </motion.div>
    </div>
  );
};

小结 ​

本文介绍了Framer Motion在企业级应用中的实践:

  • 动画系统设计
  • 企业级组件实现
  • 性能监控与优化
  • 实战案例展示

相关资源 ​