组件级异步加载与预加载策略

组件级异步加载与预加载策略

1. 核心架构设计

1.1 异步组件加载器

// 组件加载状态枚举
enum ComponentLoadStatus {
  IDLE = 'idle',
  LOADING = 'loading',
  LOADED = 'loaded',
  ERROR = 'error'
}

// 组件配置接口
interface ComponentConfig {
  name: string;
  path: string;           // 组件路径
  chunkName?: string;     // Webpack Chunk 名称
  preload?: boolean;      // 是否预加载
  prefetch?: boolean;     // 是否预获取
  loadingComponent?: string; // 加载中组件
  errorComponent?: string;   // 错误组件
  timeout?: number;       // 加载超时时间
  retryCount?: number;    // 重试次数
}

// 组件缓存项
interface ComponentCacheItem {
  component: any;
  status: ComponentLoadStatus;
  loadTime: number;
  lastUsed: number;
  size: number;
}

1.2 高级组件加载器

class AdvancedComponentLoader {
  private componentCache: Map<string, ComponentCacheItem> = new Map();
  private loadingQueue: Map<string, Promise<any>> = new Map();
  private preloadQueue: Set<string> = new Set();
  private observer: IntersectionObserver;
  private maxCacheSize: number = 50; // 最大缓存组件数
  private cacheSize: number = 0;

  constructor() {
    // 初始化 IntersectionObserver 用于可视区域预加载
    this.observer = new IntersectionObserver(
      this.handleIntersection.bind(this),
      {
        rootMargin: '100px 0px', // 提前 100px 加载
        threshold: 0.1
      }
    );
  }

  // 异步加载组件
  async loadComponent(config: ComponentConfig): Promise<any> {
    const { name, path, retryCount = 3, timeout = 30000 } = config;

    // 检查缓存
    if (this.componentCache.has(name)) {
      const cached = this.componentCache.get(name)!;
      cached.lastUsed = Date.now();
      return cached.component;
    }

    // 检查是否正在加载
    if (this.loadingQueue.has(name)) {
      return this.loadingQueue.get(name)!;
    }

    // 创建加载 Promise
    const loadPromise = this.loadWithRetry(path, retryCount, timeout)
      .then(component => {
        this.cacheComponent(name, component);
        this.loadingQueue.delete(name);
        return component;
      })
      .catch(error => {
        this.loadingQueue.delete(name);
        throw error;
      });

    this.loadingQueue.set(name, loadPromise);
    return loadPromise;
  }

