换主题
原来的 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 完成的,这条路在现有的理念下肯定是不可行了,所以另寻他路。感谢@大大的小蜗牛的这篇博客提供了现成的解决方案。
因为原文的博客是基于 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> </span> <!-- 不需要显示的星期用空格表示 -->
<span>Wed</span>
<span> </span>
<span>Fri</span>
<span> </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。比我想象中的压缩力度小一点,但是也挺显著了。