换主题

原来的 oranges 主题用的久了,难免审美疲劳,这不又蠢蠢欲动,想要进行一次改头换面了。我列举了 oranges 主题的缺点:

  • 依赖包老旧,只能在 Node12 下解析;
  • 虽然样式简单,但是受到 jquery 等影响,加载速度一般;
  • 响应式支持糟糕,手机上显示效果差。

说实话,这套主题用了快两年了,自己也贡献了不少代码。奈何实在老气横秋,继续添加 feature 只是加高 code shit,所以放弃。本身服务器架在国外(GitHub),访问速度较慢,想要优化,新主题必须是轻量化的。所以,着重在主题是否是 css 和 vanilla JS 写的,不引入 jquery 以及不必要的第三方库。于是,找到了现在这个主题:minimalism。

加自定义组件

前阵子浏览别人博客的时候,了解到一种对 hexo 主题非破坏式更新的办法。以往,直接在 ejs 上修改有很大的弊端,第一 ejs 写起来体验很差,效率不高;第二主题一旦更新,本地的更改就无法保留。解决方案是用 hexo 提供的过滤器、生成器等勾子函数,创建单独的 js/css 文件。scripts文件夹下的 js 只在hexo g所在的构建阶段执行,而source/js则会原样注入到页面中在浏览器中执行。所以从优化的角度出发,应该尽量减少浏览器加载 js 的数量,所以 js 代码尽量都放在source/custom.js这一个文件中。我用这种方式成功添加了全局搜索功能。

原来归档页的热力图是借助 echarts 完成的,这条路在现有的理念下肯定是不可行了,所以另寻他路。感谢@大大的小蜗牛的这篇博客提供了现成的解决方案。

CSS 和 JS 实现博客热力图https://www.eallion.com/blog-heatmap/#html

因为原文的博客是基于 hugo,所以迁移到 hexo 框架需要一些改动。首先是获取所有文章数据的时候,hexo 的 generator 可以获得站点的所有文章,但是 generator 的代码不能作为 js 脚本放到页面运行,而这是 injector 的工作。所以我的解决方案是把 generator 生成的 json 持久化保存,并在 injector 中注入相关脚本,其中 fetch 了 json 文件的数据。

// scripts/heatmap.js
const fs = require("fs");
const path = require("path");

hexo.extend.injector.register(
  "body_end",
  `
  <script src="/js/heatmap.js"></script>
  <link rel="stylesheet" href="/css/heatmap.css">
`,
  "archive"
);

hexo.extend.generator.register("bloginfo-json", function (locals) {
  const posts = locals.posts.map((post) => {
    let postDate = new Date(post.date);
    return {
      title: post.title,
      date: post.date.format("YYYY-MM-DD"),
      year: postDate.getFullYear(),
      month: postDate.getMonth() + 1,
      day: postDate.getDate(),
      word_count: post.word_count,
    };
  });

  if (!fs.existsSync(hexo.public_dir)) {
    fs.mkdirSync(hexo.public_dir, { recursive: true });
    console.log(`Directory created: ${hexo.public_dir}`);
  }

  const bloginfoPath = path.join(hexo.public_dir, "bloginfo.json");
  fs.writeFileSync(bloginfoPath, JSON.stringify(posts, null, 2));
  console.log(`Blog info JSON generated: ${bloginfoPath}`);
});

// source/js/heatmap.js

const bloginfoPath = "/bloginfo.json";

// 使用 fetch API 读取 JSON 文件
const blogInfo = { pages: [] };
fetch(bloginfoPath)
  .then((response) => {
    if (!response.ok) {
      throw new Error("Network response was not ok " + response.statusText);
    }
    return response.json();
  })
  .then((data) => {
    data.sort((a, b) => new Date(b.date) - new Date(a.date));
    console.log("sorted data", data);
    blogInfo.pages = data;

    createHeatmap();
  })
  .catch((error) => {
    console.error("Error fetching the JSON file:", error);
  });