  // 带重试的加载
  private async loadWithRetry(
    path: string, 
    retryCount: number, 
    timeout: number
  ): Promise<any> {
    for (let attempt = 1; attempt <= retryCount; attempt++) {
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);

        const component = await this.importComponent(path, controller.signal);
        clearTimeout(timeoutId);
        return component;

      } catch (error) {
        console.warn(`组件加载失败 (尝试 ${attempt}/${retryCount}):`, error);
        
        if (attempt === retryCount) {
          throw error;
        }

        // 指数退避重试
        await this.delay(Math.pow(2, attempt) * 1000);
      }
    }
  }

  // 动态导入组件
  private async importComponent(path: string, signal?: AbortSignal): Promise<any> {
    // 使用 Webpack 魔法注释
    const component = await import(
      /* webpackChunkName: "[request]" */
      /* webpackMode: "lazy" */
      /* webpackPrefetch: true */
      `@/components/${path}`
    );

    if (signal?.aborted) {
      throw new Error('加载被取消');
    }

    return component.default || component;
  }

  // 缓存组件
  private cacheComponent(name: string, component: any): void {
    const cacheItem: ComponentCacheItem = {
      component,
      status: ComponentLoadStatus.LOADED,
      loadTime: Date.now(),
      lastUsed: Date.now(),
      size: this.estimateSize(component)
    };

    // 检查缓存大小
    if (this.cacheSize + cacheItem.size > this.maxCacheSize) {
      this.evictCache();
    }

    this.componentCache.set(name, cacheItem);
    this.cacheSize += cacheItem.size;
  }

  // 估算组件大小
  private estimateSize(component: any): number {
    // 简单的估算方法
    return JSON.stringify(component).length;
  }

  // 缓存淘汰策略 (LRU)
  private evictCache(): void {
    const entries = Array.from(this.componentCache.entries());
    const sorted = entries.sort((a, b) => a[1].lastUsed - b[1].lastUsed);
    
    const toRemove = sorted.slice(0, Math.floor(sorted.length * 0.2)); // 移除 20%
    
    toRemove.forEach(([name]) => {
      const item = this.componentCache.get(name)!;
      this.cacheSize -= item.size;
      this.componentCache.delete(name);
    });
  }

  // 预加载组件
  preloadComponent(config: ComponentConfig): void {
    const { name, path } = config;
    
    if (this.componentCache.has(name) || this.loadingQueue.has(name)) {
      return;
    }

    // 使用 requestIdleCallback 在空闲时预加载
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        this.loadComponent(config).catch(() => {
          // 静默处理预加载错误
        });
      });
    } else {
      // 回退方案
      setTimeout(() => {
        this.loadComponent(config).catch(() => {});
      }, 1000);
    }
  }

  // 观察元素进行智能预加载
  observeElement(element: HTMLElement, config: ComponentConfig): void {
    this.observer.observe(element);
    this.preloadQueue.add(config.name);
  }

  private handleIntersection(entries: IntersectionObserverEntry[]): void {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const element = entry.target;
        this.observer.unobserve(element);
        
        // 触发预加载
        this.preloadQueue.forEach(name => {
          // 这里需要根据元素找到对应的配置
          // 简化实现
        });
      }
    });
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 清理缓存
  clearCache(): void {
    this.componentCache.clear();
    this.loadingQueue.clear();
    this.preloadQueue.clear();
    this.cacheSize = 0;
  }

  // 获取缓存状态
  getCacheStats(): { size: number; count: number; hitRate: number } {
    return {
      size: this.cacheSize,
      count: this.componentCache.size,
      hitRate: this.calculateHitRate()
    };
  }

  private calculateHitRate(): number {
    // 实现命中率计算
    return 0;
  }
}

// 全局加载器实例
export const componentLoader = new AdvancedComponentLoader();

2. Vue 异步组件实现

2.1 高级异步组件工厂

// Vue 异步组件工厂
export function createAsyncComponent(config: ComponentConfig) {
  const {
    name,
    path,
    loadingComponent = 'LoadingSpinner',
    errorComponent = 'ErrorDisplay',
    timeout = 30000,
    retryCount = 3
  } = config;

  return defineAsyncComponent({
    // 加载函数
    loader: () => componentLoader.loadComponent(config),

    // 加载中组件
    loadingComponent: defineComponent({
      name: `${name}Loading`,
      setup() {
        return () => h(resolveComponent(loadingComponent));
      }
    }),

    // 错误组件
    errorComponent: defineComponent({
      name: `${name}Error`,
      props: {
        error: {
          type: Error,
          required: true
        },
        retry: {
          type: Function,
          required: true
        }
      },
      setup(props) {
        const handleRetry = () => {
          props.retry();
        };

        return () => h(resolveComponent(errorComponent), {
          error: props.error,
          onRetry: handleRetry
        });
      }
    }),

    // 延迟显示加载状态(避免闪烁)
    delay: 200,

    // 超时时间
    timeout,

    // 重试逻辑
    onError(error, retry, fail, attempts) {
      if (error.message.includes('Failed to fetch') && attempts <= retryCount) {
        // 指数退避重试
        retry();
      } else {
        fail();
      }
    }
  });
}

2.2 组件注册管理器

class ComponentRegistry {
  private registry: Map<string, ComponentConfig> = new Map();
  private dependencies: Map<string, string[]> = new Map();

