vite 预加载插件

vite-plugin-simplest-sorted-preload.ts

import fs from 'fs';
import path from 'path';
import { globSync } from 'glob';

// 资源优先级排序数组(越靠前优先级越高)
const PRIORITY_ORDER = [
  'html',   // 页面文件
  'css',    // 样式文件
  'js',     // 脚本文件
  'svg',    // 矢量图
  'png',    // 位图
  'jpg',
  'jpeg',
  'webp',
  'gif',    // 动图
  'json',   // 数据文件
  'woff',   // 字体文件
  'woff2',
  'ttf',
  'eot'
];

export default function preloadInjectorPlugin(options: any = {}) {
  const { 
    outputDir = 'dist',
    manifestName = 'preload-manifest.json',
    globPattern = '**/*.+(html|css|js)',
    ignore = ['index.html'],
    // 预加载脚本注入位置的选择器
    injectSelector = '</head>'
  } = options;

  const manifestPath = path.resolve(outputDir, manifestName);

  return {
    name: 'preload-injector',

    closeBundle() {
      try {
        if (!fs.existsSync(outputDir)) {
          console.warn(`目标目录不存在: ${outputDir}`);
          return;
        }

        // 扫描文件并生成清单
        const files = globSync(globPattern, {
          cwd: outputDir,
          nodir: true,
          ignore,
        });

        // 处理文件路径和排序
        const sortedFiles = files
          .map(file => {
            const normalizedPath = file.replace(/\\/g, '/');
            const ext = path.extname(normalizedPath).slice(1).toLowerCase() || 'unknown';
            const priorityIndex = PRIORITY_ORDER.includes(ext) 
              ? PRIORITY_ORDER.indexOf(ext) 
              : PRIORITY_ORDER.length;

            return {
              path: normalizedPath,
              priorityIndex
            };
          })
          .sort((a, b) => {
            if (a.priorityIndex !== b.priorityIndex) {
              return a.priorityIndex - b.priorityIndex;
            }
            return a.path.localeCompare(b.path);
          });

        // 写入清单文件
        const manifest = {
          generatedAt: new Date().toISOString(),
          totalFiles: sortedFiles.length,
          files: sortedFiles.map(item => item.path)
        };

        fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
        console.log(`已生成预加载清单: ${manifestPath}`);
      } catch (error) {
        console.error('生成预加载清单失败:', error);
      }
    },

    // 注入预加载脚本到index.html
    transformIndexHtml(html: string) {
      if (process.env.NODE_ENV === 'development') return html
      // 生成预加载脚本
      const preloadScript = `
        <script>
          // 闲时预加载逻辑
          (function() {
            // 检查浏览器是否支持requestIdleCallback
            const useIdleCallback = typeof requestIdleCallback !== 'undefined';

            // 加载预加载清单并处理
            function loadPreloadManifest() {
              fetch('${manifestName}')
                .then(response => {
                  if (!response.ok) throw new Error('清单加载失败');
                  return response.json();
                })
                .then(manifest => {
                  // 处理预加载队列
                  handlePreloadQueue(manifest.files);
                })
                .catch(error => {
                  console.error('预加载清单处理失败:', error);
                });
            }

            // 处理预加载队列
            function handlePreloadQueue(files) {
              if (!files || files.length === 0) return;

              // 创建预加载元素的函数
              function createPreloadElement(filePath) {
                const ext = filePath.split('.').pop().toLowerCase();
                const link = document.createElement('link');
                link.rel = 'preload';
                link.crossOrigin = 'anonymous';
                link.href = filePath;

                // 根据文件类型设置适当的as属性
                if (ext === 'css') {
                  link.as = 'style';
                } else if (ext === 'js') {
                  link.as = 'script';
                } else if (['png', 'jpg', 'jpeg', 'webp', 'svg', 'gif'].includes(ext)) {
                  link.as = 'image';
                } else if (['woff', 'woff2', 'ttf', 'eot'].includes(ext)) {
                  link.as = 'font';
                  link.crossOrigin = 'anonymous';
                }

                return link;
              }

              // 预加载队列处理器
              let index = 0;
              function processNext() {
                if (index >= files.length) return;

                const file = files[index];
                const link = createPreloadElement(file);

                // 监听加载完成事件,继续处理下一个
                link.onload = link.onerror = () => {
                  index++;
                  // 继续处理下一个资源,使用setTimeout避免阻塞
                  setTimeout(processNext, 0);
                };

                document.head.appendChild(link);
              }

              // 使用闲时加载或延迟加载
              if (useIdleCallback) {
                requestIdleCallback(processNext, { timeout: 3000 });
              } else {
                // 不支持requestIdleCallback的浏览器使用延迟加载
                setTimeout(processNext, 1000);
              }
            }

            // 页面加载完成后再加载清单,避免影响首屏
            if (document.readyState === 'complete') {
              loadPreloadManifest();
            } else {
              window.addEventListener('load', loadPreloadManifest);
            }
          })();
        </script>
      `;

      // 将脚本注入到指定位置
      return html.replace(injectSelector, `${preloadScript}\n${injectSelector}`);
    }
  };
}

vite.config.ts

import simplestSortedPreloadManifestPlugin  from './src/plugins/vite-plugin-simplest-sorted-preload';

// https://vite.dev/config/
export default defineConfig({
  base: './',
  plugins: [
    vue(),
    // 使用方式
    simplestSortedPreloadManifestPlugin({
      outputDir: 'dist',
      manifestName: 'resource-manifest.json', // 生成相对于 outputDir 目录的清单文件名
    })
  ]
})
posted @ 2025-09-04 12:40  程序媛李李李李蕾  阅读(29)  评论(0)    收藏  举报