学完这篇,你能搞清 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,不能接受就老老实实写重写规则。