|
| 1 | +# Vue SPA + Fastify 部署指南 |
| 2 | + |
| 3 | +## 问题说明 |
| 4 | + |
| 5 | +Vue单页应用使用`createWebHistory`路由模式时,刷新页面会出现404错误。这是因为: |
| 6 | + |
| 7 | +1. 用户访问 `/apps/drplayer/settings` |
| 8 | +2. 浏览器向服务器请求 `/apps/drplayer/settings` 文件 |
| 9 | +3. 服务器找不到该文件,返回404 |
| 10 | +4. 需要让服务器将所有SPA路由请求都返回到 `index.html` |
| 11 | + |
| 12 | +## 解决方案 |
| 13 | + |
| 14 | +通过Fastify提供静态文件服务 + SPA路由回退机制,完全可以解决刷新404问题。 |
| 15 | + |
| 16 | +## 配置步骤 |
| 17 | + |
| 18 | +### 1. 构建Vue应用 |
| 19 | + |
| 20 | +```bash |
| 21 | +# 方法1:使用构建脚本(推荐) |
| 22 | +node build-for-fastify.js |
| 23 | + |
| 24 | +# 方法2:手动设置环境变量 |
| 25 | +set VITE_BASE_PATH=/apps/drplayer/ |
| 26 | +yarn build |
| 27 | +``` |
| 28 | + |
| 29 | +### 2. 复制构建文件 |
| 30 | + |
| 31 | +将 `dist/` 目录的所有内容复制到您的后端 `apps/drplayer/` 目录: |
| 32 | + |
| 33 | +``` |
| 34 | +your-backend/ |
| 35 | +├── apps/ |
| 36 | +│ └── drplayer/ |
| 37 | +│ ├── index.html |
| 38 | +│ ├── assets/ |
| 39 | +│ │ ├── index-xxx.js |
| 40 | +│ │ └── index-xxx.css |
| 41 | +│ └── ... |
| 42 | +└── server.js |
| 43 | +``` |
| 44 | + |
| 45 | +### 3. 配置Fastify服务器 |
| 46 | + |
| 47 | +#### 方法1:使用提供的路由模块(推荐) |
| 48 | + |
| 49 | +```javascript |
| 50 | +import { addSPARoutes } from './fastify-spa-routes.js'; |
| 51 | +import fastifyStatic from '@fastify/static'; |
| 52 | + |
| 53 | +// 您现有的静态文件配置 |
| 54 | +await fastify.register(fastifyStatic, { |
| 55 | + root: options.appsDir, |
| 56 | + prefix: '/apps/', |
| 57 | + decorateReply: false, |
| 58 | +}); |
| 59 | + |
| 60 | +// 添加SPA路由支持 |
| 61 | +await fastify.register(addSPARoutes, { |
| 62 | + appsDir: options.appsDir |
| 63 | +}); |
| 64 | +``` |
| 65 | + |
| 66 | +#### 方法2:直接在现有代码中添加 |
| 67 | + |
| 68 | +```javascript |
| 69 | +import path from 'path'; |
| 70 | +import fs from 'fs'; |
| 71 | + |
| 72 | +// 在您现有的Fastify应用中添加这些路由 |
| 73 | +fastify.get('/apps/drplayer/*', async (request, reply) => { |
| 74 | + const requestedPath = request.params['*']; |
| 75 | + const fullPath = path.join(options.appsDir, 'drplayer', requestedPath); |
| 76 | + |
| 77 | + try { |
| 78 | + await fs.promises.access(fullPath); |
| 79 | + return reply.callNotFound(); // 让静态文件服务处理 |
| 80 | + } catch (error) { |
| 81 | + // 返回index.html让Vue Router处理 |
| 82 | + const indexPath = path.join(options.appsDir, 'drplayer', 'index.html'); |
| 83 | + const indexContent = await fs.promises.readFile(indexPath, 'utf8'); |
| 84 | + return reply |
| 85 | + .type('text/html') |
| 86 | + .header('Cache-Control', 'no-cache, no-store, must-revalidate') |
| 87 | + .send(indexContent); |
| 88 | + } |
| 89 | +}); |
| 90 | + |
| 91 | +// 处理根路径 |
| 92 | +fastify.get('/apps/drplayer', async (request, reply) => { |
| 93 | + return reply.redirect(301, '/apps/drplayer/'); |
| 94 | +}); |
| 95 | + |
| 96 | +fastify.get('/apps/drplayer/', async (request, reply) => { |
| 97 | + const indexPath = path.join(options.appsDir, 'drplayer', 'index.html'); |
| 98 | + const indexContent = await fs.promises.readFile(indexPath, 'utf8'); |
| 99 | + return reply |
| 100 | + .type('text/html') |
| 101 | + .header('Cache-Control', 'no-cache, no-store, must-revalidate') |
| 102 | + .send(indexContent); |
| 103 | +}); |
| 104 | +``` |
| 105 | + |
| 106 | +## 工作原理 |
| 107 | + |
| 108 | +1. **静态文件服务**:`@fastify/static` 处理所有存在的静态文件(JS、CSS、图片等) |
| 109 | +2. **路由回退**:当请求的文件不存在时,返回 `index.html` |
| 110 | +3. **Vue Router接管**:`index.html` 加载后,Vue Router根据URL显示对应组件 |
| 111 | + |
| 112 | +## 路由优先级 |
| 113 | + |
| 114 | +``` |
| 115 | +请求: /apps/drplayer/assets/index-xxx.js |
| 116 | +↓ |
| 117 | +静态文件存在 → 直接返回文件 |
| 118 | +
|
| 119 | +请求: /apps/drplayer/settings |
| 120 | +↓ |
| 121 | +静态文件不存在 → 返回 index.html → Vue Router处理 |
| 122 | +``` |
| 123 | + |
| 124 | +## 缓存策略 |
| 125 | + |
| 126 | +- **HTML文件**:不缓存(`no-cache`),确保路由更新 |
| 127 | +- **静态资源**:长期缓存(1年),提高性能 |
| 128 | + |
| 129 | +## 测试验证 |
| 130 | + |
| 131 | +1. 启动Fastify服务器 |
| 132 | +2. 访问 `http://localhost:3000/apps/drplayer/` |
| 133 | +3. 导航到不同页面(如设置页面) |
| 134 | +4. 刷新页面,确认不出现404错误 |
| 135 | +5. 检查浏览器开发者工具,确认静态资源正常加载 |
| 136 | + |
| 137 | +## 常见问题 |
| 138 | + |
| 139 | +### Q: 刷新后页面空白? |
| 140 | +A: 检查 `VITE_BASE_PATH` 是否正确设置为 `/apps/drplayer/` |
| 141 | + |
| 142 | +### Q: 静态资源404? |
| 143 | +A: 确认构建时的base路径与Fastify的prefix匹配 |
| 144 | + |
| 145 | +### Q: API请求失败? |
| 146 | +A: 确保API路由在SPA路由之前注册,避免被SPA回退拦截 |
| 147 | + |
| 148 | +## 性能优化 |
| 149 | + |
| 150 | +1. **启用Gzip压缩**: |
| 151 | +```javascript |
| 152 | +import fastifyCompress from '@fastify/compress'; |
| 153 | +await fastify.register(fastifyCompress); |
| 154 | +``` |
| 155 | + |
| 156 | +2. **设置静态资源缓存**: |
| 157 | +```javascript |
| 158 | +await fastify.register(fastifyStatic, { |
| 159 | + // ... 其他配置 |
| 160 | + setHeaders: (res, path) => { |
| 161 | + if (path.endsWith('.html')) { |
| 162 | + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); |
| 163 | + } else if (path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) { |
| 164 | + res.setHeader('Cache-Control', 'public, max-age=31536000'); |
| 165 | + } |
| 166 | + } |
| 167 | +}); |
| 168 | +``` |
| 169 | + |
| 170 | +## 总结 |
| 171 | + |
| 172 | +✅ **可以解决404问题**:通过Fastify的路由回退机制 |
| 173 | +✅ **性能良好**:静态文件直接服务,只有路由请求才回退 |
| 174 | +✅ **配置简单**:只需添加几个路由处理器 |
| 175 | +✅ **开发友好**:支持热重载和开发服务器 |
0 commit comments