当前位置:首页 > 技术 > 正文内容

基于智能预加载的组件动态加载架构

访客 技术 2026年6月25日 1

核心加载引擎设计

1.1 动态组件管理接口

// 组件状态定义
enum LoadState {
  Idle = 'idle',
  Pending = 'pending',
  Success = 'success',
  Failed = 'failed'
}

// 加载配置结构
interface LoadConfig {
  id: string;
  modulePath: string;
  chunkName?: string;
  shouldPreload?: boolean;
  shouldPrefetch?: boolean;
  fallbackComponent?: string;
  errorComponent?: string;
  timeoutMs?: number;
  maxRetries?: number;
}

1.2 高级异步加载服务

class DynamicLoader {
  private cache = new Map<string, { instance: any; timestamp: number }>();
  private pendingRequests = new Map<string, Promise<any>>();
  private preloadQueue = new Set<string>();
  private observer: IntersectionObserver;
  private maxSize = 50;
  private currentSize = 0;

  constructor() {
    this.observer = new IntersectionObserver(
      this.handleVisibility.bind(this),
      {
        rootMargin: '150px',
        threshold: 0.05
      }
    );
  }

  async load(id: string, config: LoadConfig): Promise<any> {
    // 缓存命中检查
    if (this.cache.has(id)) {
      const item = this.cache.get(id)!;
      item.timestamp = Date.now();
      return item.instance;
    }

    // 防止重复请求
    if (this.pendingRequests.has(id)) {
      return this.pendingRequests.get(id)!;
    }

    const requestPromise = this.attemptLoad(config)
      .then(result => {
        this.addToCache(id, result);
        this.pendingRequests.delete(id);
        return result;
      })
      .catch(err => {
        this.pendingRequests.delete(id);
        throw err;
      });

    this.pendingRequests.set(id, requestPromise);
    return requestPromise;
  }

  private async attemptLoad(config: LoadConfig): Promise<any> {
    const { modulePath, maxRetries = 3, timeoutMs = 25000 } = config;
    let lastError: Error | null = null;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

        const module = await import(
          /* webpackChunkName: "[request]" */
          /* webpackMode: "lazy" */
          `@/components/${modulePath}`
        );

        clearTimeout(timeoutId);
        return module.default || module;

      } catch (error) {
        lastError = error as Error;
        
        if (attempt === maxRetries) break;

        await this.wait(Math.pow(2, attempt) * 800);
      }
    }

    throw lastError;
  }

  private addToCache(id: string, instance: any): void {
    const sizeEstimate = JSON.stringify(instance).length;
    
    if (this.currentSize + sizeEstimate > this.maxSize) {
      this.evictOldest();
    }

    this.cache.set(id, {
      instance,
      timestamp: Date.now()
    });
    this.currentSize += sizeEstimate;
  }

  private evictOldest(): void {
    const entries = Array.from(this.cache.entries());
    const sorted = entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
    const toRemove = sorted.slice(0, Math.floor(sorted.length * 0.15));

    toRemove.forEach(([id]) => {
      const item = this.cache.get(id)!;
      this.currentSize -= JSON.stringify(item.instance).length;
      this.cache.delete(id);
    });
  }

  preload(id: string, config: LoadConfig): void {
    if (this.cache.has(id) || this.pendingRequests.has(id)) return;

    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        this.load(id, config).catch(() => {});
      });
    } else {
      setTimeout(() => {
        this.load(id, config).catch(() => {});
      }, 2000);
    }
  }

  observeElement(element: HTMLElement, config: LoadConfig): void {
    this.observer.observe(element);
    this.preloadQueue.add(config.id);
  }

  private handleVisibility(entries: IntersectionObserverEntry[]): void {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const target = entry.target as HTMLElement;
        this.observer.unobserve(target);
        
        // 触发预加载逻辑
        this.preloadQueue.forEach(id => {
          // 实际实现需关联元素与组件映射
        });
      }
    });
  }

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

  clearCache(): void {
    this.cache.clear();
    this.pendingRequests.clear();
    this.preloadQueue.clear();
    this.currentSize = 0;
  }
}

export const loader = new DynamicLoader();

框架集成方案

2.1 Vue 异步组件封装

export function createLazyComponent(config: LoadConfig) {
  const { id, modulePath, fallbackComponent = 'LoadingSpinner', errorComponent = 'ErrorFallback' } = config;

  return defineAsyncComponent({
    loader: () => loader.load(id, config),
    loadingComponent: defineComponent({
      setup() {
        return () => h(resolveComponent(fallbackComponent));
      }
    }),
    errorComponent: defineComponent({
      props: { error: Object },
      setup(props) {
        const retry = () => {
          // 触发重试逻辑
        };
        return () => h(resolveComponent(errorComponent), { error: props.error, onRetry: retry });
      }
    }),
    delay: 300,
    timeout: 30000,
    onError: (err, retry, fail, attempts) => {
      if (err.message.includes('timeout') && attempts < 3) {
        retry();
      } else {
        fail();
      }
    }
  });
}

