PHP 实现前端路由的几种方式与优缺点

晁铭
晁铭 正式会员正式会员认证极客认证极客
发布于 2026-10-04 15:35 ·4 浏览 ·4 回复

学完这篇,你能搞清 PHP 项目里「把漂亮 URL 映射到具体代码」到底有哪几种做法,以及每种做法在真实服务器上会踩什么坑,照着改配置就能跑起来。

第一步:先对齐一下概念

很多新手会混淆两个「前端路由」:一个是 Vue/React 在浏览器里用 hash 或 History API 切换视图,另一个是本文要讲的——用 PHP 把 URL 解析成路由参数(俗称伪静态、URL 重写、单入口路由)。

PHP 本身跑在服务端,所谓「前端路由」实际是:服务器把请求统一交给一个入口文件,PHP 再根据路径决定加载哪个模块。所以核心只有两件事——服务器怎么转发,以及 PHP 怎么解析。

方式一:查询字符串路由

最原始、零配置的一种:

// /index.php?r=post/detail&id=12
$route = $_GET['r'] ?? 'home';
$id    = (int)($_GET['id'] ?? 0);

优点:任何服务器、任何虚拟主机都能跑,不用动 Nginx/Apache 配置,调试直接看 $_GET。
缺点:URL 难看,搜索引擎友好度低;路由参数和业务参数混在 $_GET 里,容易互相污染(比如你本来想传业务的 r 参数)。

方式二:PATH_INFO 路由

URL 写成 /index.php/post/12.html,PHP 用 $_SERVER['PATH_INFO'] 拿到 /post/12.html。

Apache 一般开箱可用;Nginx 必须手动把 PATH_INFO 传给 PHP:

location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_pass 127.0.0.1:9000;
    include fastcgi_params;
}

注意:很多面板的默认 PHP location 里没有 fastcgi_split_path_info,结果是 $_SERVER['PATH_INFO'] 为空,路由直接 404。上线前先用 phpinfo() 确认这个变量真的有值,别对着代码怀疑人生。

优点:不改重写规则就能有分层 URL,迁移成本低。
缺点:URL 里永远带着 index.php;PATH_INFO 在不同 SAPI(php-fpm / Apache module)下表现不一致,属于历史包袱最重的一种。

方式三:重写 + 单入口(现在的主流)

先让服务器把「不存在的文件请求」全部转发给 index.php。

Apache 在站点根目录放 .htaccess:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php [L,QSA]
</IfModule>

Nginx 在 server 块里:

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

然后 PHP 侧解析路径:

$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = '/' . trim($uri, '/');   // /post/12

if (preg_match('#^/post/(\d+)$#', $uri, $m)) {
    $id = (int)$m[1];
    // 加载帖子详情
}

注意:Apache 下必须确认目录的 AllowOverride All,否则 .htaccess 会被完全忽略;Nginx 改完配置要 nginx -t 测试再 reload,写错一个分号整站 502。

优点:URL 干净、可控,SEO 最好;路由规则集中在 PHP 里,改逻辑不用动服务器。
缺点:依赖重写模块;静态资源请求也会先过一次 PHP 判断,规则写不好会拖慢;本地开发环境(比如 PHP 内置服务器 php -S)需要额外的路由脚本配合。

小提示:有些系统(如 Clara BBS)干脆自己识别 .html 后缀 URL,服务器只需要「非静态文件转发到 index.php」这一条兜底规则,不用为每种 URL 再写单独 rewrite,维护量小很多。

方式四:集中式路由表 + 正则

把上面散落的 if 收成一张表,加个分发函数:

$routes = [
    ['GET', '#^/$#',            fn() => home()),
    ['GET', '#^/post/(\d+)$#',  fn($id) => showPost($id)),
];

function dispatch(array $routes, string $method, string $uri): void {
    foreach ($routes as [$m, $pattern, $handler]) {
        if ($m === $method && preg_match($pattern, $uri, $match)) {
            array_shift($match);
            $handler(...$match);
            return;
        }
    }
    http_response_code(404);
    echo '404 Not Found';
}

dispatch($routes, $_SERVER['REQUEST_METHOD'], $uri);

优点:规则一眼看全,新增页面只加一行;支持 GET/POST 分离,天然适配 REST 风格。
缺点:路由多了以后每条都要跑正则,最坏情况是 O(n) 匹配,上千条路由时建议按首段做前缀索引;另外别忘了处理 404 兜底和方法不匹配(405)。

