1. Hexo 静态资源压缩方案总结笔记
    1. 一、 核心思想
    2. 二、 技术栈 (使用的工具与方法)
    3. 三、 关键代码实现 (compress.js)
    4. 四、 最终效果

Hexo 静态资源压缩方案总结笔记

一、 核心思想

本方案旨在为 Hexo 博客或其他静态网站提供一个稳定、高效、易于维护的自动化资源压缩流程。我们摒弃了早期将所有复杂逻辑都堆砌在 package.jsonscripts 中的做法,因为它难以调试且容易因路径空格等问题出错。

最终方案的核心思想是:

  1. 逻辑与配置分离:将所有压缩逻辑统一封装在一个独立的 Node.js 脚本文件 compress.js 中。
  2. package.json 职责单一package.jsonscripts 只负责调用 hexo 命令和这个 compress.js 脚本,保持清晰简洁。
  3. 自动化与报告:在 hexo generate 生成网站后,自动执行压缩,并在任务结束后提供一份清晰的压缩前后大小对比报告。

二、 技术栈 (使用的工具与方法)

我们利用了一系列业界流行且高效的命令行工具来处理不同类型的静态资源。这些工具都通过 npm 安装在项目的 devDependencies 中。

  • HTML 压缩:
    • 工具: html-minifier
    • 具体方法:
      • --collapse-whitespace: 折叠和移除文档中不影响页面渲染的多余空格、换行符和制表符。这是减小 HTML 体积最有效的方法之一。
      • --remove-comments: 删除所有的 HTML 注释(例如 <!-- 这是一个注释 -->)。
      • --remove-optional-tags: 移除 HTML 标准中非强制的标签,例如 <html>, <head>, <body> 的结束标签 </head>, </body> 等,浏览器会自动修正它们。
  • CSS 压缩:
    • 工具: clean-css-cli
    • 具体方法: clean-css 默认会执行一系列高级优化,我们还开启了 --debug 模式来查看优化详情。主要优化点包括:
      • 合并规则: 将具有相同选择器的规则合并在一起。
      • 移除重复: 删除重复的选择器和属性。
      • 高级优化: 优化 font-weight, color 等属性值,例如将 rgb(255,255,255) 转换为 #fff
      • 结构重组: 重新组织 CSS 规则以实现更好的压缩效果。
  • JavaScript 压缩:
    • 工具: terser
    • 具体方法:
      • --compress: 应用各种高级压缩技术,例如移除“死代码”(永远不会被执行的代码)、常量折叠、简化布尔表达式等。
      • --mangle: 代码混淆。这是 JS 压缩中最关键的一步,它会将变量名、函数名等(如 myLongVariableName)替换为极短的单个或两个字母的名称(如 a, b, t),极大地减小文件大小。
      • --verbose: 开启详细日志,方便我们看到处理过程。
  • 图片压缩:
    • 工具: imagemin-cli (及其插件)
    • 具体方法:
      • imagemin-gifsicle: 优化 GIF 图片,通过移除帧之间的冗余像素和优化调色板来减小动图大小。
      • imagemin-jpegtran: 对 JPEG 图片进行无损压缩。它通过优化霍夫曼编码表和移除不必要的元数据(如相机信息)来减小文件大小,但不会牺牲任何图像质量。
      • imagemin-optipng: 对 PNG 图片进行无损压缩。它会尝试多种压缩算法和参数组合,找出能产生最小文件体积的最佳方案。
      • imagemin-svgo: 优化 SVG 文件。它会移除编辑器生成的元数据、注释、隐藏元素,并简化路径数据,使矢量图文件更小。
  • 脚本核心依赖:
    • glob: 一个强大的文件匹配库,用于根据通配符模式(如 public/**/*.html)查找所有需要处理的文件。
    • Node.js 内置模块:
      • child_process: 用于在 Node.js 脚本中执行外部命令行工具。
      • fspath: 用于文件系统操作(如获取文件大小)和路径处理。

三、 关键代码实现 (compress.js)

以下是整个压缩流程的核心脚本。它负责查找文件、执行压缩命令、并统计最终结果。

const { execSync } = require('child_process');
const { globSync } = require('glob');
const fs = require('fs');
const path = require('path');

