<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Molly&apos;s Blog</title><description>主页</description><link>https://mollyovo.netlify.app/</link><language>zh_CN</language><item><title>fuwari：实现随机banner</title><link>https://mollyovo.netlify.app/posts/fuwari%E5%AE%9E%E7%8E%B0%E9%9A%8F%E6%9C%BAbanner/</link><guid isPermaLink="true">https://mollyovo.netlify.app/posts/fuwari%E5%AE%9E%E7%8E%B0%E9%9A%8F%E6%9C%BAbanner/</guid><description>将script脚本注入fuwari页面布局以及创建自动扫描脚本实现随机banner</description><pubDate>Mon, 13 Apr 2026 20:15:27 GMT</pubDate><content:encoded>&lt;h1&gt;random-banner的实现&lt;/h1&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在折腾完hexo-fluid的随机banner之后发现了基于 &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt; 开发的静态博客模板，fuwari的页面过渡和动效非常丝滑，于是立马fork了下来部署在了netlify上😋&lt;/p&gt;
&lt;p&gt;本文将分享如何通过 Node.js 自动化扫描 + SPA 路由拦截实现首页固定、内页随机banner的效果。&lt;/p&gt;
&lt;h2&gt;文件准备&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;图片存放：public/images/random/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;首页固定：public/images/你的图片.jpg&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;核心配置：src/layouts/MainGridLayout.astro&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;实现原理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;文件扫描：在构建前，利用 Node.js 脚本遍历文件夹，生成包含所有图片路径的 json 索引，无需手动维护列表。&lt;/li&gt;
&lt;li&gt;路由感知：fuwari 使用 swup 实现无刷新跳转，通过监听 swup:content_replaced 事件，确保每次切换页面时都能触发banner的刷新。&lt;/li&gt;
&lt;li&gt;预加载：通过 swup:clickLink 事件，在点下链接的同时开始后台下载图片。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;h3&gt;Step1. 生成图片“索引”&lt;/h3&gt;
&lt;p&gt;主要解决的问题是让程序自动扫描&lt;code&gt;public/images/random/&lt;/code&gt;下存放的图片，免去手动写数组的步骤，这样一来不管是添加还是删除图片就方便一些。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现方式：在script下创建generate-banners.mjs文件&lt;/li&gt;
&lt;li&gt;逻辑：用fs模块读取文件夹，过滤出指定格式图片并写入src/generated-banners.json&lt;/li&gt;
&lt;li&gt;工程化：在 package.json 的 dev 和 build 命令前加上 node scripts/generate-banners.mjs，便于调试和部署&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import fs from &apos;node:fs&apos;;
import path from &apos;node:path&apos;;

// 1. 定义你的随机图片存放路径（相对于项目根目录）
const randomDir = path.join(process.cwd(), &apos;public&apos;, &apos;images&apos;, &apos;random&apos;);
// 2. 定义生成的 JSON 存放路径
const outputFile = path.join(process.cwd(), &apos;src&apos;, &apos;generated-banners.json&apos;);