  // 注册组件
  registerComponent(config: ComponentConfig): void {
    this.registry.set(config.name, config);
  }

  // 注册组件依赖关系
  registerDependency(componentName: string, dependencies: string[]): void {
    this.dependencies.set(componentName, dependencies);
  }

  // 获取组件配置
  getComponentConfig(name: string): ComponentConfig | undefined {
    return this.registry.get(name);
  }

  // 预加载组件及其依赖
  async preloadComponentWithDependencies(name: string): Promise<void> {
    const config = this.getComponentConfig(name);
    if (!config) return;

    // 预加载依赖
    const deps = this.dependencies.get(name) || [];
    await Promise.all(
      deps.map(dep => this.preloadComponent(dep))
    );

    // 预加载组件本身
    await this.preloadComponent(name);
  }

  // 智能预加载(基于路由和用户行为)
  setupIntelligentPreloading(router: any): void {
    // 路由变化时预加载相关组件
    router.afterEach((to: any) => {
      this.preloadRouteComponents(to);
    });

    // 鼠标悬停预加载
    this.setupHoverPreloading();
  }

  private async preloadRouteComponents(route: any): Promise<void> {
    const routeComponents = this.getComponentsForRoute(route);
    
    // 立即预加载关键组件
    const criticalComponents = routeComponents.slice(0, 2);
    await Promise.all(
      criticalComponents.map(comp => this.preloadComponent(comp))
    );

    // 延迟预加载非关键组件
    setTimeout(() => {
      const nonCriticalComponents = routeComponents.slice(2);
      nonCriticalComponents.forEach(comp => this.preloadComponent(comp));
    }, 1000);
  }

  private setupHoverPreloading(): void {
    document.addEventListener('mouseover', this.handleHover.bind(this), {
      capture: true,
      passive: true
    });
  }

  private handleHover(event: MouseEvent): void {
    const target = event.target as HTMLElement;
    const componentName = this.detectComponentFromElement(target);
    
    if (componentName) {
      componentLoader.preloadComponent(
        this.getComponentConfig(componentName)!
      );
    }
  }

  private detectComponentFromElement(element: HTMLElement): string | null {
    // 根据元素属性检测对应的组件
    // 简化实现
    return null;
  }

  private getComponentsForRoute(route: any): string[] {
    // 根据路由获取需要加载的组件
    // 简化实现
    return [];
  }

  private async preloadComponent(name: string): Promise<void> {
    const config = this.getComponentConfig(name);
    if (config) {
      componentLoader.preloadComponent(config);
    }
  }
}

export const componentRegistry = new ComponentRegistry();

3. React 异步组件实现

3.1 React Suspense 组件

// React 异步组件高阶函数
export function withAsyncComponent<T extends object>(
  config: ComponentConfig
) {
  const { name, path } = config;

  const AsyncComponent = React.lazy(() =>
    componentLoader.loadComponent(config).then(component => ({
      default: component
    }))
  );

  // 错误边界组件
  class ErrorBoundary extends React.Component<
    { fallback: React.ReactNode; children: React.ReactNode },
    { hasError: boolean; error?: Error }
  > {
    constructor(props: any) {
      super(props);
      this.state = { hasError: false };
    }

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

    componentDidCatch(error: Error, errorInfo: any) {
      console.error('组件加载错误:', error, errorInfo);
    }

    render() {
      if (this.state.hasError) {
        return this.props.fallback;
      }

      return this.props.children;
    }
  }

  // 返回包装后的组件
  return React.forwardRef<T, any>((props, ref) => (
    <ErrorBoundary
      fallback={
        <div>
          <h3>组件加载失败</h3>
          <button onClick={() => window.location.reload()}>重试</button>
        </div>
      }
    >
      <React.Suspense
        fallback={
          <div className="loading-container">
            <div className="loading-spinner" />
            <span>加载中...</span>
          </div>
        }
      >
        <AsyncComponent {...props} ref={ref} />
      </React.Suspense>
    </ErrorBoundary>
  ));
}

