React 中实现异步加载组件

#react

目录

1. 总结

  • 使用 React.lazy 和 Suspense 实现异步组件
  • 懒加载
  • 重试机制
  • startTransition 优化交互体验
  • 错误处理
  • 异步组件的==自己实现==
    • Class 实现 AsyncComponent
    • Hooks:useAsync

2. React.lazy 和 Suspense(推荐方式)

这是 React 16.6+ 官方提供的异步组件解决方案:

// 1. 基础用法
import React, { Suspense } from 'react';

// 使用 React.lazy 动态引入组件
const LazyComponent = React.lazy(() => import('./LazyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

// 2. 多个异步组件
const LazyComponent1 = React.lazy(() => import('./LazyComponent1'));
const LazyComponent2 = React.lazy(() => import('./LazyComponent2'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <div>
        <LazyComponent1 />
        <LazyComponent2 />
      </div>
    </Suspense>
  );
}

// 3. 嵌套 Suspense
function App() {
  return (
    <Suspense fallback={<div>Loading outer...</div>}>
      <div>
        <h1>Main Content</h1>
        <Suspense fallback={<div>Loading inner...</div>}>
          <LazyComponent />
        </Suspense>
      </div>
    </Suspense>
  );
}

3. 路由级别的代码分割

3.1. 基础路由懒加载

// 1. 基础路由懒加载
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense } from 'react';

const Home = React.lazy(() => import('./routes/Home'));
const About = React.lazy(() => import('./routes/About'));
const User = React.lazy(() => import('./routes/User'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route path="/user" component={User} />
        </Switch>
      </Suspense>
    </Router>
  );
}

3.2. 创建可重用的异步路由组件

// 2. 创建可重用的异步路由组件
const AsyncRoute = ({ component: Component, ...rest }) => (
  <Route
    {...rest}
    render={props => (
      <Suspense fallback={<div>Loading...</div>}>
        <Component {...props} />
      </Suspense>
    )}
  />
);

// 使用
function App() {
  return (
    <Router>
      <Switch>
        <AsyncRoute exact path="/" component={Home} />
        <AsyncRoute path="/about" component={About} />
        <AsyncRoute path="/user" component={User} />
      </Switch>
    </Router>
  );
}

4. 自定义异步组件实现

如果需要更细粒度的控制,可以自己实现异步组件:

4.1. 基于 Class 组件的实现

// 1. 基于 Class 组件的实现
class AsyncComponent extends React.Component {
  state = {
    Component: null
  };

  componentDidMount() {
    this.loadComponent();
  }

  loadComponent = async () => {
    const { loader } = this.props;
    const Component = await loader();
    this.setState({ Component });
  };

  render() {
    const { Component } = this.state;
    const { fallback, ...props } = this.props;

    return Component ? <Component {...props} /> : fallback;
  }
}

// 使用方式
const AsyncHome = props => (
  <AsyncComponent
    loader={() => import('./Home')}
    fallback={<div>Loading...</div>}
    {...props}
  />
);

4.2. 基于 Hooks 的实现


// 2. 基于 Hooks 的实现
function useAsync(loader) {
  const [Component, setComponent] = React.useState(null);

  React.useEffect(() => {
    loader().then(comp => {
      setComponent(comp.default || comp);
    });
  }, [loader]);

  return Component;
}

function AsyncComponent({ loader, fallback, ...props }) {
  const Component = useAsync(loader);
  return Component ? <Component {...props} /> : fallback;
}

5. 错误处理和重试机制

5.1. 使用 Error Boundary

// 1. 使用 Error Boundary
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong!</div>;
    }
    return this.props.children;
  }
}

// 使用
function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

5.2. 带重试机制的异步组件


// 2. 带重试机制的异步组件
function RetryableAsyncComponent({ loader, maxRetries = 3, ...props }) {
  const [error, setError] = React.useState(null);
  const [retries, setRetries] = React.useState(0);
  const [Component, setComponent] = React.useState(null);

  const loadComponent = async () => {
    try {
      const comp = await loader();
      setComponent(comp.default || comp);
      setError(null);
    } catch (err) {
      setError(err);
      if (retries < maxRetries) {
        setTimeout(() => {
          setRetries(r => r + 1);
          loadComponent(); // 递归调用
        }, Math.pow(2, retries) * 1000); // 指数退避
      }
    }
  };

  React.useEffect(() => {
    loadComponent();
  }, []);

  if (error && retries >= maxRetries) {
    return <div>Failed to load component after {maxRetries} retries</div>;
  }

  return Component ? <Component {...props} /> : <div>Loading...</div>;
}

5.3. 最简单的重试机制

function RetryableComponent({ retries = 3 }) {
  const [retry, setRetry] = useState(0);

  return (
    <ErrorBoundary
      onError={() => {
        if (retry < retries) {
          setRetry(r => r + 1);
        }
      }}
    >
      <Suspense fallback={<Loading />}>
        <LazyComponent key={retry} />
      </Suspense>
    </ErrorBoundary>
  );
}

6. 性能优化

6.1. 在需要时预加载

// 1. 预加载组件
const LazyComponent = React.lazy(() => import('./LazyComponent'));

// 在需要时预加载
const preloadComponent = () => {
  const componentPromise = import('./LazyComponent');
  return componentPromise;
};

// 使用
function App() {
  const handleMouseEnter = () => {
    preloadComponent(); // 用户hover时预加载
  };

  return (
    <div onMouseEnter={handleMouseEnter}>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}


6.2. 优化加载状态

// 2. 优化加载状态
function DelayedFallback({ delay = 200 }) {
  const [show, setShow] = React.useState(false);

  React.useEffect(() => {
    const timer = setTimeout(() => setShow(true), delay);
    return () => clearTimeout(timer);
  }, [delay]);

  return show ? <div>Loading...</div> : null;
}

function App() {
  return (
    <Suspense fallback={<DelayedFallback />}>
      <LazyComponent />
    </Suspense>
  );
}

6.3. 使用 startTransition 优化用户体验

import { startTransition } from 'react';

function App() {
  const [tab, setTab] = useState('home');

  const switchTab = (nextTab) => {
    startTransition(() => {
      setTab(nextTab);
    });
  };

  return (
    <div>
      <TabButton onClick={() => switchTab('home')}>Home</TabButton>
      <TabButton onClick={() => switchTab('profile')}>Profile</TabButton>
      <Suspense fallback={<Loading />}>
        {tab === 'home' ? <Home /> : <Profile />}
      </Suspense>
    </div>
  );
}

7. 数据预加载

// 1. 创建资源加载器
function createResource(promise) {
  let status = 'pending';
  let result;
  let suspender = promise.then(
    r => {
      status = 'success';
      result = r;
    },
    e => {
      status = 'error';
      result = e;
    }
  );

  return {
    read() {
      if (status === 'pending') {
        throw suspender;
      } else if (status === 'error') {
        throw result;
      } else if (status === 'success') {
        return result;
      }
    }
  };
}

// 2. 使用数据预加载
const dataResource = createResource(fetch('/api/data').then(r => r.json()));

function AsyncDataComponent() {
  const data = dataResource.read();
  return <div>{data.message}</div>;
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <AsyncDataComponent />
    </Suspense>
  );
}

8. 建议

在实际应用中,建议:

  • 优先使用 React.lazy 和 Suspense
  • 在路由层面实现代码分割
    • 实现代码分割和按需加载
  • 实现适当的错误处理
  • 添加加载状态反馈
    • 提供良好的加载体验
  • 考虑预加载策略
    • 实现数据预加载
  • 优化加载体验