博客折腾记录-向轻量化目标挺进
May 31, 2024
换主题
原来的 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> </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。比我想象中的压缩力度小一点,但是也挺显著了。