3.2 React 预加载 Hook

// 预加载 Hook
export function useComponentPreload(componentName: string): {
  preload: () => void;
  isPreloaded: boolean;
} {
  const [isPreloaded, setIsPreloaded] = useState(false);
  const config = componentRegistry.getComponentConfig(componentName);

  const preload = useCallback(() => {
    if (!config || isPreloaded) return;

    componentLoader
      .loadComponent(config)
      .then(() => {
        setIsPreloaded(true);
      })
      .catch(() => {
        // 静默处理错误
      });
  }, [config, isPreloaded]);

  // 自动预加载(在空闲时)
  useEffect(() => {
    if (!config) return;

    const handleIdle = () => {
      if ('requestIdleCallback' in window) {
        requestIdleCallback(preload);
      } else {
        // 回退方案
        setTimeout(preload, 1000);
      }
    };

    handleIdle();
  }, [config, preload]);

  return { preload, isPreloaded };
}

// 可视区域预加载 Hook
export function useVisiblePreload(
  ref: React.RefObject<HTMLElement>,
  componentName: string
): void {
  const config = componentRegistry.getComponentConfig(componentName);

  useEffect(() => {
    if (!ref.current || !config) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          componentLoader.preloadComponent(config);
          observer.disconnect();
        }
      },
      {
        rootMargin: '100px 0px',
        threshold: 0.1
      }
    );

    observer.observe(ref.current);

    return () => {
      observer.disconnect();
    };
  }, [ref, config]);
}

4. 实际应用示例

4.1 Vue 应用集成

<template>
  <div class="app">
    <nav>
      <button @click="preloadDashboard">预加载仪表板</button>
      <button @click="preloadUserProfile">预加载用户资料</button>
    </nav>
    
    <router-view v-slot="{ Component }">
      <template v-if="Component">
        <Suspense @resolve="onComponentLoaded">
          <component :is="Component" />
          
          <template #fallback>
            <LoadingSpinner />
          </template>
        </Suspense>
      </template>
    </router-view>
  </div>
</template>

