我为小说忙:(二)插件的几个前端页面

『我为小说忙』第二篇,接着上文的话题继续写。

在确定了 TPNovel 大致的开发路径之后,就可以动手写代码了。由于已经有了Typecho作为底子(以及数据源),因此第一步就先从比较简单的前端页面开始处理。

图文无关,但当用户不了解网站开发时,似乎很容易出这种事故图文无关,但当用户不了解网站开发时,似乎很容易出这种事故

1. 前言

小说阅读页与博客页,还是有一些不同的,这主要还是源自两种文体之间固有的一些差异。之前提到过,我对于这两者最大的感受就是,博客偏理性/工具化,小说偏感性/日常化。展开来说就是,博客文章更多的是针对某件事情的一个理性探讨,读者一般来说追求的都是高信息密度(因为他有可能是带着问题来的),有点类似工具书这样的东西;而小说,更多地还是作为休闲读物,有更大的可能性会『让人沉浸式阅读』,『让人一篇一篇地接着读』,『普通的读者也会来读』,『在零碎时间去读』。

正是因为这个关键区别,在对页面进行设计时,就不能按博客主题的思路去设计了。参考目前主流小说站的样式可以发现,他们为了增加阅读舒适度,普遍采用了适当加大行距,加大字号,适当降低亮度/对比度避免刺眼(但不要太低)等措施。因此,TPNovel的页面设计工作,也可以参考一下这些被普遍应用的设计,『摸着石头过河』。

2. Typecho的主题模板

但在此之前,先问个问题:Typecho 允许对输出的HTML操作到什么程度?

目前使用的是Typecho 1.3,请出老朋友Xdebug,转到var/Widget/Archive.php,在1327行附近,注释为『自定义模板』的地方下一个断点,然后用curl -v -N打开任意一个使用了模板的页面,就可以停在如下代码处:

//~ 自定义模板
if (!empty($this->themeFile)) {
    if (file_exists($this->themeDir . $this->themeFile)) {
        $valid = true;
    }
}

// 中间省略其他判断代码
require_once $this->themeDir . $this->themeFile;

可以看到,这里是直接把对应的主题文件(实际上是模板文件)给加载了进来。此时如果你观察控制台窗口,会发现没有任何输出,直到你让Xdebug继续执行,才会跳转到相关的模板文件。

以笔者目前所用的主题为例,跳转到模板文件后,往下翻几行就能看到:

<?php $this->need('header.php'); ?>

就跳转到了主题自带的header.php,里面进去就能看到标准的HTML标头,这就可以说明,HTML输出是由主题来负责的。严格来说,是至少负责到<head>区域的部分内容,因为在此区块的末尾,主题文件一般都会调用$this->header(),让Typecho输出一部分系统自带的head,不过毕竟这个调用操作也是主题来进行的,所以也可以认为是主题说了算。

既然知道了 Typecho 允许让主题全权负责 HTML,那么开发小说页面也就很容易了:只需要把自己写的页面标注为『模板』,就可以完全接管HTML输出,实现自己的样式。

如何标记为模板呢?这里有个很方便的查找方法。在站点后台编辑的时候,不难注意到,独立页面的编辑器右侧允许你选择使用哪一个主题模板:

如图所示如图所示

转到admin/write-page.php,也就是后台编写独立页面的地方,全局搜索『自定义模板』,就可以找到输出这个模板列表的代码:

<?php
    $templates = $page->getTemplates();
    foreach ($templates as $template => $name):
?>
<!-- 中间省略输出代码 -->
<?php endforeach; ?>

重点来看getTemplates()是怎么实现的,点击跳转到var/Widget/Base/Contents.php

// 省略获取目录下所有PHP文件的代码
foreach ($files as $file) {
    $info = Plugin::parseInfo($file);
    $file = basename($file);

    if ('index.php' != $file && 'custom' == $info['title']) {
        $result[$file] = $info['description'];
    }
}

Plugin::parseInfo就不放上来了,因为这是一个PHP Tokenizer。进去之后就是一个token_get_all()函数,将文件内容过一遍分词器,然后寻找并拆分T_DOC_COMMENT(PHPDoc,也就是文档注释)里面的@package字段,映射到$info['title']。外面再判断值,如果是custom,就认为这是模板文件。

