mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
- 新增统一的中间件模块,提供鉴权、CORS和错误处理功能 - 重构所有Functions使用withAuth中间件简化代码结构 - 添加安全存储模块替代localStorage,防御XSS攻击 - 引入HTML清理工具,自动转义特殊字符和验证URL安全性 - 创建代码质量检查脚本,验证语法、环境变量和依赖完整性 - 添加构建日志工具和重构脚本,统一替换console.log为Logger - 引入ESLint配置,提升代码质量和一致性 - 重构Giffgaff相关API,统一错误处理和验证逻辑 - 优化构建脚本,添加压缩和图片优化功能 - 新增健康检查端点,用于服务监控和状态报告
369 lines
11 KiB
JavaScript
369 lines
11 KiB
JavaScript
const sharp = require('sharp');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const BuildLogger = require('./logger.js');
|
|
|
|
const { promisify } = require('util');
|
|
|
|
const readdir = promisify(fs.readdir);
|
|
const stat = promisify(fs.stat);
|
|
|
|
// 配置
|
|
const config = {
|
|
inputDir: path.join(__dirname, '../src/assets/images'),
|
|
outputDir: path.join(__dirname, '../dist/images'),
|
|
maxConcurrent: 4, // Parallel processing limit
|
|
minFileSize: 1024, // Skip files smaller than 1KB
|
|
formats: {
|
|
jpeg: { quality: 80, progressive: true },
|
|
png: { compressionLevel: 9, progressive: true },
|
|
webp: { quality: 80, effort: 6 }
|
|
},
|
|
sizes: {
|
|
thumbnail: { width: 150, height: 150 },
|
|
medium: { width: 800, height: 600 },
|
|
large: { width: 1200, height: 900 }
|
|
}
|
|
};
|
|
|
|
// 确保输出目录存在
|
|
function ensureOutputDir() {
|
|
if (!fs.existsSync(config.outputDir)) {
|
|
fs.mkdirSync(config.outputDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// 获取支持的图片格式
|
|
function isImageFile(filename) {
|
|
const ext = path.extname(filename).toLowerCase();
|
|
return ['.jpg', '.jpeg', '.png', '.webp', '.gif'].includes(ext);
|
|
}
|
|
|
|
// 优化单个图片
|
|
async function optimizeImage(inputPath, outputPath, format, options = {}) {
|
|
try {
|
|
// Check if output already exists and is newer than input
|
|
if (fs.existsSync(outputPath)) {
|
|
const inputStats = await stat(inputPath);
|
|
const outputStats = await stat(outputPath);
|
|
if (outputStats.mtime > inputStats.mtime) {
|
|
BuildLogger.log(`⏭️ 跳过已优化: ${path.basename(outputPath)}`);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const image = sharp(inputPath);
|
|
|
|
// Get image metadata for size validation
|
|
const metadata = await image.metadata();
|
|
|
|
// Apply format-specific optimizations
|
|
switch (format) {
|
|
case 'jpeg':
|
|
await image
|
|
.jpeg({
|
|
quality: options.quality || config.formats.jpeg.quality,
|
|
progressive: true,
|
|
mozjpeg: true // Use mozjpeg for better compression
|
|
})
|
|
.toFile(outputPath);
|
|
break;
|
|
case 'png':
|
|
await image
|
|
.png({
|
|
compressionLevel: 9,
|
|
progressive: true,
|
|
adaptiveFiltering: true
|
|
})
|
|
.toFile(outputPath);
|
|
break;
|
|
case 'webp':
|
|
await image
|
|
.webp({
|
|
quality: options.quality || config.formats.webp.quality,
|
|
effort: 6,
|
|
smartSubsample: true
|
|
})
|
|
.toFile(outputPath);
|
|
break;
|
|
default:
|
|
await image.toFile(outputPath);
|
|
}
|
|
|
|
// Calculate compression savings
|
|
const inputSize = (await stat(inputPath)).size;
|
|
const outputSize = (await stat(outputPath)).size;
|
|
const savings = ((inputSize - outputSize) / inputSize * 100).toFixed(1);
|
|
|
|
BuildLogger.success(' 优化完成: ${path.basename(inputPath)} -> ${format.toUpperCase()} (节省 ${savings}%)');
|
|
return { success: true, inputSize, outputSize, savings };
|
|
} catch (error) {
|
|
console.error(`❌ 优化失败: ${path.basename(inputPath)}`, error.message);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
// 生成多种格式 (with parallel processing)
|
|
async function generateMultipleFormats(inputPath, filename) {
|
|
const baseName = path.parse(filename).name;
|
|
|
|
// Check file size threshold
|
|
const fileStats = await stat(inputPath);
|
|
if (fileStats.size < config.minFileSize) {
|
|
BuildLogger.log(`⏭️ 跳过小文件: ${filename} (${fileStats.size} bytes)`);
|
|
return [];
|
|
}
|
|
|
|
// Generate formats in parallel
|
|
const formatPromises = [];
|
|
|
|
// Generate JPEG version
|
|
const jpegPath = path.join(config.outputDir, `${baseName}.jpg`);
|
|
formatPromises.push(optimizeImage(inputPath, jpegPath, 'jpeg'));
|
|
|
|
// Generate WebP version
|
|
const webpPath = path.join(config.outputDir, `${baseName}.webp`);
|
|
formatPromises.push(optimizeImage(inputPath, webpPath, 'webp'));
|
|
|
|
// Generate PNG version (if original is PNG)
|
|
if (path.extname(filename).toLowerCase() === '.png') {
|
|
const pngPath = path.join(config.outputDir, `${baseName}.png`);
|
|
formatPromises.push(optimizeImage(inputPath, pngPath, 'png'));
|
|
}
|
|
|
|
const results = await Promise.all(formatPromises);
|
|
return results;
|
|
}
|
|
|
|
// 生成缩略图 (with better error recovery)
|
|
async function generateThumbnails(inputPath, filename) {
|
|
const baseName = path.parse(filename).name;
|
|
|
|
try {
|
|
const image = sharp(inputPath);
|
|
const metadata = await image.metadata();
|
|
|
|
// Skip if image is already smaller than thumbnail size
|
|
if (metadata.width <= config.sizes.thumbnail.width &&
|
|
metadata.height <= config.sizes.thumbnail.height) {
|
|
BuildLogger.log(`⏭️ 跳过缩略图生成: ${filename} (已足够小)`);
|
|
return { success: true, skipped: true };
|
|
}
|
|
|
|
// Generate thumbnail
|
|
const thumbnailPath = path.join(config.outputDir, `${baseName}-thumb.jpg`);
|
|
|
|
// Check if thumbnail already exists and is newer
|
|
if (fs.existsSync(thumbnailPath)) {
|
|
const inputStats = await stat(inputPath);
|
|
const thumbStats = await stat(thumbnailPath);
|
|
if (thumbStats.mtime > inputStats.mtime) {
|
|
BuildLogger.log(`⏭️ 跳过已存在的缩略图: ${path.basename(thumbnailPath)}`);
|
|
return { success: true, skipped: true };
|
|
}
|
|
}
|
|
|
|
await sharp(inputPath)
|
|
.resize(config.sizes.thumbnail.width, config.sizes.thumbnail.height, {
|
|
fit: 'cover',
|
|
position: 'center'
|
|
})
|
|
.jpeg({ quality: 80, progressive: true })
|
|
.toFile(thumbnailPath);
|
|
|
|
BuildLogger.success(' 缩略图生成: ${path.basename(thumbnailPath)}');
|
|
return { success: true, skipped: false };
|
|
} catch (error) {
|
|
console.error(`❌ 缩略图生成失败: ${filename}`, error.message);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
// 生成图片清单
|
|
function generateManifest() {
|
|
const manifest = {
|
|
generated: new Date().toISOString(),
|
|
images: []
|
|
};
|
|
|
|
if (fs.existsSync(config.outputDir)) {
|
|
const files = fs.readdirSync(config.outputDir);
|
|
files.forEach(file => {
|
|
if (isImageFile(file)) {
|
|
const stats = fs.statSync(path.join(config.outputDir, file));
|
|
manifest.images.push({
|
|
filename: file,
|
|
size: stats.size,
|
|
format: path.extname(file).toLowerCase().slice(1)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const manifestPath = path.join(config.outputDir, 'manifest.json');
|
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
BuildLogger.log(`📋 图片清单已生成: ${manifestPath}`);
|
|
}
|
|
|
|
// Process images with concurrency control
|
|
async function processImagesInBatches(imageFiles) {
|
|
const results = {
|
|
successful: 0,
|
|
failed: 0,
|
|
skipped: 0,
|
|
totalSavings: 0
|
|
};
|
|
|
|
// Process in batches to avoid memory issues
|
|
for (let i = 0; i < imageFiles.length; i += config.maxConcurrent) {
|
|
const batch = imageFiles.slice(i, i + config.maxConcurrent);
|
|
|
|
await Promise.all(batch.map(async (filename) => {
|
|
const inputPath = path.join(config.inputDir, filename);
|
|
BuildLogger.log(`\n🔄 处理: ${filename}`);
|
|
|
|
try {
|
|
// Generate multiple formats
|
|
const formatResults = await generateMultipleFormats(inputPath, filename);
|
|
formatResults.forEach(result => {
|
|
if (result && result.success) {
|
|
results.successful++;
|
|
if (result.savings) {
|
|
results.totalSavings += parseFloat(result.savings);
|
|
}
|
|
} else if (result && !result.success) {
|
|
results.failed++;
|
|
}
|
|
});
|
|
|
|
// Generate thumbnails
|
|
const thumbnailResult = await generateThumbnails(inputPath, filename);
|
|
if (thumbnailResult.success) {
|
|
if (thumbnailResult.skipped) {
|
|
results.skipped++;
|
|
} else {
|
|
results.successful++;
|
|
}
|
|
} else {
|
|
results.failed++;
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ 处理失败: ${filename}`, error.message);
|
|
results.failed++;
|
|
}
|
|
}));
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// 主函数
|
|
async function optimizeImages() {
|
|
BuildLogger.log('🚀 开始图片优化...');
|
|
BuildLogger.log(`📁 输入目录: ${config.inputDir}`);
|
|
BuildLogger.log(`📁 输出目录: ${config.outputDir}`);
|
|
BuildLogger.log(`⚡ 并发数: ${config.maxConcurrent}`);
|
|
|
|
// 确保输出目录存在
|
|
ensureOutputDir();
|
|
|
|
// 检查输入目录是否存在
|
|
if (!fs.existsSync(config.inputDir)) {
|
|
BuildLogger.warn(' 输入目录不存在,创建示例目录: ${config.inputDir}');
|
|
fs.mkdirSync(config.inputDir, { recursive: true });
|
|
|
|
// 创建示例文件
|
|
const examplePath = path.join(config.inputDir, 'example.txt');
|
|
fs.writeFileSync(examplePath, '请将需要优化的图片文件放在此目录中');
|
|
BuildLogger.log(`📝 已创建示例文件: ${examplePath}`);
|
|
return;
|
|
}
|
|
|
|
const files = fs.readdirSync(config.inputDir);
|
|
const imageFiles = files.filter(isImageFile);
|
|
|
|
if (imageFiles.length === 0) {
|
|
BuildLogger.warn(' 未找到图片文件');
|
|
return;
|
|
}
|
|
|
|
BuildLogger.log(`📸 找到 ${imageFiles.length} 个图片文件`);
|
|
|
|
const startTime = Date.now();
|
|
const results = await processImagesInBatches(imageFiles);
|
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
|
|
// 生成清单
|
|
generateManifest();
|
|
|
|
BuildLogger.log(`\n🎉 优化完成!`);
|
|
BuildLogger.log(`⏱️ 用时: ${duration}秒`);
|
|
BuildLogger.success(' 成功: ${results.successful}');
|
|
BuildLogger.log(`⏭️ 跳过: ${results.skipped}`);
|
|
BuildLogger.error(' 失败: ${results.failed}');
|
|
if (results.totalSavings > 0) {
|
|
const avgSavings = (results.totalSavings / results.successful).toFixed(1);
|
|
BuildLogger.log(`💾 平均节省空间: ${avgSavings}%`);
|
|
}
|
|
BuildLogger.log(`📁 输出目录: ${config.outputDir}`);
|
|
|
|
if (results.failed > 0) {
|
|
BuildLogger.warn(' 有 ${results.failed} 个文件处理失败');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// 命令行参数处理
|
|
const args = process.argv.slice(2);
|
|
if (args.includes('--help') || args.includes('-h')) {
|
|
BuildLogger.log(`
|
|
📸 图片优化工具 (Sharp版本)
|
|
|
|
用法: node optimize-images.js [选项]
|
|
|
|
选项:
|
|
--help, -h 显示帮助信息
|
|
--input <dir> 指定输入目录 (默认: src/assets/images)
|
|
--output <dir> 指定输出目录 (默认: dist/images)
|
|
--quality <n> 设置JPEG/WebP质量 (默认: 80)
|
|
--format <fmt> 指定输出格式 (jpeg, png, webp, all)
|
|
|
|
示例:
|
|
node optimize-images.js
|
|
node optimize-images.js --input ./images --output ./optimized
|
|
node optimize-images.js --quality 90 --format webp
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// 处理命令行参数
|
|
if (args.includes('--input')) {
|
|
const inputIndex = args.indexOf('--input');
|
|
if (inputIndex + 1 < args.length) {
|
|
config.inputDir = path.resolve(args[inputIndex + 1]);
|
|
}
|
|
}
|
|
|
|
if (args.includes('--output')) {
|
|
const outputIndex = args.indexOf('--output');
|
|
if (outputIndex + 1 < args.length) {
|
|
config.outputDir = path.resolve(args[outputIndex + 1]);
|
|
}
|
|
}
|
|
|
|
if (args.includes('--quality')) {
|
|
const qualityIndex = args.indexOf('--quality');
|
|
if (qualityIndex + 1 < args.length) {
|
|
const quality = parseInt(args[qualityIndex + 1]);
|
|
if (quality >= 1 && quality <= 100) {
|
|
config.formats.jpeg.quality = quality;
|
|
config.formats.webp.quality = quality;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 运行优化
|
|
optimizeImages().catch(error => {
|
|
console.error('❌ 程序执行失败:', error);
|
|
process.exit(1);
|
|
});
|