PHP内置服务器实现URL重写的实战详解
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、核心原理:PHP内置服务器的路由拦截机制</a></li><li><a href="#_label1">二、基础环境配置:从启动到调试(适配主流项目结构)</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_0">2.1 标准项目目录结构</a></li><li><a href="#_lab2_1_1">2.2 启动PHP内置服务器</a></li><li><a href="#_lab2_1_2">2.3 VS Code调试配置(launch.json)</a></li></ul><li><a href="#_label2">三、基础路由脚本:解决静态资源与简单重写需求</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_3">3.1 核心痛点:静态资源404与规则失效</a></li><li><a href="#_lab2_2_4">3.2 基础版路由脚本(兼容PHP 5.6)</a></li><li><a href="#_lab2_2_5">3.3 基础规则测试</a></li></ul><li><a href="#_label3">四、进阶:适配复杂重写规则(以ThinkPHP项目为例)</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_6">4.1 复杂规则适配方案:规则分组排序</a></li><li><a href="#_lab2_3_7">4.2 规则适配关键点</a></li></ul><li><a href="#_label4">五、常见问题与解决方案</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_8">5.1 静态资源404</a></li><li><a href="#_lab2_4_9">5.2 重写规则不生效</a></li><li><a href="#_lab2_4_10">5.3 PHP 5.6兼容性问题</a></li></ul><li><a href="#_label5">六、总结</a></li><ul class="second_class_ul"></ul></ul></div><p>在PHP开发中,URL重写是实现友好访问路径的核心手段(如将 /bazijp.html映射为 /?ac=bazijp),通常依赖Nginx/Apache的 rewrite指令。但本地开发时,PHP内置服务器( php -S)更轻量高效,仅需通过自定义路由脚本即可实现等效重写功能。本文将从基础原理、环境配置、静态资源兼容、复杂规则适配(如ThinkPHP,laravel项目)等维度,结合实际项目的重写需求,提供一套可直接复用的解决方案,兼容PHP 5.6+主流版本。</p><p class="maodian"><a name="_label0"></a></p><h2>一、核心原理:PHP内置服务器的路由拦截机制</h2>
<p>PHP内置服务器本身不支持类似Nginx的<code>rewrite</code>语法,需通过路由脚本(如<code>router.php</code>)拦截所有请求并自定义转发逻辑,核心流程如下:</p>
<ul><li>客户端发起请求(如<code>sm.tekin.cn/bazijp.html</code>);</li><li>内置服务器将请求优先转发至路由脚本;</li><li>路由脚本根据预设规则重写URL(如映射为<code>sm.tekin.cn/index.php?ac=bazijp</code>);</li><li>重写后的请求转发至项目统一入口(如<code>public/index.php</code>);</li><li>若请求为静态资源(CSS/JS/图片),则直接返回文件,不进入重写流程。</li></ul>
<p><strong>关键前提</strong>:内置服务器对命令行参数顺序有严格要求,必须遵循<code>php -S [地址:端口] -t [文档根目录] [路由脚本]</code>,参数错位会导致路由失效或静态资源无法加载。</p>
<p class="maodian"><a name="_label1"></a></p><h2>二、基础环境配置:从启动到调试(适配主流项目结构)</h2>
<p class="maodian"><a name="_lab2_1_0"></a></p><h3>2.1 标准项目目录结构</h3>
<p>以常见的“Web根目录与业务代码分离”结构为例(如ThinkPHP、Laravel等框架通用),目录结构如下:</p>
<blockquote><p>project-root/ # 项目根目录<br />├─ public/ # Web根目录(线上 访问根路径)<br />│ ├─ statics/ # 静态资源目录(CSS/JS/图片)<br />│ └─ index.php # 项目统一入口文件<br />└─ router.php # URL重写路由脚本</p></blockquote>
<p class="maodian"><a name="_lab2_1_1"></a></p><h3>2.2 启动PHP内置服务器</h3>
<p>在<strong>项目根目录</strong>执行命令,指定<code>public</code>为Web根目录,<code>router.php</code>为路由脚本,确保与线上环境目录映射一致:</p>
<div class="jb51code"><pre class="brush:bash;"># 基础启动命令(本地访问地址:http://127.0.0.1:8000)
php -S 127.0.0.1:8000 -t public router.php
# 启用Xdebug调试(兼容PHP 5.6+)
php -dxdebug.remote_enable=1 -dxdebug.remote_autostart=1 -S 127.0.0.1:8000 -t public router.php
</pre></div>
<p class="maodian"><a name="_lab2_1_2"></a></p><h3>2.3 VS Code调试配置(launch.json)</h3>
<p>若需在IDE中调试业务逻辑,需配置<code>launch.json</code>确保调试与重写协同生效,关键在于参数顺序与线上一致:</p>
<div class="jb51code"><pre class="brush:json;">{
"version": "0.2.0",
"configurations": [
{
"name": "PHP内置服务器+URL重写+调试",
"type": "php",
"runtimeExecutable": "/opt/local/bin/php56", // 本地PHP路径(需与项目版本匹配)
"request": "launch",
"runtimeArgs": [
"-dxdebug.remote_enable=1",
"-dxdebug.remote_autostart=1",
"-dxdebug.remote_port=9000", // 与php.ini中Xdebug端口一致
"-S", "127.0.0.1:8000",
"-t", "public", // 匹配线上Web根目录
"router.php" // 路由脚本必须放在最后
],
"cwd": "${workspaceRoot}",
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:(+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally" // 启动后自动打开本地调试地址
}
}
]
}
</pre></div>
<p class="maodian"><a name="_label2"></a></p><h2>三、基础路由脚本:解决静态资源与简单重写需求</h2>
<p class="maodian"><a name="_lab2_2_3"></a></p><h3>3.1 核心痛点:静态资源404与规则失效</h3>
<p>本地开发中,两类问题最为常见:</p>
<ul><li>静态资源(如<code>/statics/ffsm/global.css</code>)被路由脚本拦截,返回404;</li><li>简单重写规则(如<code>/hehun.html</code>→<code>/?ac=hehun</code>)不生效,无法访问目标模块。</li></ul>
<p class="maodian"><a name="_lab2_2_4"></a></p><h3>3.2 基础版路由脚本(兼容PHP 5.6)</h3>
<p>核心逻辑:<strong>优先处理静态资源,再执行URL重写</strong>,确保静态资源加载与动态路由分离:</p>
<div class="jb51code"><pre class="brush:php;"><?php
// 项目根目录与Web根目录定义
$projectRoot = __DIR__;
$webRoot = rtrim($projectRoot . '/public', '/') . '/';
// 1. 解析并标准化请求URI
$requestUri = $_SERVER['REQUEST_URI'];
$uriParts = parse_url($requestUri);
$uriPath = isset($uriParts['path']) ? $uriParts['path'] : '/';
$uriPath = preg_replace('#/\.\./#', '/', $uriPath); // 过滤目录遍历攻击
$uriPath = rtrim($uriPath, '/'); // 统一去除末尾斜杠(如`/suanming/scbz/`→`/suanming/scbz`)
$uriPath = $uriPath === '' ? '/' : $uriPath;
// 2. 优先处理静态资源(存在则直接返回)
$requestedFile = $webRoot . ltrim($uriPath, '/');
if (file_exists($requestedFile) && is_file($requestedFile) && !is_dir($requestedFile)) {
// 设置正确MIME类型,避免浏览器解析异常
$mimeType = getMimeType($requestedFile);
if ($mimeType) header("Content-Type: {$mimeType}");
readfile($requestedFile);
exit;
}
// 3. 定义基础URL重写规则(可根据项目需求扩展)
$rewriteRules = [
'#^/index\.html$#' => '/index.php', // 首页规则
'#^/bazijp\.html$#' => '/?ac=bazijp', // 八字精批模块规则
'#^/hehun\.html$#' => '/?ac=hehun', // 合婚模块规则
'#^/aboutus\.html$#' => '/?ac=aboutus', // 关于我们页面规则
'#^/xyd-(+)\.html$#' => '/?ac=xyd&id=$1', // 详情页动态规则
'#^/([^/]+)\.html$#' => '/index.php?ac=$1', // 最后执行:通用.html规则(最宽泛,避免覆盖前面的具体规则)
];
// 4. 应用重写规则
$newUri = $uriPath;
foreach ($rewriteRules as $pattern => $target) {
if (preg_match($pattern, $uriPath)) {
$newUri = preg_replace($pattern, $target, $uriPath);
break; // 匹配到即终止,避免规则冲突
}
}
// 5. 处理查询参数(保留原参数,新规则参数覆盖同名原参数)
$originalQuery = isset($uriParts['query']) ? $uriParts['query'] : '';
$newUriParts = parse_url($newUri);
$newPath = isset($newUriParts['path']) ? $newUriParts['path'] : '/';
$newQuery = isset($newUriParts['query']) ? $newUriParts['query'] : '';
$finalQuery = '';
if (!empty($originalQuery) && !empty($newQuery)) {
parse_str($originalQuery, $originalParams);
parse_str($newQuery, $newParams);
$mergedParams = array_merge($originalParams, $newParams);
$finalQuery = http_build_query($mergedParams);
} elseif (!empty($originalQuery)) {
$finalQuery = $originalQuery;
} else {
$finalQuery = $newQuery;
}
// 6. 更新服务器变量,转发到统一入口
$finalUri = $newPath . ($finalQuery ? "?{$finalQuery}" : '');
$_SERVER['REQUEST_URI'] = $finalUri;
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['QUERY_STRING'] = $finalQuery;
parse_str($finalQuery, $_GET); // 同步更新GET参数,适配框架参数获取
// 7. 执行入口文件
$indexFile = $webRoot . 'index.php';
if (file_exists($indexFile)) {
include_once $indexFile;
} else {
http_response_code(404);
echo "404 Not Found:public/index.php 入口文件不存在";
}
exit;
/**
* 兼容PHP 5.6的MIME类型获取
* @param string $file 文件路径
* @return string|null MIME类型
*/
function getMimeType($file) {
$extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$mimeMap = [
'css' => 'text/css', 'js' => 'application/javascript',
'html' => 'text/html', 'jpg' => 'image/jpeg',
'png' => 'image/png', 'gif' => 'image/gif', 'ico' => 'image/x-icon'
];
return isset($mimeMap[$extension]) ? $mimeMap[$extension] : null;
}
</pre></div>
<p class="maodian"><a name="_lab2_2_5"></a></p><h3>3.3 基础规则测试</h3>
<ul><li>访问<code>http://127.0.0.1:8000/bazijp.html</code>,通过<code>var_dump($_GET)</code>应看到<code>array('ac' => 'bazijp')</code>;</li><li>访问<code>http://127.0.0.1:8000/xyd-123.html</code>,应看到<code>array('ac' => 'xyd', 'id' => '123')</code>;</li><li>访问<code>http://127.0.0.1:8000/statics/ffsm/global.css</code>,应直接返回CSS文件内容。</li></ul>
<p class="maodian"><a name="_label3"></a></p><h2>四、进阶:适配复杂重写规则(以ThinkPHP项目为例)</h2>
<p>实际项目中,常需处理大量复杂重写规则(如多模块路由、动态参数拼接)。例如某命理类项目的Nginx规则片段:</p>
<div class="jb51code"><pre class="brush:bash;">rewrite ^/aboutus.html /index.php?ac=aboutus last;
rewrite ^/xyd-(+).html /index.php?ac=xyd&id=$1 last;
rewrite ^/(.*).html /index.php?ac=$1 last;
rewrite ^/show-(+).html /index.php?ct=news&ac=show&id=$1;
</pre></div>
<p>这类规则迁移时,核心挑战是<strong>避免通用规则覆盖具体规则</strong>(如<code>/aboutus.html</code>不能被<code>/.+\.html</code>的通用规则错误映射)。</p>
<p class="maodian"><a name="_lab2_3_6"></a></p><h3>4.1 复杂规则适配方案:规则分组排序</h3>
<p>核心思路:将规则按“精准度”分组,<strong>精准规则优先匹配</strong>,通用规则兜底,确保每个模块的路由逻辑与线上一致。</p>
<p><strong>完整路由脚本(适配复杂项目规则)</strong></p>
<div class="jb51code"><pre class="brush:php;"><?php
$projectRoot = __DIR__;
$webRoot = rtrim($projectRoot . '/public', '/') . '/';
// 1. 标准化请求URI
$requestUri = $_SERVER['REQUEST_URI'];
$uriParts = parse_url($requestUri);
$uriPath = isset($uriParts['path']) ? $uriParts['path'] : '/';
$uriPath = preg_replace('#/\.\./#', '/', $uriPath);
$uriPath = rtrim($uriPath, '/');
$uriPath = $uriPath === '' ? '/' : $uriPath;
// 2. 优先处理静态资源
$requestedFile = $webRoot . ltrim($uriPath, '/');
if (file_exists($requestedFile) && is_file($requestedFile) && !is_dir($requestedFile)) {
$mimeType = getMimeType($requestedFile);
if ($mimeType) header("Content-Type: {$mimeType}");
readfile($requestedFile);
exit;
}
// 3. 规则分组:精准规则 → 通用规则(避免宽覆盖窄)
// 注意下面的规则需要根据你自己的项目进行修改,这里仅做示例 更多可以参考 https://sm.tekin.cn 站点的URL重写
// --------------------------
// 第一组:精准规则(无动态参数,固定URL)
// --------------------------
$exactRules = [
// 基础页面
'#^/index\.html$#' => '/index.php',
'#^/aboutus\.html$#' => '/index.php?ac=aboutus',
'#^/contactus\.html$#' => '/index.php?ac=contactus',
];
// --------------------------
// 第二组:通用规则(带动态参数、后缀匹配)
// --------------------------
$generalRules = [
// 带ID的精准后缀规则
'#^/xyd-(+)\.html$#' => '/index.php?ac=xyd&id=$1',
// 姓名模块动态路径
'#^/xmfx/([^/]+)$#' => '/index.php?ct=xingming&ac=xmfx&name=$1',
'#^/xqlist-(+)-(+)-(+)-(+)\.html$#' => '/index.php?ct=xq&xid=$1&sex=$2&hs=$3&page=$4',
// 最后执行:通用.html规则(兜底)
'#^/([^/]+)\.html$#' => '/index.php?ac=$1',
];
// 4. 执行重写:先精准后通用
$newUri = $uriPath;
$ruleMatched = false;
// 第一步:匹配精准规则(核心业务优先)
foreach ($exactRules as $pattern => $target) {
if (preg_match($pattern, $uriPath)) {
$newUri = preg_replace($pattern, $target, $uriPath);
$ruleMatched = true;
break;
}
}
// 第二步:精准规则未匹配时,匹配通用规则
if (!$ruleMatched) {
foreach ($generalRules as $pattern => $target) {
if (preg_match($pattern, $uriPath)) {
$newUri = preg_replace($pattern, $target, $uriPath);
break;
}
}
}
// 5. 合并查询参数(同基础版逻辑)
$originalQuery = isset($uriParts['query']) ? $uriParts['query'] : '';
$newUriParts = parse_url($newUri);
$newPath = isset($newUriParts['path']) ? $newUriParts['path'] : '/';
$newQuery = isset($newUriParts['query']) ? $newUriParts['query'] : '';
$finalQuery = '';
if (!empty($originalQuery) && !empty($newQuery)) {
parse_str($originalQuery, $originalParams);
parse_str($newQuery, $newParams);
$mergedParams = array_merge($originalParams, $newParams);
$finalQuery = http_build_query($mergedParams);
} elseif (!empty($originalQuery)) {
$finalQuery = $originalQuery;
} else {
$finalQuery = $newQuery;
}
// 6. 转发到入口文件
$finalUri = $newPath . ($finalQuery ? "?{$finalQuery}" : '');
$_SERVER['REQUEST_URI'] = $finalUri;
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['QUERY_STRING'] = $finalQuery;
parse_str($finalQuery, $_GET);
$indexFile = $webRoot . 'index.php';
if (file_exists($indexFile)) {
include_once $indexFile;
} else {
http_response_code(404);
echo "404 Not Found:public/index.php 入口文件不存在";
}
exit;
// MIME类型函数(同基础版)
function getMimeType($file) {
$extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$mimeMap = [
'css' => 'text/css', 'js' => 'application/javascript',
'html' => 'text/html', 'jpg' => 'image/jpeg',
'png' => 'image/png', 'gif' => 'image/gif', 'ico' => 'image/x-icon'
];
return isset($mimeMap[$extension]) ? $mimeMap[$extension] : null;
}
?>
</pre></div>
<p class="maodian"><a name="_lab2_3_7"></a></p><h3>4.2 规则适配关键点</h3>
<ul><li><strong>分组逻辑</strong>:<code>$exactRules</code>存放固定URL(如<code>/aboutus.html</code>),<code>$generalRules</code>存放动态URL(如<code>/([^/]+)\.html</code>),确保精准规则不被覆盖;</li><li><strong>通用规则内部顺序</strong>:即使在<code>$generalRules</code>中,也需从“具体”到“宽泛”排序(如先匹配<code>/xyd-(+)\.html</code>,再匹配<code>/([^/]+)\.html</code>);</li><li><strong>参数兼容性</strong>:保留原请求中的查询参数(如<code>/user/abc?foo=bar</code>→<code>/index.php?ct=user&ac=abc&foo=bar</code>),符合线上重写习惯。</li></ul>
<p class="maodian"><a name="_label4"></a></p><h2>五、常见问题与解决方案</h2>
<p class="maodian"><a name="_lab2_4_8"></a></p><h3>5.1 静态资源404</h3>
<p><strong>原因</strong>:路由脚本未优先处理静态资源,或<code>$webRoot</code>路径拼接错误(如多写斜杠);</p>
<p><strong>解决方案</strong>:确保静态资源判断逻辑在重写规则之前,<code>$requestedFile</code>路径格式为<code>public/statics/ffsm/global.css</code>。</p>
<p class="maodian"><a name="_lab2_4_9"></a></p><h3>5.2 重写规则不生效</h3>
<p><strong>原因</strong>:内置服务器参数顺序错误(如<code>router.php</code>放在<code>-t public</code>之前),或正则表达式错误(如<code>.</code>未转义);</p>
<p><strong>解决方案</strong>:严格遵循<code>php -S 地址 -t 根目录 路由脚本</code>,正则使用<code>#^/aboutus\.html$#</code>格式。</p>
<p class="maodian"><a name="_lab2_4_10"></a></p><h3>5.3 PHP 5.6兼容性问题</h3>
<p><strong>问题</strong>:使用<code>??</code>空合并运算符导致语法错误;</p>
<p><strong>解决方案</strong>:用<code>isset()</code>+三元运算符替代(如<code>$uriPath = isset($uriParts['path']) ? $uriParts['path'] : '/'</code>)。</p>
<p class="maodian"><a name="_label5"></a></p><h2>六、总结</h2>
<p>PHP内置服务器的URL重写核心在于路由脚本的设计,通过“静态资源优先+规则分组排序”的思路,可实现与Nginx/Apache等效的重写功能。关键要点如下:</p>
<ul><li><strong>参数顺序</strong>:启动时严格遵循<code>-S 地址 -t 根目录 路由脚本</code>;</li><li><strong>静态优先</strong>:避免静态资源进入重写流程;</li><li><strong>规则分组</strong>:按“精准→通用”排序,解决规则覆盖问题;</li><li><strong>版本兼容</strong>:针对PHP 5.6等旧版本调整语法,确保项目可用性。</li></ul>
<p>这套方案可直接复用于ThinkPHP等主流框架的本地开发,无需依赖重型Web服务器,有效提升开发效率,减少环境差异导致的问题。</p>
頁:
[1]