本文目录导读:

- 方案一:通过 URL 路径区分版本(最直观、推荐)
- 方案二:通过 HTTP Header 区分版本(更“规范”但有争议)
- 方案三:使用 Package(成熟稳定)
- 最佳实践建议
- 例子:完整的路由文件结构
- 总结推荐
在 Laravel 中设计“版本路由”通常有两种核心思路:
- API 版本控制(最常见,通过 URL 或 Header 区分)
- 应用版本控制(针对不同前端版本或客户端版本返回不同数据)
下面主要围绕 API 版本路由 来详细说明设计方法。
通过 URL 路径区分版本(最直观、推荐)
这是最流行的方式,/api/v1/users 和 /api/v2/users。
基础结构:使用 Route Group + Prefix
// routes/api.php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes v1
|--------------------------------------------------------------------------
*/
Route::prefix('v1')->group(function () {
Route::get('/users', [App\Http\Controllers\Api\V1\UserController::class, 'index']);
Route::post('/users', [App\Http\Controllers\Api\V1\UserController::class, 'store']);
// ... 其他 v1 路由
});
/*
|--------------------------------------------------------------------------
| API Routes v2
|--------------------------------------------------------------------------
*/
Route::prefix('v2')->group(function () {
Route::get('/users', [App\Http\Controllers\Api\V2\UserController::class, 'index']);
// v2 可能对 store 方法做了修改
Route::post('/users', [App\Http\Controllers\Api\V2\UserController::class, 'store']);
// ... 其他 v2 路由
});
控制器结构建议:
// app/Http/Controllers/Api/V1/UserController.php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\User;
class UserController extends Controller
{
public function index()
{
return User::all(); // v1 逻辑(字段、关系不同)
}
}
// app/Http/Controllers/Api/V2/UserController.php
namespace App\Http\Controllers\Api\V2;
use App\Http\Controllers\Controller;
use App\Models\User;
class UserController extends Controller
{
public function index()
{
return User::with('profile')->get(); // v2 增加了 profile 关系
}
}
优化:使用命名空间
可以在 RouteServiceProvider 中统一设置命名空间,简化书写。
// app/Providers/RouteServiceProvider.php
public function boot(): void
{
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
// 或者单独设置不同版本的命名空间
Route::prefix('api/v1')
->middleware('api')
->namespace('App\Http\Controllers\Api\V1')
->group(base_path('routes/api_v1.php'));
Route::prefix('api/v2')
->middleware('api')
->namespace('App\Http\Controllers\Api\V2')
->group(base_path('routes/api_v2.php'));
}
通过 HTTP Header 区分版本(更“规范”但有争议)
不使用 URL 路径,而是通过 Accept 或自定义 X-Version 头来决定使用哪个控制器。
定义中间件
php artisan make:middleware ApiVersion
// app/Http/Middleware/ApiVersion.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ApiVersion
{
public function handle(Request $request, Closure $next)
{
// 从 Header 中获取版本号,Accept: application/vnd.myapp.v2+json
$acceptHeader = $request->header('Accept');
$version = 'v1'; // 默认 v1
if (preg_match('/application\/vnd\.myapp\.(v\d+)\+json/', $acceptHeader, $matches)) {
$version = $matches[1];
}
// 将版本号绑定到请求属性中
$request->attributes->set('api_version', $version);
// 动态调用不同版本的控制器
// 注意:这里需要确保对应的控制器类存在
return $next($request);
}
}
使用 Trait 或 Base Controller 动态路由
可以写一个基类控制器或 Trait,根据版本号调用不同业务逻辑。
// app/Http/Controllers/Api/BaseController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class BaseController extends Controller
{
protected function getVersionedController(string $controllerName, Request $request)
{
$version = $request->attributes->get('api_version', 'v1');
$class = "App\\Http\\Controllers\\Api\\{$version}\\{$controllerName}";
if (!class_exists($class)) {
abort(404, "API version {$version} not found");
}
return app($class);
}
}
然后在路由中:
Route::get('/users', function (Request $request) {
$controller = app()->make(\App\Http\Controllers\Api\BaseController::class);
$versionedController = $controller->getVersionedController('UserController', $request);
return $versionedController->index($request);
})->middleware('api.version');
这种方式的优点是 URL 保持
/api/users不变,缺点是:
- 增加了复杂性
- 不够直观,调试困难
- 对于缓存、文档生成不友好 现实项目中较少使用,更推荐 URL 路径方案。
使用 Package(成熟稳定)
如果需要更强大、规范化的版本控制(包括 Header 版本、过期策略等),推荐使用现成包:
- Laravel API Versioning (如
mnahmad/laravel-api-versioning) - Laravel API Resource 版本(结合 Transformer)
- 自定义路由宏
不过对于多数项目,方案一(URL 前缀 + 独立控制器)足够清晰高效。
最佳实践建议
- API 版本尽量少变:设计好 v1,不要轻易改 v2,如果只是加字段,用可选的属性扩展(如
additional字段),而不是切版本。 - URL 路径版本为首选:
/api/v1/resource最清晰、易维护。 - 控制器按版本分离:不要在一个控制器里
if v1/if v2,那样会变得混乱。 - 使用 Repository / Service 模式:公共业务逻辑放在 Service 层,控制器只是负责转换(Transformer/Resource)。
- 版本过期机制:可以在
AppServiceProvider中注册一个v1的过期告警中间件,告诉客户端要升级。
// 在 v1 路由组中增加
Route::prefix('v1')->middleware('api.version.obsolete')->group(...);
// middleware 内输出 Header: X-API-Version-Obsolete: true
例子:完整的路由文件结构
routes/
├── api.php # 公共路由(默认版本)
├── api_v1.php # v1 专有路由
├── api_v2.php # v2 专有路由
├── web.php # Web 路由
└── ...
// routes/api_v1.php
Route::prefix('v1')->group(function () {
Route::get('/users', ...);
Route::post('/users', ...);
});
// routes/api_v2.php
Route::prefix('v2')->group(function () {
Route::get('/users', ...);
Route::post('/users', ...);
});
然后在 RouteServiceProvider 加载它们:
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api_v1.php'));
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api_v2.php'));
});
这样结构清晰,方便多人协作。
总结推荐
| 场景 | 建议方案 |
|---|---|
| 简单 API 版本 | URL 路径版本 (/api/v1, /api/v2) |
| 要求 URL 统一、Header 控制版本 | Header 版本(配合中间件,但慎用) |
| 大型项目、需要多种版本策略 | Package (如 Spatie 的 laravel-query-builder 中的版本支持) |
最终结论:Laravel 里版本路由最简单的设计就是 Route::prefix('v1') + Route::prefix('v2'),每个版本对应一个控制器命名空间,清晰、可维护,也是 Laravel 官方文档推荐的入门做法。