打算用 WordPress 开发网站,准确的说是加企业网站后台。WordPress 主题控制着网站的前端。学习 WordPress 开发,第一步就是要学习怎么开发 WordPress 主题,也就是怎么套现成的前端模版。
目标小一点
由于是刚开始接触 WordPress,尽量把第一个主题的目标定小一点,只考虑传统主题,且只支持几个页面:
- 首页
- 单页面,比如关于我们页面
- 列表页,比如分类列表页
- 详情页,比如详情页
据说 WordPress 占用率高达 40%,但找一个不太老旧的主题开发教程可不轻松,这是我正在用或者打算用的参考:
- Theme Developer Handbook | WordPress Developer Resources 必备,官方主题开发手册
- Underscores | A Starter Theme for WordPress 官方出品,生成一个 classic 主题,用来参考
- How to Create a Custom WordPress Theme - Full Course - YouTube freeCodeCamp 出品,视频教程,一步一步把 HTML 模版变成 WordPress 主题
搭建本地开发环境
在尝试 Docker 受挫之后(猜测部分原因来自于 Apple M1 芯片,一些 docker image 不支持 ARM64?),我打算用传统方法。
WordPress 官方建议 PHP 7.4+, MySQL 5.7+,兼顾日后部署环境,就不用最新版了。服务器采用 Apache,安装他们:
1brew install php@7.4 mysql@5.7 apache2
Apache 支持 PHP 还需要设置下,采用 Apache 内置的 mod_php
方案简单些(我参考了 macOS 12.0 Monterey Apache Setup: Multiple PHP
Versions | Grav
CMS)
1# /opt/homebrew/etc/httpd/httpd.conf 2 3LoadModule rewrite_module lib/httpd/modules/mod_rewrite.so 4LoadModule php7_module /opt/homebrew/opt/php@7.4/lib/httpd/modules/libphp7.so 5 6<IfModule dir_module> 7 DirectoryIndex index.php index.html 8</IfModule> 910<FilesMatch \.php$>11 SetHandler application/x-httpd-php12</FilesMatch>
然后启动服务:
1# 启动 Apache (httpd 和 apache 相同)2brew services start httpd3 4# 启动 MySQL,默认用户 root 密码为空5brew services start mysql@5.7
主题开发
WordPress 不是 Web Framework
现代 Web Framework(如 Laravel)常常采用 MVC 方式,开发者自定义路由,指定控制器来处理,在控制器里获得模型最后传给模版渲染。
WordPress 不同,WordPress 里采用约定俗成的规定——Template Hierarchy,主题开发者需要根据这些规定来命名模版的文件名。
按照这个规定,我们的主题包含这几个文件:
- style.css 必须,用来设置主题的元数据(主题名称、版本信息等等)
- index.php 必须,fallback template,如果其他的模版不匹配,就用这个模版
- functions.php 调整主题的功能
- home.php 首页模版
- page.php 单页面模版
- archive.php 列表页模版
- single.php 详情页模版
撕裂
我们有四个模版需要共享同一个 HTML 结构。正式的模版语言,如 Laravel 的 Blade 或者 Symfony 的 Twig,支持继承扩展。但是原生的 PHP 模版不支持,由于缺少对 HTML 的理解,只能硬生生把完整的 HTML 结构撕开:
1<!doctype html> 2<html lang="zh"> 3<head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <title>WordPress 主题开发</title> 7 <link rel="stylesheet" href="<?= get_stylesheet_uri() ?>"> 8</head> 9<body>10 11<header></header>12 13<main></main>14 15<footer></footer>16 17<script src="<?= get_template_directory_uri() . '/script.js' ?>"></script>18</body>19</html>
按 WordPress 的习惯,分成 header.php 和 footer.php。好在后面使用起来没有这种撕裂,比如这是我们的首页:
1<!-- home.php -->2<?php get_header() ?>3 4<main>5 This is the homepage!6</main>7 8<?php get_footer() ?>
上面我也没有用 wp_head
wp_footer
来通过 WordPress 自带机制添加
<title>
, CSS,
JS,因为一来我不大了解[^wp_head],二来我想先保持简单,能自己实现就自己实现。
[^wp_head]: 的确,后来发现不用的话,WordPress
富文本编辑器的居中对齐,前端无效了,是因为不用 wp_head
的话,aligncenter
类就没有定义了。
奇怪的循环
模版准备好了,就该展示内容了。每一个模版预设获得的内容也是 WordPress 事先规定好的,比如首页会获得最新的 10 篇文章。通过循环获得:
1<!-- home.php --> 2<?php get_header() ?> 3 4<main> 5 <h1>最新文章</h1> 6 7 <?php if ( have_posts() ): ?> 8 <ul> 9 <?php while ( have_posts() ): the_post(); ?>10 <li>11 <h3><?php the_title(); ?></h3>12 <p><?= get_the_excerpt() ?></p>13 <a href="<?= get_permalink() ?>">Read More</a>14 </li>15 <?php endwhile; ?>16 </ul>17 <?php endif; ?>18 19</main>20 21<?php get_footer() ?>
这段代码是这样工作的:
- 依赖全局变量,
$wp_query
和$post
-
have_posts()
是循环结束条件 -
the_post()
迭代一次,更新$post
-
the_title()
/get_the_excerpt()
/get_permalink()
获取从全局变量$post
中获得信息
这样的代码看起来怪,也不直观。一定要用全局变量呢?为什么不用 foreach 呢?
更奇怪是即便只有一篇文章,比如详情页,也得用这个循环:
1<!-- single.php --> 2<?php get_header() ?> 3 4<main> 5 <?php if ( have_posts() ): ?> 6 <?php while ( have_posts() ): the_post(); ?> 7 <h1><?php the_title(); ?></h1> 8 <time><?php the_date() ?></time> 9 <div class="user-rich-content">10 <?php the_content(); ?>11 </div>12 <?php endwhile; ?>13 <?php endif; ?>14</main>15 16<?php get_footer() ?>
有了循环,内容太多时,就需要分页了,在列表页中调用
the_posts_pagination()
会生成分页器;而在详情页里要生成上一篇下一篇,调用
previous_post_link()
/ next_post_link()
。
生成标题
页面标题 <title></title>
需要动态生成,第一种方法是让 WordPress
帮我们生成,调用 wp_head()
,再配合上:
1<!-- functions.php -->2<?php3 4add_action( 'after_setup_theme', fn() => add_theme_support( 'title-tag' ) );
另一种方法,是手动生成,由于我上面就没有调用
wp_head()
,我们手动生成,定义函数:
1<!-- functions.php --> 2<?php 3 4function myfirsttheme_get_page_title() { 5 if ( is_home() ) { 6 return get_bloginfo( 'name' ); 7 } 8 if ( is_single() ) { 9 return get_the_title();10 }11 if ( is_page() ) {12 return get_the_title();13 }14 if ( is_category() ) {15 return single_cat_title( '', false );16 }17 18 return 'ERROR: Cannot get the page title';19}
之后在 header.php 里调用就行了。
生成导航菜单
WordPress 自带让用户编辑菜单的功能,首先,主题需要注册菜单显示的位置,假设我们页眉和页脚需要导航,注册这 2 个位置:
1add_action( 'init', fn() => register_nav_menus( [2 'header-menu' => __( 'Header Menu' ),3 'footer-menu' => __( 'Footer Menu' ),4] ) );
之后,进入后台,会发现多了个 Menu 入口。分别给这两个位置加一个菜单,菜单栏目可以包含子菜单栏目,是个树结构。
最后,我们在主题里合适位置调用 wp_nav_menu()
打印他们。
1// header.php2<?php wp_nav_menu( array( 'theme_location' => 'header-menu' ) ); ?>3 4// footer.php5<?php wp_nav_menu( array( 'theme_location' => 'footer-menu' ) ); ?>
下一步
我们上面定的小目标已经完成了,那下一步呢?
首先,有一个样式丢失问题:我在后台编辑了篇文章,插入了一张图片,居中显示,但是前端却没有居中。那是因为这个居中依赖于
aligncenter
,但是我们有意不引入 WordPress 的
CSS,所以就少了这个类定义。这个问题留着以后研究也不迟。
然后,默认文章作为新闻差不多够了,但是像产品这样的内容,需要创建专门的文章类型,顺便还要了解下自定义字段。