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