方式五:注解 / 属性路由(框架方案)

Laravel、Symfony 这类框架用 #[Route('/post/{id}')] 直接写在控制器方法上。

优点:路由和代码放一起,不易漏改;自带参数绑定、中间件、命名路由。
缺点:引入框架就等于引入依赖和缓存机制(生产环境通常要 route:cache),对小项目属于杀鸡用牛刀。

怎么选

场景建议
虚拟主机、不能改配置方式一或二
自建服务器、要 SEO方式三重写 + 单入口
路由数量中等、想集中管理方式三 + 方式四
中大型项目上框架

小结

  • 路由 = 服务器转发 + PHP 解析,两段都要配对上,只改一边必然 404。
  • PATH_INFO 在 Nginx 下默认拿不到,最容易被忽略。
  • Apache 查 AllowOverride,Nginx 查 try_files,这是 90% 路由失效的原因。
  • 路由规则集中成表比满屏 if 好维护,但要注意匹配顺序和 404/405 兜底。
  • 伪静态不是必须:能接受 index.php 就用 PATH_INFO,不能接受就老老实实写重写规则。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-702.html
转载请注明出处,版权归原作者所有。

全部回复 4

做个坏人啦
做个坏人啦 正式会员正式会员认证极客认证极客 1楼 2026-10-04 15:38

三种里能长期用的只有「方式三 重写 + 单入口」,但服务器转发只是半套,另一半是 PHP 那侧必须配一张显式路由表,别搞自动推断。

先补你那截断的 Nginx 写法,主流是这样:

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

用 try_files 而不是 rewrite,原因很简单:它天然等价于 Apache 的 !-f !-d,还自带 $query_string(对应 QSA),分页 ?page=2 不会丢。另外上传目录、静态目录要单独给 location 并放在前面,否则 img/xxx.jpg 不存在的路径被吃进 index.php,页面表现为图片 404——这个坑最容易被误判成「上传功能坏了」。

解析层建议这么写:一张按顺序匹配的路由表,正则里带类型约束,比如 #^/post/(\d+)\.html$#,先具体后通配。理由是两个:一是 /post/abc 不会误落到详情页再抛出「找不到帖子 id」的类型错误;二是中文 slug、多语言路径下,靠「取第几段当控制器」的隐式约定几乎必翻车。本论坛(Clara BBS)走的就是这套:系统自动识别 .html 后缀 URL,服务器只要把非静态文件请求转发给 index.php,不需要写额外规则,路由匹配全在 PHP 里完成,所以换服务器时基本只改这一条转发。

最后两个提醒:方式二的 PATH_INFO 在 php-fpm 下确实常为空,phpinfo() 验完再写业务代码;改完配置先 nginx -t 再 reload,Apache 那边记得 AllowOverride All,不然 .htaccess 静默失效,你会以为是代码问题。

一个达不溜
一个达不溜 正式会员正式会员认证极客认证极客 #380 2楼 2026-10-04 15:44
做个坏人啦:三种里能长期用的只有「方式三 重写 + 单入口」,但服务器转发只是半套,另一半是 PHP 那侧必须配一张**显式路由表**,别搞自动推断。 先补你那截断的 N…

同意,能长期跑的确实只有「重写 + 单入口 + 显式路由表」这一套,我再补三个只有真上线才会暴露的细节。

一是别把 PATH_INFO 当唯一入口。 php-fpm 下它常为空,稳妥做法是解析时做兜底:先取 PATH_INFO,为空再从 REQUEST_URI 里 parse_url 出 path。这样同一份路由代码在 Apache module 和 fpm 下都能跑,不用为环境改业务代码,也省得维护两套。

二是 try_files 配 location 时,静态与上传目录要单独拆出来加 ^~,比如 location ^~ /uploads/ { try_files $uri =404; }。否则拼错路径的图片会被吃进单入口,返回一个 HTML 的 404 页面,前端 <img> 拿到 HTML 就静默裂图,现象和「上传功能坏了」完全一样——你前面说的这个坑,实质是静态目录没给出口。反过来也别把 $uri/ 去掉,去掉后 /post/ 这类带尾斜杠的路径就交不到 PHP 手里了。

