从零开始搭一个能本地热重载的 Zsens Admin 插件骨架:我的目录不是拍脑袋定的,是调试器逼出来的
第一次写 Zsens Admin 插件时,我把所有文件往一个文件夹里一塞,require 写得到处都是,本地改一行代码就得手动刷新后台看效果,效率低得想弃坑。后来把目录拆开、入口理顺、配上本地调试链路,才算真正进入开发状态。这篇记录我现在的起手式,给同样刚入门的兄弟参考。
一、目录结构:按"加载顺序"而不是"功能模块"来分层
早期我按 controller/、model/、view/ 这种 MVC 思维分,结果入口文件里一堆 require 路径错到怀疑人生。现在我的目录长这样:
zsens-admin-demo/
├── zsens-admin-demo.php # 唯一入口,只做一件事:bootstrap
├── bootstrap/
│ └── Kernel.php # 注册自动加载、初始化容器、挂载钩子
├── config/
│ └── plugin.php # 版本号、依赖检查、最小 WP 版本
├── src/
│ ├── Contracts/ # 接口定义,先写这里再写实现
│ ├── Services/ # 业务逻辑,不直接碰 WP 函数
│ ├── Http/
│ │ ├── Controllers/ # 只负责收参数、调 Service、返回响应
│ │ └── Middleware/ # 权限校验、CSRF 过滤
│ └── Admin/
│ ├── Menus/ # 菜单注册与路由绑定
│ └── Assets/ # css/js 的 enqueue 逻辑
├── resources/
│ ├── views/ # 模板文件,用原生 PHP 不用引擎
│ └── assets/ # 源码 scss/ts,构建后不进版本控制
├── routes/
│ └── admin.php # 后台路由表,纯数组配置
└── tests/
└── local/ # 本地调试专用脚本,见下文
关键变化:bootstrap/Kernel.php 是实际入口,zsens-admin-demo.php 只负责 require __DIR__ . '/bootstrap/Kernel.php'; 然后 (new Kernel)->boot();。这样 WP 激活插件时只做最小启动,避免解析错误直接白屏。
二、入口文件的三条铁律
我的 zsens-admin-demo.php 现在固定这么写,多了没有:
<?php
/**
* Plugin Name: Zsens Admin Demo
* Version: 0.1.0
* Requires at least: 6.0
*/
if (!defined('ABSPATH')) {
exit; // 直接访问断掉,这条不能省
}
// 环境探测:本地开发时走热重载分支
$isLocal = (defined('WP_DEBUG') && WP_DEBUG && $_SERVER['HTTP_HOST'] ?? '' === 'zsens.test');
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/bootstrap/Kernel.php';
$kernel = new \ZsensAdminDemo\Bootstrap\Kernel($isLocal);
$kernel->boot();
注意 $isLocal 这个开关,后面调试全靠它。
三、本地调试环境:不是装个 Xdebug 就完事
我踩过的坑:Xdebug 配好了,但插件代码改完还得手动刷新后台,断点打在入口文件发现 WP 已经跑完了,自己的逻辑还没加载。
现在的解法分三层:
1. 文件监听 + 软链接热替换
在 tests/local/ 下放一个 watcher.php,用 inotifywait(Linux/Mac)或 fswatch 监听 src/ 变动,触发时:
// tests/local/watcher.php
$pluginDir = '/var/www/zsens.test/wp-content/plugins/zsens-admin-demo';
$sourceDir = '/home/你的项目路径/zsens-admin-demo';
// 只同步 src/ 和 resources/views/,vendor 不动
$syncCmd = "rsync -av --delete {$sourceDir}/src/ {$pluginDir}/src/";
exec($syncCmd);
配合 browser-sync 代理本地站点,保存文件后 200ms 内后台自动刷新,断点能命中最新代码。
2. 容器替换:本地用 Mock 实现
Kernel 里根据 $isLocal 换绑定:
// bootstrap/Kernel.php
public function registerBindings(): void
{
if ($this->isLocal) {
// 本地不走真实 WP 数据库,用内存数组测流程
$this->container->bind(
\ZsensAdminDemo\Contracts\ConfigRepository::class,
\ZsensAdminDemo\Tests\Local\InMemoryConfig::class
);
return;
}
$this->container->bind(
\ZsensAdminDemo\Contracts\ConfigRepository::class,
\ZsensAdminDemo\Services\WpOptionConfig::class
);
}
这样本地调试配置保存逻辑时,不用真的写 wp_options,速度差一个数量级。
3. 路由直连:绕过 WP 菜单体系
后台菜单注册后调试麻烦,本地我加了一条"后门"路由:
// routes/admin.php
return [
// 正常菜单路由
['page' => 'zsens-demo', 'controller' => 'DashboardController@index'],
// 本地专用:直接访问 ?page=zsens-dev&route=dashboard/edit/3
['page' => 'zsens-dev', 'controller' => 'DevController@dispatch', 'local_only' => true],
];
DevController 里解析 $_GET['route'],直接调目标 Controller,跳过 admin_menu 钩子的权限检查,专注测业务逻辑。
四、一个能跑的最小验证
按上面搭完后,我的第一个可调试入口是这样一个 Controller:
// src/Http/Controllers/DashboardController.php
namespace ZsensAdminDemo\Http\Controllers;
class DashboardController
{
public function index(): void
{
$version = apply_filters('zsens_demo_version', '0.1.0');
// 本地调试用:直接抛异常看堆栈
if (defined('ZSENS_LOCAL_THROW') && ZSENS_LOCAL_THROW) {
throw new \RuntimeException("调试点命中:版本号 {$version}");
}
require ZSENS_DEMO_PATH . '/resources/views/dashboard.php';
}
}
本地 wp-config.php 里加 define('ZSENS_LOCAL_THROW', true);,访问后台页面直接看到异常堆栈,确认自动加载、容器绑定、视图路径全通。
五、现在还在纠结的点
资源文件(JS/CSS)的热重载还没找到完美方案。目前是用 wp_enqueue_script 时判断 $isLocal,指向 browser-sync 的代理端口,但偶尔有缓存穿透。兄弟们本地怎么处理的?