说人话就是,通过这样操作,插件/模板/主题的元数据,可以合并在指定文件的文档注释里,而非单独装一个配置文件。至于为什么要这么折腾,根据笔者查询到的资料发现,Typecho开发初期最流行的CMS系统,也就是WordPress,就是这么干的,于是可能Typecho就学了过来。顺带一提,这种获取元数据的性能,恐怕不会太高,但考虑到不是每次都要调用,所以可能问题不大。

换句话说,如果我们需要添加新的模板文件,就需要做到:

  • 主题目录下非index的.php文件
  • 开头包含 PHPDoc
  • PHPDoc 中包含 @package custom
  • 其余普通注释内容作为模版描述

为了验证这一猜想,可以来做一个小实验。在主题目录下新建一个PHP文件,名字可以任取(除index.php外),比如我们取名叫tpNovel.php,内容如下:

<?php
/**
 * 小说
 *
 * @package custom
 */
?>

<?= $this -> title; ?>
<?= $this -> content; ?>

保存之后,刷新编辑器页面,马上就可以看到,多出了一个可选的『小说』模板。

如图所示如图所示

在左侧的编辑器里随便写点东西,然后保存并发布。

内容是什么无所谓,测试而已内容是什么无所谓,测试而已

最后打开这个页面,就可以看到预期的结果,也就是没有任何样式的,没有原来的主题的样子的,甚至没有HTML标头的,干净的独立页面:

查看源代码会更明显,这里的p标签是Markdown解析的结果查看源代码会更明显,这里的p标签是Markdown解析的结果

所以,我们的开发思路也就很明确了:

  1. 先编写各个页面
  2. 在开头用 PHPDoc 标注为模板
  3. 在各个地方添加上获取页面内容的语句

就可以用 Typecho 来生成小说页面了。

3. 页面设计

笔者虽然有一定的前端开发能力,但这种能力更像是以前被笑称为『切图仔』的开发人员的活,也就是给我一个图片,能大致模仿出来,但如果让我从头设计一个页面,就是心有余而力不足。作为参考,笔者在小学美术课学不会画画,中学美术课还是不会画画,可以说是完全没有艺术细胞了。

不过将军莫虑,近有西洋秘术唤作『歪脖扣钉』者,可解救于水火之中。正好前段时间给DeepSeek的API充了点钱,不用白不用,直接打开OpenCode,把自己的需求发给他,让他给我起草页面,写响应式的各种规则什么的,自己再针对具体业务需求修改细节部分,岂不美哉?

而且价格也不贵,才几毛钱而且价格也不贵,才几毛钱

所以今天博文的主要内容就是,聊聊前端页面大概是怎么设计的,以及穿插讲一点元数据存储的事情。后者的话,在插件本体代码部分还会有详细讲述,因此这里就不多提了,大概有个印象就行。

3.1 阅读页

阅读页其实没有什么特殊的地方,直接看图吧、

就是普通的阅读页就是普通的阅读页

标题的输出,就是调用上面提到的$this -> title;,复用独立页面的标题,正文内容也同理。不过如图所示,页面上方有书名,这在Typecho里就没有直接可用的字段了。你可能会想到我在上一篇文章里面提到的『自定义字段』,这是对的,但并非直接在字段里存书名,而是存『小说ID』,插件代码会据此多做一次数据库查询,去目录页取具体的书本标题。关于这一点,下面会讲,所以此处暂时略过。

另外,笔者比较坚持的一个观点是,尽可能少用JavaScript。所以页面上的『上一章』『目录』『下一章』按钮(包括页面滚动到最下面的三个按钮),其实就是<a>,指向另外的页面,而不是做成SPA然后通过js加载,确保基础阅读功能不依赖js,这对于部分默认禁用js的用户来说会更友好。

当然,少用并非完全不用,一些超出了HTML范围的东西还是有必要用一下的,比如说设置:

阅读页的设置阅读页的设置

对话框本身基于HTML的<dialog>元素,但开关对话框,切换字号与亮暗色,以及把用户设置储存到localStorage,这部分用了一点js。所以在关闭js的情况下,页面会固定为使用亮色,中等字号。虽然这个问题有很多办法可以解决,比如说把设置内容也存一份到cookies,让后端预先返回带有特定属性的HTML元素,等等,但考虑到开发时间成本的权衡(适当用一点js减少心智负担),以及默认不启用js的用户数量占比较低,最后还是没有实现这一点。

