前端工程化资源预加载
资源预加载
1、通过函数实现
/*** 并发预加载文件(支持图片、字体、脚本、样式等)* @param {string[]} files - 文件 URL 数组* @param {number} maxConcurrent - 最大并发数,默认 3* @returns {Promise<{ loaded: string[], failed: { url: string, error: string }[] }>}*/
export async function preloadFiles(files, maxConcurrent = 3) {// 输入验证if (!files || !Array.isArray(files) || files.length === 0) {throw new Error('必须提供有效的文件数组');}if (typeof maxConcurrent !== 'number' || maxConcurrent < 1) {throw new Error('maxConcurrent 必须是大于 0 的数字');}// 去重 + 过滤无效 URLconst uniqueFiles = [...new Set(files)].filter(file => {try {new URL(file);return true;} catch {console.warn(`无效的 URL: ${file}`);return false;}});if (uniqueFiles.length === 0) {return { loaded: [], failed: [] };}const results = {loaded: [],failed: []};// 使用索引代替 shift(),避免 O(n) 操作let currentIndex = 0;// 根据扩展名确定 as 类型const getTypeFromExtension = (url) => {const extension = url.split('.').pop().toLowerCase().split(/\#|\?/)[0]; // 忽略 query/hashconst typeMap = {'jpg': 'image', 'jpeg': 'image', 'png': 'image', 'gif': 'image', 'webp': 'image', 'avif': 'image','woff': 'font', 'woff2': 'font', 'ttf': 'font', 'eot': 'font', 'otf': 'font','js': 'script', 'mjs': 'script','css': 'style','json': 'json','xml': 'xml','svg': 'image','ico': 'image'};return typeMap[extension] || 'fetch';};// 预加载单个文件,返回 Promiseconst preloadFile = (url) => {return new Promise((resolve, reject) => {const link = document.createElement('link');link.rel = 'preload';link.as = getTypeFromExtension(url);link.href = url;// 加载成功link.onload = () => {results.loaded.push(url);resolve();};// 加载失败link.onerror = () => {const errorMsg = `Failed to preload: ${url}`;results.failed.push({ url, error: errorMsg });console.warn(errorMsg);reject(new Error(errorMsg));};document.head.appendChild(link);});};// 工作器函数:使用索引安全递增,不依赖 shift()const worker = async () => {while (currentIndex < uniqueFiles.length) {const url = uniqueFiles[currentIndex];currentIndex++; // 先取后加,确保每个 URL 只被一个 worker 处理await preloadFile(url); // 等待完成(串行发起,但多个 worker 并发执行)}};// 创建并发工作器(最多 maxConcurrent 个)const workers = Array(Math.min(maxConcurrent, uniqueFiles.length)).fill(null).map(() => worker());// 等待所有工作器完成await Promise.allSettled(workers);return results;
}
2、通过插件实现
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { preloadFiles } from './plugins/preloadFiles'export default defineConfig({plugins: [vue(),preloadFiles({dir: 'prefetch/*.{js,css,png,jpg,jpeg,gif,webp,woff,woff2}',rel: 'prefetch' // 或 preload})],resolve: {alias: {'@': path.resolve(__dirname, 'src')}}
})
plugins/preloadFiles.ts
import type { Plugin } from 'vite'
import fg from 'fast-glob'interface PreloadFileOption {dir: stringrel: 'prefetch' | 'preload'
}// 根据扩展名推断 as 类型
function getTypeFromExtension(url: string): string {const ext = url.split('.').pop()?.toLowerCase().split(/[?#]/)[0]const map: Record<string, string> = {// imagesjpg: 'image', jpeg: 'image', png: 'image', gif: 'image',webp: 'image', avif: 'image', svg: 'image', ico: 'image',// fontswoff: 'font', woff2: 'font', ttf: 'font', eot: 'font', otf: 'font',// scriptsjs: 'script', mjs: 'script',// stylescss: 'style',// datajson: 'json', xml: 'xml'}return map[ext!] || 'fetch'
}export const preloadFiles = (option: PreloadFileOption): Plugin => {const { dir, rel = 'prefetch' } = optionreturn {name: 'vite-plugin-file-preload',transformIndexHtml(html, ctx) {const publicDir = ctx.server?.config.publicDir || 'public'const base = ctx.server?.config?.base || '/'// 使用 fast-glob 匹配文件const matchedFiles = fg.sync(dir, {cwd: publicDir,absolute: false,onlyFiles: true})// 构造 HTML 注入标签return matchedFiles.map(file => {const href = base + fileconst tagConfig = {tag: 'link',attrs: {rel,href} as Record<string, string>}// 只有 preload 才需要 as 属性if (rel === 'preload') {tagConfig.attrs.as = getTypeFromExtension(href)}return tagConfig})}}
}