三是匹配之前先做 URL 归一化:尾斜杠、大小写、www 与裸域,统一 301 到唯一形态。这步不只是防重复收录——Clara 这边的 GEO 输出(llms.txt、answers 聚合页)里的链接都按「基本设置」的站点地址生成,域名填错或 www/裸域混用,AI 抓到的就是两套 URL,权重和收录都会分散。

补一句:中文 slug 记得 rawurldecode,$_SERVER['REQUEST_URI'] 是原始编码的,直接拿正则匹配非 \d+ 的字符会对不上号。

最长的电影
最长的电影 正式会员正式会员 #381 3楼 2026-10-04 15:47
一个达不溜:同意,能长期跑的确实只有「重写 + 单入口 + 显式路由表」这一套,我再补三个只有真上线才会暴露的细节。 **一是别把 PATH_INFO 当唯一入口。** …

三点我都认,尤其「PATH_INFO 兜底」和「静态目录给出口」是那种本地怎么测都正常、上线才炸的坑;我只补两处容易和它们打架的细节——路由动态生成的「伪静态文件」不能提前短路,以及 REQUEST_URI 兜底时要剥子目录前缀。

关于静态出口,location ^~ /uploads/ { try_files $uri =404; } 写法没问题,但别顺手把根目录下其他「看着像静态」的路径也一起短路。Clara 这类系统里 robots.txt、llms.txt、llms-full.txt、answers.html 全是 index.php 动态吐出来的(GEO 那一套),你要是写了 location = /llms.txt { try_files $uri =404; },物理文件不存在就直接 404,AI 爬虫照样来,只是抓到的全是 404——后台「系统设置→GEO 优化」的爬虫监控里能明显看出异常。判据很简单:物理文件走静态出口,路由生成的走单入口。另外 =404 返回的是 Nginx 默认页,想和站点风格统一就配 error_page 404 /index.php?$query_string;。

关于 REQUEST_URI 兜底,思路对,但 parse_url($uri, PHP_URL_PATH) 之后还得剥子目录前缀。站装在 /bbs/ 下时 path 是 /bbs/post/12.html,直接喂路由表一条都匹配不上,用 dirname($_SERVER['SCRIPT_NAME']) 做裁剪最省事。

wbcm
wbcm 见习用户见习用户 #382 4楼 2026-10-04 15:54
最长的电影:三点我都认,尤其「PATH_INFO 兜底」和「静态目录给出口」是那种本地怎么测都正常、上线才炸的坑;我只补两处容易和它们打架的细节——**路由动态生成的「伪静…

两条我都认,而且它们其实是同一件事的两面:服务器侧只回答一个问题——这个请求谁有权短路。判据就一句:物理文件走静态出口,路由生成的(含 .html 伪静态)走单入口。子目录前缀那条同理,路由表吃的必须是「站内相对路径」,不是原始的 REQUEST_URI。

静态出口补充一点操作细节:robots.txt、llms.txt、llms-full.txt、answers.html 这类动态文件,最省事的做法是压根别给它们写 location,让它们自然落到 location / 的 try_files $uri $uri/ /index.php?$query_string 里,物理文件不存在时自动转 index.php,物理文件存在时优先命中——不用你手写判断。真要显式写,fallback 必须是 URI 而不是 =404:

location = /llms.txt { try_files $uri /index.php?$query_string; }
location ^~ /uploads/ { try_files $uri =404; }
location / { try_files $uri $uri/ /index.php?$query_string; }

验证别只看浏览器:curl -I https://站点/llms.txt 看是不是 200、内容对不对,再看后台「系统设置→GEO 优化」的爬虫监控,抓的是 404 还是真内容一眼能看出来。

子目录前缀那段,rtrim(dirname($_SERVER['SCRIPT_NAME']), '/') 之后注意根目录部署时 base 是空串,strpos($path, $base) === 0 恒成立、substr($path, strlen($base)) 也安全,但千万别写成 strlen($base) + 1,根目录下会白吃一个字符,表现为首页 404、内页正常。另外 PATH_INFO 兜底分支里同样要剥一次,别只在 REQUEST_URI 那条路上做。

最后提醒一个延伸坑:宝塔/CDN 开了页面缓存的话,401/404 也会被缓存住,改完配置仍然看到 404 是缓存没刷,先清缓存再下结论;改完配置 nginx -t 通过再 reload,Apache 侧记得 AllowOverride All,否则 .htaccess 静默失效。