ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Typecho主题开发核心:调用函数体系与Widget用法详解

Typecho主题开发核心:调用函数体系与Widget用法详解 1. 认识Typecho的调用函数体系做Typecho主题开发的朋友迟早会碰到一个绕不开的问题模板里到处都是$this-xxx()和Typecho_Widget::widget(xxx)第一次接触时确实容易懵。和WordPress那套直接在模板里塞PHP函数、全局变量满天飞的做法相比Typecho的调用函数体系走的是另一条路——它把所有数据都封装在Widget层模板页通过继承同一个对象来访问当前页面的数据静态方法则用来获取全局数据。搞懂这套体系主题开发基本就通了一半。先说说这套设计的核心逻辑。Typecho的每个页面首页、文章页、分类页、搜索页都会在路由解析时实例化一个Widget_Archive对象这个对象里装着当前页面需要的一切文章列表、当前文章详情、分页信息、SEO标题等等。而模板文件里看到的$this其实就是这个对象的引用。所以你在index.php里用$this-title()能拿到文章标题在archive.php里用$this-archiveTitle()能拿到分类页的标题——这不是什么魔法是同一个对象在不同场景下存储了不同数据。这种设计带来的直接好处是不需要像WordPress那样记忆一长串get_the_title()、the_content()之类的全局函数名所有数据来源统一不会出现“这里能取到、那里取不到”的诡异问题主题代码非常干净一个模板文件里几乎看不到require、include所有子模板通过$this-need(xxx.php)引入如果你以前学过C语言函数的定义、调用、参数与返回值理解Typecho这套东西会更快$this-content()相当于一个“方法”它内部封装了数据库查询、HTML过滤、摘要截断的逻辑你只管调用不用关心它怎么实现的。这种“面向对象调用”的思路贯穿整个Typecho主题开发也是它与众不同的核心所在。很多新手问为什么我在自定义函数里用$this-date()会报错原因很简单你那个自定义函数是普通PHP函数不在Widget_Archive对象的上下文里自然没有$this。解决办法要么把$this作为参数传进去要么在函数内用Typecho_Widget::widget(Widget_Archive)重新获取实例。这个坑我在第5节会细讲这里先有个概念就好。2. 最常用的核心调用函数与页面改造实例2.1 全局数据与站点信息函数任何主题都离不开站点的基础信息这些信息存放在$this-options这个属性里它本质上是Typecho_Config对象对应后台“设置 - 基本”里填写的各种选项。常用字段有这些// 站点标题对应后台设置的“站点名称” $this-options-title // 站点描述对应“站点描述” $this-options-description // 站点地址带斜杠结尾比如 https://example.com/ $this-options-siteUrl // 主题目录地址 $this-options-themeUrl // 后台地址 $this-options-adminUrl // 当前主题的版本号如果主题的index.php头信息里声明了version $this-options-version这里有个特别容易踩的坑siteUrl结尾是带斜杠的而themeUrl结尾也有斜杠。拼路径的时候千万别再加斜杠否则会变成https://example.com//usr/themes/xxx。虽然浏览器一般能自动纠正但有些CDN和本地静态资源加载会出问题。用户登录状态的判断也属于全局信息用的是$this-user// 是否已登录 $this-user-hasLogin() // 当前登录用户名未登录为空字符串 $this-user-screenName // 当前登录用户ID未登录为0 $this-user-uid这段代码在做“仅登录可见”功能时非常有用。比如你想在文章底部显示一段只有管理员能看到的调试信息可以这么写?php if ($this-user-hasLogin() $this-user-uid 1): ? div classadmin-only这里是管理员可见内容/div ?php endif; ?2.2 文章与页面内容输出函数文章输出是主题开发的重头戏这些函数几乎每个模板都会用到。首先是文章标题和链接// 输出当前文章/页面的标题 $this-title() // 输出当前文章/页面的永久链接 $this-permalink注意title()是带括号的函数调用permalink是不带括号的属性。别小看这个区别Typecho的代码风格就是需要加工输出的用函数直接存放的数据用属性。permalink就是纯数据所以它是属性。然后是正文和摘要// 输出文章全文括号里可以传“”链接的文字 $this-content(阅读剩余部分); // 输出文章摘要第一个参数是截断字数第二个是省略符 $this-excerpt(150, ...);这里有个关键点excerpt()默认会去除HTML标签只保留纯文本。如果你在文章里插入了图片摘要区域不会显示图片。有些主题想实现“有图则显示图、无图则显示摘要”的效果就需要先判断?php if ($this-fields-thumb): ? img src?php $this-fields-thumb(); ? / ?php else: ? p?php $this-excerpt(80); ?/p ?php endif; ?$this-fields是自定义字段对象这个用法在做封面图功能时非常常见。字段调用同样遵循“属性取值、函数输出”的规律在PHP标签里用$this-fields-thumb赋值给变量在直接输出时用$this-fields-thumb()。文章元信息相关函数// 作者名称带链接 $this-author(); // 作者名称不带链接 $this-authorName(); // 发布时间括号内是时间格式 $this-date(Y年m月d日); // 分类默认输出一个带链接的分类列表 $this-category(,); // 标签默认输出全部标签并用逗号分隔参数是分隔符 $this-tags(,, true);date()的时间格式和PHP原生date函数一致Y是四位年份、m是两位月份、d是两位日期想显示时分秒就加H:i:s。category()和tags()括号里的参数都是分隔符传一个逗号和传一个空格页面效果完全不一样。评论数这个函数偶尔会有人问// 输出评论数括号里是显示的文本 $this-commentsNum(暂无评论, 1 条评论, %d 条评论);三个参数分别是无评论时显示什么、只有一条评论时显示什么、有多条评论时显示什么。%d是评论数的占位符。这个函数在文章列表页和文章详情页都很常用。2.3 头部与底部公共函数每个模板页几乎都会有header()和footer()这两个调用?php $this-header(); ? ?php $this-footer(); ?header()负责输出当前页面的meta标签、SEO标题、博客URL等内容会直接作用在head区域。它还有一个重要功能Typecho插件可以通过接口在头部注入CSS或JS代码如果不调用这个函数插件的资源加载会失效。footer()相对简单主要是输出一些统计代码钩子和结束标签。有人会问为什么我直接写死title网站标题/title比header()输出的标题更精确因为header()会根据当前页面自动生成不同的标题首页显示站点标题描述、文章页显示文章标题、分类页显示分类名。这是SEO友好设计不建议自己写死。这些函数的源码都在var/Widget/Archive.php的___header()和___footer()方法里有兴趣可以翻一翻源码看看每个标签是怎么拼接出来的。2.4 分类、标签与统计类调用函数文章列表页之外侧边栏通常需要展示全站分类和标签。这些数据不在当前Archive对象里需要通过Typecho_Widget::widget()静态方法获取// 获取分类列表赋给 $categories 变量 ?php $this-widget(Widget_Metas_Category_List)-to($categories); ? // 在循环中输出分类 ?php while ($categories-next()): ? a href?php $categories-permalink(); ??php $categories-name(); ?/a span?php $categories-count(); ?/span ?php endwhile; ?-to($categories)这步容易搞错。它把Widget对象赋值给一个变量然后这个变量就可以像$categories-next()这样循环遍历了。不理解这个语法的同学可以把它类比成C语言里把函数返回的结构体指针交给了局部变量后面的访问方式完全一致。标签列表的获取方式类似只是Widget名称改为Widget_Metas_Tag_List?php $this-widget(Widget_Metas_Tag_List)-to($tags); ? ?php while ($tags-next()): ? a href?php $tags-permalink(); ??php $tags-name(); ?/a ?php endwhile; ?全站文章数、评论数这类统计信息用的是Widget_Stat?php $stat Typecho_Widget::widget(Widget_Stat); ? 文章总数?php $stat-publishedPostsNum(); ? 评论总数?php $stat-publishedCommentsNum(); ? 分类总数?php $stat-categoriesNum(); ?这些统计数据的数值来自数据库的实时统计每次刷新页面都会重新查询如果站点文章数量很大建议加一层静态缓存避免每次侧边栏渲染都做聚合查询。3. 完整实操从零搭建一个文章列表模板3.1 模板结构与循环调用纸上谈兵多了容易虚还是直接看一个完整模板的实际写法。假设我们要做一个标准的文章列表页也就是首页或分类页加载时使用的模板。Typecho的模板文件名决定了它的用途index.php是首页、archive.php是分类/标签/搜索页的通用模板、post.php是文章详情页、page.php是独立页面。我们以index.php为例把上文提到的函数串起来。文件开头必须是模板头信息否则Typecho不认这个模板?php if (!defined(__TYPECHO_ROOT_DIR__)) exit; ? ?php $this-need(header.php); ? div classmain div classcontent ?php while ($this-next()): ? article classentry h2 classentry-title a href?php $this-permalink(); ??php $this-title(); ?/a /h2 div classentry-meta span?php $this-author(); ?/span span?php $this-date(Y-m-d); ?/span span?php $this-category(,); ?/span span?php $this-commentsNum(无评论, 1 条评论, %d 条评论); ?/span /div div classentry-summary ?php $this-excerpt(120, ...); ? /div /article ?php endwhile; ? ?php $this-pageNav(上一页, 下一页, 3, ...); ? /div ?php $this-need(sidebar.php); ? /div ?php $this-need(footer.php); ?3.2 循环内的调用顺序与注意事项这段模板里有几个隐性规则值得说透。首先while ($this-next())是整个列表页的核心。next()方法每调用一次就取出下一篇文章数据并更新当前$this的各个属性。所以循环内如果调用了$this-title()第一次循环输出第一篇的标题第二次循环自动变成第二篇的标题。这在C语言里等同于while ((data fetch_row()) ! NULL) { ... }的思路。其次$this-pageNav()是Typecho内置的分页函数。它接收四个参数上一页文字、下一页文字、当前页两边显示的页码数量、省略号显示的字符串。如果你在主题里加了这个函数但页面没出现分页大概率是文章数量不够一页或者当前页是搜索页/404页没有分页数据。第三$this-need(header.php)本质上是include一个文件区别在于被引入的文件会自动继承当前$this对象。所以在header.php里同样可以用$this-options、$this-header()不需要重新声明。这一点非常关键我在早期做主题时曾习惯性地在子模板里用global $this结果直接报语法错误后来才明白Typecho已经把对象绑定好了。3.3 侧边栏的常用函数组合侧边栏sidebar.php是调用函数最密集的区域。一个标准的侧边栏通常包含站点简介、最新文章、分类列表、标签云、随机文章。综合运用上文提到的静态调用函数可以写成这样section classwidget h3最新文章/h3 ul ?php $this-widget(Widget_Contents_Post_Recent)-to($newest); ? ?php while ($newest-next()): ? lia href?php $newest-permalink(); ??php $newest-title(); ?/a/li ?php endwhile; ? /ul /section section classwidget h3分类/h3 ul ?php $this-widget(Widget_Metas_Category_List)-to($categories); ? ?php while ($categories-next()): ? lia href?php $categories-permalink(); ??php $categories-name(); ?/a/li ?php endwhile; ? /ul /section注意这里Widget_Contents_Post_Recent是“最新文章”的Widget默认拉取10篇。如果你只想展示5篇需要在调用时传入参数?php $this-widget(Widget_Contents_Post_Recentrecent, array(pageSize 5))-to($newest); ?recent是给这个Widget实例起一个别名避免和其他Widget实例冲突。pageSize是可选参数Typecho内部缺省值是10。这种“别名”的写法在同一个模板里多次实例化同一个Widget时尤其重要不加别名在部分场景下会导致数据错乱。标签云稍微特殊因为Typecho没有现成的“按文章数排序标签云Widget”通常的做法是遍历所有标签?php $this-widget(Widget_Metas_Tag_List)-to($tags); ? ?php while ($tags-next()): ? a href?php $tags-permalink(); ? stylefont-size:?php echo max(12, $tags-count() / 5 10); ?px; ?php $tags-name(); ? /a ?php endwhile; ?字体大小按标签文章数粗略控制文章越多字号越大。这里用$tags-count()获取标签下的文章数量除以5再加10是为了保证最小字号不低于12px。3.4 文章详情页的函数调用文章详情页post.php比列表页复杂一些除了基础的标题、正文、时间、作者通常还有上下篇文章导航、相关文章推荐和评论列表。核心调用是这样article classpost-content h1?php $this-title(); ?/h1 div classmeta ?php $this-date(Y年m月d日); ? | ?php $this-author(); ? | ?php $this-category(,); ? | ?php $this-tags(,); ? /div div classcontent ?php $this-content(); ? /div /article nav classpost-navi ?php $this-thePrev(%s, 没有了, array(title «上一篇)); ? ?php $this-theNext(%s, 没有了, array(title 下一篇»)); ? /navthePrev()和theNext()是上下篇导航函数。第一个参数%s是文章标题的占位符第二个参数是当没有上一篇/下一篇时显示的占位文字第三个参数是额外选项数组title用来指定链接的文字前缀。想让“上一篇”显示在“标题前还是标题后”完全由模板结构决定。相关文章推荐没有现成函数需要自己写SQL或者用标签匹配。最简单的实现是用Tags数组做匹配查询?php $tags $this-tags; if (!empty($tags)) { $tagSlugs array_map(function($tag) { return $tag[slug]; }, $tags); $this-widget(Widget_Contents_Relatedrelated, array(type post, tags $tagSlugs))-to($relatedArticles); } ?注意$this-tags是当前文章的标签数组每个元素是包含slug、name等字段的关联数组。把slug列表传给Widget_Contents_Related它就会查询这些标签下的其他文章。4. 常用函数进阶用法与性能经验4.1 在自定义函数中安全调用Typecho函数做主题到一定阶段你会发现很多逻辑需要复用比如“输出某个分类下最新的5篇文章标题列表”。如果每次都把循环代码复制一遍模板会变得冗长难维护。这时候应该把逻辑封装到functions.php里。functions.php是Typecho主题的公共函数文件前提是在index.php的头部信息里声明了/** * 主题名称MyTheme * 主题版本1.0 */在functions.php里定义的函数不能直接用$this因为函数作用域没有绑定Archive对象。正确姿势是把需要的数据通过参数传入或者用Typecho_Widget::widget()重新获取function theme_show_recent_posts($num 5) { $posts Typecho_Widget::widget(Widget_Contents_Post_Recentcustom); $posts-parameter-pageSize $num; $posts-to($recent); while ($recent-next()) { echo lia href . $recent-permalink . . $recent-title . /a/li; } }这里$recent-permalink和$recent-title是属性访问在函数中直接输出时用属性而不是带括号的方法能减少一次函数调用的开销。这个方法在PHP层面看就是普通对象属性返回的就是字符串拼接方便。另一个更省事的做法是把你封装好的函数定义为接收$this对象作为参数调用时传入?php function theme_show_related($widget, $limit 5) { if (!$widget instanceof Widget_Archive) return; $widget-widget(Widget_Contents_Relatedrelated, array(type post, limit $limit))-to($related); while ($related-next()) { echo a href . $related-permalink . . $related-title . /a; } } ? ?php theme_show_related($this); ?哪种风格更好看个人习惯。参数注入的方式更显式适合在模板中已知$this的上下文使用静态获取的方式更独立适合做通用工具函数插件里也能复用。4.2 自定义字段与函数配合Typecho的自定义字段是主题开发中非常灵活的一套机制配合调用函数能实现大量“无插件功能”。字段定义方法在文章编辑页往下找到“自定义字段”添加thumb字段值填图片URL。模板中读取自定义字段有两种方式// 方式一属性访问 $thumb $this-fields-thumb; // 方式二方法调用输出 ?php if ($this-fields-thumb): ? img src?php $this-fields-thumb(); ? / ?php endif; ?如果这个字段没有设置$this-fields-thumb返回的是null用作if判断正好。在列表页想显示缩略图没有字段就显示默认图可以这么写?php $thumb $this-fields-thumb ? $this-fields-thumb : $this-options-themeUrl . /img/default.jpg; ? img src?php echo $thumb; ? /这段代码同时用到了自定义字段和themeUrl全局属性几乎每个带缩略图的主题都会用到。字段类型还支持数组和对象比如多选下拉框的字段值可能是个数组输出前需要implode处理。4.3 插件开发中函数调用的差异如果你的主题要做成商业化产品或者涉及复杂功能不可避免地会接触到插件钩子。Typecho插件用Typecho_Plugin::factory()注册钩子而主题模板中触发钩子的方式是在模板中调用函数?php $this-content(); // 所有文章正文都会经过内容插件处理 ?插件可以通过Typecho_Plugin::factory(Widget_Archive)-content注册回调在正文输出时做替换、加版权声明、自动加链接等操作。理解这一点很重要你调用的content()不只是函数本身还会触发生态系统中的钩子链。所以不要在自定义函数里重复实现content()的过滤逻辑否则会和插件功能冲突。目录输出函数也存在类似情况。Typecho的正文解析默认支持Markdown如果你用的是富文本编辑器content()输出时插件会处理HTML。这些联动关系在文档里很难查全最可靠的办法是查看插件源码里factory()注册的回调名称。4.4 性能注意事项与缓存策略调用函数本质是触发查询虽然Typecho的数据库层做了不少优化但架不住页面里调用次数过多。比如侧边栏最新文章调一次查数据库、分类列表调一次查数据库、标签云调一次又查数据库一个首页总共可能有七八次数据库查询。对于个人博客来说完全没问题但如果是日IP过万的站点就得想想缓存方案。一个常见的做法是静态化输出到变量减少redis或文件缓存压力?php if (!isset($GLOBALS[_cached_categories])) { $categoriesObj Typecho_Widget::widget(Widget_Metas_Category_List); $categoriesObj-to($tempCategories); $GLOBALS[_cached_categories] array(); while ($tempCategories-next()) { $GLOBALS[_cached_categories][] array( name $tempCategories-name, permalink $tempCategories-permalink ); } } foreach ($GLOBALS[_cached_categories] as $cat) { echo a href . $cat[permalink] . . $cat[name] . /a; } ?这里先把分类数据读取到全局变量数组后续模板多处使用时不再重复查询。注意Typecho的next()是游标式的遍历一次就到底了想要二次遍历必须重新实例化Widget。所以上面的技巧在首页侧边栏需要两次输出同一个分类列表时特别好使。5. 常见报错与排查实战5.1 Call to undefined method xxx这是新手最容易遇到也最头疼的报错Call to undefined method Widget_Archive::xxx()。出现这个报错95%是因为用了当前Widget类不存在的方法。比如在首页模板里调用$this-pageNavi()结果Typecho根本没有pageNavi()这个函数正确的是pageNav()。函数名大小写不对也会触发这个错误PHP方法名虽然不区分大小写但Typecho内部的魔术方法__call()会先检查方法是否存在如果不存在且没有处理逻辑就直接抛异常。排查思路是先确认这个模板适用于什么场景。content()只能在文章和独立页面模板里用在分类列表模板里根本没有正文概念thePrev()和theNext()只对文章详情页生效放在首页循环里调用就会报错。很多情况下不是函数拼错而是放错了模板文件。5.2 在自定义函数中调用 $this 报错这个问题在前面已经提到过现在说细致的解决方案。比如你在functions.php里写了function theme_get_excerpt($len) { // 这里的$this有问题 return $this-excerpt($len); }PHP解析器会直接报Using $this when not in object context。正确的修正方式是把要操作的对象传进来function theme_get_excerpt($archive, $len) { return $archive-excerpt($len); }模板中调用时传$this即可?php echo theme_get_excerpt($this, 120); ?。这个过程看起来很简单但实际主题开发中很多人会忘记把$this传递下去导致多层函数嵌套后突然报错。我的习惯是所有自定义函数第一行先声明参数类型比如function theme_get_excerpt(Widget_Archive $archive, $len)这样传错对象时能立刻在IDE和PHP层面暴露问题而不是等到运行时才炸。5.3 404页面模板调用函数异常Typecho的404页面也是Archive对象只不过它的have()方法返回falsenext()进入不了循环。如果你在404.php里强行调用$this-content()会得到异常。正确的做法是先判断?php if ($this-have()): while ($this-next()): ? article content ?php endwhile; else: ? p文章不存在或已被删除/p ?php endif; ?$this-have()在整个模板开发中都是安全的第一步判断。列表页、详情页、404页共用一套头部文件和侧边栏时头部里尽量不要调用内容相关函数否则404页面会一并报错。SEO标题也要用$this-archiveTitle而不是固定的文章标题因为404页根本没有文章标题。5.4 分页函数不显示或页码错误pageNav()不工作先检查当前页类型。首页和分类页有分页数据独立页面没有。其次检查options-pageSize是否设置后台“设置 - 阅读”里每页文章数如果为0等于关闭了分页所有文章会挤在同一页。再者检查模板是否在循环外调用如果把pageNav()放在了while ($this-next())内部每次循环都输出一遍分页视觉上简直灾难。页码数量参数也有讲究$this-pageNav(prev, next, 3, ...)的第三个参数3表示当前页左右各显示3个页码总共最多7个页码。如果站点只有4页Typecho会自动收缩到实际页数不必担心下标越界。5.5 分类列表与标签列表的数据不一致有时候侧边栏的分类列表显示的数量和后台不一致最常见的原因是Widget_Stat的统计信息没有包含未发布和隐藏分类。分类列表默认包含所有分类包括私密分类除非你在后台里把分类设为“隐藏”。这里的隐藏并非删除在Widget层面仍会被查询出来。解决办法是用Widget_Metas_Category_List的参数排除?php $this-widget(Widget_Metas_Category_List, array(ignore array(hidden)))-to($categories); ?不过说实话Typecho后台的“隐藏”选项藏得比较深大多数用户遇到这类问题其实是插件冲突或者缓存。可以先关掉所有插件刷新看是否恢复正常再逐一开启排查。5.6 SEO标题输出不正确$this-header()输出的title标签内容由___header()方法控制组装规则大致是文章页输出文章标题站点名称分类页输出分类名站点名称。如果你用的是插件自定义SEO标题比如单独填写每个页面的titleheader()不会识别这些需要你先读取自定义字段、拼装成想要的title、再用$this-response-setStatus(200)配合手动重定向头处理。更简单的方案是直接在模板里接管title输出不依赖header()?php $this-header(); ? ?php if ($this-is(post)): ? title?php $this-title(); ? - ?php $this-options-title(); ?/title ?php elseif ($this-is(category)): ? title?php $this-archiveTitle(); ? - ?php $this-options-title(); ?/title ?php else: ? title?php $this-options-title(); ?/title ?php endif; ?如果你用了这种写法留意header()内部已经输出过一次title会导致页面出现两个title标签浏览器只会取第一个。所以要给header()单独加参数禁止输出title?php $this-header(, ); ?第一个参数是关键词、第二个是描述如果都传空字符串它内部就不会输出这两个meta信息。这个技巧在很多高级主题里都会用到。6. 从我几次实战中总结的调用经验做Typecho主题这几年踩过的坑不少最后分享几条我觉得最能提高效率的习惯。第一不要死记硬背函数名学会看源码。var/Widget/Archive.php文件里是所有模板函数的核心定义每个方法的注释里都写着参数说明和返回值。遇到不确定的函数直接在编辑器里打开这个文件搜索比查任何文档都准。第二善用functions.php封装重复逻辑但封装时一定要显式传递$this不要尝试在函数内global。第三模板里能少一次数据库查询就少一次尤其是分类列表和标签云这类全站数据缓存到全局变量再复用首页加载速度能明显提升。Typecho的调用函数体系虽然不如某些现代框架那样“高大上”但胜在简单、直接、够用。你只要理解了$this就是当前页面的数据对象、Typecho_Widget::widget()就是获取全局数据的入口剩下的无非是查参数、拼模板、看效果。多写几个页面结构慢慢就会形成自己的调用习惯那时候回头看会发现这套体系确实把“博客系统”这个事做得很透。
返回列表