/**
 * 格式化文件大小单位
 * @param {number} bytes - 文件字节大小
 * @param {number} decimals - 小数位数
 * @returns {string} 格式化后的大小字符串
 */
function formatBytes(bytes, decimals = 2) {
    if (!+bytes) return '0 Bytes';
    const k = 1024;
    const dm = decimals < 0 ? 0 : decimals;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}

/**
 * 获取文件夹的总大小
 * @param {string} folderPath - 文件夹路径
 * @returns {number} 文件夹总字节大小
 */
function getFolderSize(folderPath) {
    let totalSize = 0;
    try {
        const allFiles = globSync(`${folderPath}/**/*`, { nodir: true, dot: true });
        for (const file of allFiles) {
            totalSize += fs.statSync(file).size;
        }
    } catch (error) {
        console.error(`Error calculating folder size for ${folderPath}:`, error);
    }
    return totalSize;
}

// 定义所有压缩任务
const tasks = [
    {
        name: 'HTML',
        patterns: ['public/**/*.html'],
        command: (file) => `html-minifier \"${file}\" -o \"${file}\" --collapse-whitespace --remove-comments --remove-optional-tags`
    },
    {
        name: 'CSS',
        patterns: ['public/**/*.css'],
        command: (file) => `cleancss --debug -o \"${file}\" \"${file}\"`
    },
    {
        name: 'JavaScript',
        patterns: ['public/**/*.js'],
        command: (file) => `terser \"${file}\" -o \"${file}\" --compress --mangle --verbose`
    },
    {
        name: 'Images',
        patterns: ['public/**/*.{jpg,jpeg,png,gif,svg}'],
        command: (file) => {
            const outDir = path.dirname(file);
            return `imagemin \"${file}\" --out-dir=\"${outDir}\"`;
        }
    }
];

/**
 * 主执行函数
 */
async function main() {
    console.log('🚀 Starting asset compression...');

    const publicDir = 'public';
    if (!fs.existsSync(publicDir)) {
        console.error(`\n❌ Error: '${publicDir}' directory not found. Please run 'hexo generate' first.`);
        process.exit(1);
    }

    const beforeSize = getFolderSize(publicDir);
    console.log(`\nTotal size before compression: ${formatBytes(beforeSize)}`);

    // 按顺序执行每个任务
    for (const task of tasks) {
        console.log(`\n--- Compressing ${task.name} ---`);
        try {
            const files = globSync(task.patterns);
            if (files.length === 0) {
                console.log('No files found to compress.');
                continue;
            }
            files.forEach(file => {
                console.log(`Processing: ${file}`);
                execSync(task.command(file), { stdio: 'inherit' });
            });
        } catch (error) {
            console.error(`\n❌ An error occurred during ${task.name} compression.`, error);
            process.exit(1); // 如果出错则停止
        }
    }

    const afterSize = getFolderSize(publicDir);
    const saved = beforeSize - afterSize;
    const percent = beforeSize > 0 ? (saved / beforeSize * 100).toFixed(2) : 0;

    console.log('\n✅ Compression finished!');
    console.log('======================================');
    console.log('📊 COMPRESSION SUMMARY');
    console.log('======================================');
    console.log(`Before:    ${formatBytes(beforeSize)}`);
    console.log(`After:     ${formatBytes(afterSize)}`);
    console.log(`Saved:     ${formatBytes(saved)} (${percent}%)`);
    console.log('======================================');
}

main();

四、 最终效果

当你在命令行运行 npm run build 时,会依次发生以下事情:

  1. Hexo 生成所有静态文件到 public 目录。
  2. compress.js 脚本被触发。
  3. 脚本首先计算 public 目录的初始大小。
  4. 然后依次对 HTML, CSS, JS, Images 文件进行压缩,并打印详细的处理日志。
  5. 所有任务完成后,脚本会再次计算 public 目录的大小,并打印出类似下面的总结报告:
✅ Compression finished!
======================================
📊 COMPRESSION SUMMARY
======================================
Before:    25.47 MB
After:     19.64 MB
Saved:     5.83 MB (22.88%)
======================================

这使得整个优化过程的效果一目了然。


转载请注明来源