// 添加heatmap dom
const archieveDiv = document.querySelector(".content-container");
const heatmapDiv = document.createElement("div");
heatmapDiv.id = "heatmap_wrapper";
heatmapDiv.innerHTML = `
<div class="heatmap_container" data-theme="dark"> <!-- 全部用 Flex 排版 -->
    <div class="heatmap_content">
        <div class="heatmap_week">
            <span>Mon</span>
            <span>&nbsp;</span> <!-- 不需要显示的星期用空格表示 -->
            <span>Wed</span>
            <span>&nbsp;</span>
            <span>Fri</span>
            <span>&nbsp;</span>
            <span>Sun</span>
        </div>
        <div class="heatmap_main">
            <div class="month heatmap_month">
                <!-- js 检测屏幕宽度动态生成月份 -->
            </div>
            <div id="heatmap" class="heatmap">
                <!-- js 检测屏幕宽度动态生成年度日历小方块 -->
            </div>
        </div>
    </div>
    <div class="heatmap_footer">
        <div class="heatmap_less">Less</div>
        <div class="heatmap_level">
            <span class="heatmap_level_item heatmap_level_0"></span>
            <span class="heatmap_level_item heatmap_level_1"></span>
            <span class="heatmap_level_item heatmap_level_2"></span>
            <span class="heatmap_level_item heatmap_level_3"></span>
            <span class="heatmap_level_item heatmap_level_4"></span>
        </div>
        <div class="heatmap_more">More</div>
    </div>
</div>
`;
archieveDiv.insertBefore(heatmapDiv, archieveDiv.firstChild);

// 将日期数据运用到dom中

let currentDate = new Date();
currentDate.setFullYear(currentDate.getFullYear() - 1);

let startDate;

let monthDiv = document.querySelector(".month");
let monthNames = [
  "Jan",
  "Feb",
  "Mar",
  "Apr",
  "May",
  "Jun",
  "Jul",
  "Aug",
  "Sep",
  "Oct",
  "Nov",
  "Dec",
];

if (window.innerWidth < 768) {
  numMonths = 6;
} else {
  numMonths = 12;
}

let startMonthIndex = (currentDate.getMonth() - (numMonths - 1) + 12) % 12;
for (let i = startMonthIndex; i < startMonthIndex + numMonths; i++) {
  let monthSpan = document.createElement("span");
  let monthIndex = i % 12;
  monthSpan.textContent = monthNames[monthIndex];
  monthDiv.appendChild(monthSpan);
}

function getStartDate() {
  const today = new Date();

  if (window.innerWidth < 768) {
    numMonths = 6;
  } else {
    numMonths = 12;
  }

  const startDate = new Date(
    today.getFullYear(),
    today.getMonth() - numMonths + 1,
    1,
    today.getHours(),
    today.getMinutes(),
    today.getSeconds()
  );

  while (startDate.getDay() !== 1) {
    startDate.setDate(startDate.getDate() + 1);
  }

  return startDate;
}

function getWeekDay(date) {
  const day = date.getDay();
  return day === 0 ? 6 : day - 1;
}

function createDay(date, title, count, post) {
  const day = document.createElement("div");

  day.className = "heatmap_day";

  day.setAttribute("data-title", title);
  day.setAttribute("data-count", count);
  day.setAttribute("data-post", post);
  day.setAttribute("data-date", date);

  day.addEventListener("mouseenter", function () {
    const tooltip = document.createElement("div");
    tooltip.className = "heatmap_tooltip";

    let tooltipContent = "";

    if (post && parseInt(post, 10) !== 0) {
      tooltipContent +=
        '<span class="heatmap_tooltip_post">' +
        "共 " +
        post +
        " 篇" +
        "</span>";
    }

    if (count && parseInt(count, 10) !== 0) {
      tooltipContent +=
        '<span class="heatmap_tooltip_count">' +
        " " +
        count +
        " 字;" +
        "</span>";
    }

    if (title && parseInt(title, 10) !== 0) {
      tooltipContent +=
        '<span class="heatmap_tooltip_title">' + title + "</span>";
    }

    if (date) {
      tooltipContent +=
        '<span class="heatmap_tooltip_date">' + date + "</span>";
    }

    tooltip.innerHTML = tooltipContent;
    day.appendChild(tooltip);
  });

  day.addEventListener("mouseleave", function () {
    const tooltip = day.querySelector(".heatmap_tooltip");
    if (tooltip) {
      day.removeChild(tooltip);
    }
  });

  if (count == 0) {
    day.classList.add("heatmap_day_level_0");
  } else if (count > 0 && count < 1000) {
    day.classList.add("heatmap_day_level_1");
  } else if (count >= 1000 && count < 2000) {
    day.classList.add("heatmap_day_level_2");
  } else if (count >= 2000 && count < 3000) {
    day.classList.add("heatmap_day_level_3");
  } else {
    day.classList.add("heatmap_day_level_4");
  }

  return day;
}

