建站项目里,纯从零开发整套系统的情况并不多见,更多是基于成熟的开源 CMS 做二次开发。WordPress、DedeCMS、PHPCMS 都是常见选择。但拿到一套开源系统的源码,如果不懂它的目录结构和运行逻辑,改起来会处处踩坑——动了一个文件全站报错,升级时自己的修改全被覆盖。本篇以主流 CMS 为例,帮你建立源码结构的整体认知。
一、典型目录结构拆解
主流 CMS 虽然实现细节各异,但目录组织遵循相似的模式。理解这个模式,换一套系统也能快速上手。下面是一个典型 CMS 的目录骨架:
/cms-root/
├── index.php # 单入口文件,所有请求经此转发
├── admin.php # 后台入口
├── config/ # 配置文件目录
│ ├── database.php # 数据库连接配置
│ ├── app.php # 应用级配置
│ └── route.php # 路由规则
├── app/ # 核心应用代码
│ ├── Controller/ # 控制器
│ ├── Model/ # 数据模型
│ ├── View/ # 视图模板
│ └── Service/ # 业务逻辑层
├── public/ # 对外开放的静态资源
│ ├── static/ # CSS/JS/图片
│ └── uploads/ # 用户上传文件
├── runtime/ # 运行时产物(缓存、日志)
├── extend/ # 扩展类库
└── template/ # 前台主题模板
├── default/ # 默认主题
└── mobile/ # 移动端主题
这个结构的核心思想是"分层隔离":配置与代码分离、静态资源与业务代码分离、运行时产物与源码分离。这样做的好处是:升级时只替换 app/ 和 config/,public/uploads/ 里的用户上传文件不受影响;切换主题只改 template/ 目录,不动核心代码。尧图做二次开发时严格遵循这个边界——该放哪里的代码就放哪里,绝不图省事乱塞。
二、单入口与请求分发
现代 CMS 几乎都采用"单入口"模式:所有请求(除静态资源外)都经过根目录的 index.php,由它加载框架、解析路由、分发到对应控制器。相比早期 PHP 每个页面一个文件的写法,单入口更安全(统一鉴权)、更易维护(公共逻辑集中)、更利于 URL 美化(配合伪静态实现语义化 URL)。
// index.php 单入口简化流程
<?php
// 1. 定义常量
define('APP_PATH', __DIR__ . '/app/');
define('ROOT_PATH', __DIR__ . '/');
// 2. 加载Composer自动加载
require __DIR__ . '/vendor/autoload.php';
// 3. 加载配置
$config = require __DIR__ . '/config/app.php';
// 4. 初始化应用
$app = new Application($config);
// 5. 解析路由并分发请求
// URL: /article/123 → Controller\Article::show(123)
$request = Request::createFromGlobals();
$response = $app->dispatch($request);
// 6. 输出响应
$response->send();
理解单入口的关键是理解"路由分发"。用户访问 /article/123,框架根据路由规则把它映射到 Article 控制器的 show 方法,参数 123 自动传入。这个映射规则定义在 config/route.php 里,二次开发时新增页面只需加一条路由规则、写一个控制器方法,不用新建物理文件。
三、二次开发的正确姿势
二次开发最大的坑是"直接改核心代码"。一旦改了,系统升级时要么放弃升级,要么升级后改动全丢。尧图的原则是:核心代码只读,所有定制通过扩展机制实现。成熟 CMS 都提供了扩展点——WordPress 的主题和插件、DedeCMS 的标签和模块、ThinkPHP 的钩子和行为。把定制代码放进扩展体系,升级核心时完全不受影响。
// 反面示例:直接改核心文件(升级会丢失)
// 文件:app/Controller/Article.php
public function show($id) {
$article = $this->model->find($id);
// 直接在核心控制器里加自己的逻辑
$article['custom_field'] = '我加的字段'; // 升级覆盖!
return view('article', $article);
}
// 正面示例:通过钩子/事件扩展(升级安全)
// 文件:extend/Hook/ArticleHook.php
class ArticleHook {
// 监听文章加载完成事件
public function onArticleLoaded($article) {
$article['custom_field'] = '我加的字段';
return $article;
}
}
// 核心代码里预留了钩子触发点:
// Hook::listen('article_loaded', $article);
// 升级核心不影响你的 Hook 类
模板层是二次开发的主战场。尧图的做法是:复制默认主题到新目录(如 template/yaotu/),在后台切换为当前主题,然后只在新目录里改模板。这样默认主题保持原样,随时可以回退。配置文件用"环境变量覆盖"机制——config/database.php 写默认值,.env 文件写本机实际配置,提交代码时 .env 被 gitignore,避免数据库密码等敏感信息进版本库。把"不改核心、扩展优先、配置分离"这三条原则贯彻到底,二次开发才能既满足定制需求又不丢升级能力。