*此博文由博主提供markdown导纲,具体内容由DeepSeek撰写

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

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

现在它开源了:github.com/QnMax-Hub/Qndocs —— MIT 许可,零依赖,把目录丢到 Web 根目录、浏览器访问一次走完安装向导就能用。

技术栈

层

选型

说明

运行环境

PHP 8.2 + MySQL 5.7

虚拟主机直接丢上去就能跑

后端

原生 PHP,无框架

不用 Composer,没有 vendor 目录

数据访问

PDO 预处理

表名前缀用 {table} 占位符自动替换

后台前端

原生 JS + CSS 变量

无构建步骤,改完刷新即可

编辑器

自研富文本(contenteditable)

不引第三方编辑器

输出

静态 HTML + 标记校验自愈

生成的页面本身就是交付物

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

开源前又推了一步:三层自由

整理代码准备开源的时候发现,「只要正文」这个诉求其实分三个层次,于是把它们都做成了可选项 —— 同一份内容,任意组合。

站点级:输出模式

  • 独立文档(默认):每篇只有正文,没有页头、页脚、侧栏、导航、脚本,天生就是给软件嵌的;

  • 整合文档站:页头 + 侧边文档树 + 右栏本页目录 + 面包屑 + 上一篇/下一篇 + 页脚,用浏览器当一整套文档站浏览。

后台点一下就切换,静态页自动重建(缓存标记里带了模式位),内容一个字都不用改,连配色变量都是两边共用。

文档级:内容类型

  • 富文本:所见即所得,存成 HTML;

  • 纯文本:等宽文本框,缩进和空行一个字符都不改,前台渲染成 <pre class="qn-text"> —— 放配置文件、命令、日志正合适。

URL 级:取用形式

整页 / 片段 / 纯正文 / 纯文本四种取法,任何模式下都只返回内容本体,永远不会把整站框架带回来。

几个关键取舍

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>
/install.html?text=1           纯文本(text/plain,标签全去掉)

软件端可以:

  • 直接 WebView 载 ?fragment=1

  • 或者抓整页,取 <article class="qn-doc">…</article>

  • 只要文字就抓 ?text=1,喂给终端或别的程序

  • 还有 /api.php 返回 JSON 文档目录(含四种地址),不用把路径写死在代码里

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

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

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

后台「样式中心」调的变量,最终拼成一段 .qn-doc { --primary: …; } 内联进页面。所以把片段塞进宿主软件,宿主样式不受影响,宿主也不会污染文档。整合文档站的框架样式则单独放在 site.css,选择器全部限定在 .qn-site 内。

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.2.0 r0 m s-->

读取时校验它(基准路径 + 版本号 + 伪静态状态 + 输出模式 + 当前 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 丢上去就能跑

  • 输出干净:文档就是文档,没有页头页脚

  • 嵌入安全:样式作用域隔离

  • 维护省心:样式全局统一,缓存自愈

项目已开源:github.com/QnMax-Hub/Qndocs,MIT 许可,欢迎 Issue 与 PR。