本文约 7,636 字,预计阅读 20 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:主题选项写好了,换主题后设置全没了
用 get_theme_mod() 做了一套主题选项:Logo、主色、页脚版权、布局开关,客户填了三十多项。半年后换主题,所有设置归零,客户以为网站被重置了,投诉到运营。
问题不在代码,在选型:theme_mods 是按主题存储的,换主题必然丢失。这类”设置存在哪里”的问题,必须在动手写第一行代码之前就想清楚。
原理分析:三种方案的本质区别
| 方案 | 数据存储 | 换主题后 | 实时预览 | 实现成本 |
|---|---|---|---|---|
Customizer(theme_mod) |
wp_options 的 theme_mods_{stylesheet} |
丢失 | 支持 | 中 |
Customizer(option) |
wp_options 独立 option |
保留 | 支持(需 postMessage) |
中 |
| 自建设置页 | wp_options(Settings API) |
保留 | 不支持 | 高 |
| 框架(Kirki / Redux / Carbon Fields) | 取决于配置 | 取决于配置 | 支持 | 低 |
关键点解释:
theme_mods_{stylesheet} 是”每个主题一份”的设置包。 key 里带主题目录名,换主题就是换一个 option,旧数据还在数据库里(可迁移),但新主题读不到。这是 WordPress 的设计意图:主题选项应该”跟着主题走”,比如”这个主题要不要显示侧边栏”。
get_theme_mod() vs get_option():
// theme_mod:按主题隔离,多数带默认值的样式开关适合用它
$color = get_theme_mod( 'mmf_primary_color', '#1a73e8' );
// option:全局,与主题无关的数据(如备案号、统计 ID)适合用它
$icp = get_option( 'mmf_icp_number', '' );
WP_Customize_Setting 的 type 参数决定存哪儿:'theme_mod'(默认)或 'option'。
Customizer 相比自建设置页的独特价值是”实时预览”:不需要保存就能看到改动效果,靠的是 transport => 'postMessage' + 一段 JS。自建设置页做不到这一点,除非自己写。
保姆级实操
方案 A:Customizer(官方推荐,wordpress.org 强制)
add_action( 'customize_register', function ( $wp_customize ) {
// 1. 面板
$wp_customize->add_panel( 'mmf_options', array(
'title' => '主题选项',
'description' => '站点外观相关设置',
'priority' => 160,
) );
// 2. 区块
$wp_customize->add_section( 'mmf_colors', array(
'title' => '颜色',
'panel' => 'mmf_options',
'priority' => 10,
) );
// 3. 设置项(theme_mod 类型)
$wp_customize->add_setting( 'mmf_primary_color', array(
'default' => '#1a73e8',
'sanitize_callback' => 'sanitize_hex_color', // 必须提供清理函数
'transport' => 'postMessage', // 实时预览
) );
// 4. 控件
$wp_customize->add_control( new WP_Customize_Color_Control(
$wp_customize,
'mmf_primary_color',
array(
'label' => '主色调',
'section' => 'mmf_colors',
'settings' => 'mmf_primary_color',
)
) );
// 与主题无关的数据:用 option 类型存
$wp_customize->add_setting( 'mmf_icp_number', array(
'type' => 'option',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
) );
$wp_customize->add_control( 'mmf_icp_number', array(
'label' => 'ICP 备案号',
'section' => 'mmf_colors',
'type' => 'text',
) );
} );
// 5. 实时预览脚本
add_action( 'customize_preview_init', function () {
wp_enqueue_script(
'mmf-customizer',
get_stylesheet_directory_uri() . '/assets/js/customizer.js',
array( 'customize-preview' ),
'1.0.0',
true
);
} );
customizer.js:
( function ( $ ) {
wp.customize( 'mmf_primary_color', function ( value ) {
value.bind( function ( newval ) {
$( ':root' ).css( '--mmf-primary', newval );
} );
} );
} )( jQuery );
前台读取与输出:
add_action( 'wp_head', function () {
$color = get_theme_mod( 'mmf_primary_color', '#1a73e8' );
printf( '<style>:root{--mmf-primary:%s;}</style>', esc_attr( $color ) );
} );
方案 B:自建设置页(Settings API)
add_action( 'admin_menu', function () {
add_menu_page(
'站点设置',
'站点设置',
'manage_options',
'mmf-settings',
'mmf_render_settings_page',
'dashicons-admin-generic',
60
);
} );
add_action( 'admin_init', function () {
register_setting( 'mmf_settings_group', 'mmf_icp_number', array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
) );
add_settings_section( 'mmf_section_basic', '基础信息', function () {
echo '<p>与主题无关、换主题也需保留的信息。</p>';
}, 'mmf-settings' );
add_settings_field( 'mmf_icp_number', 'ICP 备案号', function () {
printf(
'<input type="text" name="mmf_icp_number" value="%s" class="regular-text">',
esc_attr( get_option( 'mmf_icp_number', '' ) )
);
}, 'mmf-settings', 'mmf_section_basic' );
} );
function mmf_render_settings_page() {
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors(); ?>
<form method="post" action="options.php">
<?php
settings_fields( 'mmf_settings_group' );
do_settings_sections( 'mmf-settings' );
submit_button();
?>
</form>
</div>
<?php
}
方案 C:用框架加速
| 框架 | 特点 | 适合 |
|---|---|---|
| Kirki | 封装 Customizer API,控件丰富,配置式写法 | 想用 Customizer 又嫌原生啰嗦 |
| Redux Framework | 独立设置面板,控件最多,体积较大 | 商业主题、选项特别多 |
| Carbon Fields | 支持 Customizer + 后台页面 + term meta,无界面依赖 | 需要大量自定义字段 |
| Codestar Framework | 轻量独立面板,配置项清晰 | 商业主题快速搭建 |
| ACF PRO 选项页 | 用熟悉的字段组搭后台页面 | 站点已有 ACF 生态 |
Kirki 的写法示例(配置式,比原生少一半代码):
Kirki::add_config( 'mmf_config', array(
'capability' => 'edit_theme_options',
'option_type' => 'theme_mod',
) );
Kirki::add_section( 'mmf_colors', array(
'title' => '颜色',
'panel' => '',
) );
Kirki::add_field( 'mmf_config', array(
'type' => 'color',
'settings' => 'mmf_primary_color',
'label' => '主色调',
'section' => 'mmf_colors',
'default' => '#1a73e8',
'transport' => 'auto', // Kirki 自动处理 postMessage 预览
'output' => array(
array(
'element' => ':root',
'property' => '--mmf-primary',
),
),
) );
注意 Kirki 是面向 Customizer 的封装,数据仍按 option_type 落到 theme_mod 或 option。
第 4 步:按数据性质决定存哪儿(选型清单)
| 数据类型 | 例子 | 存哪 | 方案 |
|---|---|---|---|
| 纯外观,换主题理应重置 | 主色、字号、布局开关、Logo | theme_mod |
Customizer |
| 与主题无关的业务信息 | 备案号、统计 ID、客服电话、公司名 | option |
Customizer(option) 或设置页 |
| 需要复杂表单/多步骤 | 邮件模板、API 对接配置 | option |
自建设置页 |
| 大量字段(几十项以上) | 商业主题全套配置 | 视情况 | 框架 |
第 5 步:换主题时迁移 theme_mods
数据其实还在数据库里,可以导出再导入:
# 导出旧主题的设置(JSON)
wp option get theme_mods_oldtheme --format=json > old-mods.json
# 把需要的键合并到新主题
wp eval '
$old = json_decode( file_get_contents( "old-mods.json" ), true );
$new = get_option( "theme_mods_newtheme", array() );
foreach ( array( "mmf_icp_number", "custom_logo", "background_color" ) as $k ) {
if ( isset( $old[ $k ] ) ) { $new[ $k ] = $old[ $k ]; }
}
update_option( "theme_mods_newtheme", $new );
echo "迁移完成\n";
'
菜单位置指派(nav_menu_locations)也存在 theme_mods 里,换主题后需要重新指派,这是最容易漏的一项,注册与指派的完整说明见register_nav_menus 注册菜单位置。
常见踩坑
- 所有选项都用
theme_mod。 换主题后备案号、统计代码、客服电话全丢,这类数据应该用option。 sanitize_callback缺失。 Customizer 保存时不做清理,颜色字段存进奇怪的值,前台输出未转义直接进入 CSS,形成注入点。核心提供了sanitize_hex_color、sanitize_text_field、absint等现成函数,直接传函数名即可。- 自定义 sanitize 回调忘了返回默认值。 校验失败时应返回
$setting->default,否则存进空值。 transport => 'postMessage'但没写预览 JS。 后台改了颜色前台不动,需要刷新才看到,等于”假实时”。- 把框架打包进主题却没处理更新。 Redux、Kirki 这类框架若随主题分发,主题更新时框架版本不动,长期会积累安全隐患;用 TGM Plugin Activation 之类的推荐安装机制更稳妥。
- 案例: 某商业主题用独立框架做了六十多项设置,客户要求换主题时发现:主色、Logo、页脚文案全丢,更要命的是”客服微信二维码””备案号””统计代码”也一起消失,而这些本应与主题无关。补救只能从数据库里把
theme_mods老 option 导出后逐项手工迁移,耗时一天。教训是:写选项之前先按”换主题后该不该保留”把字段分两类,业务类一律走option。这类数据归属的判断,与functions.php 与插件职责划分里的思路是同一个。 - 每个控件都单独写一段
wp_head输出。 几十个add_action('wp_head')让<head>里塞满<style>片段。应统一收集后一次性输出,或用 Kirki 的output参数自动生成。
总结
选型标准只有一条:换主题之后这个设置该不该保留? 不该保留的(颜色、布局、Logo)用 Customizer + theme_mod;该保留的(备案号、统计 ID、联系方式)用 option,放 Customizer 或自建设置页都行。面向 wordpress.org 分发必须用 Customizer(官方硬性要求);商业主题选项量大、要求独立面板时用 Kirki / Redux / Carbon Fields 这类框架加速。无论哪种方案,每个设置项都必须提供 sanitize_callback,每个输出都必须转义——选项面板是主题里最容易出现注入的地方。
