Initial commit
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
# MeshRay - StaticFS 路由映射问题修复
|
||||
|
||||
**修复时间**: 2026-03-24
|
||||
**问题类型**: 静态文件路径映射错误
|
||||
**影响范围**: 所有前端静态资源加载
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **问题根因**
|
||||
|
||||
### ❌ **错误的配置**
|
||||
|
||||
```go
|
||||
// 当前代码(错误)
|
||||
httpFS := http.FS(staticFS)
|
||||
s.engine.StaticFS("/assets", httpFS)
|
||||
```
|
||||
|
||||
### 🔍 **问题分析**
|
||||
|
||||
**文件结构**:
|
||||
```
|
||||
embed.FS (staticFS)
|
||||
├── index.html
|
||||
└── assets/
|
||||
├── index-AIAUiq89.js
|
||||
├── Login-Brnne-RL.js
|
||||
└── ...
|
||||
```
|
||||
|
||||
**请求流程**(错误配置):
|
||||
```mermaid
|
||||
graph TD
|
||||
A[浏览器请求 /assets/index-AIAUiq89.js] --> B[Gin 路由匹配 /assets]
|
||||
B --> C[在 httpFS 根目录查找]
|
||||
C --> D[查找 index-AIAUiq89.js]
|
||||
D --> E[❌ 文件不存在!在根目录找不到]
|
||||
E --> F[NoRoute 拦截]
|
||||
F --> G[返回 index.html]
|
||||
G --> H[❌ 浏览器收到 HTML 而非 JS]
|
||||
H --> I[MIME 类型错误:text/html 而非 application/javascript]
|
||||
```
|
||||
|
||||
**问题本质**:
|
||||
- `/assets` 路由映射到 `httpFS` 的**根目录**
|
||||
- 但实际文件在 `assets/` **子目录**
|
||||
- 导致所有 `/assets/*` 请求都找不到文件
|
||||
|
||||
---
|
||||
|
||||
## ✅ **修复方案**
|
||||
|
||||
### ⭐ **正确的配置**
|
||||
|
||||
```go
|
||||
// ✅ 正确:使用 fs.Sub 创建子目录文件系统
|
||||
if assetsFS, err := fs.Sub(staticFS, "assets"); err == nil {
|
||||
s.engine.StaticFS("/assets", http.FS(assetsFS))
|
||||
} else {
|
||||
s.logger.Warn("无法创建 assets 文件系统", zap.Error(err))
|
||||
}
|
||||
|
||||
// static 目录(如果有)
|
||||
if staticSubFS, err := fs.Sub(staticFS, "static"); err == nil {
|
||||
s.engine.StaticFS("/static", http.FS(staticSubFS))
|
||||
}
|
||||
```
|
||||
|
||||
### 🔧 **修复原理**
|
||||
|
||||
**fs.Sub 的作用**:
|
||||
```go
|
||||
// 原始文件系统
|
||||
staticFS: embed.FS
|
||||
├── index.html
|
||||
└── assets/
|
||||
└── index-AIAUiq89.js
|
||||
|
||||
// 使用 fs.Sub 创建子目录视图
|
||||
assetsFS, _ := fs.Sub(staticFS, "assets")
|
||||
// assetsFS 看到的结构:
|
||||
assetsFS: embed.FS (视图)
|
||||
└── index-AIAUiq89.js ← 根目录就是 assets/ 目录
|
||||
```
|
||||
|
||||
**请求流程**(修复后):
|
||||
```mermaid
|
||||
graph TD
|
||||
A[浏览器请求 /assets/index-AIAUiq89.js] --> B[Gin 路由匹配 /assets]
|
||||
B --> C[在 assetsFS 中查找]
|
||||
C --> D[查找 index-AIAUiq89.js]
|
||||
D --> E[✅ 文件存在!]
|
||||
E --> F[返回 JavaScript 内容]
|
||||
F --> G[✅ MIME 类型:application/javascript]
|
||||
G --> H[✅ 浏览器正常执行]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 **对比说明**
|
||||
|
||||
### 方案 A: 直接映射(错误)❌
|
||||
|
||||
```go
|
||||
httpFS := http.FS(staticFS)
|
||||
s.engine.StaticFS("/assets", httpFS)
|
||||
```
|
||||
|
||||
| 请求路径 | 实际查找位置 | 结果 |
|
||||
|----------|-------------|------|
|
||||
| `/assets/index-AIAUiq89.js` | `./index-AIAUiq89.js` | ❌ 不存在 |
|
||||
| `/assets/Login-Brnne-RL.js` | `./Login-Brnne-RL.js` | ❌ 不存在 |
|
||||
| `/assets/vue-vendor-BBChLKcR.js` | `./vue-vendor-BBChLKcR.js` | ❌ 不存在 |
|
||||
|
||||
**结果**: 所有文件都返回 404 → NoRoute 返回 HTML → MIME 类型错误
|
||||
|
||||
---
|
||||
|
||||
### 方案 B: 子目录映射(正确)✅
|
||||
|
||||
```go
|
||||
assetsFS, _ := fs.Sub(staticFS, "assets")
|
||||
s.engine.StaticFS("/assets", http.FS(assetsFS))
|
||||
```
|
||||
|
||||
| 请求路径 | 实际查找位置 | 结果 |
|
||||
|----------|-------------|------|
|
||||
| `/assets/index-AIAUiq89.js` | `assets/index-AIAUiq89.js` | ✅ 存在 |
|
||||
| `/assets/Login-Brnne-RL.js` | `assets/Login-Brnne-RL.js` | ✅ 存在 |
|
||||
| `/assets/vue-vendor-BBChLKcR.js` | `assets/vue-vendor-BBChLKcR.js` | ✅ 存在 |
|
||||
|
||||
**结果**: 所有文件都正常返回 → JavaScript 正确加载 → 页面正常显示
|
||||
|
||||
---
|
||||
|
||||
## 🔍 **为什么之前能工作?**
|
||||
|
||||
### 疑问:之前的配置有时能工作?
|
||||
|
||||
**答案**: 可能是以下原因:
|
||||
|
||||
1. **开发模式**(双服务器架构)
|
||||
- 前端运行在 `localhost:5173`
|
||||
- Vite 开发服务器直接提供文件
|
||||
- 不经过 Gin 的 StaticFS
|
||||
|
||||
2. **NoRoute 兜底**
|
||||
- StaticFS 找不到文件
|
||||
- NoRoute 尝试读取文件
|
||||
- 偶尔成功但不稳定
|
||||
|
||||
3. **缓存效应**
|
||||
- 浏览器缓存了旧的 JS 文件
|
||||
- 暂时掩盖了问题
|
||||
|
||||
---
|
||||
|
||||
## 📋 **完整修复代码**
|
||||
|
||||
### server.go 修改(第 117-130 行)
|
||||
|
||||
**修改前**:
|
||||
```go
|
||||
if staticFS != nil {
|
||||
// 创建 HTTP 文件系统
|
||||
httpFS := http.FS(staticFS)
|
||||
|
||||
// 先注册静态文件目录(优先级高)
|
||||
s.engine.StaticFS("/assets", httpFS)
|
||||
s.engine.StaticFS("/static", httpFS)
|
||||
|
||||
// 再注册 NoRoute 处理 SPA 路由(优先级低)
|
||||
s.engine.NoRoute(func(c *gin.Context) {
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**修改后**:
|
||||
```go
|
||||
if staticFS != nil {
|
||||
// ✅ 重要:先创建 assets 子目录的 HTTP 文件系统
|
||||
// 这样 /assets/* 请求会映射到 assets/* 文件
|
||||
if assetsFS, err := fs.Sub(staticFS, "assets"); err == nil {
|
||||
s.engine.StaticFS("/assets", http.FS(assetsFS))
|
||||
} else {
|
||||
s.logger.Warn("无法创建 assets 文件系统", zap.Error(err))
|
||||
}
|
||||
|
||||
// static 目录(如果有)
|
||||
if staticSubFS, err := fs.Sub(staticFS, "static"); err == nil {
|
||||
s.engine.StaticFS("/static", http.FS(staticSubFS))
|
||||
}
|
||||
|
||||
// 再注册 NoRoute 处理 SPA 路由(优先级低)
|
||||
s.engine.NoRoute(func(c *gin.Context) {
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 **验证测试**
|
||||
|
||||
### 测试 1: 检查 JS 文件加载
|
||||
|
||||
```bash
|
||||
curl.exe "http://localhost:9531/assets/index-AIAUiq89.js" -I
|
||||
```
|
||||
|
||||
**期望输出**:
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/javascript; charset=utf-8
|
||||
```
|
||||
|
||||
**之前输出**(错误):
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/html; charset=utf-8 ← ❌ HTML!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 测试 2: 检查 CSS 文件加载
|
||||
|
||||
```bash
|
||||
curl.exe "http://localhost:9531/assets/Login-DB9x1P_y.css" -I
|
||||
```
|
||||
|
||||
**期望输出**:
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/css; charset=utf-8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 测试 3: 检查页面完整性
|
||||
|
||||
```bash
|
||||
curl.exe http://localhost:9531/ | Select-String -Pattern "DOCTYPE|title|script"
|
||||
```
|
||||
|
||||
**期望输出**:
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<title>MeshRay - 简单、高效异地组网</title>
|
||||
<script type="module" crossorigin src="/assets/index-AIAUiq89.js"></script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 **视觉效果对比**
|
||||
|
||||
### 修复前 ❌
|
||||
|
||||
**浏览器 Console**:
|
||||
```
|
||||
Failed to load module script: Expected a JavaScript-or-Wasm module
|
||||
script but the server responded with a MIME type of "text/html".
|
||||
Source URL: /assets/index-AIAUiq89.js
|
||||
```
|
||||
|
||||
**页面显示**: 空白页
|
||||
|
||||
**Network 标签**:
|
||||
```
|
||||
index-AIAUiq89.js 200 OK text/html ← ❌ 类型错误
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 修复后 ✅
|
||||
|
||||
**浏览器 Console**: 无错误
|
||||
|
||||
**页面显示**:
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🔗 MeshRay (Logo) │
|
||||
│ │
|
||||
│ ┌───────────────────────────┐ │
|
||||
│ │ 用户名:[_________] 👤 │ │
|
||||
│ │ 密码: [_________] 🔒 │ │
|
||||
│ │ [ 登录 ] │ │
|
||||
│ └───────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Network 标签**:
|
||||
```
|
||||
index-AIAUiq89.js 200 OK application/javascript ← ✅ 类型正确
|
||||
Login-Brnne-RL.js 200 OK application/javascript ← ✅
|
||||
Login-DB9x1P_y.css 200 OK text/css ← ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 **知识点总结**
|
||||
|
||||
### Go embed + Gin 静态文件服务最佳实践
|
||||
|
||||
#### 1️⃣ **理解 fs.Sub**
|
||||
|
||||
```go
|
||||
// fs.Sub 返回一个新的 FS,根目录为 dir
|
||||
func Sub(fsys FS, dir string) (FS, error)
|
||||
|
||||
// 示例
|
||||
originalFS: embed.FS
|
||||
├── index.html
|
||||
└── assets/
|
||||
└── app.js
|
||||
|
||||
subFS, _ := fs.Sub(originalFS, "assets")
|
||||
// subFS 看到的结构:
|
||||
subFS: embed.FS
|
||||
└── app.js ← 这就是新的根目录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2️⃣ **Gin StaticFS 工作原理**
|
||||
|
||||
```go
|
||||
// StaticFS 将 URL 路径映射到文件系统
|
||||
engine.StaticFS(url_path, filesystem)
|
||||
|
||||
// 请求处理:
|
||||
// GET /assets/app.js
|
||||
// ↓
|
||||
// 在 filesystem 中查找 app.js
|
||||
// ↓
|
||||
// 返回文件内容
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- URL 路径会被剥离前缀(`/assets`)
|
||||
- 剩余部分作为文件路径在 file system 中查找
|
||||
|
||||
---
|
||||
|
||||
#### 3️⃣ **正确的组合方式**
|
||||
|
||||
```go
|
||||
// ✅ 标准模式
|
||||
embedFS, _ := fs.Sub(dist.WebAssets, ".")
|
||||
assetsFS, _ := fs.Sub(embedFS, "assets")
|
||||
engine.StaticFS("/assets", http.FS(assetsFS))
|
||||
|
||||
// 等价于:
|
||||
// GET /assets/foo.js → 在 assets/foo.js 查找
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ **常见误区**
|
||||
|
||||
### 误区 1: 直接使用 http.FS
|
||||
|
||||
```go
|
||||
// ❌ 错误
|
||||
httpFS := http.FS(staticFS)
|
||||
engine.StaticFS("/assets", httpFS)
|
||||
// 这会在根目录查找文件
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```go
|
||||
// ✅ 正确
|
||||
assetsFS, _ := fs.Sub(staticFS, "assets")
|
||||
engine.StaticFS("/assets", http.FS(assetsFS))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 误区 2: 认为路由会自动拼接路径
|
||||
|
||||
```go
|
||||
// ❌ 错误想法
|
||||
engine.StaticFS("/assets", httpFS)
|
||||
// 以为会自动查找 assets/ 目录
|
||||
|
||||
// ✅ 正确理解
|
||||
engine.StaticFS("/assets", httpFS)
|
||||
// 只是剥离 /assets 前缀,在 httpFS 根目录查找
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 误区 3: 忽略错误处理
|
||||
|
||||
```go
|
||||
// ❌ 不好
|
||||
assetsFS, _ := fs.Sub(staticFS, "assets")
|
||||
engine.StaticFS("/assets", http.FS(assetsFS))
|
||||
|
||||
// ✅ 推荐
|
||||
if assetsFS, err := fs.Sub(staticFS, "assets"); err == nil {
|
||||
engine.StaticFS("/assets", http.FS(assetsFS))
|
||||
} else {
|
||||
logger.Warn("无法创建 assets 文件系统", zap.Error(err))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **总结**
|
||||
|
||||
### 核心要点
|
||||
|
||||
1. ⭐ **fs.Sub 是必需的**
|
||||
- 用于创建子目录视图
|
||||
- 将 URL 路径正确映射到文件
|
||||
|
||||
2. ⭐ **StaticFS 不会自动拼接路径**
|
||||
- 它只剥离 URL 前缀
|
||||
- 在 file system 的根目录查找
|
||||
|
||||
3. ⭐ **错误处理很重要**
|
||||
- 始终检查 fs.Sub 的返回值
|
||||
- 记录日志便于排查
|
||||
|
||||
---
|
||||
|
||||
### 一句话总结
|
||||
|
||||
**使用 Gin + embed 提供静态资源时,必须用 `fs.Sub` 创建子目录视图,否则路由映射会失败!**
|
||||
|
||||
---
|
||||
|
||||
**状态**: ✅ **问题已彻底修复**
|
||||
**教训**: Go embed + Gin StaticFS 必须正确使用 fs.Sub 进行路径映射
|
||||
|
||||
*MeshRay - 又解决一个顽固问题!* ✨🔧
|
||||
Reference in New Issue
Block a user