vite-plugin-trae-image-preloader
Version:
Vite plugin for preloading images in development mode
506 lines (460 loc) • 20.6 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
default: () => index_default,
imagePreloader: () => imagePreloader
});
module.exports = __toCommonJS(index_exports);
function imagePreloader(options = {}) {
const {
extensions = ["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"],
verbose = true,
framework = "auto",
devOnly = true,
delay = 1e3
} = options;
let isDevMode = false;
let detectedFramework = "vanilla";
const discoveredImages = /* @__PURE__ */ new Set();
const imageExtPattern = extensions.join("|");
const patterns = {
// import xxx from './image.png'
// import { xxx } from './image.png'
importStatement: new RegExp(`import\\s+(?:[^'"]+\\s+from\\s+)?['"]([^'"]+\\.(${imageExtPattern}))['"]`, "gi"),
// require('./image.png')
requireStatement: new RegExp(`require\\(['"]([^'"]+\\.(${imageExtPattern}))['"]\\)`, "gi"),
// url('./image.png') 或 url("./image.png")
urlStatement: new RegExp(`url\\(['"]([^'"]+\\.(${imageExtPattern}))['"]\\)`, "gi"),
// src="./image.png" 或 src='./image.png'
srcAttribute: new RegExp(`src=['"]([^'"]+\\.(${imageExtPattern}))['"]`, "gi"),
// background-image: url('./image.png')
cssUrl: new RegExp(`background-image\\s*:\\s*url\\(['"]([^'"]+\\.(${imageExtPattern}))['"]\\)`, "gi"),
// HTTP/HTTPS 图片URL (支持查询参数)
// 匹配 https://domain.com/path/to/image 或带查询参数的URL
httpImageUrl: new RegExp(`['"](https?://[^'"\\s]+(?:\\?[^'"\\s]*)?(?:&[^'"\\s]*)*)['"]`, "gi"),
// 匹配包含 image 关键词的API URL
apiImageUrl: new RegExp(`['"]([^'"]*(?:image|img|photo|pic)[^'"]*(?:\\?[^'"]*)?(?:&[^'"]*)*)['"]`, "gi")
};
const extractImagesFromCode = (code, id) => {
const images = [];
let match;
while ((match = patterns.importStatement.exec(code)) !== null) {
images.push({
source: match[0],
url: match[1],
type: "import"
});
}
patterns.requireStatement.lastIndex = 0;
while ((match = patterns.requireStatement.exec(code)) !== null) {
images.push({
source: match[0],
url: match[1],
type: "require"
});
}
patterns.urlStatement.lastIndex = 0;
while ((match = patterns.urlStatement.exec(code)) !== null) {
images.push({
source: match[0],
url: match[1],
type: "url"
});
}
patterns.srcAttribute.lastIndex = 0;
while ((match = patterns.srcAttribute.exec(code)) !== null) {
images.push({
source: match[0],
url: match[1],
type: "url"
});
}
patterns.cssUrl.lastIndex = 0;
while ((match = patterns.cssUrl.exec(code)) !== null) {
images.push({
source: match[0],
url: match[1],
type: "url"
});
}
patterns.httpImageUrl.lastIndex = 0;
while ((match = patterns.httpImageUrl.exec(code)) !== null) {
const url = match[1];
if (url.includes("image") || url.includes("img") || url.includes("photo") || url.includes("pic") || url.includes("avatar") || url.includes("thumb") || url.includes("banner") || // 检查是否包含图片文件扩展名
new RegExp(`\\.(${imageExtPattern})(?:\\?|$)`, "i").test(url)) {
images.push({
source: match[0],
url,
type: "url"
});
}
}
patterns.apiImageUrl.lastIndex = 0;
while ((match = patterns.apiImageUrl.exec(code)) !== null) {
const url = match[1];
if (url.startsWith("http://") || url.startsWith("https://")) {
images.push({
source: match[0],
url,
type: "url"
});
}
}
return images;
};
const generatePreloaderScript = () => {
const imageUrls = JSON.stringify(Array.from(discoveredImages));
return `
<script>
(function() {
const imageUrls = ${imageUrls};
const preloadedImages = new Set();
const batchSize = 5; // \u6BCF\u6279\u5904\u74065\u5F20\u56FE\u7247
const batchInterval = 3000; // \u6BCF\u6279\u95F4\u96943\u79D2
// \u9884\u52A0\u8F7D\u56FE\u7247\u5E76\u5904\u7406 301 \u91CD\u5B9A\u5411
function preloadImage(url, retryCount = 0) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
// \u68C0\u67E5\u662F\u5426\u88AB\u91CD\u5B9A\u5411\u5230\u9ED8\u8BA4\u56FE\u7247
// 301\u91CD\u5B9A\u5411\u540E\uFF0Cimg.src\u4F1A\u662F\u91CD\u5B9A\u5411\u540E\u7684URL
const isDefaultImage = img.src.includes('page_image/default.jpeg');
if (${verbose}) {
if (isDefaultImage) {
console.log(\`\u{1F504} [Image Preloader] \u68C0\u6D4B\u5230\u9ED8\u8BA4\u56FE\u7247\uFF0C\u5F00\u59CB\u540E\u53F0\u8F6E\u8BE2: \${url}\`);
console.log(\` \u91CD\u5B9A\u5411\u5230: \${img.src}\`);
} else {
console.log(\`\u2705 [Image Preloader] \u6210\u529F\u52A0\u8F7D\u771F\u5B9E\u56FE\u7247: \${url}\`);
}
}
// \u65E0\u8BBA\u662F\u5426\u4E3A\u9ED8\u8BA4\u56FE\u7247\uFF0C\u90FD\u6807\u8BB0\u4E3A\u5DF2\u52A0\u8F7D
preloadedImages.add(url);
// \u5982\u679C\u662F\u9ED8\u8BA4\u56FE\u7247\uFF0C\u542F\u52A8\u540E\u53F0\u8F6E\u8BE2\u4EFB\u52A1
if (isDefaultImage && retryCount < 30) {
// \u5148\u8BA9\u9ED8\u8BA4\u56FE\u7247\u663E\u793A\uFF0C\u7136\u540E\u5F00\u59CB\u8F6E\u8BE2
resolve();
startBackgroundPolling(url, img.src, retryCount);
} else {
resolve();
}
};
img.onerror = () => {
if (${verbose}) {
console.warn(\`\u274C [Image Preloader] \u56FE\u7247\u52A0\u8F7D\u5931\u8D25: \${url}\`);
}
resolve();
};
// \u6DFB\u52A0\u65F6\u95F4\u6233\u907F\u514D\u7F13\u5B58
const timestamp = Date.now();
const separator = url.includes('?') ? '&' : '?';
img.src = \`\${url}\${separator}_t=\${timestamp}\`;
});
}
// \u540E\u53F0\u8F6E\u8BE2\u51FD\u6570
function startBackgroundPolling(originalUrl, defaultImageUrl, retryCount = 0) {
if (retryCount >= 30) {
if (${verbose}) {
console.log(\`\u23F0 [Image Preloader] \u8FBE\u5230\u6700\u5927\u91CD\u8BD5\u6B21\u6570\uFF0C\u505C\u6B62\u8F6E\u8BE2: \${originalUrl}\`);
}
return;
}
setTimeout(() => {
if (${verbose}) {
console.log(\`\u{1F504} [Image Preloader] \u540E\u53F0\u8F6E\u8BE2\u4E2D... (\${retryCount + 1}/30): \${originalUrl}\`);
}
const img = new Image();
img.onload = () => {
const isStillDefaultImage = img.src.includes('page_image/default.jpeg');
if (!isStillDefaultImage) {
// \u83B7\u53D6\u5230\u771F\u5B9E\u56FE\u7247\u4E86\uFF0C\u66F4\u65B0\u9875\u9762\u4E2D\u6240\u6709\u4F7F\u7528\u8BE5URL\u7684\u56FE\u7247
updatePageImages(originalUrl, img.src);
if (${verbose}) {
console.log(\`\u{1F389} [Image Preloader] \u540E\u53F0\u8F6E\u8BE2\u6210\u529F\uFF0C\u5DF2\u66F4\u65B0\u9875\u9762\u56FE\u7247: \${originalUrl}\`);
console.log(\` \u65B0\u56FE\u7247URL: \${img.src}\`);
}
} else {
// \u8FD8\u662F\u9ED8\u8BA4\u56FE\u7247\uFF0C\u7EE7\u7EED\u8F6E\u8BE2
startBackgroundPolling(originalUrl, defaultImageUrl, retryCount + 1);
}
};
img.onerror = () => {
// \u8BF7\u6C42\u5931\u8D25\uFF0C\u7EE7\u7EED\u8F6E\u8BE2
startBackgroundPolling(originalUrl, defaultImageUrl, retryCount + 1);
};
// \u6DFB\u52A0\u65F6\u95F4\u6233\u907F\u514D\u7F13\u5B58
const timestamp = Date.now();
const separator = originalUrl.includes('?') ? '&' : '?';
img.src = \`\${originalUrl}\${separator}_t=\${timestamp}\`;
}, 10000); // 10\u79D2\u540E\u91CD\u8BD5
}
// \u66F4\u65B0\u9875\u9762\u4E2D\u6240\u6709\u4F7F\u7528\u6307\u5B9AURL\u7684\u56FE\u7247\u5143\u7D20
function updatePageImages(originalUrl, newImageUrl) {
if (${verbose}) {
console.log(\`\u{1F504} [Image Preloader] \u5F00\u59CB\u66F4\u65B0\u9875\u9762\u56FE\u7247: \${originalUrl} -> \${newImageUrl}\`);
}
let updateCount = 0;
// \u66F4\u65B0\u6240\u6709img\u6807\u7B7E
const images = document.querySelectorAll('img');
images.forEach(img => {
// \u68C0\u67E5\u591A\u79CD\u53EF\u80FD\u7684\u5339\u914D\u65B9\u5F0F
const imgSrc = img.src;
const imgSrcWithoutTimestamp = imgSrc.split('_t=')[0].split('?_t=')[0];
const originalUrlWithoutTimestamp = originalUrl.split('_t=')[0].split('?_t=')[0];
// \u68C0\u67E5\u662F\u5426\u5339\u914D\uFF08\u5305\u62EC301\u91CD\u5B9A\u5411\u540E\u7684URL\uFF09
const isMatch =
imgSrcWithoutTimestamp === originalUrlWithoutTimestamp ||
img.getAttribute('data-original-src') === originalUrl ||
img.getAttribute('src') === originalUrl ||
// \u68C0\u67E5\u662F\u5426\u662F\u9ED8\u8BA4\u56FE\u7247\u9700\u8981\u66F4\u65B0
(imgSrc.includes('page_image/default.jpeg') && img.hasAttribute('data-api-url') && img.getAttribute('data-api-url') === originalUrl);
if (isMatch) {
if (${verbose}) {
console.log(\`\u{1F504} [Image Preloader] \u66F4\u65B0\u9875\u9762\u56FE\u7247\u5143\u7D20: \${imgSrc} -> \${newImageUrl}\`);
}
// \u4FDD\u5B58\u539F\u59CBURL\u5230data\u5C5E\u6027\uFF0C\u7528\u4E8E\u4E0B\u6B21\u5339\u914D
img.setAttribute('data-original-src', originalUrl);
img.setAttribute('data-api-url', originalUrl);
img.src = newImageUrl;
updateCount++;
}
});
// \u66F4\u65B0\u6240\u6709\u80CC\u666F\u56FE\u7247
const allElements = document.querySelectorAll('*');
allElements.forEach(el => {
const computedStyle = window.getComputedStyle(el);
const backgroundImage = computedStyle.backgroundImage;
if (backgroundImage && backgroundImage !== 'none') {
// \u63D0\u53D6URL
const urlMatch = backgroundImage.match(/url\\(['"]?([^'"\\)]+)['"]?\\)/);
if (urlMatch) {
const bgUrl = urlMatch[1];
const bgUrlWithoutTimestamp = bgUrl.split('_t=')[0].split('?_t=')[0];
const originalUrlWithoutTimestamp = originalUrl.split('_t=')[0].split('?_t=')[0];
const isBgMatch =
bgUrlWithoutTimestamp === originalUrlWithoutTimestamp ||
(bgUrl.includes('page_image/default.jpeg') && el.hasAttribute('data-api-bg-url') && el.getAttribute('data-api-bg-url') === originalUrl);
if (isBgMatch) {
if (${verbose}) {
console.log(\`\u{1F504} [Image Preloader] \u66F4\u65B0\u80CC\u666F\u56FE\u7247: \${bgUrl} -> \${newImageUrl}\`);
}
el.setAttribute('data-api-bg-url', originalUrl);
el.style.backgroundImage = \`url('\${newImageUrl}')\`;
updateCount++;
}
}
}
});
if (${verbose}) {
console.log(\`\u2705 [Image Preloader] \u56FE\u7247\u66F4\u65B0\u5B8C\u6210\uFF0C\u5171\u66F4\u65B0\u4E86 \${updateCount} \u4E2A\u5143\u7D20\`);
}
}
// \u5206\u6279\u9884\u52A0\u8F7D\u56FE\u7247
function preloadImagesBatch() {
// \u5C06\u56FE\u7247URL\u5206\u6210\u6279\u6B21
const batches = [];
for (let i = 0; i < imageUrls.length; i += batchSize) {
batches.push(imageUrls.slice(i, i + batchSize));
}
if (${verbose}) {
console.log('%c\u{1F5BC}\uFE0F Image Preloader:', 'color: #10b981; font-weight: bold;',
\`Starting batch preload: \${imageUrls.length} images in \${batches.length} batches\`);
}
// \u9010\u6279\u5904\u7406
batches.forEach((batch, batchIndex) => {
setTimeout(() => {
if (${verbose}) {
console.log('%c\u{1F5BC}\uFE0F Image Preloader:', 'color: #10b981; font-weight: bold;',
\`Processing batch \${batchIndex + 1}/\${batches.length} (\${batch.length} images)\`);
}
const promises = batch.map(url => preloadImage(url).catch(e => null));
Promise.allSettled(promises).then(results => {
const loaded = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
if (${verbose}) {
console.log('%c\u{1F5BC}\uFE0F Image Preloader:', 'color: #10b981; font-weight: bold;',
\`Batch \${batchIndex + 1} completed: \${loaded} loaded\` + (failed > 0 ? \`, \${failed} failed\` : ''));
// \u5982\u679C\u662F\u6700\u540E\u4E00\u6279\uFF0C\u663E\u793A\u603B\u7ED3
if (batchIndex === batches.length - 1) {
const totalLoaded = preloadedImages.size;
const totalFailed = imageUrls.length - totalLoaded;
console.log('%c\u{1F5BC}\uFE0F Image Preloader:', 'color: #10b981; font-weight: bold;',
\`All batches completed: \${totalLoaded} total loaded\` + (totalFailed > 0 ? \`, \${totalFailed} failed\` : ''));
}
}
});
}, batchIndex * batchInterval);
});
}
// \u517C\u5BB9\u65E7\u7684API
function preloadAllImages() {
preloadImagesBatch();
}
// \u76D1\u63A7\u9875\u9762\u4E0A\u7684\u56FE\u7247\uFF0C\u81EA\u52A8\u5904\u7406\u9ED8\u8BA4\u56FE\u7247
function monitorPageImages() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
// \u68C0\u67E5\u65B0\u6DFB\u52A0\u7684img\u5143\u7D20
if (element.tagName === 'IMG') {
checkAndMarkApiImage(element);
}
// \u68C0\u67E5\u5B50\u5143\u7D20\u4E2D\u7684img
const imgs = element.querySelectorAll ? element.querySelectorAll('img') : [];
imgs.forEach(checkAndMarkApiImage);
}
});
});
});
// \u5F00\u59CB\u89C2\u5BDF
observer.observe(document.body, {
childList: true,
subtree: true
});
// \u68C0\u67E5\u5DF2\u5B58\u5728\u7684\u56FE\u7247
document.querySelectorAll('img').forEach(checkAndMarkApiImage);
}
function checkAndMarkApiImage(img) {
const src = img.src;
if (!src) return;
// \u68C0\u67E5\u662F\u5426\u662F\u6211\u4EEC\u5173\u5FC3\u7684API URL
const isApiUrl = imageUrls.some(url => {
const cleanUrl = url.split('_t=')[0].split('?_t=')[0];
const cleanSrc = src.split('_t=')[0].split('?_t=')[0];
return cleanUrl === cleanSrc;
});
if (isApiUrl) {
// \u627E\u5230\u5BF9\u5E94\u7684\u539F\u59CBURL
const originalUrl = imageUrls.find(url => {
const cleanUrl = url.split('_t=')[0].split('?_t=')[0];
const cleanSrc = src.split('_t=')[0].split('?_t=')[0];
return cleanUrl === cleanSrc;
});
if (originalUrl) {
img.setAttribute('data-api-url', originalUrl);
if (${verbose}) {
console.log(\`\u{1F3F7}\uFE0F [Image Preloader] \u6807\u8BB0API\u56FE\u7247: \${src} (\u539F\u59CB: \${originalUrl})\`);
}
// \u5982\u679C\u68C0\u6D4B\u5230\u9ED8\u8BA4\u56FE\u7247\uFF0C\u542F\u52A8\u8F6E\u8BE2
if (src.includes('page_image/default.jpeg')) {
if (${verbose}) {
console.log(\`\u{1F504} [Image Preloader] \u9875\u9762\u68C0\u6D4B\u5230\u9ED8\u8BA4\u56FE\u7247\uFF0C\u542F\u52A8\u540E\u53F0\u8F6E\u8BE2: \${originalUrl}\`);
}
startBackgroundPolling(originalUrl, src, 0);
}
}
}
}
// \u5EF6\u8FDF\u9884\u52A0\u8F7D\uFF0C\u907F\u514D\u963B\u585E\u521D\u59CB\u9875\u9762\u6E32\u67D3
setTimeout(preloadImagesBatch, ${delay});
// \u542F\u52A8\u9875\u9762\u56FE\u7247\u76D1\u63A7
setTimeout(monitorPageImages, 100);
// \u5BFC\u51FA\u5230\u5168\u5C40\uFF0C\u65B9\u4FBF\u8C03\u8BD5
window.__imagePreloader = {
preloadAllImages,
preloadImagesBatch,
startImagePolling: startBackgroundPolling, // \u66F4\u65B0\u4E3A\u540E\u53F0\u8F6E\u8BE2
getPreloadedImages: () => Array.from(preloadedImages),
imageUrls,
batchSize,
batchInterval,
pollInterval: 10000,
maxAttempts: 30,
updatePageImages // \u65B0\u589E\uFF1A\u624B\u52A8\u66F4\u65B0\u56FE\u7247\u7684\u65B9\u6CD5
};
})();
</script>`;
};
return {
name: "vite-plugin-image-preloader",
configResolved(config) {
isDevMode = config.command === "serve";
if (framework === "auto") {
try {
const packageJsonPath = config.root + "/package.json";
const fs = require("fs");
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
if (allDeps.react) {
detectedFramework = "react";
} else if (allDeps.vue) {
detectedFramework = "vue";
}
}
} catch {
}
} else {
detectedFramework = framework;
}
if (verbose && isDevMode) {
console.log("\u{1F5BC}\uFE0F Image Preloader Plugin enabled for", detectedFramework);
}
},
transform(code, id) {
if (devOnly && !isDevMode) return null;
const shouldProcess = id.endsWith(".tsx") || id.endsWith(".ts") || id.endsWith(".jsx") || id.endsWith(".js") || id.endsWith(".vue") || id.endsWith(".css") || id.endsWith(".scss") || id.endsWith(".sass") || id.endsWith(".less");
if (!shouldProcess) return null;
const images = extractImagesFromCode(code, id);
if (images.length > 0) {
images.forEach((img) => {
let resolvedUrl = img.url;
if (img.url.startsWith("./") || img.url.startsWith("../")) {
resolvedUrl = img.url;
} else if (!img.url.startsWith("/") && !img.url.startsWith("http")) {
resolvedUrl = "/" + img.url;
}
discoveredImages.add(resolvedUrl);
});
if (verbose) {
console.log(`\u{1F5BC}\uFE0F Found ${images.length} image(s) in ${id.split("/").pop()}`);
images.forEach((img) => {
console.log(` ${img.type}: ${img.url}`);
});
}
}
return null;
},
transformIndexHtml: {
order: "post",
handler(html) {
if (devOnly && !isDevMode) return html;
if (discoveredImages.size === 0) return html;
const preloaderScript = generatePreloaderScript();
const headIndex = html.indexOf("</head>");
if (headIndex !== -1) {
return html.slice(0, headIndex) + preloaderScript + html.slice(headIndex);
}
return html;
}
}
};
}
var index_default = imagePreloader;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
imagePreloader
});
//# sourceMappingURL=index.js.map