# haonav 网址导航 — 开发文档 > 面向开发者。站长使用说明见 [haonav_user_guide.md](haonav_user_guide.md)。 - 命名空间:`addon\haonav\` - 版本:1.0.9(`addon/haonav/info.php`) - 路由前缀:`/haonav`(由 `AppService::loadAddonRoutes()` 在 boot 阶段统一包裹) --- ## 1. 目录结构 ``` addon/haonav/ ├── Addon.php 插件生命周期钩子 ├── info.php 插件元信息(版本/入口/事件) ├── common.php 插件公共函数 ├── route/app.php 路由表(相对规则,自动加 /haonav 前缀) ├── command/ │ └── CheckLinks.php 死链检测命令行 ├── controller/ │ ├── Index.php 前台骨架控制器(12 个 trait 组合) │ ├── Account.php 会员登录/注册/资料 │ ├── Favorite.php 云端收藏 + 分享 │ ├── Task.php 计划任务入口(checklinks / autocheck) │ ├── Widget.php 小组件代理(热搜/天气) │ ├── trait/ ★ 前台控制器按职责拆分(12 个) │ │ ├── BaseTrait.php 配置/SEO/用户/广告/点击统计/静态资源输出 │ │ ├── HomeTrait.php index + rank │ │ ├── SearchTrait.php search + suggest + rate │ │ ├── SiteDetailTrait.php site 详情(JSON-LD) │ │ ├── CategoryTrait.php category │ │ ├── ApplyTrait.php submit + apply │ │ ├── SitemapTrait.php sitemap 双格式 │ │ ├── ReadMetaTrait.php read │ │ ├── FaviconTrait.php favicon 代理输出 │ │ ├── SnapshotTrait.php 网站快照 │ │ ├── SwTrait.php PWA Service Worker 输出 │ │ └── AdTrait.php adClick 广告点击跳转 │ └── backend/ 后台控制器 │ ├── HaonavBackend.php 后台基类 │ ├── Dashboard.php 数据概览 + 拼音回填 │ ├── Links.php 链接管理 + 回收站 │ ├── Category.php 分类管理 │ ├── Configs.php 配置管理 │ ├── Ad.php 广告位 │ └── Apply.php 申请审核 ├── model/ │ ├── Links.php / Category.php / Configure.php / Ad.php / Apply.php │ ├── Favorite.php / FavoriteShare.php │ └── ... ├── library/Pinyin.php 拼音转换 ├── validate/ 验证器(Category / Link) └── view/frontend/ 前台视图 ├── index/ 7 个页面 │ ├── index.html / search.html / rank.html / detail.html │ ├── category.html / apply.html ├── favorite/shared.html 收藏分享页 └── public/ 公共 include ├── styles.html ★ CSS 入口壳(见 §5 陷阱) └── navbar.html 顶部导航 public/static/haonav/ ├── css/ │ ├── nav-base.css 全局重置/变量/卡片/响应式(全页面) │ ├── nav-theme.css 主题面板 + 背景遮罩层 │ ├── nav-festival.css 节日粒子/徽章 │ └── nav-page-{index|category|detail|apply|share}.css 页面专属 ├── js/ │ ├── nav-core.js $ / $all / safeStorage / ready 基础设施 │ ├── nav-theme.js 主题/背景/主色引擎 + 面板 │ ├── nav-festival.js ★ 节日/节气自动换装引擎(36 项) │ ├── nav-fav.js / nav-search.js / nav-rating.js / nav-widgets.js / auth.js ├── sw.js Service Worker(缓存版本化) ├── manifest.json PWA manifest └── img/ 默认图标等静态资源 ``` --- ## 2. 路由表(addon/haonav/route/app.php) > 规则**不含** `/haonav` 前缀(外层 group 自动包裹)。URL 生成用 `url('haonav/xxx')` 或 `url('[haonav_site]', ['id' => $id])`,**不要写 `haonav/index/xxx` 三段式**(MultiApp 会剥首段导致对不上)。 ### 前台 | 规则 | 控制器@方法 | name | 说明 | |---|---|---|---| | `index` | Index@index | haonav_index | 首页 | | `category/:id` | Index@category | haonav_category | 分类页 | | `site/:id` | Index@site | haonav_site | 详情页 | | `search` / `suggest` / `rate` | Index@* | — | 搜索/建议/评分 | | `rank` | Index@rank | — | 排行 | | `submit` / `apply` | Index@* | — | 投稿 | | `favicon` / `snapshot` | Index@* | favicons | 图标/快照代理 | | `sw` | Index@sw | — | SW 输出 | | `ad/click` | Index@adClick | — | 广告点击 | | `sitemap(.xml)` | Index@sitemap | — | 站点地图 | | `account/{login\|register\|logout\|profile}` | Account@* | — | 会员 | | `widget/{hot\|weather}` | Widget@* | — | 小组件代理 | | `favorite/{list\|add\|remove\|sort\|merge}` | Favorite@* | — | 云端收藏 API | | `favorite/share` / `sharesave` | Favorite@* | — | 分享配置(登录) | | `favorite/shared/:token` | Favorite@shared | — | 公开分享页 | | `task/checklinks` / `task/autocheck` | Task@* | — | 巡检(需 token) | ### 后台(backend 组) `dashboard / links / category / configs / ad / apply` + 各自 `:action` 子规则。 ### 2.1 URL 生成规范(重点) YwxApp 提供 **3 种**生成插件 URL 的方式,对应不同场景: #### ① `url()` ThinkPHP 内置助手(推荐,name 化) ```php // 方式 A:直接路径(规则须全字符匹配) url('haonav/index') // → /haonav/index.html url('haonav/category/5') // → /haonav/category/5.html // 方式 B:用 route name(路由表里 ->name() 声明的)—— 带参数规则必须用 name url('[haonav_site]', ['id' => 5]) // → /haonav/site/5.html url('[haonav_category]', ['id' => 5])// → /haonav/category/5.html // 附加 GET 参数 url('haonav/search', ['q' => 'haonav']) // → /haonav/search.html?q=haonav ``` **模板语法**: ```html ... 详情 详情 ``` #### ② `addon_url()` 框架插件助手(ywxapp/helper.php) ```php addon_url('haonav', 'site/5'); // → /haonav/site/5 addon_url('haonav', '', ['page' => 2]);// → /haonav?page=2 addon_url('haonav', 'admin/index'); // → /haonav/admin/index ``` 适用:跨插件、动态拼接、不依赖路由 name 的场景。 #### ③ 直接拼接(不推荐,仅 static 资源) ```php // 仅限静态资源、JS 接口路径等无需经过路由解析的链接 '/static/haonav/css/nav-base.css' ``` #### ⚠️ 三大常见错误 | 错误写法 | 后果 | 正确 | |---|---|---| | `'haonav/index/rank'`(**三段式**) | MultiApp 剥首段后变成 `index/rank` → 对不上路由 → 404 | `'haonav/rank'`(两段:插件名+规则) | | `url('haonav/category/5')`(带参数裸写) | 路由表规则 `category/:id` 必须有 name 才能把 5 填进路径段,否则退化为 `?id=5` | `url('[haonav_category]', ['id' => 5])` | | `'addon_url'('haonav', '/index')`(重复斜杠) | 会生成 `/haonav//index`(无害但脏) | `addon_url('haonav', 'index')` | #### 绑定域名下的 URL 行为 `url()` 和 `addon_url()` 都依赖 `request()->domain()`,**绑定域名后自动用绑定域名生成**: ``` 未绑定:example.com/haonav/index.html 绑定后:hao.example.com/haonav/index.html(自动) ``` 无需在模板里手写域名。 ### 2.2 域名绑定机制(开发侧) #### 配置文件 `config/app.php` ```php 'domain_bind' => [ 'hao' => 'haonav', // 二级域名匹配 // 'hao.example.com' => 'haonav', // 完整域名匹配 ], ``` #### 解析时序(ywxapp/middleware/MultiApp.php) ``` HTTP 请求进入 ↓ 1. 独立入口判断(如 haonav.php 存在 → 直接解析) ↓ 未命中 2. 域名绑定(domain_bind) ├── 完整域名命中 bind[host()] → 取 appName ├── 子域名命中 bind[subDomain()] → 取 appName └── 通配 bind['*'] → 取 appName ↓ 未命中 3. URL 路径首段识别 → app_map / 多根查找 ↓ resolve(appName) → 在 app_roots 中查找 ├── prefix='app' → app/ 目录 └── prefix='addon' → addon/ 目录 ``` `app_roots` 默认配置: ```php 'app_roots' => [ 'app' => $app->getBasePath(), // app/ 'addon' => $app->getRootPath() . 'addon/', // addon/ ], ``` #### 多根查找的关键规则 - 裸 appName(如 `haonav`):按 roots 顺序找,**第一个匹配即用**(不是 fallback) - 显式前缀(如 `addon:haonav`):**只在指定根找**,找不到直接 404 - 因此 `addon/haonav/` 存在 → 自动命中,无需额外配置 #### 调试技巧 ```bash # 1. 打印当前生效的域名绑定 php -r "echo json_encode(config('app.domain_bind'));" # 2. 模拟请求命中哪个应用 php -r " \$req = \think\facade\Request::create('http://hao.example.com/foo'); echo \$req->host() . PHP_EOL; // haon.example.com echo \$req->subDomain() . PHP_EOL; // haon " # 3. 路由调试 访问 /haonav/test (Index::test 会 dump 当前路由表) ``` #### 完整域名 vs 子域名匹配的实际差异 ``` 请求:hao.example.com 完整域名模式 ('hao.example.com' => 'haonav') host(true) = 'hao.example.com' ← 直接命中 子域名模式 ('hao' => 'haonav') subDomain() = 'hao' ← 命中 host(true) = 'hao.example.com' ← 不会用作 key ``` 两种方式**结果一致**,推荐**子域名模式**(部署同一证书多子域名复用、迁移服务器只改 DNS 即可)。 #### 绑定域名后的冗余首段兼容(框架层,MultiApp::stripBoundAppPrefix) 绑定域名后 URL **首段若与应用名相同**(即插件路径模式的旧链接,如 `docs.example.com/docs/xxx`),MultiApp 会把该段视为冗余前缀剥掉再分发——旧链接继续可用,不会 404。 ``` docs.example.com/docs/xxx → 剥掉 docs → docs 应用内路由 xxx ✓(旧链接兼容) docs.example.com/xxx → 首段 ≠ 应用名 → 不剥,直接路由 xxx ✓(新链接) ``` 配套约定(以 docs 插件为参考实现):**插件前台链接生成走"动态前缀"**—— ```php // 插件前台基类(DocsFrontend::docsBase() 同款逻辑) protected function addonBase(): string { // 1) 独立入口 public/.php → '' $script = pathinfo((string) ($_SERVER['SCRIPT_FILENAME'] ?? ''), PATHINFO_FILENAME); if ($script === '') { return ''; } // 2) domain_bind 命中本插件(支持 '' / 'addon:' 两种绑定值)→ '' $bind = app()->config->get('app.domain_bind', []); // ... 遍历 host(true) / subDomain() / '*' 三级 key 对比 return '/'; // 3) 路径模式 → '/docs'、'/haonav' 等 } ``` - 模板:`href="{$docsBase}/xxx"`(基类统一 assign),**禁止硬编码 `/docs/`** - 前端 JS:layout 注入 `window.DOCS_BASE = '{$docsBase}'`,JS 用 `BASE + '/xxx'` 拼接;解析 pathname 时先剥掉 `BASE` 段 - 后台模板无需处理(后台始终走主域名路径模式) #### ⚠️ `$this->fetch()` 返回字符串,不是 Response 对象 框架(ThinkPHP)里控制器的 `fetch()` **返回渲染后的 HTML 字符串**,因此**不能链式调用 Response 方法**: ```php // ❌ 报错:Call to a member function code() on string return $this->fetch('read/notfound')->code(404); // ✅ 用 response() 包装(第 2 参为状态码,第 3 参为 header) return response($this->fetch('read/notfound'), 404); // ✅ 需要自定义 Content-Type 时(如 sitemap) return response($xml, 200, ['Content-Type' => 'application/xml; charset=utf-8']); ``` 同理 `$this->fetch()` 也不能直接 `->header()` / `->contentType()`,一律经 `response()` 中转。 (直接 `return $this->fetch(...)` 时框架会自动包装成 200 HTML 响应。) --- ## 3. 控制器架构 ### 3.1 前台骨架模式(Index.php) `Index` 仅是 trait 组合壳 + 路由注释,**业务全在 12 个 trait**。新增前台功能 → 新建 trait → 挂到 Index 类 + 加路由。 ```php class Index extends FrontendBase { protected $noNeedLogin = ['*']; // 前台全放行(游客) protected $noNeedVerify = ['*']; // 跳过用户组校验 use BaseTrait; // 共享 helper(siteConfig/assignSeo/assignAppearance...) use HomeTrait; use SearchTrait; ... // 按职责 protected function initialize() { $this->assignAppearance(); // 外观配置注入所有页面 } } ``` ### 3.2 视图根切换(框架 FrontendBase) 插件上下文(`addon\` 命名空间)自动把 `view_path` 切到 `addon/haonav/view/frontend/`。`fetch()` 钩子叠加皮肤覆盖层(SkinOverlay)、配色变量(SkinVariables,`