打开一个内容型网站,你会看到文章链接可能是 /article1、/post/123.html 这样的形式,而不是 article.php?id=1。这种干净整洁的 URL 通常被称为伪静态地址,它的本质是服务器把看似静态的路径重写为带参数的动态请求,再交给程序处理。实现它主要靠两条路:一是服务器层面的 URL 重写,二是应用层面的动态路由。本文把两种方式都拆开讲清楚,并给出可直接使用的配置和代码。

一、为什么要用 /article1 而不是 ?id=1
带查询参数的 URL 并非不能用,但它的缺点在实际运营中会逐渐暴露。首先是对搜索引擎不够友好,虽然现代爬虫已经能抓取带参数的地址,但层级清晰、语义明确的路径更容易获得好的权重评估。其次是安全性问题,?id=1 直接暴露了实现细节,攻击者可以顺手尝试 ?id=2、?page=../../etc/passwd 之类的注入探测,而伪静态地址在表面上抹掉了这些线索。
再次是用户体验和链接传播。/article1 这种短地址在口头传播、印刷物料、社交分享时都更不容易出错。想象一下你要在电话里念一个带三层参数的 URL,出错概率会高很多。另外,如果将来想更换后端技术,比如从 PHP 迁移到其他语言,静态化的路径结构可以原样保留,不会造成全站链接失效。
需要强调一点:伪静态并不等于静态化。伪静态只是把 URL 的外观改了,每次请求仍然会执行动态程序、查询数据库;而真正的静态化是把渲染结果写成真实的 HTML 文件。两者目的不同,不要混淆。
二、服务器层面的 URL 重写配置
URL 重写的思路是:当用户访问 /article1 时,服务器在内部把请求改写为 /article.php?id=1,浏览器地址栏保持不变。下面分别给出 Nginx 和 Apache 的写法。
Nginx 没有独立的 rewrite 模块文件,规则直接写在 location 块里:
location / {
# 如果请求的文件真实存在,直接返回,交给静态资源处理
if (-f $request_filename) {
break;
}
# 匹配 /article123,提取数字部分作为 id 参数
if ($uri ~ ^/article(\d+)$) {
rewrite ^/article(\d+)$ /article.php?id=$1 last;
}
# 兜底:其他路径交给入口文件
rewrite ^/(.*)$ /index.php?path=$1 last;
}注意 last 和 break 的区别:last 会重新开始匹配 location 规则,break 则停止后续 rewrite 处理。写错了容易造成循环重定向,浏览器报"重定向次数过多"基本就是这个问题。
Apache 依赖著名的 mod_rewrite 模块,规则写在站点根目录的 .htaccess 文件中:
RewriteEngine On
# 如果请求的文件或目录真实存在,不做重写
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# /article123 重写为 article.php?id=123
RewriteRule ^article(\d+)$ article.php?id=$1 [L,QSA]
其中 [L] 表示本条匹配后停止后续规则,[QSA] 表示保留原有的查询参数。RewriteCond 的两条判断很关键,它保证了真实存在的 CSS、JS、图片文件不会被误重写,这是新手最常踩的坑——配置完成后页面打开是白的,样式全丢,多半就是静态资源被重写到 PHP 处理了。
正则表达式里的捕获组 (\d+) 会作为 $1 传给目标地址。如果你想支持 /article/123.html 这种带后缀的形式,把正则改成 ^article/(\d+)\.html$ 即可。规则越靠前优先级越高,复杂站点建议把更具体的规则放在前面。
三、应用层面的动态路由实现
服务器重写适合简单场景,一旦 URL 结构复杂起来,比如 /article/123/comment/456,靠正则硬写会非常痛苦。更优雅的做法是:服务器把所有非静态请求都交给一个入口文件,由程序内部解析路径,这就是动态路由,也是现代框架的通行做法。
以 PHP 为例,先在 Nginx 中把请求统一转发:
location / {
try_files $uri $uri/ /index.php?$query_string;
}然后在 index.php 中实现一个极简路由器:
<?php
// 获取路径部分,去掉开头的斜杠
$path = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
// 简单路由表:路径 => 处理函数
$routes = [
'' => 'homePage',
'article/{id}' => 'articlePage',
];
foreach ($routes as $pattern => $handler) {
// 把 {id} 转成捕获组的正则
$regex = '#^' . preg_replace('#\{(\w+)\}#', '(?P<$1>\d+)', $pattern) . '$#';
if (preg_match($regex, $path, $m)) {
// 依次执行所有匹配到的处理逻辑
call_user_func($handler, $m['id'] ?? null);
return;
}
}
http_response_code(404);
echo '页面不存在';
function articlePage($id) {
// 根据 id 查询数据库并渲染模板
echo '这里是文章 ' . htmlspecialchars($id) . ' 的内容';
}
function homePage() {
echo '首页';
}
这段代码虽然简陋,但体现了路由的核心思想:把 URL 模式与处理逻辑解耦。访问 /article1 时,路由器匹配到 article/{id} 模式,提取出 id 为 1,交给 articlePage 函数渲染。参数经过了 htmlspecialchars 处理,避免了输出型注入。
实际项目中很少手写路由器,主流框架都内置了完善的实现。以 PHP 的 ThinkPHP 为例,一条路由声明就够了:
use think\facade\Route;
// 访问 /article/123 时执行 Article 控制器的 read 方法
Route::get('article/:id', 'Article/read')
->pattern(['id' => '\d+']);
Python 的 Flask 框架写法类似,用装饰器注册路由:
from flask import Flask
app = Flask(__name__)
@app.route('/article<int:aid>')
def show_article(aid):
# 查询数据库后渲染模板
return f'文章 {aid} 的内容'
注意 Flask 中 <int:aid> 的转换器写法,它不仅匹配数字,还自动把参数转成整数类型,非法路径直接返回 404,省去了手动校验。无论用哪个框架,思路都是一样的:路由表集中管理 URL 结构,改地址不需要动服务器配置。
四、常见问题排查与优化建议
配置完成后如果访问 404,排查要有顺序。第一步确认服务器是否加载了重写模块,Apache 下检查 mod_rewrite 是否启用,.htaccess 是否被 AllowOverride All 允许;Nginx 下检查配置语法是否正确并执行了 reload。第二步在目标 PHP 文件里打印 $_SERVER 变量,确认请求到底有没有被转发过来,这样能快速区分是重写没生效还是程序逻辑问题。
性能方面有两个值得注意的点。一是正则规则要尽量精简,Nginx 的 rewrite 在每个请求上都会执行,写一堆复杂正则会拖慢处理速度,高并发站点尤其明显。二是配合缓存使用,伪静态页面背后仍然是动态查询,如果文章内容更新不频繁,可以加上 Redis 缓存或者页面片段缓存,让 /article1 的响应速度真正接近静态文件。
最后一点关于 URL 规划:上线前把规则定好,之后尽量不要改动。如果不得不改,务必为旧地址配置 301 永久重定向到新地址,否则搜索引擎已经收录的链接会大量失效,权重损失需要很长时间才能恢复。统一的 URL 规范加上合理的路由设计,是内容型网站长期运营的基础设施,值得在项目初期就认真对待。