另外,页面上的其他按钮,也就是实现为链接的那些,是需要通过插件来计算具体链接的,这里同样略过,下次再讲。

3.2 目录页

目录页,顾名思义,就是用来展示书本目录与封面的。

目录页大概长这样,因笔者不会画画,封面为AI生成目录页大概长这样,因笔者不会画画,封面为AI生成

从前端页面上来看,同样没有什么特别的,估计在其他的小说阅读网站上也能找到类似的东西。右上角的『暗色』按钮,顾名思义,就是切换亮暗色,这和阅读页的亮暗色切换是同步的,免得用户读一半出来被闪瞎。

页面下方是章节选择,但再往下,就没有了。原本设计有(包括让DS写了)书评区,这就直接对应Typecho的评论系统,然而适配评论区有个问题:之前文章讲过的Typecho反垃圾评论系统,依赖一段混淆后的js代码插入一个token。虽然这个保护很弱鸡,但总比没有好,所以如果想要在不关闭这个保护的情况下正常使用评论区,要么就得拿到这个token来自己混淆,要么就得用Typecho的header输出函数并剥离不需要的部分(也就是传入参数,关掉一大堆在小说区用不到的header)。权衡过开发难度之后,决定还是先不考虑书评区,等系统上线后再做打算。

页面之外,有暗藏玄机的地方。如果打开后台编辑器页面,或者打开数据库看typecho_fields表,会发现有奥妙:

后续可能会根据开发进程,适当增减字段后续可能会根据开发进程,适当增减字段

上面讲阅读页的时候提到,需要在数据库多查一次,原因就是在这里。为了避免额外添加数据表(及配套的书本管理页面),TPNovel选择把小说的元数据都存储在目录页的自定义字段里,在数据库中就表现为KV(其实更像EAV)记录。对于具体的阅读页,就只有一个自定义字段tpn_bookID,指向目录页的cid,然后就可以在插件中进行查询了。

举例来说,如上图所示,只要在阅读页指定tpn_bookID == 1051,插件就会执行查询:

SELECT * FROM `typecho_fields` WHERE `cid` = 1051 AND `name` LIKE 'tpn_%';

拿到结果之后,将name作为键,根据type选择对应字段作为值,存入PHP数组中,就可以在后续流程里很方便地获取元数据了。虽然总体来看会比直接在阅读页的字段里取值要多绕一步,但结果是利大于弊的,况且还可以想办法做缓存,因此也就无所谓了。

3.3 书架页

顾名思义,书架页就是展示所有小说书本的页面,非常简单。

恐怕简单程度比阅读区还要高恐怕简单程度比阅读区还要高

初中时候学校让买的学习平板,里面的书架页美观多了,真就有个仿真的木头书架。然而对于笔者这个,可能顶多也就放两本书的页面来说,做太好看反而复杂了,还不如就这样简洁一些。

亮暗色按钮依然要依赖js去读写,但跳转到小说详情页不用。整个小说卡片就是一个<a>,作用等效于链接,笔者习惯的『鼠标中键打开新标签页』在这里是同样适用的。

数据方面,这个页面并没有什么太多额外的内容,在后台只需新建一个空白的独立页面,模板设置好,直接发布即可。到时候会调用插件代码进行数据查询与渲染的,比如说获取所有小说列表,可能类似于:

SELECT *
FROM `typecho_fields`
WHERE `cid` IN (
    SELECT `cid`
    FROM `typecho_fields`
    WHERE `name` = 'tpn_bookName'
);

所以也很简单。

4. 写在最后

前端页面的设计大致就是这样。

你会发现,上面介绍的过程其实有些别扭,感觉有些地方的数据不太对劲。这实际上是因为,这些地方预计是使用插件来处理的,目前你看到的只是占位符罢了。下一篇文章将会开始记录如何编写插件代码,所以今天就先略过了。

(完)


木头箱子脆脆,但是这样正好

如无特殊声明,本站内容遵循 CC BY-NC-SA 4.0 协议

转载请注明出处并保留作者信息,谢谢!

本站由 搬瓦工VPS 强力驱动

none
最后修改于:2026年07月14日 22:24

添加新评论

提醒:『评论回复邮件提醒』功能正在测试中
评论后,如果站长有回复,会有邮件通知