Laravel+Vue前后端分离实战:从API认证到Nginx部署

发布时间:2026/9/7 22:48:33
Laravel+Vue前后端分离实战:从API认证到Nginx部署 前后端分离这件事说小了是一次接口对接说大了是一场协作模式的重构。Laravel负责出API、Vue负责渲染页面两者各管一摊中间靠HTTP协议沟通。用这个组合做项目最直接的好处是前端不再被Blade模板绑死后端也不用操心页面长什么样团队可以并行开发部署也能各走各的。这篇博文我会从一个完整的项目角度切入讲清楚Laravel和Vue是怎么配合的从环境搭建、接口设计、认证方案到部署上线再到我实际踩过的坑。适合刚入门前后端分离不久、用Laravel做API后端的朋友也适合那些想从传统服务端渲染迁移过来的团队参考。内容会比较细能直接照着抄。1. 整体架构与方案选型为什么是Laravel Vue1.1 Laravel和Vue在分离架构里各自扮演什么角色前后端分离之后项目从“一个应用”变成了“两个应用”。Laravel这边只暴露JSON接口不再返回HTML视图用户能不能登录、文章能不能删都由它说了算。Vue这边则负责把数据变成界面用户在页面上点的每一个按钮最终都转化成对Laravel接口的HTTP请求。Laravel做API后端有几个特别顺手的地方路由分组和中间件机制非常成熟api路由文件天生就是为接口设计的Eloquent配合迁移文件管理数据库很舒服模型关联写起来不绕还有官方提供的Sanctum做API认证几乎是开箱即用。这些都是我在项目里真正用到的。Vue这边选Vue 3理由也很实在组合式API让逻辑复用简单了太多一个登录逻辑抽成composables能在多个页面共享这在Vue 2里要做mixin维护起来够呛。配合Vue Router做前端路由、Pinia做状态管理整个前端的骨架算是齐了。我见过很多团队做分离时纠结“要不再加个Node中间层”我觉得在人员有限的情况下没必要。Laravel自己就能把API做得很好Vue直接请求它就行中间层反而增加链路复杂度和排障成本。项目起步阶段简单直接就是最好的架构。1.2 认证方案的关键决定Sanctum还是JWT认证是前后端分离里第一个绕不开的决策。传统Blade项目靠Session和Cookie但分离之后前端可能跑在另一台服务器甚至App里Cookie跨域那一套非常麻烦所以业界更常用Token认证。Laravel官方给了两个主流选择Sanctum和Passport。Passport是完整的OAuth2.0服务端支持授权码、客户端凭证等流程适合第三方开放接口这种重场景。但大多数普通业务系统用不上OAuth那么重的东西拿Sanctum就够了。我这几个项目用的是Sanctum的personal_access_token模式实现原理很简单用户登录时后端帮他生成一个随机token存到personal_access_tokens表里同时把token返回给前端前端每次请求在Authorization: Bearer token里带上Laravel通过中间件解析这个token找到对应的用户。提示Sanctum的Sanctum::actingAs()方法在写接口测试时很好用不需要真的走登录流程直接模拟认证用户能省下大量测试代码。我建议不要一上来就上JWT除非你确实需要无状态、多端共享同一套token体系。JWT过期时间难控制、注销麻烦、密钥管理还要操心在纯前后端Web项目里优势并不明显。Sanctum的token存数据库虽然每次请求多一次数据库查询但换来的是可撤销、可控这对大多数业务系统更重要。1.3 接口设计与前端目录结构规划结构决定沟通成本。我习惯在Laravel这边按资源划分控制器比如PostController所有跟文章相关的接口都放这里前端则在src/api目录里建对应的模块文件比如post.js专门封装文章的增删改查请求。这样两边对接口文档的命名是对应的出问题了好沟通。接口路径规划也有讲究我推荐用RESTful语义GET /api/posts获取列表POST /api/posts创建文章GET /api/posts/{id}获取单篇PUT /api/posts/{id}更新DELETE /api/posts/{id}删除。Laravel的资源路由一行Route::apiResource(posts, PostController::class)就能把这一整套注册完非常省事。前端目录结构我通常是这样resources/js/ ├── api/ # 接口请求封装 │ ├── auth.js │ ├── post.js │ └── request.js # axios实例 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── views/ # 页面组件 ├── App.vue └── main.js这个结构看着简单但足够支撑一个中型项目。接口请求集中管理之后哪天后端的URL变了只需要改api目录下的一个文件不用满项目找axios.get。2. 开发环境搭建与跨域处理一步都不能省2.1 Laravel后端初始化与API路由配置我假定你已经装好了PHP和Composer。创建一个新项目composer create-project laravel/laravel laravel-vue-app cd laravel-vue-app接下来安装Sanctumcomposer require laravel/sanctum php artisan vendor:publish --providerLaravel\Sanctum\SanctumServiceProvider php artisan migrate因为只做API认证我在config/sanctum.php里把stateful数组留空避免混入Session逻辑stateful [],然后修改app/Http/Kernel.php把Sanctum的中间件加入api中间件组api [ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, throttle:api, \Illuminate\Routing\Middleware\SubstituteBindings::class, ],路由方面我在routes/api.php里这样规划Route::post(/register, [AuthController::class, register]); Route::post(/login, [AuthController::class, login]); Route::middleware(auth:sanctum)-group(function () { Route::post(/logout, [AuthController::class, logout]); Route::get(/user, [AuthController::class, user]); Route::apiResource(posts, PostController::class); });/api前缀是Laravel自动加的前端不用管只要最终URL一致就行。2.2 开发环境最快路径Vite代理而非配置CORS前后端联调时第一关就是跨域问题。前端跑在http://localhost:5173后端跑在http://localhost:8000两个端口不同浏览器的同源策略会拦下所有请求。网上很多教程让你装fruitcake/laravel-cors但Laravel 9之后已经内置了CORS支持在config/cors.php里配就行。不过我更推荐开发环境直接用Vite的代理功能从根上规避跨域// vite.config.js import { defineConfig } from vite; import vue from vitejs/plugin-vue; import laravel from laravel-vite-plugin; export default defineConfig({ plugins: [ laravel({ input: [resources/css/app.css, resources/js/app.js], refresh: true, }), vue(), ], server: { host: true, port: 5173, proxy: { /api: { target: http://localhost:8000, changeOrigin: true, }, }, }, });这样配置之后前端请求axios.get(/api/posts)会先被Vite的开发服务器拦截然后转发到Laravel的8000端口。浏览器看到的请求是同源的不存在跨域问题也就不需要CORS了。注意changeOrigin一定要设为true否则后端收到的Host头还是前端域名某些中间件会判断异常。那生产环境怎么办生产环境我通常把前端打包后的静态文件直接交给Nginx托管再用Nginx反代/api请求到Laravel同样不需要跨域。这个后面部署章节细讲。2.3 前端初始化Vite、Vue Router、Pinia、Axios前端的依赖安装我用npmnpm install vue-router4 pinia axios入口文件resources/js/main.js这样写import { createApp } from vue; import { createPinia } from pinia; import App from ./App.vue; import router from ./router; const app createApp(App); app.use(createPinia()); app.use(router); app.mount(#app);axios实例需要单独封装统一处理token和错误。我在src/api/request.js里写import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000, }); // 请求拦截器自动带上token request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); // 响应拦截器统一处理错误 request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token); window.location.href /login; } return Promise.reject(error); } ); export default request;这个封装的思路是业务代码里不需要关心token怎么带也不需要到处写try-catch所有请求的错误处理都收敛到拦截器里。401就跳转登录页其他错误在组件里按需处理。2.4 数据库迁移与模型设计数据库我用MySQL先建好库然后在.env里配置连接信息DB_CONNECTIONmysql DB_HOST127.0.0.1 DB_PORT3306 DB_DATABASElaravel_vue_app DB_USERNAMEroot DB_PASSWORDyour_password接下来创建迁移文件php artisan make:model Post -mposts表结构如下Schema::create(posts, function (Blueprint $table) { $table-id(); $table-foreignId(user_id)-constrained()-onDelete(cascade); $table-string(title); $table-text(content); $table-timestamps(); });user_id外键关联用户表这样每篇文章都有作者归属。模型里补充关联关系class Post extends Model { protected $fillable [user_id, title, content]; public function user() { return $this-belongsTo(User::class); } }User模型里也要加posts关联public function posts() { return $this-hasMany(Post::class); }填好.env之后执行php artisan migrate表结构就建好了。3. 核心功能实现从登录到文章管理的全流程串联3.1 注册和登录接口的实现后端需要生成一个AuthControllerphp artisan make:controller AuthController注册逻辑public function register(Request $request) { $validated $request-validate([ name required|string|max:255, email required|string|email|max:255|unique:users, password required|string|min:8|confirmed, ]); $user User::create([ name $validated[name], email $validated[email], password Hash::make($validated[password]), ]); $token $user-createToken(auth_token)-plainTextToken; return response()-json([ user $user, token $token, ], 201); }登录逻辑public function login(Request $request) { $credentials $request-validate([ email required|email, password required, ]); if (!Auth::attempt($credentials)) { return response()-json([ message 邮箱或密码错误, ], 401); } $user Auth::user(); $token $user-createToken(auth_token)-plainTextToken; return response()-json([ user $user, token $token, ]); }注意密码必须用Hash::make加密登录时用Auth::attempt自动校验。千万不要把明文密码存进数据库这是底线。退出登录就简单了让它失效当前tokenpublic function logout(Request $request) { $request-user()-currentAccessToken()-delete(); return response()-json([message 已退出登录]); }3.2 前端登录页与token持久化前端的登录页长这样template div classlogin-container form submit.preventhandleLogin h2登录/h2 input typeemail v-modelemail placeholder邮箱 required / input typepassword v-modelpassword placeholder密码 required / button typesubmit :disabledloading {{ loading ? 登录中... : 登录 }} /button /form /div /template script setup import { ref } from vue; import { useRouter } from vue-router; import { loginAPI } from ../api/auth; const email ref(); const password ref(); const loading ref(false); const router useRouter(); const handleLogin async () { loading.value true; try { const data await loginAPI({ email: email.value, password: password.value }); localStorage.setItem(token, data.token); localStorage.setItem(user, JSON.stringify(data.user)); router.push(/); } catch (error) { alert(error.response?.data?.message || 登录失败); } finally { loading.value false; } }; /scriptloginAPI在src/api/auth.js里import request from ./request; export const loginAPI (data) request.post(/login, data); export const registerAPI (data) request.post(/register, data); export const logoutAPI () request.post(/logout); export const getUserAPI () request.get(/user);token存localStorage每次请求由拦截器自动带上去。虽然localStorage有XSS风险但对于普通业务系统来说在Vue的插值语法保护下这个风险可控没必要上cookie那套复杂的方案。3.3 路由守卫未登录不让进光拿到token还不够前端路由要拦截未登录的访问。我在router/index.js里配置全局守卫import { createRouter, createWebHistory } from vue-router; const router createRouter({ history: createWebHistory(), routes: [ { path: /, component: () import(../views/Home.vue), meta: { requiresAuth: true } }, { path: /login, component: () import(../views/Login.vue) }, { path: /register, component: () import(../views/Register.vue) }, { path: /posts/create, component: () import(../views/PostCreate.vue), meta: { requiresAuth: true } }, { path: /posts/:id/edit, component: () import(../views/PostEdit.vue), meta: { requiresAuth: true } }, ], }); router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else { next(); } }); export default router;用meta.requiresAuth标记需要登录的路由刷新页面时token还留在localStorage里用户状态就不会丢。这个方案简单可靠是个合格MVP该有的样子。3.4 文章列表与API Resource很多教程在返回数据时直接用$posts模型转JSON但API开发最好用资源类来做数据整形。生成API Resourcephp artisan make:resource PostResourcePostResource里定义返回字段把作者信息带上来public function toArray(Request $request): array { return [ id $this-id, title $this-title, content $this-content, created_at $this-created_at-format(Y-m-d H:i:s), user [ id $this-user-id, name $this-user-name, ], ]; }控制器里这样写public function index() { $posts Post::with(user)-latest()-paginate(10); return PostResource::collection($posts); } public function store(StorePostRequest $request) { $post $request-user()-posts()-create([ title $request-title, content $request-content, ]); return new PostResource($post); }StorePostRequest是表单请求类处理校验逻辑public function authorize(): bool { return true; } public function rules(): array { return [ title required|string|max:255, content required|string, ]; }这样控制器的逻辑非常干净校验、鉴权、数据组装都拆分到各自的类里这就是Laravel推荐的结构。3.5 前端文章管理页面的完整对接前端的文章列表页script setup import { ref, onMounted } from vue; import { getPostsAPI, deletePostAPI } from ../api/post; const posts ref([]); const loading ref(true); const fetchPosts async () { loading.value true; try { const data await getPostsAPI(); posts.value data.data; } finally { loading.value false; } }; const handleDelete async (id) { if (!confirm(确定删除这篇文章)) return; await deletePostAPI(id); await fetchPosts(); }; onMounted(fetchPosts); /scriptapi/post.js里的封装import request from ./request; export const getPostsAPI (page 1) request.get(/posts, { params: { page } }); export const getPostAPI (id) request.get(/posts/${id}); export const createPostAPI (data) request.post(/posts, data); export const updatePostAPI (id, data) request.put(/posts/${id}, data); export const deletePostAPI (id) request.delete(/posts/${id});整个链路就是这样前端组件调用封装好的API函数请求打到Laravel的控制器控制器处理数据和权限资源类整形JSON返回前端拿到数据更新页面状态。4. 部署与常见问题排查上生产环境之前的那些坑4.1 Nginx配置前端history路由与API反向代理部署时我选择Nginx作为统一入口。前端打包后的静态文件放在/var/www/laravel-vue-app/dist默认是public目录下的js/css资源Laravel项目在/var/www/laravel-vue-app。Nginx配置这样写server { listen 80; server_name your-domain.com; root /var/www/laravel-vue-app/public; index index.php; # 前端静态资源 location /assets { alias /var/www/laravel-vue-app/public/build/assets; expires 30d; add_header Cache-Control public, immutable; } # 后端API location /api { try_files $uri $uri/ /index.php?$query_string; } # 前端history路由所有非文件请求都交给index.html location / { root /var/www/laravel-vue-app/public; try_files $uri $uri/ /index.html; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; } # Laravel storage 软链接 location /storage { alias /var/www/laravel-vue-app/storage/app/public; } }关键是location /的try_files $uri $uri/ /index.html这行解决了Vue Router的history模式刷新404问题。用户访问/posts/1刷新时Nginx发现没有这个文件就回退到入口index.html由前端路由接管渲染。location /api的try_files $uri $uri/ /index.php?$query_string则把API请求传递给Laravel的入口文件。注意生产环境不要再走Vite代理也不要开启CORS。同域名部署前端和API同源没有跨域问题配置也最干净。4.2 前端打包与Laravel集成在打包之前我习惯先执行npm run build构建出的文件一般在public/build目录下如果Laravel和前端是同一个仓库vite.config.js默认就是这么配置的。然后在resources/views/里建一个app.blade.php作为唯一的入口!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{{ config(app.name) }}/title vite([resources/js/app.js]) /head body div idapp/div vite([resources/css/app.css]) /body /html路由定义成Route::get(/{any}, function () { return view(app); })-where(any, .*);这样所有非API请求都返回Vue应用的HTML壳子由前端路由接管。API请求则在前面Nginx的配置中直接绕过这个路由进入PHP逻辑。这个方案的好处是前端能直接用Laravel的vite指令加载带哈希的打包文件不用手动管理版本号。4.3 阿里云部署的基本流程在阿里云ECS上部署我的步骤大致是装环境apt install nginx php-fpm php-mysql php-bcmath php-xml php-curl php-zip composer。把代码拉到服务器可以用Git也可以直接上传压缩包。安装依赖composer install --no-dev --optimize-autoloader前端执行npm ci npm run build。配置.env执行php artisan key:generate配置数据库连接和APP_URL。php artisan migrate --force。php artisan storage:link把存储目录软链到public下。给存储目录写权限chown -R www-data:www-data storage bootstrap/cache。配置Nginx站点重启服务。还要记得执行Laravel的优化命令php artisan config:cache php artisan route:cache php artisan view:cache这几条命令会把配置、路由、视图缓存起来能显著提升响应速度。但注意改完配置后必须重新执行php artisan config:cache否则新配置不生效。我踩过一次这个坑改了数据库密码但缓存没清排查了很久才找到原因。4.4 常见问题速查表我把实际项目中遇到的高频问题整理成了一张表基本覆盖了前后端分离衔接阶段的大部分坑现象根因解决方案前端请求API返回419CSRF Token匹配不上说明走了Session认证而不是Token认证确认请求头带Authorization: Bearer token确认Sanctum的stateful配置检查路由是否在auth:sanctum中间件下跨域请求带不了token前端没有设置withCredentials或者后端CORS响应头不允许源开发环境用Vite代理规避生产环境同源部署若必须跨域正确配置allowed_origins和allowed_headers刷新文章详情页404Nginx没有把路由回退到index.html检查location /的try_files $uri $uri/ /index.htmlVue应用能访问但API返回404Nginx的try_files把API请求也交给了前端路由先让location /api匹配传回PHP处理上传的图片在页面上打不开缺少storage:link软链接执行php artisan storage:link登录后token存在localStorage但刷新页面后状态丢失前端没有在路由守卫或Pinia初始化时恢复用户信息登录成功时把user信息一并存到localStorage初始化时从存储里恢复下载PDF在iOS上变成预览这是WebView和浏览器的默认行为不是Laravel的问题设置Content-Disposition: attachment文件名加filename*参数若业务要求下载而不是预览需配合前端a[download]属性处理PDF请求报CORS错误静态资源服务的CORS响应头缺失在Nginx为/storage或/files路径增加add_header Access-Control-Allow-Origin *;同样只在对的场景使用4.5 音频视频资源的处理补充聊到媒体资源就想提一句m3u8。前后端分离项目里如果涉及视频播放很多方案是让Laravel管理视频文件前端用video.js配合hls.js播放m3u8格式。Laravel这边只需要保证视频文件能被正确访问在存储软链接配好的前提下Nginx对.m3u8和.ts分片文件要关闭压缩、设置正确的MIME类型location ~ \.(m3u8|ts)$ { add_header Cache-Control no-cache; default_type application/vnd.apple.mpegurl; }这不是前后端分离的核心但真遇到了能省不少时间。5. 项目实践中的心得与避坑经验5.1 接口设计要“一次性想清楚”否则后期改接口是连环爆炸前后端分离最痛苦的事不是写代码是改接口。前端所有页面都依赖接口返回的字段结构你这边改了字段名前端就要跟着改十几个组件的渲染逻辑。所以接口设计最好在开工前就定稿宁可多花一天讨论也不要在开发中期返工。我常用的做法是先写一份接口文档不用专门的工具Markdown就行把URL、请求方式、参数、返回结构全列出来前后端各留一份。开发时哪边需要调整就先改文档再改代码避免两个人闷头开发最后对不上。具体到字段命名我推荐统一用小写下划线比如created_at、user_idLaravel默认就是这个风格前端也照着用不要中途换成驼峰。5.2 调试工具是前后端分离的“第三只手”前后端分开之后定位问题要从浏览器DevTools开始。我常用的调试路径是先看Network面板确认请求到底发出去没有、状态码是多少再看响应体看Laravel返回的JSON是否符合预期最后看Console看前端有没有报错。Vue官方推荐的Vue Devtools插件也必须装上它能直接查看组件树、Pinia状态、路由信息。有一次排查一个“页面数据怎么刷新都不变”的问题打开Devtools一看发现组件里用错了响应式对象数据被当成普通对象存了页面自然永远不会更新。Laravel这边也建议开启Debug模式.env里APP_DEBUGtrue时API返回的错误信息会很详细对联调很有帮助。但生产环境务必关掉否则敏感信息全暴露了。5.3 状态管理别过度设计Pinia用最简单的模式我看到很多新手项目一个简单的用户登录状态也要建一堆store写一堆getter和action结果项目复杂度上去了维护反而更累。我的习惯是只有在多个组件之间共享状态时才用Pinia比如用户信息和token、购物车数据、全局通知列表。页面内部的临时状态就用ref或reactive存着别什么都塞store。等真需要store了再用最朴素的写法import { defineStore } from pinia; export const useAuthStore defineStore(auth, { state: () ({ user: JSON.parse(localStorage.getItem(user) || null), token: localStorage.getItem(token) || , }), getters: { isLoggedIn: (state) !!state.token, }, actions: { setAuth(user, token) { this.user user; this.token token; localStorage.setItem(user, JSON.stringify(user)); localStorage.setItem(token, token); }, clearAuth() { this.user null; this.token ; localStorage.removeItem(user); localStorage.removeItem(token); }, }, });这样简单明了不同组件里useAuthStore().isLoggedIn一下就知道有没有登录。5.4 安全性哪些接口一定要保护好前后端分离把后端完全暴露在了前端之外接口安全就成了重中之重。Sanctum保护的接口必须有auth:sanctum中间件这个不用多说。另外要注意的是所有涉及数据变更的接口必须做权限校验不能只靠前端隐藏按钮。用户直接拿Postman调接口就能穿过后端校验的话那前端隐藏得再好也白搭。响应数据要脱敏。用户对象返回时password、remember_token这些字段一定要隐藏用UserResource做白名单返回是最稳的。限流要用上。Laravel自带throttle:api中间件默认一分钟60次对正常用户没影响但能挡住不少暴力请求。文件上传要校验类型和大小服务端必须再拦截一次不能只在前端做限制。5.5 从Blade迁移到前后端分离的团队要注意什么如果你的团队之前一直在用Blade全家桶突然切换到前后端分离最先要适应的是思维模式的变化。Blade时代是“页面由服务端生成”前后端分离后变成了“页面由前端拼装服务端只出数据”。很多以前在后端顺手处理的逻辑比如分页、排序、时间格式化现在都要在接口层或前端重新设计。我的建议是团队里至少要有一个把数据流彻底理清的人。碰到复杂页面先画一下数据流这个页面需要哪些接口、每个接口返回什么、数据如何流转到组件、变更后如何通知后端。数据流理顺了前后端分离就不难数据流理不顺用再新的框架也是堆代码。6. 扩展思考这套架构还能怎么演进6.1 接入GraphQL的代价与收益热词里有关心Laravel GraphQL我提一嘴。GraphQL的优势是前端可以按需取字段减少过度获取的问题但代价是不再是简单URL要维护Schema、解析器、类型定义Laravel的GraphQL库如nuwave/lighthouse学习曲线并不低。我的判断是如果项目只有几个简单页面REST接口足够清爽没必要上GraphQL。如果是中大型项目前端界面高度可配置、字段组合多变GraphQL才值得考虑。而且GraphQL的缓存和监控比REST复杂要提前做好技术储备。6.2 前后端分离的部署形态选择除了我上面说的同域名Nginx部署还有一种常见形态是前后端完全分离部署前端打包的静态资源放在OSS/CDN上API独立部署在ECS或容器服务里。这种形态前端加载更快、弹性扩容更方便但需要处理跨域和鉴权Cookie的问题。这个方案我实践过一次跨域通过Nginx配置Access-Control-Allow-Origin解决但记住不能配成*同时又要带Cookie必须明确指定域名add_header Access-Control-Allow-Origin https://your-frontend-domain.com; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS; add_header Access-Control-Allow-Headers Authorization, Content-Type;如果你用Token认证而不用Cookie那问题简单很多Access-Control-Allow-Origin: *也可以但真要对外开放的接口还是按白名单来比较稳。关于本地调试的另一种需求——在iOS/Android的WebView里加载本地打包好的Vue文件这种场景下后端API就得是公网可访问的地址前端请求的baseURL要改成API服务器的地址跨域配置也得对WebView的源做兼容。这类问题往往不出在代码出在部署策略提前想清形态排障会容易很多。6.3 团队协作与代码管理建议前后端分离之后代码仓库怎么管理也值得想一下。小团队我建议前后端放同一个仓库用目录区分分支策略也简单大团队前后端分开成独立仓库各走各的发布流程。无论哪种方式接口文档和Mock数据一定要跟上。前端可以在后端接口还没写好时先按文档写死假数据开发后端好了再切换成真实请求。这需要前端把request.js的基地址做成可通过环境变量切换开发环境指向Mock联调时指向真实后端。环境变量这块用Vite的import.meta.env很方便const baseURL import.meta.env.VITE_API_BASE_URL || /api;开发时在.env.development里写VITE_API_BASE_URL/api走代理联调时改成后端地址打包时用.env.production默认走同域。7. 写在最后的实操体会做过的LaravelVue项目多了之后我最大的感受是这个组合的上手门槛比想象中低但要做得优雅靠的仍然是基本功。Laravel那边的模型设计、中间件、资源类、表单请求Vue这边的组件拆分、状态管理、路由守卫每一块单独看都不难难的是把它们串成一个顺畅的链路。我个人在实际协作里踩过最多的坑集中在两处一是接口字段结构不稳定导致前端反复跟着改二是部署环境差异导致开发好好的、上线就崩最典型的就是Nginx路由配置和Laravel缓存没清理。所以项目一开头就要明确接口契约部署脚本里强制加缓存清理步骤这两件事做扎实项目能省掉一半的排障时间。最后再分享一个小技巧在routes/api.php里给每个路由组加上统一的前缀和中间件比如所有接口都返回JSON格式的错误响应可以在app/Exceptions/Handler.php里自定义异常渲染public function render($request, Throwable $e) { if ($request-is(api/*)) { return response()-json([ message $e-getMessage(), success false, ], method_exists($e, getStatusCode) ? $e-getStatusCode() : 500); } return parent::render($request, $e); }这样前端统一处理错误就轻松很多。前后端分离就是这么回事分的是技术栈合的是一套流程。只要流程顺了这套组合在很长一段时间内都是中小型Web项目的实用选择。