前阵子要给一款桌面软件配一份「帮助文档」。试了几个现成的自建文档系统,都卡在同一个地方:它们默认你要的是一个文档网站 —— 带页头、侧边栏、导航、页脚、搜索、主题。而我要的恰恰相反:每篇文档就是一份干净的 HTML,软件用 WebView 一载就完事。

于是有了 Qndocs:原生 PHP 写的文档管理系统,后台管内容,前台只输出文档本体。

技术栈

层选型说明
运行环境PHP 8.2 + MySQL 5.7虚拟主机直接丢上去就能跑
后端原生 PHP,无框架不用 Composer,没有 vendor 目录
数据访问PDO 预处理表名前缀用 {table} 占位符自动替换
后台前端原生 JS + CSS 变量无构建步骤,改完刷新即可
编辑器自研富文本(contenteditable)不引第三方编辑器
输出静态 HTML + 标记校验自愈生成的页面本身就是交付物

一句话总结:整站没有一个构建命令,也没有一个第三方依赖。

几个关键取舍

1. 文档 = 独立 HTML,而不是「网站里的一页」

一篇文档有三种取法,都不带页头页脚:

/install.html                  完整 HTML(head + 样式 + 正文)
/install.html?fragment=1       <style> + <article class="qn-doc">正文</article>
/install.html?bare=1           只有 <article class="qn-doc">正文</article>

软件端可以:

  • 直接 WebView 载 ?fragment=1
  • 或者抓整页,取 <article class="qn-doc">…</article>
  • 还有 /api.php 返回 JSON 文档目录(含三种地址),不用把路径写死在代码里

2. 样式作用域化:嵌进别人的界面,不能污染别人

所有规则都限定在 .qn-doc 里,连 CSS 变量也定义在容器上而不是 :root:

.qn-doc {
  --primary: #2563eb;
  --content-width: 860px;
  color: var(--text);
}
.qn-doc h2 { /* … */ }

后台「样式中心」调的变量,最终拼成一段 .qn-doc { --primary: …; } 内联进页面。所以把片段塞进宿主软件,宿主样式不受影响,宿主也不会污染文档。

3. 富文本编辑器自己写

需求是「所见即所得」,但不想为了它带几百 KB 的依赖。思路:

  • 编辑区本身就是一个 .qn-doc 容器 —— 编辑时看到的就是最终效果
  • 工具栏基于 document.execCommand(列表、表格、对齐、颜色、组件…)
  • 图片支持直接拖进编辑区自动上传
  • 粘贴净化:从 Word / 网页粘过来会带一堆 style 和垃圾标签,用 DOMParser 解析后按白名单过滤
var ALLOWED_TAGS = ('P,BR,DIV,SPAN,B,STRONG,I,EM,U,S,H1,H2,H3,H4,UL,OL,LI,'
  + 'BLOCKQUOTE,PRE,CODE,A,IMG,TABLE,THEAD,TBODY,TR,TH,TD,HR').split(',');
// 不在白名单的标签 → 退化成纯文本
// on* 事件属性、javascript: / vbscript: / data: 协议一律清掉

同时保留「源码」按钮,需要精细控制时直接写 HTML。

4. 静态缓存会自己「认领」内容

生成的静态页第一行埋了一个标记:

<!--qndocs:cache  v1.1.0 r0-->

读取时校验它(基准路径 + 版本号 + 当前 URL 形式)。对不上就当无效缓存丢掉,在同一次请求里重新渲染并写回。所以改样式、换域名、切伪静态、升级版本,都不用手动清缓存 —— 访问即自愈。

5. 路径基准不能依赖 SCRIPT_NAME

这个坑踩得很实:静态页是在后台生成的,那时 $_SERVER['SCRIPT_NAME'] 是 /admin/editor.php,dirname() 出来是 /admin,于是页面里所有链接都成了 /admin/admin/、/admin/xxx.html —— 前台点「进入后台」直接 404。

改成三级推导:

// 1) 安装时写入配置的 base_path(最可靠)
// 2) DOCUMENT_ROOT + 项目物理路径
// 3) 兜底:当前脚本路径去掉 /admin 后缀

6. 不假设服务器类型

nginx 不读 .htaccess,Apache 读。所以:

  • 安装时按服务器类型决定伪静态默认值
  • nginx 环境下,伪静态开关需要额外确认「我已配置重写规则」
  • 都没配也照常工作,自动退化成 index.php?p=slug.html

7. 出问题要能看见

生产模式下致命的错误只给用户看白屏,体验太差。所以:

  • 注册 shutdown handler,致命错误时输出可读的错误页(错误信息 + 排查建议 + 日志路径)
  • 所有错误写入 data/logs/app.log,后台「工具 → 运行日志」直接看
  • 放一个 data/debug.lock 空文件可临时打开详细报错

另外做了独立的 /check.php:只用配置文件就能跑,列出 7 张表的结构状态、目录权限、错误日志,并能一键补建缺失的表和字段(只新增,不删数据)。后台打不开时它是唯一的入口。

顺手的配套

  • 安装向导:首次访问自动进入,四步(环境检查 → 数据库 → 站点与管理员 → 完成),预填库名、域名、管理员,装完直接可用
  • 修订历史:每次保存留档,可对比、可回滚
  • 媒体库:上传即用,编辑器里直接插入
  • 备份:后台一键下载 SQL 备份、导出站点 JSON

小结

这套东西的适用场景很明确:你有一批文档要喂给某个程序(软件的帮助、产品的说明、给客户端看的规则),而且不想让文档系统反过来决定你的界面长什么样。

  • 无依赖:PHP + MySQL 丢上去就能跑
  • 输出干净:文档就是文档,没有页头页脚
  • 嵌入安全:样式作用域隔离
  • 维护省心:样式全局统一,缓存自愈

项目地址:doc.qnorg.com(后台在 /admin/)。