Skip to content

开发指南 ​

本文档面向 WarmIsland 主题的开发者,介绍如何搭建开发环境、构建主题以及参与贡献。

环境要求 ​

工具最低版本推荐版本
Node.js22.12.x22.x LTS
pnpm12.x12.x
Git2.x最新

安装 pnpm ​

bash
npm install -g pnpm

验证环境 ​

bash
node -v    # v22.x.x(需 >= 22.12.0)
pnpm -v    # 12.x.x
git --version  # git version 2.x.x

克隆与安装 ​

bash
git clone https://git.nxxy335.top/nxxy335/halo-theme-WarmIsland.git
cd halo-theme-WarmIsland
pnpm install

开发模式 ​

pnpm dev 用 nodemon 监听 src/、public/、settings.yaml 等文件的变化, 每次变化自动执行一次 astro build,把结果写入 templates/。 本项目是「静态模板产物 + Halo 运行时渲染」模式,没有 Astro 开发服务器, 也不依赖 vite-plugin-halo-theme;本地预览请直接用 Halo 实例加载 templates/。

构建 ​

生产构建 ​

bash
pnpm build

构建产物位于 templates/(21 个 .html 模板 + assets/ 静态资源), 并由 scripts/postbuild.mjs 合并 compat/assets 中的历史哈希资源、校验资源引用。 注意:pnpm build 不会生成 ZIP 安装包。

打包主题 ​

bash
pnpm release          # 推荐:build → snapshot → package(含合规审计)
# 或分步:pnpm build && pnpm snapshot && pnpm audit:external && pnpm package