<script setup lang="ts">
import { onMounted, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
import { componentRegistry, componentLoader } from '@/utils/componentLoader';

// 注册组件
componentRegistry.registerComponent({
  name: 'UserDashboard',
  path: 'dashboard/UserDashboard.vue',
  preload: true,
  prefetch: true
});

componentRegistry.registerComponent({
  name: 'UserProfile',
  path: 'profile/UserProfile.vue',
  preload: false,
  prefetch: true
});

// 创建异步组件
const UserDashboard = defineAsyncComponent(() =>
  componentLoader.loadComponent({
    name: 'UserDashboard',
    path: 'dashboard/UserDashboard.vue'
  })
);

const UserProfile = defineAsyncComponent(() =>
  componentLoader.loadComponent({
    name: 'UserProfile',
    path: 'profile/UserProfile.vue'
  })
);

const router = useRouter();

// 预加载函数
const preloadDashboard = () => {
  componentLoader.preloadComponent({
    name: 'UserDashboard',
    path: 'dashboard/UserDashboard.vue'
  });
};

const preloadUserProfile = () => {
  componentLoader.preloadComponent({
    name: 'UserProfile',
    path: 'profile/UserProfile.vue'
  });
};

const onComponentLoaded = () => {
  console.log('组件加载完成');
};

// 初始化预加载策略
onMounted(() => {
  componentRegistry.setupIntelligentPreloading(router);
});
</script>

4.2 React 应用集成

import React, { useRef, useEffect } from 'react';
import { withAsyncComponent, useVisiblePreload } from '@/utils/asyncComponents';

// 异步组件
const AsyncDashboard = withAsyncComponent({
  name: 'UserDashboard',
  path: 'dashboard/UserDashboard',
  preload: true
});

const AsyncProfile = withAsyncComponent({
  name: 'UserProfile', 
  path: 'profile/UserProfile',
  preload: false
});

// 主应用组件
function App() {
  const dashboardRef = useRef<HTMLDivElement>(null);
  
  // 可视区域预加载
  useVisiblePreload(dashboardRef, 'UserDashboard');

  // 路由级别的预加载
  useEffect(() => {
    // 预加载可能访问的页面组件
    const preloadPotentialComponents = () => {
      componentRegistry.preloadComponentWithDependencies('UserProfile');
      componentRegistry.preloadComponentWithDependencies('Settings');
    };

    // 用户空闲时预加载
    if ('requestIdleCallback' in window) {
      requestIdleCallback(preloadPotentialComponents);
    } else {
      setTimeout(preloadPotentialComponents, 5000);
    }
  }, []);

  return (
    <div className="app">
      <nav>
        <button onClick={() => componentLoader.preloadComponent({
          name: 'UserProfile',
          path: 'profile/UserProfile'
        })}>
          预加载用户资料
        </button>
      </nav>

      <div ref={dashboardRef}>
        <AsyncDashboard userId="123" />
      </div>
    </div>
  );
}

export default App;

5. 性能监控和优化

5.1 性能监控系统

class ComponentPerformanceMonitor {
  private metrics: Map<string, ComponentMetric[]> = new Map();
  private observer: PerformanceObserver;

  constructor() {
    // 监控资源加载性能
    this.observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        this.recordResourceMetric(entry);
      });
    });

    this.observer.observe({ entryTypes: ['resource'] });
  }

  // 记录组件加载指标
  recordComponentLoad(
    componentName: string, 
    startTime: number, 
    endTime: number
  ): void {
    const duration = endTime - startTime;
    const metric: ComponentMetric = {
      componentName,
      duration,
      timestamp: Date.now(),
      type: 'load'
    };

    if (!this.metrics.has(componentName)) {
      this.metrics.set(componentName, []);
    }

    this.metrics.get(componentName)!.push(metric);

    // 上报到监控系统
    this.reportMetric(metric);
  }

  // 获取组件性能报告
  getComponentReport(componentName: string): ComponentReport {
    const metrics = this.metrics.get(componentName) || [];
    
    return {
      componentName,
      loadCount: metrics.length,
      averageLoadTime: this.calculateAverage(metrics.map(m => m.duration)),
      p95LoadTime: this.calculatePercentile(metrics.map(m => m.duration), 95),
      failures: metrics.filter(m => m.type === 'error').length
    };
  }

  private calculateAverage(numbers: number[]): number {
    return numbers.reduce((a, b) => a + b, 0) / numbers.length;
  }

  private calculatePercentile(numbers: number[], percentile: number): number {
    const sorted = numbers.sort((a, b) => a - b);
    const index = Math.ceil((percentile / 100) * sorted.length) - 1;
    return sorted[index];
  }

  private recordResourceMetric(entry: PerformanceEntry): void {
    // 记录资源加载指标
  }

  private reportMetric(metric: ComponentMetric): void {
    // 上报到 APM 系统
    if (navigator.sendBeacon) {
      const data = JSON.stringify(metric);
      navigator.sendBeacon('/api/performance', data);
    }
  }
}

interface ComponentMetric {
  componentName: string;
  duration: number;
  timestamp: number;
  type: string;
}

interface ComponentReport {
  componentName: string;
  loadCount: number;
  averageLoadTime: number;
  p95LoadTime: number;
  failures: number;
}

这种组件级异步加载与预加载策略提供了:

  1. 按需加载:只在需要时加载组件
  2. 智能预加载:基于用户行为和路由预测
  3. 性能监控:详细的加载性能指标
  4. 错误处理:完善的错误处理和重试机制
  5. 缓存优化:智能缓存和淘汰策略
  6. 可视区域加载:基于 Intersection Observer 的优化

这些策略可以显著提升大型应用的加载性能和用户体验。

posted @ 2025-10-16 00:27  阿木隆1237  阅读(10)  评论(0)    收藏  举报