ThinkPHP 做前后端分离的核心是绕开模板渲染、Session 依赖和默认路由陷阱:用 TP6/8 + RESTful 路由 + JSON 响应 + JWT 无状态认证;跨域用 think-cors 扩展统一配置;验证码改用 Redis 存储并传递 captcha_id;Vue history 模式需后端 fallback 至 index.html;JSON 响应须统一封装避免格式不一致。

ThinkPHP 做前后端分离,核心不是“能不能”,而是“怎么绕开模板渲染、Session 依赖和默认路由陷阱”。直接上结论:用 TP6/8 + RESTful 路由 + JSON 响应 + 无状态认证(如 JWT),前端 Vue 独立运行,后端只管接口,不碰 HTML 渲染。
跨域请求总被拦?别硬配 header,用 think-cors 扩展
手动在中间件或行为里写 header('Access-Control-Allow-Origin: *') 容易漏掉 OPTIONS 预检、withCredentials 兼容、expose-headers 等细节,一上线就 403 或 500。
正确做法是统一用官方扩展:composer require topthink/think-cors,然后在 config/cors.php 中配置:
return [
'origin' => ['http://localhost:8080', 'https://your-vue-app.com'],
'allow_credentials'=> true,
'allow_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
'allow_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
'max_age' => 3600,
];
- 生产环境务必把
origin改成具体域名,禁用*; -
allow_credentials设为true时,origin不能是*,否则浏览器拒绝; - Vue 开发时 Axios 请求要加
withCredentials: true,否则 Cookie / Session 不会发过去(如果还用 Session 的话); - JWT 场景下其实可以关掉
allow_credentials,改用Authorization: Bearer xxx传 token。
验证码总校验失败?Session 在跨域下根本不可靠
前端请求 /captcha 图片,后端存到 $this->session->set('captcha.key', $hash);前端提交表单带验证码文本,后端调 $this->session->get('captcha.key') —— 这套逻辑在前后端同域时没问题,但跨域后,浏览器不会自动携带 Cookie,Session ID 断了,get() 永远返回 null。
立即学习“PHP免费学习笔记(深入)”;
解决方案必须放弃 Session 绑定,改用可传递的唯一标识:
使用 `@ainative/vue-sdk` 为 Vue 3 应用添加 AI 聊天功能。适用于以下场景:(1) 安装 `@ainative/vue-sdk`,(2) 在 Vue 组件中使用 `useChat` 组合式函数,(3) 提供……
- 生成验证码时,后端返回
json(['captcha_id' => 'abc123', 'image' => 'data:image/png;base64,...']); - 前端把
captcha_id存进表单 hidden 字段或请求头; - 校验接口接收
captcha_id和用户输入的code,从 Redis 查captcha:abc123对比; - 查完立刻
DEL captcha:abc123,防重放; - 不要用文件或数据库存验证码,Redis TTL 控制 2 分钟足够,性能和清理都干净。
Vue 路由和 ThinkPHP 路由别打架:前端接管所有非 API 路径
Vue Router 默认是 history 模式,访问 /user/profile 时,浏览器会向后端发 GET 请求。但 ThinkPHP 如果没配 fallback,就会 404。
解决方法是在 route/app.php 最后加一条兜底规则,把所有非 API 路径交给前端处理:
use think\facade\Route;
// API 路由放前面
Route::group('api', function () {
Route::get('user', 'api/User/index');
Route::post('login', 'api/Login/login');
});
// 所有其他路径返回前端 index.html(仅开发/测试环境)
if (env('APP_DEBUG')) {
Route::rule('*', function () {
return \think\Response::create(public_path() . '/index.html', 'html')->code(200);
});
}
- 线上 Nginx/Apache 应该用服务器配置做 fallback,比如 Nginx 的
try_files $uri $uri/ /index.html;; - 这条规则必须放在所有 API 路由之后,否则
/api/user也会被匹配到; -
public_path() . '/index.html'要确保 Vue 构建后的index.html确实放在 public 目录下; - TP8 中
public_path()是内置函数,TP6 需要自己定义或用__PUBLIC__ . '/index.html'。
JSON 返回总缺字段或格式乱?别手拼数组,用统一响应封装
每个控制器都写 return json(['code' => 200, 'msg' => 'ok', 'data' => $xxx]) 看似简单,但容易漏 code、错写 message 而不是 msg、忘记空数组兜底,导致前端反复适配。
推荐在 app/common.php 或基类里定义一个助手函数:
function api_response($data = [], $msg = 'success', $code = 200) {
return json([
'code' => $code,
'msg' => $msg,
'data' => $data,
'time' => time(),
])->contentType('application/json', 'utf-8');
}
- 调用时直接
return api_response($user);或return api_response([], '参数错误', 400);; - 加上
time字段方便前端做本地缓存或时间戳比对; -
contentType显式声明,避免某些代理或 CDN 强制改 content-type; - 异常场景(如数据库查询失败)建议统一用
throw new HttpException(500, '数据库异常'),再配合全局异常处理中间件转成标准 JSON 格式,而不是每个 try-catch 里手写 response。
真正卡住人的从来不是“怎么写个接口”,而是跨域配置漏项、验证码绑定失效、前端路由 fallback 缺失、响应格式不一致这些细节。每一步都得验证——用 Postman 测接口,用 Chrome Network 看请求头和响应体,用 Redis CLI 查验证码 key 是否写入成功。跑通一次,比看十篇教程管用。


















