1. 项目概述与需求拆解
做社交音乐分享平台这个项目,我最开始的想法很简单:做一个能让人上传歌曲、创建歌单、还能互相评论点赞的音乐社区。结果越做越发现,这东西不只是一个“带播放器的网站”,背后其实牵扯到用户体系、社交关系、内容管理、文件处理、推荐逻辑一大堆事。项目选定 thinkphp 做后端、vue 做前端,也是基于一个非常现实的原因:这两套技术正好可以覆盖一个中小型 Web 项目从接口到交互的全部需求。
想明白“这个平台到底要解决什么问题”特别重要。纯粹的播放器网上大把,但社交音乐平台的核心差异在于“用户之间有互动”,也就是:我能听到你分享的歌,我能评论你的歌单,我能关注你、看你最近听了什么。所有功能设计都要围绕这个社交属性展开,不然就是拿了一套通用 CRUD 在硬凑项目。
1.1 核心功能拆解与用户场景
先把需求拆开看。一个社交音乐分享平台,按用户角色来分基本是两类:普通用户和管理员。普通用户需要的功能,我梳理了下面的清单作为第一版迭代目标:
- 用户注册与登录,支持头像上传和基础资料修改
- 歌曲上传,包括音频文件、封面图、歌曲名、歌手、专辑、风格标签
- 歌单创建与维护,可以把自己喜欢的歌整理成歌单,一键分享
- 歌曲与歌单的评论、点赞、收藏,这是社交属性的核心
- 关注与粉丝体系,关注后能在首页看到关注用户的最新分享
- 个人主页展示,包括我上传的歌、创建的歌单、收藏列表和动态记录
- 后台管理,管理员可以审核上传内容、管理用户、统计平台数据
拿用户场景来验证这些功能是否合理。假设一个叫小林的用户,他注册后上传了一首自己翻唱的歌,创建了一个“深夜听的老歌”歌单,关注了几个同好,然后在首页看到关注的人分享了一首新歌,点进去听了一下、评论了一句“这首歌的吉他前奏绝了”。整个流程里,上传、歌单、关注、信息流、评论这些功能全部串起来了。这就是社交音乐平台和普通音乐库最本质的差别。
1.2 为什么选 thinkphp + vue 这套组合
选型阶段我也纠结过,是直接用 thinkphp 做服务端渲染模板,还是做前后端分离?后来考虑到两个点:第一,vue 的组件化开发和响应式交互,做播放器、歌单拖拽、评论实时刷新这类功能体验好得多;第二,接口化的后端将来可以复用给小程序或者移动端 App。所以最终敲定:thinkphp 提供 API 接口,vue 构建前端 SPA,整个项目按前后端分离架构来设计。
thinkphp 这边用起来最顺手的是它的目录规范和内置 ORM。数据库表建好之后,模型关系直接用hasMany、belongsToMany就能关联起来,处理歌单和歌曲之间的多对多关系非常省事。路由方面,新版 thinkphp(6.x)已经全面转向think\route注解路由和控制器自动绑定,接口开发效率和代码可读性都比老版本强了不少。另外它的验证器、中间件、JWT 认证扩展都比较成熟,做用户登录鉴权不用自己造轮子。
vue 这边我用的是 vue 3 + vue-router + pinia + axios 的组合。为什么不用 vue 2?vue 3 的组合式 API 在逻辑复用上优势太明显了,播放器状态、用户登录状态、歌单播放列表这些跨组件共享的状态,用组合式函数 + store 管理起来非常直观。vue-router 做路由守卫,pinia 做持久化状态管理,axios 做请求拦截和 token 注入,一个标准的 vue 3 中后台开发套件。
2. 整体架构设计与技术选型背后的思考
确定了 thinkphp + vue 前后端分离之后,紧接着要把整个项目的架构搭起来。这一步如果不提前想清楚,开发到中期一定会陷入接口混乱、代码到处重复的泥潭。
整体架构我分了三个层次:前端 vue 应用、后端 thinkphp API 服务、数据库与文件存储。前端通过 HTTP 请求访问后端接口,后端负责业务逻辑、数据校验和权限控制,音频文件和图片走独立的上传接口,保存到服务器本地存储目录。数据库用的 MySQL 5.7,原因很简单:稳定、生态成熟、thinkphp 的 ORM 对 MySQL 的支持最顺畅,部署环境也容易找。
2.1 前后端分离架构下的接口通信设计
前后端分离最核心的问题就是接口约定。我见过很多项目,前端和后端各自开发,联调的时候接口对不上,返工成本极高。所以这个项目一开始就要把接口规范定死。
我采用了统一的 RESTful 风格接口,所有接口返回格式保持一致,这样前端 axios 的响应拦截器可以统一处理。
{ "code": 200, "msg": "success", "data": { "token": "xxx", "user": { "id": 1, "nickname": "小林" } } }code 代表业务状态码,200 是成功,401 是未登录,403 是没权限,500 是服务器异常。前端响应拦截器里判断 code,统一弹消息提示。接口地址按模块划分:
| 模块 | 接口前缀 | 说明 |
|---|---|---|
| 用户认证 | /api/user | 注册、登录、资料修改、头像上传 |
| 歌曲管理 | /api/song | 上传、列表、详情、删除 |
| 歌单管理 | /api/playlist | 创建、编辑、歌曲关联、删除 |
| 社交互动 | /api/comment | 评论、点赞、收藏、关注 |
| 首页信息流 | /api/feed | 关注动态、推荐内容 |
| 后台管理 | /api/admin | 用户管理、内容审核、数据统计 |
前端根据.env.development和.env.production配置不同的后端地址,dev 环境用 vite 代理解决跨域问题,生产环境通过 Nginx 反向代理把/api转发到 thinkphp。这样一套设计下来,前后端可以完全并行开发,我自己一个人做也明显感觉到效率提升很大。
2.2 数据库表设计的关键权衡
数据库是这类项目的生命线,表结构设计好,后面开发一路顺风;表设计不合理,写到一半就得回头改,那真是欲哭无泪。我最终设计了 10 张核心表,这里挑几张最关键的说。
用户表user除了基础字段外,我专门加了avatar、signature、follower_count、following_count。粉丝数和关注数用冗余字段存储,不实时去 count,因为个人主页和用户列表展示会频繁用到,实时 count 会带来不必要的查询压力。
歌曲表song有个字段我要重点强调一下:status。因为这是一个有内容审核机制的平台,用户上传的歌曲不能直接发布,必须先进入待审核状态,管理员审核通过后才能被搜索和展示。状态我用 0 待审核、1 已发布、2 已下架,这个字段一切设计都围绕内容安全运转。
歌单和歌曲的多对多关系,我建了一张中间表playlist_song,字段是id、playlist_id、song_id、sort_order。sort_order 用来记录歌曲在歌单里的排序,用户可以手动调整顺序,这比单纯依赖 id 排序自然得多。
CREATE TABLE `playlist_song` ( `id` int(11) NOT NULL AUTO_INCREMENT, `playlist_id` int(11) NOT NULL, `song_id` int(11) NOT NULL, `sort_order` int(11) DEFAULT '0', `created_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_playlist` (`playlist_id`), KEY `idx_song` (`song_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;社交这块,关注关系我单独建了一张follow表,字段是id、user_id、follow_user_id、created_at,加唯一索引uk_user_follow。评论表comment用type字段区分是针对歌曲还是歌单的评论,parent_id支持楼中楼回复。点赞表like也是类似设计,通过type区分点赞对象,这样就不用为歌曲、歌单、评论各建一张点赞表,代价是查询时要多带一个 type 条件,但整体架构更简洁。
3. 核心功能模块设计与实现细节
需求拆完、架构搭好、表建完,就到了实际动手写业务逻辑的阶段。这个项目里最能体现“社交音乐平台”特点的,是几个有代表性的核心模块:用户认证与权限体系、歌曲上传与流式播放、歌单管理、信息流推荐、评论与点赞。这一节我把每个模块的设计思路和关键实现讲透。
3.1 用户认证与权限体系实现
用户认证我用了 JWT 方案,thinkphp 框架本身不内置 JWT,我引入了firebase/php-jwt这个扩展包,封装成一个中间件。登录成功之后,后端生成一个 token 返回给前端,前端存到 localStorage,之后每次请求在 axios 拦截器里带上Authorization: Bearer <token>。
JWT 的 payload 里我只放了user_id和exp过期时间,不存任何敏感信息。token 过期时间设置为 7 天,前端在响应拦截器里遇到 401 状态码就自动跳转登录页。管理员权限我通过一个字段role来区分,普通用户是 0,管理员是 1。后台接口统一走authMiddleware,然后判断当前用户的 role 是否为 1,不是就直接返回 403。
thinkphp 6.x 中间件的写法非常简洁,我写了一个全局认证中间件,逻辑是这样的:
public function handle($request, \Closure $next) { $token = $request->header('Authorization'); if (!$token) { return json(['code' => 401, 'msg' => '未登录', 'data' => null]); } try { $payload = JWT::decode(str_replace('Bearer ', '', $token), config('app.jwt_key'), ['HS256']); $request->userId = $payload->user_id; $request->userRole = $payload->role; } catch (\Exception $e) { return json(['code' => 401, 'msg' => '登录已过期', 'data' => null]); } return $next($request); }这里有个经验点:不要把用户的所有资料都塞进 JWT。用户在修改头像后,旧 token 里的 session 信息不会自动更新,如果不小心把头像路径放进了 token,就会出现用户改了头像但接口返回的头像还是旧的问题。最稳妥的做法就是只放user_id和role,其他信息需要的时候再查表获取。
3.2 歌曲上传与播放链路
歌曲上传是技术含量比较高的一个模块。音频文件和普通图片不一样,文件体积大、上传时间长、格式多样。前端我用 el-upload 组件处理文件选择,限制格式为 mp3、wav、flac,大小上限 20MB。后端接收文件后,通过 thinkphp 的文件验证机制检查。
$file = request()->file('audio'); $validate = [ 'size' => 20 * 1024 * 1024, 'ext' => 'mp3,wav,flac' ]; if (!$file->check($validate)) { return json(['code' => 400, 'msg' => $file->getError(), 'data' => null]); } $path = $file->move('/uploads/audio');有一点很容易忽略:文件上传目录的权限和路由访问规则。thinkphp 的 public 目录是 Web 根目录,我习惯把上传文件放在public/uploads/audio下面,这样前端可以直接通过 URL 访问https://域名/uploads/audio/xxx.mp3,不用额外加文件服务路由。如果放在 runtime 或者项目根目录之外,就需要单独写文件访问接口,不必要的复杂度就上来了。
播放链路这块,前端用 HTML5 的 audio 标签直接播放 mp3 格式,兼容性最好。不过 wav 格式体积太大,flac 格式在部分浏览器不直接支持,所以我做了层处理:用户上传 wav/flac 文件时,后端用 ffmpeg 转换为 mp3 再保存。这只是一个小细节,但对播放兼容性提升立竿见影。转换代码需要在服务器上安装 ffmpeg,然后通过 PHP 的exec命令调用。
播放器的设计上,我做了一个全局唯一的 audio 实例,挂在播放器 store 里,所有页面共享。这样切歌、暂停、播放状态就不会因为页面跳转而重置。歌曲的播放列表我维护在 store 中,播放器组件只关心“当前播放哪首歌”和“播放列表是什么”,逻辑非常清晰。
3.3 歌单管理的关联操作与排序
歌单和歌曲的多对多关系,在 thinkphp 的 ORM 里用belongsToMany定义关系后,通过关联方法就能很方便地增删和查询。我在 Playlist 模型里定义了:
public function songs() { return $this->belongsToMany(Song::class, 'playlist_song', 'playlist_id', 'song_id') ->withPivot(['sort_order', 'created_at']); }添加歌曲到歌单的时候,需要先判断这首歌是否已经在歌单里,避免重复添加。这个判断在中间表里查一下playlist_id和song_id的组合即可。删除歌曲的操作,直接调用detach方法就行,ORM 内部会处理中间表的删除。但有一个坑:detach默认按主键删除,如果你在中间表上有自定义字段的复杂条件,需要手动处理。比如我只想删除这个歌单里这首歌,不想动其他歌单的数据,这个时候直接用detach($songId)是没问题的,因为 ORM 知道当前歌单对象的关联条件。
歌单的排序功能,初始添加歌曲时就按添加顺序写入sort_order,前端允许用户拖拽排序,调整后把整个歌单的排序数据提交到后端,后端循环更新。这个逻辑简单粗暴但很有效,我直接写了一个sortSongs接口,接收一个数组,每个元素包含song_id和sort_order。
public function sortSongs(Request $request) { $playlistId = $request->param('playlist_id'); $items = $request->param('items/a'); foreach ($items as $item) { Db::name('playlist_song') ->where('playlist_id', $playlistId) ->where('song_id', $item['song_id']) ->update(['sort_order' => $item['sort_order']]); } return json(['code' => 200, 'msg' => '排序成功', 'data' => null]); }4. 前端 vue 3 实战:路由、状态管理与页面交互
后端接口设计得再合理,前端做不好体验一样白搭。vue 3 部分是这个项目交互体验的命脉。我按功能把前端拆成了十几个组件,核心页面包括首页信息流、歌曲详情页、歌单详情页、个人主页、播放器、上传歌曲、后台管理。这里挑几个最有代表性的实现细节展开讲。
4.1 路由配置与登录守卫
vue-router 4 的配置逻辑比 vue 2 直观很多。路由分两块:需要登录的页面和公开页面。公开页面只有注册和登录,其他所有页面都要先登录才能看,这是社交平台的基本逻辑。我通过路由元信息requiresAuth来控制。
const routes = [ { path: '/login', component: LoginView, meta: { requiresAuth: false } }, { path: '/', component: HomeView, meta: { requiresAuth: true } }, { path: '/song/:id', component: SongDetail, meta: { requiresAuth: true } }, { path: '/playlist/:id', component: PlaylistDetail, meta: { requiresAuth: true } }, { path: '/user/:id', component: UserProfile, meta: { requiresAuth: true } }, { path: '/upload', component: UploadSong, meta: { requiresAuth: true } } ]路由守卫的逻辑我放在router.beforeEach里,检查 store 里的 token 是否存在,不存在就跳转登录页。还有一个动态标题的小技巧:路由变化时根据meta.title修改浏览器标签标题,这个小细节对用户感知项目完成度很有帮助。
router.beforeEach((to, from) => { const token = useUserStore().token if (to.meta.requiresAuth && !token) { return { path: '/login' } } })动态路由这块我也考虑过。如果后台管理模块的页面很多,按需加载不是问题,不需要复杂的动态路由配置。但如果后续要做用户自定义页面、插件系统,那就需要用到 vue-router 的动态添加路由addRoute,后台返回权限菜单,前端按需注册。目前这个项目用静态路由就够了,没必要在一开始就上复杂度。
4.2 播放器状态管理与全局音频控制
播放器是这个项目最让我花心思的部分。一个全局播放器需要维护非常多的状态:当前播放歌曲、播放列表、播放状态、当前播放时间、音量、是否循环播放。我之前见过一些项目把这些状态散落在各个组件里,切个页面播放进度就丢了,体验支离破碎。所以播放器的状态我全部放在 pinia 的一个usePlayerStore里。
export const usePlayerStore = defineStore('player', { state: () => ({ currentSong: null, playlist: [], playing: false, currentTime: 0, duration: 0, volume: 0.8, loopMode: 'list' }), actions: { playSong(song, list) { this.currentSong = song this.playlist = list this.playing = true }, next() { // 根据 loopMode 计算下一首 }, prev() { // 上一首 } } })audio 元素本身我不直接写在组件里,而是在一个全局的 AudioPlayer 组件中维护,这个组件只做一件事:渲染一个audio标签,监听它的timeupdate、ended、loadedmetadata事件,实时把数据写回 store。这样所有页面想控制播放器,只需要操作 store 的方法,不用真的去操作 DOM。
播放列表的注入方式是这样的:无论在歌曲详情页、歌单详情页还是用户主页,用户点了任何一首歌的播放按钮,前端会把“当前页面展示的所有歌曲”作为一个列表传进去。这样用户在一个歌单页里点第二首,播放器会接着列表顺序播放下面几首,体验和主流音乐平台一致。这个逻辑虽然不难,但一开始就要设计好,不然后面各个页面中同一个按钮的逻辑会很混乱。
4.3 评论区与信息流的交互实现
评论功能我用了两个 tab:歌曲评论和歌单评论。评论的基础逻辑就是发评论、列表展示、回复、点赞、删除。评论区的交互在 vue 3 里实现起来很顺,因为reactive数组可以直接被列表渲染响应式更新,评论提交成功后unshift到列表顶部,立刻就出现在界面上了。
被删评论的实时性,我也做了一个细节处理:评论区接口返回的评论对象带上is_liked字段,表示当前用户是否已经点过赞。点赞按钮点击后,前端把is_liked取反,同时更新点赞计数,后端在接口里处理点赞或取消赞。这样比传统的“点赞接口返回总数,前端整组刷新”体验好得多。
信息流页面是一个双列瀑布流布局,每张卡片显示歌曲封面、歌名、作者和描述。这个列表我通过一个feed接口加载,接口返回的内容已经按照关注时间倒序排列。滚动到底部自动加载更多,用到的是 vue 的@scroll事件监听加上触底判断,也可以直接用现成的 v-infinite-scroll 指令。我个人建议在项目早期就用这种轮询加载、不做 WebSocket 实时推送,因为实时推送对服务端的压力大,而且音乐分享场景对实时性要求不高,用户刷新页面就能看到新内容。
5. thinkphp 后端 API 开发与安全实践
后端是数据的中枢,安全性和稳定性必须认真对待。这一章把 thinkphp 后端开发中的几个关键点单独拎出来讲,包括统一异常处理、参数校验、上传安全、SQL 注入防护和内容审核。
5.1 统一返回格式与异常处理
接口风格统一的前提是异常处理也要统一。我在项目里重写了 thinkphp 的异常处理机制,所有业务异常都抛出一个ApiException,通过全局异常处理类统一捕获并转为 JSON 返回。这样无论控制器哪里出错,前端拿到的都是结构一致的响应。
public function render($request, Throwable $e): Response { if ($e instanceof ApiException) { return json(['code' => $e->getCode(), 'msg' => $e->getMessage(), 'data' => null]); } // 记录日志 Log::error($e->getMessage(), ['trace' => $e->getTraceAsString()]); return json(['code' => 500, 'msg' => '服务器开小差了', 'data' => null]); }参数校验我直接用 thinkphp 的验证器。每个控制器里定义了对应的验证规则,比如注册接口要求用户名 3-20 位、密码 6-20 位、邮箱格式正确。验证失败直接抛异常,不用在控制器里写一堆 if/else。这条规则要严格执行,因为后端接口是面向所有客户端的,不能依赖前端校验。
5.2 上传安全与内容审核机制
文件上传是 Web 项目攻击面比较大的地方。我在上传模块里做了三重防护:
- 扩展名白名单,只允许 mp3、wav、flac、jpg、png、jpeg
- 文件大小限制,图片 2MB 以内,音频 20MB 以内
- 重命名策略,时间戳加随机字符串作为文件名,不保留原始文件名
为什么不能让前端传文件名?因为文件名是用户可控的,攻击者完全可以传一个shell.php的文件名进来,尽管扩展名校验能拦住大部分,但稳妥起见,文件名必须由后端生成,彻底杜绝路径穿越和恶意文件名注入。这是我在做过一次安全测试之后才踩过的坑,现在直接成了我的固定习惯。
内容审核这块,我设计了一个中间层。用户上传歌曲时,status默认是 0(待审核),管理员在后台见到待审核歌曲列表后,可以点击通过或驳回。驳回的理由可以填,驳回后用户会收到消息提示。评论内容我用了最简单的敏感词过滤方案:在服务端对评论内容做正则匹配,命中敏感词列表的直接拒绝发布。这个方案虽然简单,但对中小体量项目完全够用,也不影响评论区的正常交流。
5.3 SQL 注入与数据安全细节
thinkphp 的 ORM 框架本身对 SQL 注入做了比较好的防护,但前提是你要正确使用查询构造器。我的原则是:所有动态查询条件都用where的数组写法或者参数绑定,不在任何地方手动拼接 SQL 字符串。即使写了复杂查询,也优先用 ORM 的hasWhere或者查询构造器的whereRaw加参数绑定,绝不直接拼接用户输入。
还有一个容易被忽视的安全点:接口越权。普通用户只能操作自己的数据,比如删除歌曲时必须校验当前用户的 id 和歌曲的上传者 id 是否一致。这个校验我习惯写在模型层,做成一个作用域方法,而不是在每个控制器里复制粘贴。
public function scopeOwnedBy($query, $userId) { return $query->where('user_id', $userId); }数据统计和列表接口的性能也要提前考虑。歌曲列表和歌单详情页如果直接查表展示,量大了会有很明显的性能瓶颈。我在列表接口做了关联预加载with('user')和with('songs'),避免 N+1 查询问题。首页信息流接口用了分页机制,一次只加载 20 条,这已经是最基本的性能保障了。如果数据量再大,后续可以接入 Redis 做缓存,把热门歌曲和歌单数据缓存起来,进一步降低数据库压力。
6. 环境搭建、部署上线与问题排查实录
整个项目开发完之后,环境搭建和部署也是绕不开的一环。很多项目在本地跑得欢,一上服务器就各种问题。我把从本地环境到服务器部署的完整流程走了一遍,顺便记录下了过程中踩过的一些坑,这些经验比代码本身更有价值。
6.1 本地开发环境配置
本地开发我用的环境是 PHP 8.1 + MySQL 5.7 + Nginx。thinkphp 6.x 要求 PHP 8.0 以上,所以如果你还在用 PHP 7.x,赶紧升级到 8.1,性能和语法支持都会好很多。前端开发是通过 vite 启动的开发服务器,默认跑在 5173 端口,后端接口跑在 8000 端口。
跨域问题在开发阶段比较麻烦。我直接在 vite 配置里加了 proxy 代理,前端请求的/api路径自动转发到后端地址,这样前端代码里写的接口路径就是/api/xxx,到了生产环境用 Nginx 统一处理,不用改一行代码。这个配置极其重要,没有它你会在浏览器里看到一堆 CORS 报错。
server: { port: 5173, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } }6.2 生产环境部署流程
生产环境部署我用了宝塔面板作为操作面板,方便管理 Nginx、MySQL 和 PHP-FPM。整个流程分四步:
- 前端代码执行
npm run build,生成 dist 静态文件,配置 Nginx 将 dist 目录作为 Web 根目录 - 配置 Nginx 反向代理,将
/api请求转发到 thinkphp 服务的地址 - 后端代码上传到服务器,配置伪静态规则,把除静态文件外的所有请求都交给
index.php处理 - 导入数据库结构,修改
.env数据库配置,清理 runtime 缓存
有个大坑我必须提醒:thinkphp 6.x 的伪静态规则和 thinkphp 5 不完全一样。如果你把 TP5 的 Nginx 配置直接拿来用,路由可能全部报 404。我在宝塔里用的是下面的这份配置,亲测可用:
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; break; } }还有一点,生产环境的APP_DEBUG必须改成 false。开发时开着调试模式能看错误详情,但生产环境开着会把堆栈信息暴露给用户,这在安全上是不可接受的。改完之后如果页面报错,可以在 runtime 日志里定位问题,而不是看浏览器输出。
6.3 常见问题与排查技巧速查表
开发过程中我积攒了一份问题清单,挑几个典型的放出来,方便遇到同样问题的朋友直接对照排查。
| 问题现象 | 排查方向 | 解决方案 |
|---|---|---|
| 前端请求接口 404 | Nginx 伪静态配置不正确 | 改写/index.php?s=$1规则并重启 Nginx |
| 上传歌曲后无法播放 | 文件目录权限不足 | 检查uploads目录的写权限,chmod 755 或 775 |
| 登录后每次刷新都跳登录页 | pinia 没有持久化 token | 使用 pinia-plugin-persistedstate 或者在初始化 store 时从 localStorage 恢复 |
| 图片加载很慢 | 没有做图片压缩 | 接入 tinypng 压缩,或者在服务端用 intervention/image 动态压缩 |
| 评论发出去不显示 | 前端缓存了旧列表 | 发评论成功后清理列表缓存,重新请求接口或者直接 unshift 到本地数组 |
| 接口返回 500 | 查看 runtime/log 日志 | 逐行看异常报错,常见是 SQL 语句有问题或 PHP 版本不兼容 |
| 歌单里歌曲顺序乱了 | sort_order 没有正确排序 | 查询时显式加order('sort_order asc') |
| Vue 页面打包后首屏白屏 | 路由模式是 history 但 Nginx 没做 rewrite | 在 Nginx 的 location 里加try_files $uri $uri/ /index.html; |
最后再说一个小技巧。前后端分离项目上线后排查问题,直接用浏览器的开发者工具看 Network 面板,能非常直观地看到是哪个接口报错、状态码是什么、响应体是什么。如果响应体和预期不符,先用 Postman 单独测接口,排除前端因素。这套排查链路基本覆盖了 90% 的日常问题。我的经验是:先把静态资源的加载顺序理顺,再看接口,最后看数据库,问题通常就藏在链路里。