7
0

基于Halo Fuwari 主题的自定义功能重构:Astro 化改造 + 视频放映室 + 音乐播放器

2026-05-04
2026-05-04
基于Halo Fuwari 主题的自定义功能重构:Astro 化改造 + 视频放映室 + 音乐播放器

基于Halo Fuwari 主题的自定义功能重构:Astro 化改造 + 视频放映室 + 音乐播放器

前言

之前对 Halo Fuwari 主题的视频放映室功能做了一版「手术刀式」改造——直接编辑 templates/*.html,在 page.html 里塞了 270 行 JS 实现视频播放。能用,但维护困难,且无法跟进上游更新。

这次做了一次彻底的架构升级:跟随原项目的重构,将主题迁移到上游的 Astro 6 构建流程,同时保留了我们所有的自定义功能。以下是完整的技术记录。


一、架构变更总览

之前

templates/page.html  ← 直接编辑,270行JS内联
templates/index.html ← 侧边栏归档硬编码
settings.yaml        ← 手动添加 video 配置

问题:

  • 无法跟进上游更新(音乐播放器、Admonition 等新功能)
  • 代码耦合度高,改一处牵动全局
  • 没有组件化,样式和逻辑混在一起

之后

src/                          ← Astro 源码
├── components/
│   ├── video/
│   │   └── VideoCinema.astro ← 视频放映室独立组件
│   └── widget/
│       └── SideBar.astro     ← 新增 archives widget
├── pages/
│   ├── page.astro            ← slug 条件路由
│   └── photos.astro          ← masonry 布局
├── styles/
│   └── video-cinema.css      ← 视频样式独立文件
└── layouts/
    └── Layout.astro          ← CSS/i18n 全局注入

pnpm build → templates/       ← 构建产物,上传到 Halo

优势:

  • 组件化开发,视频、音乐、归档各管各的
  • pnpm build 一键构建,输出纯 HTML 给 Halo
  • 可以随时 git merge upstream/main 同步上游更新

二、视频放映室组件化(VideoCinema.astro)

核心改动

将原来 page.html 中的内联代码迁移到 Astro 组件 src/components/video/VideoCinema.astro

Astro 对 Halo 主题的支持方式很巧妙:Astro 文件中的 Thymeleaf 指令(th:ifth:text 等)在编译后原样保留,Halo 依然能用服务端渲染。

<!-- page.astro 中的路由判断 -->
<div th:if="${singlePage.spec.slug == 'videos'}">
  <VideoCinema />
</div>
<div th:if="${singlePage.spec.slug != 'videos'}">
  <!-- 普通页面内容 -->
</div>

JS 逻辑的处理

Astro 中需要用 <script is:inline> 来保留原始 JS(不做模块化处理),配合 define:vars 注入配置:

<script is:inline define:vars={{ videoConfig: "${theme.config.video}" }}>
  (() => {
    const themeConfig = JSON.parse(
      document.getElementById("theme-config").textContent
    );
    const videoList = themeConfig.video.videos || [];
    // ... 视频播放逻辑
  })();
</script>

这种模式参照了上游的 MusicPlayer.astro,保持了代码风格一致。

CDN 直连播放优化

视频播放架构做了优化,服务器只做"指路",不搬运视频流:

优化前:浏览器 → 服务器(代理) → CDN    (服务器带宽瓶颈)
优化后:浏览器 → CDN 直连              (服务器零带宽消耗)

实现方式:

  1. <meta name="referrer" content="no-referrer"> — 破解 Referer 防盗链
  2. DPlayer 直接播放 API 返回的 CDN 直链,不经服务器代理

对于 CORS 限制的 CDN(如腾讯云),仍走 Nginx 代理。


三、server.js 解析服务增强

HLS (m3u8) 视频支持

部分影视网站使用 HLS 流而非 MP4 直链,之前的 server.js 无法捕获 m3u8 地址。三处修复:

1. 从中间 URL 参数提取真实视频地址

中间URL: https://yun.92cj.com/mp4hls/?vid=https://xxx/index.m3u8
                                      ↑ 直接提取这个参数
// 从中间播放器 URL 的参数中提取真实视频地址
const vidMatch = url.match(/[?&]vid=(https?:\/\/[^&]+)/i);
if (vidMatch) {
  const realUrl = decodeURIComponent(vidMatch[1]);
  if (isTargetVideo(realUrl, type)) {
    resolved = true;
    resolve(realUrl);
    return;
  }
}

2. 扩展 Content-Type 匹配

// 之前:只匹配 video/mp4 和 octet-stream
// 现在:匹配所有 video/* + mpegurl + octet-stream
if (ct.includes('video/') || ct.includes('mpegurl') || ct.includes('octet-stream'))

3. 补充点击选择器

// 新增 div[class*="play"] 和 .video-play
const selectors = [
  'iframe', '.play-btn', 'button[class*="play"]',
  'div[class*="play"]', '.video-play',  // ← 新增
  ...
];

HLS.js 播放支持

前端新增 hls.js 加载,DPlayer 可以播放 m3u8 流:

<script src="https://cdn.jsdelivr.net/npm/hls.js/dist/hls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.js"></script>

四、上游新功能合并

通过 Astro 构建流程,自动获得了上游的所有更新:

侧边栏音乐播放器

支持网易云、QQ 音乐、酷狗等平台,有歌词显示、播放列表、播放模式切换。

音乐 API 需要通过 Nginx 反代解决 CORS:

location /meting-api/ {
    proxy_pass https://api.injahow.cn/meting/;
    proxy_set_header Host api.injahow.cn;
    proxy_ssl_server_name on;
    add_header Access-Control-Allow-Origin * always;
}

主题设置中自定义 Meting API 填入 /meting-api/?server=:server&type=:type&id=:id&r=:r

其他上游更新

  • Admonition 提示块 — 支持 [!NOTE][!TIP][!WARNING] 等语法
  • Iconify 自定义图标 — 社交媒体图标支持 Iconify 图标库
  • 配置转义修复 — HTML 配置不再被转义

五、侧边栏归档 Widget

将之前的硬编码归档改为标准 widget 类型,在 SideBar.astro 中注册:

<div class="contents" th:case="'archives'">
  <div th:with="recentPosts = ${postFinder.list(1, 10)}">
    <!-- 最近 10 篇文章列表 -->
    <a th:each="post : ${recentPosts.items}"
       th:href="@{${post.status.permalink}}">
      <span th:text="${post.spec.title}">Post</span>
      <span th:text="${#dates.format(post.spec.publishTime, 'MM-dd')}">01-01</span>
    </a>
    <a th:href="@{/archives}">查看全部</a>
  </div>
</div>

settings.yaml 中注册 widget 选项,用户可以在后台自由拖拽排序。


六、图库瀑布流布局

一行 CSS 改动,从固定网格改为瀑布流:

- class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"
+ class="columns-1 sm:columns-2 xl:columns-3 gap-4"

同时需要在每个 <figure> 上添加 break-inside-avoid 防止卡片跨列断裂。


七、开发与部署流程

本地开发

pnpm install          # 安装依赖(需要 Node.js >= 22)
pnpm dev              # 开发模式,文件变更自动重新构建
pnpm build:only       # 构建,输出到 templates/

部署

# 构建
pnpm build:only

# 上传到服务器 Halo 主题目录
scp -P 端口 -r templates/ settings.yaml theme.yaml i18n/ \
  root@服务器:/path/to/halo/themes/theme-fuwari/

# 重启 Halo
docker restart halo

同步上游

git fetch upstream
git merge upstream/main
# 解决冲突后重新构建
pnpm build:only

八、技术栈版本

依赖版本
Halo>= 2.24.0
Astro6.x
Node.js(开发)>= 22.12.0
Node.js(服务器)18.x(仅 server.js 解析服务)
Tailwind CSS4.x
DPlayer最新(CDN)
HLS.js最新(CDN)
Playwright>= 1.40

九、文件改动清单

文件操作说明
src/components/video/VideoCinema.astro新建视频放映室组件
src/styles/video-cinema.css新建视频卡片/播放器样式
src/pages/page.astro修改slug 路由 + meta referrer
src/layouts/Layout.astro修改CSS 引入 + i18n key
src/components/widget/SideBar.astro修改新增 archives widget
src/pages/photos.astro修改masonry 布局
settings.yaml修改video 组 + archives 选项
i18n/*.properties (3个)修改video + archives 翻译
m3u8-crawler/server.js修改m3u8 支持 + vid 提取

十、Nginx 完整配置参考

server {
    listen 443 ssl;
    server_name blog.fallrain0905.top;

    # Halo 主站
    location / {
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 视频解析 API → Node.js
    location /api/getVideo {
        proxy_pass http://127.0.0.1:3000/api/getVideo;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 30s;
    }

    # B站 API 代理
    location /bili-api/ {
        proxy_pass https://api.bilibili.com/;
        proxy_set_header Host api.bilibili.com;
        proxy_set_header Referer "https://www.bilibili.com";
        proxy_set_header User-Agent "Mozilla/5.0";
        proxy_ssl_server_name on;
    }

    # 音乐 Meting API 代理
    location /meting-api/ {
        proxy_pass https://api.injahow.cn/meting/;
        proxy_set_header Host api.injahow.cn;
        proxy_ssl_server_name on;
        add_header Access-Control-Allow-Origin * always;
    }
}

评论