生成 dist/warm-island-<version>.zip,可直接在 Halo 后台上传安装。 ZIP 根目录直接放 theme.yaml / settings.yaml / templates/,不要额外套一层目录; docs/*.md 与预览图也会一并打包,保证上架说明里的引用在制品中真实存在。

templates/ 被 .gitignore 忽略,克隆仓库后默认不存在。若跳过 pnpm build 直接 pnpm package,过去会产出不含任何模板的空壳包,上传后 Halo 报 Template index was not found. 并让首页 404。现在打包脚本会先校验 index.html 等模板、资源目录与版本号,不满足即报错退出。

上架合规审计 ​

scripts/audit-external.mjs(pnpm audit:external)在打包前自动执行,对应应用市场审核指南 1.4 / 4.4.4 / 4.6 / 5.1,检查:

  • 模板中不含第三方 preconnect / dns-prefetch / preload
  • 不含 src / href 指向 http(s) 的远程脚本、样式、图片、媒体
  • 不含硬编码的外部 fetch() / 动态 import() / CSS url(https://...), 也不含 Prism autoloader 之类的动态加载痕迹与被移除的服务域名
  • README 中的相对链接都能在安装包清单里找到对应文件
  • templates/logo.png 不存在(只允许一份压缩后的 templates/assets/logo.png)

任一不通过,pnpm package 直接 exit 1,避免打出不合规制品。 <a href="https://..."> 这类需要访客主动点击的外链是允许的,但必须在 README 「外部资源与隐私」章节逐项披露。新增第三方资源时,请先自托管到 public/assets/ 并在 README 的许可表中登记来源与协议。

性能与滚动顺滑 ​

首页滚动曾经卡顿,根因不是 DOM 数量,而是「每帧高成本绘制 + 劫持滚动输入」:

问题原因现在的做法
全屏模糊逐帧重算滚动回调每帧改写 backdrop-filter: blur(Npx),Chromium 每帧重做一次全屏背景采样 + 高斯模糊模糊半径固定,滚动只改整层 opacity(合成层操作)
吸附吞掉滚动惯性旧实现在 wheel / touchend 上 preventDefault() 并固定锁 800ms保留整屏吸附,但只在 0 ↔ 内容区顶部 边界接管一次;进入内容区后完全交给原生滚动。解锁用 scrollend + 600ms 兜底,已在目标位置时不再接管
Hero 一直绘制固定全屏 Hero 的光球动画与模糊层在滚过首屏后仍在跑内容区盖满视口后加 wi-hero--offscreen 隐藏并暂停动画
状态反复写入导航栏每个滚动帧都 classList.add/remove缓存状态,只在变化时写

为什么吸附不用 CSS scroll-snap:v1.2.5 初版试过 html { scroll-snap-type: y proximity } 加内容区 scroll-snap-align: start,实际几乎不吸附——只有内容区顶部一个吸附点,页面顶部 0 没有吸附点,proximity 只在滚动结束位置接近吸附点时才生效;换成 mandatory 则会在 长内容的每次滚动后把页面拉回内容区顶部,正文无法阅读。prefers-reduced-motion 下 CSS 方案 还会直接关闭吸附。因此改为脚本的边界吸附,CSS 侧不再设置 scroll-snap,避免与 scrollTo 平滑动画抢夺滚动位置。

涉及文件:src/pages/index.astro(滚动渐隐 + 边界吸附脚本)、src/components/HeroSection.astro(模糊层与离屏规则)、 src/components/Navbar.astro(状态去重)、src/styles/main.scss(平滑滚动与 reduced-motion)。

可调项:后台「总体样式 → 启用动效」可一键关闭滚动渐入,「Hero 首屏」可关闭整个 Hero; 系统开启「减少动态效果」(prefers-reduced-motion: reduce)时,平滑滚动与光球 / 箭头动画停用, 吸附改为即时定位(仍然可用)。长列表使用 content-visibility: auto 跳过视口外渲染, 图片统一 loading="lazy" decoding="async"。

改动滚动逻辑后建议用浏览器 Performance 面板录制一次首页滚动:滚动帧里不应出现大面积 Paint / Update Layer Tree;也不要在 scroll 回调里读取 getBoundingClientRect() 之类 会强制同步布局的属性(当前实现只在初始化 / resize 时测量内容区位置)。

项目结构 ​

以下按磁盘实际文件列出(src/pages/ 下每个 .astro 对应一个输出模板,共 21 个):

WarmIsland/
├── src/
│   ├── components/       # 18 个组件:Navbar / Footer / HeroSection / WelcomePopup /
│   │                     # LightGallery / ClickEffect / ScrollReveal / ThemeSwitcher 等
│   ├── layouts/           # Layout.astro(含页面布局契约渲染模式)
│   ├── pages/            # index / post / page / archives / categories / category /
│   │                     # tags / tag / links / friends(历史兼容)/ moments / moment / photos /
│   │                     # douban / equipments / important-dates / steam / weread /
│   │                     # page_messageboard / page_subscriptions / layout
│   └── styles/           # main.scss + 12 个 _*.scss 片段
├── public/
│   ├── assets/
│   │   ├── data/         # hitokoto.json 内置本地句子库
│   │   ├── fonts/        # LXGW WenKai 本地 woff2 分片与 result.css
│   │   ├── vendor/       # 自托管 prism-warm.css / prism-cold.css
│   │   └── logo.png      # 主题图标(唯一一份,256×256 压缩版)
│   ├── error/            # 错误页静态资源
│   └── favicon.ico
├── compat/assets/        # 历史版本哈希静态资源,构建时合并进 templates/assets
├── scripts/              # postbuild / snapshot-assets / package / audit-external / smoke-clickfx
├── docs/                 # VitePress 文档站(*.md 随安装包分发)
├── templates/            # 构建产物(被 .gitignore 忽略)
├── dist/                 # ZIP 安装包(被 .gitignore 忽略)
├── theme.yaml            # 主题清单与自定义页面模板注册
├── settings.yaml         # 后台设置项定义
├── astro.config.mjs / package.json / pnpm-lock.yaml / tsconfig.json
└── WarmIsland Preview.webp

技术栈 ​

核心框架 ​

技术版本用途
Halo2.27.0+博客运行时,提供 Finder API 与主题渲染
Astro6.x组件开发,构建输出静态 .html 模板
Thymeleaf-Halo 服务端模板引擎(源码里直接写 th:*)

前端技术 ​

技术用途
SCSS样式预处理,CSS 变量驱动的配色系统
原生 JavaScript滚动、菜单、主题切换、无限滚动等交互,无前端框架运行时
LightGallery 2.9.0(GPLv3)图片灯箱,默认内置;安装 lightgallery.js 插件后自动让位
LXGW WenKai(OFL 1.1)本地字体分片,随主题自托管,无远程字体请求
Halo 插件生态评论、代码高亮、服务端字数统计等由 Halo 与可选插件提供

构建工具 ​

技术用途
Vite(Astro 内置)资源打包与内容哈希
pnpm包管理
TypeScript类型定义(env.d.ts / pages.ts)

模板语法 ​

WarmIsland 使用 Astro 编写组件,构建后输出为 Thymeleaf 模板供 Halo 使用。

Astro 组件 → Thymeleaf 模板 ​

Astro 组件里直接书写 Thymeleaf 属性(th:*),构建时原样保留并输出成 .html 模板, 由 Halo 在运行时渲染:

astro
---
// Astro 前置脚本
const title = "Hello";
---
<h1>{title}</h1>

转换为 Thymeleaf:

html
<h1 th:text="${title}">Hello</h1>

Halo Finder API ​

在模板中使用 Halo 的 Finder API 获取数据:

html
<!-- 获取文章列表 -->
<th:block th:each="post : ${postFinder.list(1, 10)}">
  <a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a>
</th:block>

<!-- 获取分类 -->
<th:block th:each="category : ${categoryFinder.listAll()}">
  <span th:text="${category.spec.displayName}"></span>
</th:block>

<!-- 获取标签 -->
<th:block th:each="tag : ${tagFinder.listAll()}">
  <span th:text="${tag.spec.displayName}"></span>
</th:block>

Vue 3 交互组件 ​

需要客户端交互的组件使用 Vue 3 编写,通过 <script> 标签引入:

html
<div id="toc-app"></div>
<script type="module" src="/assets/js/toc.js"></script>

Docker 部署 ​

构建镜像 ​

bash
docker build -t warmisland-theme .

运行 ​

bash
docker run -d \
  --name warmisland \
  -p 8090:8090 \
  -v ~/.halo2:/root/.halo2 \
  warmisland-theme

Docker Compose ​

yaml
version: '3'
services:
  halo:
    image: warmisland-theme
    ports:
      - "8090:8090"
    volumes:
      - ./halo2:/root/.halo2
    restart: unless-stopped

贡献指南 ​

提交 Issue ​

  • 使用 GitHub Issues 提交 Bug 报告或功能建议
  • 请提供详细的复现步骤和环境信息
  • 附上截图或错误日志

提交 Pull Request ​

  1. Fork 本仓库
  2. 创建特性分支:git checkout -b feature/your-feature
  3. 提交更改:git commit -m 'feat: add your feature'
  4. 推送分支:git push origin feature/your-feature
  5. 提交 Pull Request

Commit 规范 ​

使用 Conventional Commits 规范:

类型说明
feat新功能
fix修复 Bug
docs文档更新
style样式调整(不影响功能)
refactor代码重构
perf性能优化
test测试相关
chore构建/工具变更

代码规范 ​

BEM 命名 ​

WarmIsland 使用 BEM(Block Element Modifier)命名规范,前缀为 wi-:

.wi-block
.wi-block__element
.wi-block__element--modifier

示例:

css
.wi-post { }                    /* Block */
.wi-post__title { }             /* Element */
.wi-post__title--large { }      /* Modifier */
.wi-post__content { }           /* Element */
.wi-post__meta { }              /* Element */
.wi-post__meta-item { }         /* Element */
.wi-post__like { }              /* Element */
.wi-post__like--active { }      /* Modifier */

SCSS 规范 ​

  • 使用 SCSS 变量($variable)定义设计 token
  • 使用 CSS 变量(--variable)暴露可覆盖的值
  • 嵌套不超过 3 层
  • 使用 @mixin 封装复用样式
  • 使用 @include 引入混入

Vue 组件规范 ​

  • 使用 Composition API
  • 使用 <script setup> 语法
  • Props 使用 TypeScript 类型定义
  • 事件使用 defineEmits

文件命名 ​

类型规范示例
Astro 组件PascalCasePostCard.astro
Vue 组件PascalCaseTableOfContents.vue
SCSS 文件kebab-case_variables.scss
页面文件kebab-casepage_messageboard.astro
工具文件camelCaseutils.ts

基于 GPL-3.0 许可发布