function createWeek() {
  const week = document.createElement("div");
  week.className = "heatmap_week";
  return week;
}

function createHeatmap() {
  const container = document.getElementById("heatmap");
  const startDate = getStartDate();
  const endDate = new Date();
  const weekDay = getWeekDay(startDate);

  let currentWeek = createWeek();
  container.appendChild(currentWeek);

  let currentDate = startDate;
  let i = 0;

  while (currentDate <= endDate) {
    if (i % 7 === 0 && i !== 0) {
      currentWeek = createWeek();
      container.appendChild(currentWeek);
    }

    const dateString = `${currentDate.getFullYear()}-${(
      "0" +
      (currentDate.getMonth() + 1)
    ).slice(-2)}-${("0" + currentDate.getDate()).slice(-2)}`;
    const articleDataList = blogInfo.pages.filter(
      (page) => page.date === dateString
    );

    if (articleDataList.length > 0) {
      const titles = articleDataList.map((data) => data.title);
      const title = titles.map((t) => `${t}`).join("<br />");

      let count = 0;
      let post = articleDataList.length;

      articleDataList.forEach((data) => {
        count += parseInt(data.word_count, 10);
      });

      const formattedDate = formatDate(currentDate);
      const day = createDay(formattedDate, title, count, post);
      currentWeek.appendChild(day);
    } else {
      const formattedDate = formatDate(currentDate);
      const day = createDay(formattedDate, "", "0", "0");
      currentWeek.appendChild(day);
    }

    i++;
    currentDate.setDate(currentDate.getDate() + 1);
  }
}

function formatDate(date) {
  const options = { month: "short", day: "numeric", year: "numeric" };
  return date.toLocaleDateString("en-US", options);
}

gulp 压缩

将 html\css\js 文件压缩,也是一种常见的优化措施。先本地安装 gulp:

yarn add gulp gulp-clean-css gulp-htmlmin gulp-uglify gulp-plumber --save-dev

gulp 的运行文件名为gulpfile.js:

const gulp = require("gulp");
const cleanCSS = require("gulp-clean-css");
const htmlmin = require("gulp-htmlmin");
const uglify = require("gulp-uglify");
const plumber = require("gulp-plumber");

// Compress CSS files
gulp.task("minify-css", () => {
  return gulp
    .src("public/**/*.css")
    .pipe(plumber())
    .pipe(cleanCSS({ compatibility: "ie8" }))
    .pipe(gulp.dest("public"));
});

// Compress HTML files
gulp.task("minify-html", () => {
  return gulp
    .src("public/**/*.html")
    .pipe(plumber())
    .pipe(
      htmlmin({
        collapseWhitespace: true,
        removeComments: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        useShortDoctype: true,
        minifyJS: true,
        minifyCSS: true,
        ignoreCustomFragments: [/<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/],
      })
    )
    .pipe(gulp.dest("public"));
});

// Compress JS files
gulp.task("minify-js", () => {
  return gulp
    .src("public/**/*.js")
    .pipe(plumber())
    .pipe(uglify())
    .pipe(gulp.dest("public"));
});

// Default task to run all compression tasks
gulp.task("compress", gulp.parallel("minify-css", "minify-html", "minify-js"));

最后,修改 github action workflow:

// prev
      - name: Install Dependencies
        run: npm install
      - name: Build
        run: npm run build
      - name: Run Gulp to Compress Files
        run: |
          npx gulp compress
      - name: Upload Pages artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./public
// after

压缩前后对比:5.86MB -> 4.88MB。比我想象中的压缩力度小一点,但是也挺显著了。