本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:20260806备份/htmx+Alpine.js 重构计划(修正版).md。
SoSite — htmx + Alpine.js 重构计划(修正版)
基于 2026-07-20 实际改造经验编写
核心教训:不要引入 Tailwind CSS,只做交互和 AJAX 的统一
目标:消除零散 JS、统一交互模式,保持原有 CSS 体系不变
目录
- 血的教训
- 改造范围(只改什么)
- 改造范围(不改什么)
- 第一阶段:引入基础环境
- 第二阶段:Alpine.js 替换交互
- 第三阶段:htmx 替换 AJAX
- 第四阶段:收尾清理
- 本次改造的实际文件变更清单
- 注意事项与坑
- 改造对照表
1. 血的教训
1.1 不要引入 Tailwind CSS ❌
原因:SoSite 是单人维护的轻量级 PHP 社区,已经有自洽的 mn-* 类名体系和合理的 CSS 文件拆分(layout.css/forum.css/blog.css 等)。Tailwind 的收益(团队协作统一规范)在这个场景下为零,但代价(两套体系混用、类名变长、构建工具链)是实实在在的。
结论:CSS 体系保持原样不动,所有 mn-、c-、fs-* 类名保留,不替换为 Tailwind。
1.2 不要做"机械替换" ❌
原因:把 mn-flex-center 改成 flex items-center 只是换了命名方式,没有解决任何实际问题。页面看起来一样,加载速度一样,但维护者需要同时记住两套体系。
结论:只改交互逻辑,不改样式类名。
1.3 改造前先想清楚收益
每次改动前问自己三个问题:
- 改了这个,用户能感知到吗? → 不能就不改
- 改了这个,维护成本降低了吗? → 没降低就不改
- 改了这个,后续扩展更方便吗? → 没变好就不改
2. 改造范围(只改什么)
2.1 值得改的 ✅
| 改造项 | 原因 | 收益 |
|---|
onclick="confirm(...)" → Alpine x-on:submit.prevent | 消除 30 处内联事件 | 交互统一 |
onclick="toggleNav()" → Alpine x-on:click | 消除 nav.js 依赖 | 少一个 JS 文件 |
onclick="showTab()" → Alpine x-on:click + x-show | 消除 Tab 切换的零散 JS | 交互统一 |
onclick="toggleVote()" → htmx hx-post | 消除 vote.js 依赖 | 少一个 JS 文件 |
onclick="toggleAll()" → Alpine x-on:change | 消除全选/取消的零散 JS | 交互统一 |
骰子 XMLHttpRequest → htmx hx-post | 消除 AJAX 样板代码 | 统一 AJAX 模式 |
猜题 XMLHttpRequest → htmx hx-post | 消除 AJAX 样板代码 | 统一 AJAX 模式 |
签到表单提交 → htmx hx-post | 消除页面刷新 | 局部刷新 |
模态框显隐 → Alpine x-show | 消除模态框的零散 JS | 交互统一 |
2.2 引入的库
| 库 | 版本 | 大小 | 用途 |
|---|
alpine.min.js | v3.15.x | ~10KB gzip | 所有交互行为 |
htmx.min.js | v2.x | ~14KB gzip | 所有 AJAX 请求 |
3. 改造范围(不改什么)
3.1 不动 CSS 体系 ❌
- 所有
mn-* 类名保留(mn-btn、mn-section、mn-flex-center 等) - 所有
c- / fs- 类名保留(c-999、fs-14 等) - 8 个旧 CSS 文件不动(
style.css、layout.css、forum.css、pages.css、components.css、blog.css、home.css、modern.css、admin.css) - 不引入 Tailwind CSS,不生成 SoCSS,不构建任何 CSS 工具链
3.2 不动动态 PHP 颜色 ❌
style="color:<?php echo $thread['color']; ?>"style="background:<?php echo $tcfg['color']; ?>"- 这些是 PHP 动态生成的,无法用类名替代,保持原样
3.3 不动编辑器 ❌
- CKEditor 保持独立,不做任何改造
editor.js、editor.css 不修改
3.4 不动图标系统 ❌
- FontAwesome 4.x 保持原样
$this->icon() 方法不修改
4. 第一阶段:引入基础环境
4.1 复制静态文件
assets/lib/
├── htmx.min.js ← v2.x
└── alpine.min.js ← v3.x
4.2 在 layouts 中引入
两个 layout 都要加:
assets/themes/modern/templates/layouts/main.phpapp/Views/layouts/main.php
在 <head> 中添加:
4.3 CSP 安全策略(重要!)
Alpine.js 需要 unsafe-eval 才能运行,如果服务器有 CSP 头,需要在 app/Core/Controller.php 中添加:
header("Content-Security-Policy: script-src 'self' 'unsafe-eval' 'unsafe-inline'; ...");
如果不加,Alpine 的 x-data 和 x-on:click 会完全不工作,且控制台无报错。
5. 第二阶段:Alpine.js 替换交互
5.1 替换 confirm 确认弹窗(~30 处)
涉及文件:
app/Views/admin/blog_categories.phpapp/Views/admin/categories.phpapp/Views/admin/database.phpapp/Views/admin/tags.phpapp/Views/admin/threads.phpapp/Views/admin/trashed.phpapp/Views/admin/users.phpapp/Views/message/inbox.php、sent.php、view.phpapp/Views/plugins/content_review/notifications.phpapp/Views/plugins/content_review/admin.phpapp/Views/plugins/friend_links/admin.phpassets/themes/modern/templates/thread/show.phpapp/Views/thread/show.php
改造前:
改造后:
5.2 替换模态框(~10 处)
涉及文件:
app/Views/plugins/announcements/admin.php + Modern 版app/Views/plugins/friend_links/admin.php + Modern 版app/Views/plugins/content_review/admin.php + Modern 版
改造前:
改造后:
5.3 替换导航切换(4 处)
涉及文件:
app/Views/layouts/main.php + Modern 版
改造前:
菜单
改造后:
菜单
5.4 替换 Tab 切换(3 处)
涉及文件:
assets/themes/modern/templates/profile/index.phpapp/Views/profile/index.php
改造前:
基本资料
...
改造后:
5.5 替换全选/取消(4 处)
涉及文件:
app/Views/admin/blog_manage.phpapp/Views/admin/threads.phpapp/Views/plugins/content_review/admin.php + Modern 版
改造前:
改造后:
5.6 关于 x-show 与 CSS 冲突的注意事项
重要:如果现有 CSS 用 .父类.子类 { display: block; } 模式控制显隐(如下拉菜单),不能直接用 x-show,因为 Alpine 移除内联 display:none 后,CSS 样式表中的 display:none 权重更高,导致菜单永远不显示。
正确做法:用 :class 在父容器上绑定类,由现有 CSS 机制控制显隐。
切换
切换
6. 第三阶段:htmx 替换 AJAX
6.1 骰子游戏(1 处)
文件:plugins/dice/index.php(两个模板)
改造前:plugins/dice/assets/script.js 中有 XMLHttpRequest。
改造后:
注意:骰子动画的 CSS 类(rolling)保留,不可删除。
6.2 猜题对决(2 处)
文件:plugins/quiz_duel/create.php、detail.php
改造前:plugins/quiz_duel/assets/script.js 中有 XMLHttpRequest。
改造后:
发布挑战
6.3 每日签到(1 处)
文件:plugins/daily_checkin/index.php(两个模板)
改造前:表单 POST 提交,页面完全刷新。
改造后:
立即签到
6.4 投票(1 处,在 thread/show.php 中)
文件:assets/themes/modern/templates/thread/show.php、app/Views/thread/show.php
改造前:vote.js 中的 toggleVote() 函数。
改造后:
<?php echo $this->icon('like', 14); ?>
<?php echo (int)($tv['likes'] ?? 0); ?>
注意:后端接口需要返回 HTML 片段而非 JSON。对应的 Vote.php 或 ApiController.php 需要调整。
7. 第四阶段:收尾清理
7.1 删除废弃文件
# 已废弃的 JS 文件(功能已被 Alpine/htmx 替代)
assets/js/nav.js ← 导航切换已用 Alpine 替代
assets/js/vote.js ← 投票已用 htmx 替代
# 插件 JS 文件(功能已被 htmx 替代)
plugins/dice/assets/script.js ← 骰子 AJAX 已用 htmx 替代
plugins/quiz_duel/assets/script.js ← 猜题 AJAX 已用 htmx 替代
7.2 删除旧 layouts 中的 JS 引用
从两个 layout 的 main.php 中移除:
7.3 删除 ziyuan 目录(已用完的资源包)
确认 ziyuan/ 目录中的资源已复制到 assets/lib/ 后,可以删除。
8. 本次改造的实际文件变更清单
8.1 新增文件
| 文件 | 说明 |
|---|
assets/lib/alpine.min.js | Alpine.js 运行时 |
assets/lib/htmx.min.js | htmx 运行时 |
assets/lib/so-state.js | Alpine.js 别名(可选) |
assets/lib/so-fetch.js | htmx 别名(可选) |
8.2 修改文件
Alpine.js 改造(~35 个文件):
| 文件 | 改造内容 |
|---|
app/Views/layouts/main.php | 引入 Alpine + htmx,导航切换 Alpine 化 |
assets/themes/modern/templates/layouts/main.php | 同上 |
app/Views/admin/*.php(约 15 个) | confirm 弹窗 Alpine 化 |
app/Views/plugins/*/admin.php(约 6 个) | confirm 弹窗 + 模态框 Alpine 化 |
app/Views/message/inbox.php、sent.php | confirm 删除 Alpine 化 |
app/Views/profile/index.php | Tab 切换 Alpine 化 |
assets/themes/modern/templates/profile/index.php | Tab 切换 Alpine 化 |
htmx 改造(~5 个文件):
| 文件 | 改造内容 |
|---|
plugins/dice/index.php(两个模板) | 骰子 AJAX htmx 化 |
plugins/quiz_duel/create.php、detail.php | 猜题 AJAX htmx 化 |
plugins/daily_checkin/index.php(两个模板) | 签到 AJAX htmx 化 |
assets/themes/modern/templates/thread/show.php | 投票 htmx 化 |
app/Views/thread/show.php | 投票 htmx 化 |
8.3 删除文件
| 文件 | 原因 |
|---|
assets/js/nav.js | 导航切换已用 Alpine 替代 |
assets/js/vote.js | 投票已用 htmx 替代 |
plugins/dice/assets/script.js | 骰子 AJAX 已用 htmx 替代 |
plugins/quiz_duel/assets/script.js | 猜题 AJAX 已用 htmx 替代 |
9. 注意事项与坑
9.1 CSP 安全策略(踩坑记录)
症状:Alpine 的下拉菜单、弹窗完全不工作,浏览器控制台无报错。
原因:服务器 CSP 头缺少 'unsafe-eval',Alpine 的 x-data 解析需要 eval。
修复:在 app/Core/Controller.php 中修改 CSP 头。
9.2 x-show 与 CSS display:none 冲突(踩坑记录)
症状:用 x-show 控制显隐的元素一直不显示。
原因:现有 CSS 中有 .dropdown-menu { display: none; },权重高于 Alpine 的内联 display。
修复:改用 :class 在容器上绑定类名,由 CSS 控制显隐。
9.3 投票按钮图标不一致
症状:Vote::renderButtons() 用 SVG 图标,视图用 FontAwesome 图标,样式不一致。
修复:统一为 FontAwesome 图标。
9.4 投票二次点击无效
症状:第一次点击投票正常,第二次点击无效。
原因:htmx 的 hx-vals 中缺少 CSRF token。
修复:在 hx-vals 中加入 csrf 参数。
9.5 猜题按钮文字乱码
症状:猜题按钮显示乱码。
原因:x-text 属性中包含 PHP 图标 HTML 代码,破坏了属性解析。
修复:移除 x-text,保留 :disabled。
9.6 骰子无效押注类型
症状:骰子投注时提示"无效押注类型"。
原因:bet_type 和 amount 控件在 <form> 外部。
修复:将控件移入 <form> 内。
9.7 默认模板变宽
症状:改造后默认模板的页面宽度变宽了。
原因:Tailwind 的 .container 类覆盖了原有布局的 max-width。
修复:在 tailwind.config.js 中设置 container: false(如果引入了 Tailwind)。
10. 改造对照表
10.1 内联事件替换对照表
| 当前模式 | 替换为 | 数量 |
|---|
onsubmit="return confirm('...')" | x-on:submit.prevent="if(!confirm('...')) return; $el.submit()" | 30 处 |
onclick="closeModal()" | x-on:click="open = false" | 10 处 |
onclick="toggleNav(event)" | x-on:click="$refs.nav.classList.toggle('open')" | 4 处 |
onclick="showTab('info')" | x-on:click="tab = 'info'" | 3 处 |
onclick="toggleVote(this, 1)" | hx-post="/api/vote" hx-vals='{"vote":"1"}' | 8 处 |
onchange="toggleAll(this)" | x-on:change="document.querySelectorAll(...).forEach(...)" | 4 处 |
10.2 AJAX 替换对照表
| 功能 | 当前方式 | 替换为 |
|---|
| 骰子投注 | XMLHttpRequest + JSON | hx-post="/dice/play" + HTML 片段 |
| 创建挑战 | XMLHttpRequest + JSON | hx-post="/duel/create" + HTML 片段 |
| 应战 | XMLHttpRequest + JSON | hx-post="/duel/accept" + HTML 片段 |
| 签到 | 表单 POST + 页面刷新 | hx-post="/checkin" + 局部刷新 |
| 投票 | XMLHttpRequest + JSON | hx-post="/api/vote" + HTML 片段 |
附录 A:Alpine.js 常用指令速查
| 指令 | 用途 | 示例 |
|---|
x-data | 定义组件数据 | x-data="{ open: false }" |
x-show | 显隐控制 | x-show="open" |
x-text | 设置文本 | x-text="count" |
x-on:click | 点击事件 | x-on:click="count++" |
x-on:submit.prevent | 表单提交 | x-on:submit.prevent="handle()" |
:class | 动态 class | :class="{ 'active': tab === 'info' }" |
x-ref | 引用元素 | $refs.myEl |
x-on:click.outside | 点击外部 | x-on:click.outside="open = false" |
x-on:keydown.escape | 键盘事件 | x-on:keydown.escape.window="open = false" |
附录 B:htmx 常用属性速查
| 属性 | 用途 | 示例 |
|---|
hx-get | GET 请求 | hx-get="/api/data" |
hx-post | POST 请求 | hx-post="/api/submit" |
hx-target | 目标元素 | hx-target="#result" |
hx-swap | 替换方式 | hx-swap="innerHTML" |
hx-trigger | 触发方式 | hx-trigger="click" |
hx-vals | 附加数据 | hx-vals='{"key":"value"}' |
hx-confirm | 确认弹窗 | hx-confirm="确定?" |
hx-indicator | 加载指示器 | hx-indicator="#spinner" |
附录 C:核心原则(下次改造必读)
- 只解决实际问题,不为技术而技术
- 不改 CSS 类名——你的
mn-* 体系是自洽的,不需要换 - 不改内联样式——动态 PHP 颜色保留
- 不改编辑器——CKEditor 保持独立
- 不改图标系统——FontAwesome 保持原样
- 每次改完跑一遍全站——确认功能正常再继续
- 不要引入任何构建工具链——SoSite 是 PHP 项目,不需要 npm/Webpack/Vite
- 如果某个改动只是"换了个写法"但没有实际收益,那就不要做