三股水
Typecho 自定义 Wiki 知识库页面开发教程
本文从 Obsidian 撰写发布

Typecho 自定义 Wiki 知识库页面开发教程

一、功能概述

这是一个为 Typecho 1.3 开发的独立页面模板,实现了类似 Wiki 的知识库功能:

核心功能

  • 无限级分类菜单:左侧显示指定父级分类下的所有子分类,支持无限级嵌套
  • 分类折叠/展开:每个分类都可以独立折叠或展开,默认全部收起
  • 文章列表:末级分类下方显示该分类下的所有文章列表
  • 无刷新加载:点击文章链接后,右侧内容区无刷新加载文章内容
  • 分类/标签链接:文章内显示所属分类和标签,点击可跳转到对应的分类/标签文章列表
  • 面包屑导航:文章详情页、分类列表页、标签列表页均显示面包屑导航
  • PJAX 兼容:完全兼容主题的 PJAX 无刷新加载

页面布局

  • 整体宽度 1200 px,居中显示
  • 左侧边栏固定宽度 260 px,fixed 定位,滚动时始终可见
  • 右侧内容区占据剩余宽度

二、技术实现要点

1. 分类树构建

Typecho 的分类存储在 table.metas 表中,通过 parent 字段关联父子关系。使用递归函数 buildCategoryTree() 构建无限级分类树。

2. 分类下的文章获取

文章与分类的关联存储在 table.relationships 表中,通过 INNER JOIN 查询获取指定分类下的文章。

3. 文章数据收集

在页面渲染时,一次性收集所有分类下的文章数据,包括:

  • 文章标题、内容、发布时间、更新时间
  • 文章所属分类(categories
  • 文章所属标签(tags

所有数据以 JSON 格式传递给 JavaScript,实现无刷新加载。

4. 服务端 Markdown 渲染

文章内容在服务端使用 Markdown::convert() 渲染为 HTML,避免客户端二次解析。

三、踩坑与解决方案

坑 1:SQL 查询错误 - Column 'category' not found

错误现象

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'category' in 'where clause'

原因:类型cho 的 内容s 表中没有 category 字段,文章和分类的关联通过 relationships 表实现。

解决方案:使用 INNER JOIN 关联查询。

$select = $db->select()
    ->from('table.内容s')
    ->join('table.relationships', 'table.内容s.cid = table.relationships.cid', 'INNER')
    ->where('table.relationships.mid = ?', $catId);

坑 2:类型cho\Db\Query::alias() 方法不存在

错误现象

Call to undefined method 类型cho\Db\Query::alias()

原因:类型cho 的查询构建器不支持 alias() 方法。

解决方案:直接在表名后使用 AS 关键字。

// 错误写法
->from('table.metas')->alias('m')

// 正确写法
->from('table.metas as m')

坑 3:自定义页面带查询参数刷新报错

错误现象

Path '/page-wiki.html' not found
类型cho\Router\Exception: Path '/page-wiki.html' not found

原因:类型cho 的路由器无法正确解析带查询参数(?cid=xxx)的自定义页面 URL。当直接访问 page-wiki.html?cid=269 时,路由器把整个路径当作一个整体来匹配,导致找不到路由规则。

解决方案:使用 Hash(锚点) 代替查询参数。

方案对比说明
?cid=269查询参数会发送到服务器,类型cho 路由器无法解析
#cid=269Hash 部分不会发送到服务器,服务器只收到 page-wiki.html

具体实现

  1. 点击文章链接时,使用 history.replaceState 更新 URL 的 Hash 部分
  2. 页面加载时,JavaScript 从 location.hash 中读取 cid,加载对应文章
  3. 监听 hashchange 事件,支持浏览器前进/后退
// 更新 URL(使用 Hash)
window.history.replaceState({ cid: cid }, '', window.location.pathname + '#cid=' + cid);

// 从 Hash 读取参数
function getParamsFromHash() {
    var hash = window.location.hash;
    if (!hash || hash.length < 2) return {};
    var query = hash.substring(1);
    var params = {};
    var parts = query.split('&');
    for (var i = 0; i < parts.length; i++) {
        var pair = parts[i].split('=');
        if (pair.length === 2) {
            params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
        }
    }
    return params;
}

坑 4:面包屑导航重复显示

错误现象:分类/标签列表页的面包屑显示了两次。

原因initWiki()restoreFromUrl() 中重复调用 addBreadcrumbToList()

解决方案:使用 breadcrumbAdded 标志变量,确保面包屑只添加一次。

var breadcrumbAdded = false;

function addBreadcrumbToList(type, listName) {
    if (breadcrumbAdded) return;
    // ... 添加面包屑
    breadcrumbAdded = true;
}

在 PJAX 完成后重置标志:

function onPjaxComplete() {
    setTimeout(function() {
        breadcrumbAdded = false;
        initWiki();
    }, 50);
}

坑 5:getRequestUri() 生成错误链接

错误现象:点击文章链接后跳转到错误路径。

原因$this->request->getRequestUri() 返回的是带查询参数的完整路径,不适合用于生成链接。

解决方案:使用 $this->permalink 获取当前页面的干净 URL。

// 错误写法
<a href="<?php echo $this->request->getRequestUri(); ?>?cid=<?php echo $p['cid']; ?>">

// 正确写法
<a href="<?php echo $this->permalink; ?>#cid=<?php echo $p['cid']; ?>">

四、关键配置

1. 父级分类 ID

在模板中修改 $wiki_parent_id 变量:

$wiki_parent_id = 43; // 你的 Wiki 根分类 ID

2. 页面缩略名

确保独立页面的缩略名(slug)为 page-wiki,或在 getWikiPageUrl() 函数中修改。

五、最终效果

场景表现
访问 page-wiki.html显示欢迎页,左侧菜单全部收起
点击分类标题展开/收起该分类下的文章列表
点击文章标题右侧无刷新加载文章内容,URL 变为 page-wiki.html#cid=xxx
刷新页面保持当前文章内容,不报错
点击文章中的分类/标签跳转到对应的分类/标签文章列表页
浏览器前进/后退正常恢复文章状态
PJAX 加载兼容主题的 PJAX 无刷新加载

六、经验总结

  1. 了解 类型cho 的数据结构内容s 表存文章/页面,metas 表存分类/标签,relationships 表存关联关系
  2. 避免使用查询参数:类型cho 自定义页面的 URL 尽量不要带查询参数,使用 Hash 是更可靠的方案
  3. 善用事件委托:使用 document.addEventListener('click', function(e) { ... }) 确保 PJAX 加载后事件仍然有效
  4. 数据预加载:在服务端一次性收集所有数据,避免客户端多次请求
  5. 标志变量防重复:在动态添加 DOM 元素时,使用标志变量防止重复添加
  6. 兼容性优先:始终考虑 PJAX 兼容性,在 pjax:complete 事件中重新初始化 JavaScript
😊
提交