路由是 Web 框架的"交通警察"——用户访问一个 URL,路由决定这个请求由哪个控制器的哪个方法处理。早期 PHP 每个页面一个文件(article.php、contact.php),URL 与文件路径一一对应,简单但难维护。现代框架用路由表把 URL 与处理逻辑解耦,/article/123 可以映射到任意控制器,URL 结构不再受文件结构束缚。本篇带你从零实现一个简易路由,理解它的底层原理。
一、路由匹配与参数提取
路由的核心是"匹配"——拿用户请求的 URL 去匹配预定义的路由规则,找到对应的处理函数。最简单的匹配是字符串完全相等(/about 匹配 /about),但实际项目里 URL 常带参数(/article/123 里的 123 是文章 ID),这就需要"占位符"匹配。主流框架用 :id 或 {id} 表示参数位。
// 路由表定义
const routes = [
{ method: 'GET', pattern: '/', handler: 'Index@index' },
{ method: 'GET', pattern: '/article/:id', handler: 'Article@show' },
{ method: 'POST', pattern: '/article/create',handler: 'Article@create' },
{ method: 'GET', pattern: '/category/:cat/page/:page', handler: 'Category@list' }
];
// 路由匹配
function matchRoute(method, url) {
for (const route of routes) {
if (route.method !== method) continue;
// 把 /article/:id 转成正则 /^\/article\/([^\/]+)$/
const regexStr = route.pattern
.replace(/\//g, '\\/') // 转义斜杠
.replace(/:([^\/]+)/g, '([^\\/]+)'); // 参数转捕获组
const regex = new RegExp('^' + regexStr + '$');
const match = url.match(regex);
if (match) {
// 提取参数名
const paramNames = (route.pattern.match(/:([^\/]+)/g) || [])
.map(p => p.slice(1));
// 参数名与捕获组一一对应
const params = {};
paramNames.forEach((name, i) => {
params[name] = match[i + 1];
});
return { handler: route.handler, params };
}
}
return null; // 无匹配
}
// 测试:GET /article/123 → { handler: 'Article@show', params: { id: '123' } }
这个简易实现揭示了路由匹配的本质:把带占位符的规则转成正则,用正则匹配 URL 并提取参数。生产级框架在此基础上做了更多优化:编译路由表为正则数组加速匹配、支持参数类型约束(:id\d+ 只匹配数字)、支持可选参数与通配符。但万变不离其宗,核心逻辑就是"规则转正则 → 匹配 → 提参"。
二、分组路由与中间件
实际项目里路由数量可能上百条,全堆在一个数组里难以管理。主流框架提供"分组路由"功能,把同前缀的路由归到一组,共享前缀和中间件。比如后台所有路由都以 /admin 开头且都要经过登录校验,用分组就能避免每条路由重复写前缀和中间件。
// 分组路由实现
class Router {
constructor() {
this.routes = [];
this.groupStack = []; // 当前分组上下文
}
// 开始一个分组
group(options, callback) {
this.groupStack.push(options);
callback(this); // 执行分组内的路由注册
this.groupStack.pop();
}
// 注册路由(自动合并分组属性)
get(pattern, handler) {
let prefix = '';
let middlewares = [];
// 合并所有外层分组的属性
for (const group of this.groupStack) {
if (group.prefix) prefix += group.prefix;
if (group.middleware) middlewares = middlewares.concat(group.middleware);
}
this.routes.push({
method: 'GET',
pattern: prefix + pattern,
handler,
middlewares
});
}
}
// 使用示例
const router = new Router();
router.group({ prefix: '/admin', middleware: ['auth', 'log'] }, (r) => {
r.get('/dashboard', 'Admin@dashboard'); // /admin/dashboard
r.get('/users', 'Admin@users'); // /admin/users
r.group({ prefix: '/article' }, (r) => {
r.get('/list', 'Admin\Article@list'); // /admin/article/list
r.get('/edit/:id','Admin\Article@edit'); // /admin/article/edit/:id
});
});
中间件是路由的好搭档——请求到达控制器前先经过一系列中间件处理(鉴权、日志、CORS、限流)。执行顺序像洋葱一样:请求从外向内穿过中间件,响应从内向外返回。尧图项目里常用中间件做统一鉴权:前台路由组挂"访问统计"中间件,后台路由组挂"登录校验+权限检查"中间件,API 路由组挂"频率限制+签名校验"中间件,各司其职互不干扰。
三、伪静态与 URL 美化
搜索引擎对静态 HTML 的友好度高于动态 URL(?id=123),所以建站时会把动态 URL 伪静态成 /article/123.html 的形式。这需要 Web 服务器配合——Nginx/Apache 把所有非静态文件请求重写到入口文件,由框架路由接管。
# Nginx 伪静态配置
server {
listen 80;
server_name ldpk.cn;
root /var/www/ldpk.cn/public;
location / {
# 不存在的文件全部转给 index.php
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
# 框架内路由规则把 .html 后缀去掉再匹配
// /article/123.html → 去掉.html → /article/123 → 匹配 Article@show
$url = preg_replace('/\.html$/', '', $requestUri);
伪静态的关键是 try_files 指令:先尝试匹配真实文件(CSS、JS、图片等静态资源直接返回),找不到才转给 index.php。这样静态资源不走 PHP,性能无损;动态请求统一由框架路由处理。尧图在 URL 设计上偏好"语义化"——/service/brand 而非 /service?id=1,既利于 SEO 又便于用户理解。理解了路由原理,你就能自如地设计 URL 结构、定制匹配规则、组织中间件体系,这是深度掌握一个建站框架的必经之路。