2.2 React 预加载工具

export function usePreload(id: string, config: LoadConfig) {
  const [isLoaded, setIsLoaded] = useState(false);

  const triggerPreload = useCallback(() => {
    if (isLoaded) return;
    loader.load(id, config).then(() => setIsLoaded(true)).catch(() => {});
  }, [id, isLoaded]);

  useEffect(() => {
    if (!config.shouldPreload) return;

    const idleCallback = () => {
      if ('requestIdleCallback' in window) {
        requestIdleCallback(triggerPreload);
      } else {
        setTimeout(triggerPreload, 3000);
      }
    };

    idleCallback();
  }, [config, triggerPreload]);

  return { triggerPreload, isLoaded };
}

export function useVisibilityPreload(ref: React.RefObject<HTMLElement>, id: string, config: LoadConfig) {
  useEffect(() => {
    if (!ref.current) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          loader.preload(id, config);
          observer.disconnect();
        }
      },
      { rootMargin: '200px', threshold: 0.1 }
    );

    observer.observe(ref.current);

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

实际应用示例

3.1 Vue 项目集成

<template>
  <div class="container">
    <button @click="preloadDashboard">预加载仪表盘</button>
    <Suspense :timeout="5000">
      <component :is="currentView" />
      <template #fallback>
        <LoadingSpinner />
      </template>
    </Suspense>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { createLazyComponent } from '@/utils/loader';

const currentView = ref(null);

const Dashboard = createLazyComponent({
  id: 'dashboard',
  modulePath: 'views/Dashboard.vue',
  shouldPreload: true
});

const Profile = createLazyComponent({
  id: 'profile',
  modulePath: 'views/Profile.vue',
  shouldPrefetch: true
});

const preloadDashboard = () => {
  loader.preload('dashboard', {
    id: 'dashboard',
    modulePath: 'views/Dashboard.vue',
    shouldPreload: true
  });
};

onMounted(() => {
  // 自动化预加载策略
  document.addEventListener('mousemove', () => {
    loader.preload('profile', {
      id: 'profile',
      modulePath: 'views/Profile.vue'
    });
  });
});
</script>

3.2 React 项目集成

import React, { useRef } from 'react';
import { usePreload, useVisibilityPreload } from '@/utils/preload';

const AsyncDashboard = React.lazy(() =>
  loader.load('dashboard', {
    id: 'dashboard',
    modulePath: 'views/Dashboard.tsx',
    shouldPreload: true
  }).then(m => ({ default: m }))
);

function App() {
  const dashboardRef = useRef<HTMLDivElement>(null);

  useVisibilityPreload(dashboardRef, 'dashboard', {
    id: 'dashboard',
    modulePath: 'views/Dashboard.tsx',
    shouldPreload: true
  });

  usePreload('profile', {
    id: 'profile',
    modulePath: 'views/Profile.tsx',
    shouldPreload: false
  });

  return (
    <div className="app">
      <button onClick={() => loader.preload('profile', { id: 'profile', modulePath: 'views/Profile.tsx' })}>
        预加载资料页
      </button>
      <div ref={dashboardRef}>
        <React.Suspense fallback={<div>加载中...</div>}>
          <AsyncDashboard />
        </React.Suspense>
      </div>
    </div>
  );
}

export default App;

性能监控体系

class PerformanceTracker {
  private metrics = new Map<string, Array<{ duration: number; timestamp: number }>>();

  constructor() {
    const observer = new PerformanceObserver(list => {
      list.getEntries().forEach(entry => {
        if (entry.entryType === 'resource') {
          this.recordResource(entry.name, entry.duration);
        }
      });
    });

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

  recordResource(url: string, duration: number): void {
    const key = url.split('/').pop() || url;
    const metric = { duration, timestamp: Date.now() };

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

    this.metrics.get(key)!.push(metric);
  }

  getReport(componentName: string): {
    avgTime: number;
    p95: number;
    totalAttempts: number;
    failureCount: number;
  } {
    const data = this.metrics.get(componentName) || [];
    const durations = data.map(d => d.duration);
    
    const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
    const sorted = [...durations].sort((a, b) => a - b);
    const p95Index = Math.ceil(0.95 * sorted.length) - 1;
    const p95 = sorted[p95Index];

    return {
      avgTime: avg,
      p95: p95,
      totalAttempts: data.length,
      failureCount: data.filter(d => d.duration > 5000).length
    };
  }
}

export const tracker = new PerformanceTracker();

该架构通过以下机制提升系统表现:

  • 按需分发:仅在需要时触发模块加载
  • 预测性预取:结合用户交互和视口监测提前准备资源
  • 自适应缓存:基于访问频率自动淘汰低频组件
  • 容错恢复:具备指数退避的重试逻辑
  • 运行时监控:实时采集加载指标用于持续优化
  • 多框架兼容:支持 Vue 3 与 React 18 无缝集成

整体方案显著降低首屏加载延迟,改善长尾性能体验。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。