文章

Halo 插件开发实录:从 HTML 项目预览插件到应用市场发布

项目背景

近期在折腾 Halo 这款开源建站工具,我需要一个把纯前端项目(zip 包)一键发布为静态站点的插件——上传 zip 后立刻获得 /preview/{slug}/ 公开访问链接。本文记录 halo-plugin-html-preview 从需求到上架应用市场的完整开发历程。

需求迭代:从最小可用到体验完整

初版只做了最朴素的两件事:上传 zip 解压 + 路由公开访问。但实际使用中暴露出三个明显痛点:

  • 路径用 8 位随机串,记不住、跟项目对不上号

  • 管理页只是表格,信息密度低、不好看

  • 上传错了就只能删了重来,公开链接会失效

针对这三点,本轮迭代做了对应的增强:

  • 自定义 slug:上传时可指定 slug,正则 ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ 校验,冲突直接报错

  • 响应式卡片网格 UI:统计卡 + 项目卡,每张卡含预览 / 复制链接 / 更新 / 删除四个图标按钮

  • 在线更新:新增 PUT /projects/{name}/replace,重传 zip 即替换原项目,slug 保持不变,公开链接不失效

技术实现要点

后端:HtmlProjectService

upload 与 replace 共用核心逻辑,只在元数据落库时走不同分支:

  • 自定义 slug 走 client.fetch 查重,冲突抛 400

  • 未传 slug → 8 位随机串(36 字母表,冲突概率约 1/2.8T)

  • replace 删除旧目录 → 解压新 zip → 复用 metadata.versionclient.update

解压安全

zip 解压逐 entry 校验,以下情形一律拒绝:.. 路径穿越、绝对路径、反斜杠、NUL 字符。同时设上限:单 entry ≤ 50 MB、文件数 ≤ 5000、解压总大小 ≤ 200 MB。

前端:ProjectManagementTab.vue

@halo-dev/ui-plugin-bundler-kit 打包 IIFE 格式,基于 Semi Design 组件库。上传 / 更新共用同一个对话框,slug 输入框带 /preview/[slug]/ 前后缀提示,实时转小写、剔除非法字符;选择 zip 后从文件名自动猜测 slug。

交付材料清单

本项目一次性交付了下列资产,可作为同类 Halo 插件项目的参考清单:

类别

文件 / 资产

源码

Java + Vue 单仓库,build.gradle 统一构建

插件元数据

plugin.yaml(homepage / repo / issues / license / logo)

构建脚本

build.gradle 支持 -Pversion 覆盖,适配 Release tag

CI/CD

接入 halo-sigs/reusable-workflows 的 plugin-ci@v4 与 plugin-cd@v4

许可证

LICENSE(MIT)

图标

src/main/resources/logo.png(256×256)

截图

6 张:插件列表 / 详情 / 项目管理 / 上传框 / 预览站点 / 子页面

文档

README(特性 / API / 安装升级卸载 / 隐私说明 / 第三方服务披露)

发布到 GitHub

仓库地址:github.com/boommanpro/halo-plugin-html-preview

  • 初始化 git,补 .gitignore、README、LICENSE

  • CI 接入官方 reusable-workflows,推送即触发构建

  • CD 触发条件是 release: [published],需手动打 tag 并发布 Release

发布到 Halo 应用市场

官方流程文档:发布应用 | Halo 文档。关键步骤:

  1. 申请开发者:halo.run/uc/developer/join

  2. 创建应用,填写名称 / Logo / 简介 / 开源仓库 / 许可证

  3. 创建首个版本,版本号与 gradle.properties 一致(0.1.0),Halo 兼容范围与 plugin.yaml 一致(≥2.20.0)

  4. 打 tag 并发布 GitHub Release → cd.yaml 自动构建上传 jar

  5. 把 jar 上传到应用市场版本制品,提交审核

CD 工作流首次审核前 skip-appstore-release: true,避免在未审核通过前自动同步到应用市场;审核通过后配置 HALO_PAT secret 与 app-id,后续版本即自动同步。

踩坑记录

  • version 赋值位置:在 build.gradle 里写 version = project.findProperty('version') ?: '0.1.0' 会被 devtools 插件的 generatePluginComponentsIdx 任务报 Project version must be set。最终方案是放到 gradle.properties,命令行 -Pversion 会覆盖 properties 里的同名键。

  • reusable-workflows 默认值:plugin-ci / plugin-cd 默认 ui-path: console、Node 18、Java 17,我的插件 UI 在 ui/、用 Java 21 + Node 22,必须显式覆盖。

  • CD 不触发:cd.yaml 是 on: release: [published],只 push commit 不创建 Release 不会触发,需要打 tag + 在 Releases 页面点 Publish。

下一步

  • 完善应用市场审核所需的截图与说明文案

  • 审核通过后接入 appstore 自动同步

  • 考虑支持单项目独立密码、自定义 404 页等增量

仓库持续更新:github.com/boommanpro/halo-plugin-html-preview,欢迎 issue 与 PR。