TUTORIAL

自定义路由实现原理

自定义路由实现原理

自定义路由实现原理

路由是 Web 框架的"交通警察"——用户访问一个 URL,路由决定这个请求由哪个控制器的哪个方法处理。早期 PHP 每个页面一个文件(article.phpcontact.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 结构、定制匹配规则、组织中间件体系,这是深度掌握一个建站框架的必经之路。

返回教程列表