try {
    if (fs.existsSync(randomDir)) {
        const files = fs.readdirSync(randomDir)
            .filter(file =&amp;gt; /\.(png|jpe?g|gif|svg|webp|avif)$/i.test(file))
            .map(file =&amp;gt; `/images/random/${file}`); // 生成相对 URL 路径

        fs.writeFileSync(outputFile, JSON.stringify(files, null, 2));
        console.log(`✅ 成功扫描到 ${files.length} 张随机 Banner 图！`);
    } else {
        console.warn(`⚠️ 找不到目录: ${randomDir}`);
        fs.writeFileSync(outputFile, JSON.stringify([]));
    }
} catch (err) {
    console.error(&apos;❌ 生成 Banner 列表失败:&apos;, err);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step2. 注入页面布局&lt;/h3&gt;
&lt;p&gt;主要实现当Astro 渲染页面时，将图片和逻辑注入到所有页面的公共父级。&lt;/p&gt;
&lt;p&gt;源代码位置：&lt;code&gt;src/layouts/MainGridLayout.astro&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;2.1  全局变量与常量&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const FIXED_BANNER_SRC = &quot;/images/你的图片.jpg&quot;; // 首页固定
window.BANNER_LIST = banners;                // 注入后端生成的列表
window.lastBannerUrl = &quot;&quot;;                    // 记录上一张
window.isBannerProcessing = false;            // 核心状态锁
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2 获取下一个随机图片 URL&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function getNextRandomUrl() {
  if (!window.BANNER_LIST || window.BANNER_LIST.length === 0) return null;
  let next = window.BANNER_LIST[Math.floor(Math.random() * window.BANNER_LIST.length)];
  if (window.BANNER_LIST.length &amp;gt; 1 &amp;amp;&amp;amp; next === window.lastBannerUrl) {
    next = window.BANNER_LIST.find(url =&amp;gt; url !== window.lastBannerUrl);
  }
  return next;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果列表长度 &amp;gt;1 且随机到的 URL 与上次相同，则手动取另一个不同的 URL（简单防重复）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.3 设置横幅图片&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function setRandomBanner() {
  if (window.isBannerProcessing) return;          // 避免并发

  const bannerContainer = document.getElementById(&apos;banner&apos;);
  if (!bannerContainer) return;
  const img = bannerContainer.querySelector(&apos;img&apos;);
  if (!img) return;

  const isHomePage = window.location.pathname === &apos;/&apos; || window.location.pathname === &apos;/index.html&apos;;

  if (isHomePage) {
    // 首页：强制显示固定图片
    if (!img.src.includes(FIXED_BANNER_SRC)) {
      img.src = FIXED_BANNER_SRC;
      img.style.opacity = &quot;1&quot;;
    }
    return;
  }

  // 非首页：随机切换
  const randomImg = getNextRandomUrl();
  if (!randomImg) return;

  window.isBannerProcessing = true;
  window.lastBannerUrl = randomImg;

  // 预加载图片，避免切换时闪烁
  const tempImg = new Image();
  tempImg.src = randomImg;
  tempImg.onload = () =&amp;gt; {
    img.src = randomImg;
    img.style.opacity = &quot;1&quot;;
    setTimeout(() =&amp;gt; {
      window.isBannerProcessing = false;  // 延迟释放锁，保证过渡完成
    }, 600);
  };
  tempImg.onerror = () =&amp;gt; { window.isBannerProcessing = false; };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;主要判断是否为首页，是则固定图片，否则调用 getNextRandomUrl 获取随机图片，然后锁定处理标志，用 Image 对象预加载该图片。加载完成后替换 src，恢复透明度，并延迟释放锁。加载失败时直接释放锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4 初始化执行时机&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;if (document.readyState === &apos;complete&apos;) {
  setRandomBanner();
} else {
  window.addEventListener(&apos;load&apos;, setRandomBanner);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果文档已完全加载，立即执行；否则等待 load 事件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.5 配合 swup 的事件监听&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;document.addEventListener(&apos;swup:clickLink&apos;, () =&amp;gt; {
  const next = getNextRandomUrl();
  if (next) {
    const preloader = new Image();
    preloader.src = next;   // 提前加载下一张随机图片，提升切换体验
  }
});

document.addEventListener(&apos;swup:content_replaced&apos;, () =&amp;gt; {
  setTimeout(setRandomBanner, 50);  // 新内容替换后，延迟 50ms 重新设置横幅
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;swup:clickLink&lt;/code&gt;：用户点击链接时触发。预加载一张新的随机图片（不显示，仅缓存）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;swup:content_replaced&lt;/code&gt;：新页面内容替换到 DOM 后触发。延迟调用 &lt;code&gt;setRandomBanner&lt;/code&gt; 更新图片，确保新页面中的横幅元素已存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step3. 优化全局样式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style is:global&amp;gt;
  #banner img {
    object-fit: cover !important;
    object-position: center !important;
    transition: opacity 0.5s ease-out, height 0.5s ease-out !important;
    will-change: opacity;
    transform: translateZ(0);
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图片高度是可以修改的，文件位于&lt;code&gt;/src/constants/constants.ts&lt;/code&gt;，我这里直接统一了所有页的高度.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Banner height unit: vh
export const BANNER_HEIGHT = 65; // 默认35
export const BANNER_HEIGHT_EXTEND = 0; // 默认30
export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;整体流程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;页面初次加载&lt;/strong&gt; → 判断是否首页 → 首页显示固定图片，其他页面随机显示一张。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户点击内部链接（swup）&lt;/strong&gt; → 预加载下一张随机图片 → 页面内容替换后重新设置横幅图片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;随机策略&lt;/strong&gt;：每次随机取一张，避免连续重复（列表长度 &amp;gt;1 时）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防闪烁&lt;/strong&gt;：使用临时 &lt;code&gt;Image&lt;/code&gt; 对象预加载完成后再替换 &lt;code&gt;src&lt;/code&gt;，配合 CSS 透明度过渡。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;p&gt;在我commit之后出现了两个报错：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Error: File content differs from formatting output/The imports and exports are not sorted.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;error ts(2322): Type &apos;PostForList[]&apos; is not assignable to type &apos;Post[]&apos;.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Fuwari 使用了 Biome 作为代码格式化工具。当你修改了 MainGridLayout.astro 或添加了新文件时，如果代码的缩进、空格、甚至 import 的顺序没有完全符合 Biome 的预设规则，CI（持续集成）就会报错。这个问题可以执行Biome的格式检查试一下。&lt;/p&gt;
&lt;p&gt;第二个报错指向的是 &lt;code&gt;Navbar.astro&lt;/code&gt; 和 &lt;code&gt;archive.astro&lt;/code&gt;，但实际并没有动这两个文件，所以我认为是引入了新的 json 索引文件后，Astro 的类型生成器（&lt;code&gt;astro sync&lt;/code&gt;）重新扫描全站发现了意外错误。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;尽管遇到了两个提交的报错，但代码还是跑起来了（bushi），这种修改方式本质上是在&lt;strong&gt;静态架构上叠加动态体验&lt;/strong&gt;，修改过程中出现了不少问题。例如页内跳转页面时，出现了闪过2-3张图片的奇怪效果，在Gemini的帮助下还是解决了，伟大无需多盐。&lt;/p&gt;
</content:encoded></item><item><title>Fluid：实现 banner 图片随页面刷新而切换</title><link>https://mollyovo.netlify.app/posts/fluid%E5%AE%9E%E7%8E%B0-banner-%E5%9B%BE%E7%89%87%E9%9A%8F%E9%A1%B5%E9%9D%A2%E5%88%B7%E6%96%B0%E9%9A%8F%E6%9C%BA%E5%88%87%E6%8D%A2---%E5%89%AF%E6%9C%AC/</link><guid isPermaLink="true">https://mollyovo.netlify.app/posts/fluid%E5%AE%9E%E7%8E%B0-banner-%E5%9B%BE%E7%89%87%E9%9A%8F%E9%A1%B5%E9%9D%A2%E5%88%B7%E6%96%B0%E9%9A%8F%E6%9C%BA%E5%88%87%E6%8D%A2---%E5%89%AF%E6%9C%AC/</guid><description>通过 Node.js 脚本修改 HTML 渲染逻辑，使浏览器在每次加载页面时实时随机选择图片，个人称之为“暴力注入式修改”</description><pubDate>Sat, 11 Apr 2026 15:04:27 GMT</pubDate><content:encoded>&lt;h1&gt;random-banner的实现&lt;/h1&gt;
&lt;p&gt;通过 Node.js 脚本修改 HTML 渲染逻辑，使浏览器在每次加载页面时实时随机选择图片，个人称之为“暴力注入式修改”。&lt;/p&gt;
&lt;h2&gt;实现原理&lt;/h2&gt;
&lt;p&gt;代码使用 Hexo 的 &lt;code&gt;after_render:html&lt;/code&gt; 过滤器。该功能在 Markdown 转化为 HTML 后介入，通过修改 HTML 字符串，在 &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; 标签前插入一段 JavaScript 脚本。&lt;/p&gt;
&lt;p&gt;该脚本会执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;扫描本地 &lt;code&gt;source/img/random/&lt;/code&gt; 目录下的图片文件。&lt;/li&gt;
&lt;li&gt;将图片路径数组传递给前端 JavaScript。&lt;/li&gt;
&lt;li&gt;前端脚本在页面加载时，通过 &lt;code&gt;Math.random()&lt;/code&gt; 选取一张图片，并强制修改 CSS 背景属性。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;p&gt;图片存放目录：img/random/&lt;/p&gt;
&lt;p&gt;支持图片格式：png, jpg, jpeg, gif, svg, webp&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主题配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;修改_config.fluid.yml文件（若未&lt;a href=&quot;https://hexo.fluid-dev.com/docs/guide/#%E8%A6%86%E7%9B%96%E9%85%8D%E7%BD%AE&quot;&gt;覆盖主题配置&lt;/a&gt;，则需修改hexo默认配置）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置 &lt;code&gt;banner.random_img: true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;由于脚本仅在banner_img为空时生效，如需固定页面图片，填入具体图片路径即可&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;脚本位置&lt;/h2&gt;
&lt;p&gt;由于是“暴力式修改”，只用在博客根目录创建 &lt;code&gt;scripts/fluid_random_banner.js&lt;/code&gt;，Hexo 启动时会自动加载此目录下的 js 文件，便于文件迁移。&lt;/p&gt;
&lt;h2&gt;完整代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&apos;use strict&apos;;

const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);

hexo.extend.filter.register(&apos;after_render:html&apos;, function(str, data) {
  const theme = hexo.theme.config;
  const page = data.page;

  // 检查是否开启
  if (!theme.banner || theme.banner.random_img !== true) return str;

  // 识别当前页面路径配置（优先级：页面 Front-matter &amp;gt; 主题布局配置 &amp;gt; 首页配置）
  let currentBanner = page.banner_img || 
                     (page.layout &amp;amp;&amp;amp; theme[page.layout] &amp;amp;&amp;amp; theme[page.layout].banner_img) ||
                     (page.path === &apos;index.html&apos; &amp;amp;&amp;amp; theme.index &amp;amp;&amp;amp; theme.index.banner_img);

  // 若路径不为空，则跳过随机逻辑
  if (currentBanner &amp;amp;&amp;amp; currentBanner.trim() !== &apos;&apos;) return str;

  // 获取本地图片列表并处理路径
  const randomDir = path.join(hexo.source_dir, &apos;img&apos;, &apos;random&apos;);
  if (!fs.existsSync(randomDir)) return str;
  const files = fs.readdirSync(randomDir).filter(f =&amp;gt; /\.(png|jpe?g|gif|svg|webp)$/i.test(f));
  if (files.length === 0) return str;

  const url_for = require(&apos;hexo-util&apos;).url_for.bind(hexo);
  const bannerList = JSON.stringify(files.map(f =&amp;gt; url_for(`/img/random/${f}`)));

  // 构造并注入前端脚本
  const injectJs = `
    &amp;lt;script&amp;gt;
    (function() {
      var banners = ${bannerList};
      var randomImg = banners[Math.floor(Math.random() * banners.length)];
      var selectors = [&apos;#banner&apos;, &apos;#board + .full-bg-img&apos;, &apos;.header-inner .full-bg-img&apos;, &apos;.full-bg-img&apos;, &apos;.banner&apos;];
      
      var inject = function() {
        selectors.forEach(function(s) {
          var el = document.querySelector(s);
          if (el) el.style.setProperty(&apos;background-image&apos;, &apos;url(&quot;&apos; + randomImg + &apos;&quot;)&apos;, &apos;important&apos;);
        });
      };

      if (document.readyState === &apos;loading&apos;) {
        document.addEventListener(&apos;DOMContentLoaded&apos;, inject);
      } else { inject(); }
      setTimeout(inject, 100); 
      setTimeout(inject, 1000); 
    })();
    &amp;lt;/script&amp;gt;
  `;

  return str.replace(&apos;&amp;lt;/body&amp;gt;&apos;, injectJs + &apos;&amp;lt;/body&amp;gt;&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行 &lt;code&gt;hexo clean &amp;amp;&amp;amp; hexo s&lt;/code&gt;，刷新浏览器即可看到随机效果。&lt;/p&gt;
&lt;h2&gt;小建议&lt;/h2&gt;
&lt;p&gt;若存放的图片过大，又不想自己压缩，可以配合 &lt;a href=&quot;https://github.com/chenzhutian/hexo-all-minifier&quot;&gt;hexo-all-minifier&lt;/a&gt;图片压缩插件使用，且脚本引用的路径依然指向压缩后的静态资源，不影响加载速度。&lt;/p&gt;
</content:encoded></item><item><title>我的第一篇博客</title><link>https://mollyovo.netlify.app/posts/%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87%E5%8D%9A%E5%AE%A2/</link><guid isPermaLink="true">https://mollyovo.netlify.app/posts/%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87%E5%8D%9A%E5%AE%A2/</guid><description>本文作为第一篇博客测试，记录漫画源</description><pubDate>Sat, 11 Apr 2026 13:28:27 GMT</pubDate><content:encoded>&lt;h1&gt;mihon配置&lt;/h1&gt;
&lt;h2&gt;mihon&lt;/h2&gt;
&lt;p&gt;一款免费开源的漫画阅读软件，用户可以通过安装各种“扩展（Extensions）”来连接全球各地的漫画源，或者阅读本地文件。&lt;/p&gt;
&lt;h2&gt;常用漫画源&lt;/h2&gt;
&lt;p&gt;插件仓库：https://raw.githubusercontent.com/keiyoushi/extensions/repo/index.min.json&lt;/p&gt;
&lt;p&gt;拷贝漫画：https://github.com/stevenyomi/copymanga/&lt;/p&gt;
</content:encoded></item></channel></rss>