Initial commit
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
# MeshRay - 前端服务机制完整排查报告
|
||||
|
||||
**排查时间**: 2026-03-24
|
||||
**排查内容**: 程序启动后如何提供前端页面
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **核心结论**
|
||||
|
||||
✅ **前端服务完全正常!** 所有静态资源都正确加载。
|
||||
|
||||
---
|
||||
|
||||
## 📊 **完整的前端服务流程**
|
||||
|
||||
### 流程图:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[程序启动 main.go] --> B[调用 api.NewServer]
|
||||
B --> C[初始化 Gin 引擎]
|
||||
C --> D[加载 embed.FS 静态资源]
|
||||
D --> E{检查 index.html}
|
||||
E -->|存在 | F[使用内嵌资源 ✅]
|
||||
E -->|不存在 | G[使用外部目录]
|
||||
F --> H[注册 StaticFS 路由]
|
||||
H --> I[注册 NoRoute 处理 SPA]
|
||||
I --> J[启动 HTTP 服务 :9531]
|
||||
J --> K[用户访问 /]
|
||||
K --> L[返回 index.html]
|
||||
L --> M[浏览器加载 JS/CSS]
|
||||
M --> N[Vue 应用启动]
|
||||
N --> O[显示登录页面]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 **详细排查结果**
|
||||
|
||||
### Step 1: 后端启动流程
|
||||
|
||||
**文件**: [`cmd/meshray/main.go`](file://e:\Project\MeshRay\cmd\meshray\main.go)
|
||||
|
||||
```go
|
||||
// 第 94 行:创建 API 服务器
|
||||
server, err := api.NewServer(cfg, logger, store)
|
||||
if err != nil {
|
||||
logger.Fatal("创建服务器失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 第 103 行:启动服务
|
||||
server.Start()
|
||||
```
|
||||
|
||||
**执行结果**:
|
||||
```
|
||||
✅ 配置加载成功
|
||||
✅ 日志系统初始化成功
|
||||
✅ 数据库初始化成功
|
||||
✅ 默认策略初始化成功
|
||||
🚀 正在启动 MeshRay...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 静态资源嵌入
|
||||
|
||||
**文件**: [`internal/api/server.go`](file://e:\Project\MeshRay\internal\api\server.go#L92-L110)
|
||||
|
||||
```go
|
||||
// 第 94-109 行:使用 embed 的静态文件
|
||||
var staticFS fs.FS
|
||||
var useEmbed bool
|
||||
|
||||
// 直接使用 web/dist 包中的 WebAssets
|
||||
if embedFS, err := fs.Sub(dist.WebAssets, "."); err == nil {
|
||||
// 检查是否有 index.html
|
||||
if _, statErr := fs.Stat(embedFS, "index.html"); statErr == nil {
|
||||
staticFS = embedFS
|
||||
useEmbed = true
|
||||
s.logger.Info("使用内嵌的静态文件")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键组件**:
|
||||
|
||||
1. **`dist.WebAssets`**: 来自 `web/dist/embed.go`
|
||||
2. **`embed.FS`**: Go 1.16+ 的内嵌文件系统
|
||||
3. **`fs.Sub()`**: 创建子目录视图
|
||||
|
||||
---
|
||||
|
||||
### Step 3: embed.go 定义
|
||||
|
||||
**文件**: [`web/dist/embed.go`](file://e:\Project\MeshRay\web\dist\embed.go)
|
||||
|
||||
```go
|
||||
package dist
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *
|
||||
var WebAssets embed.FS // MeshRay frontend assets
|
||||
```
|
||||
|
||||
**作用**:
|
||||
- ✅ 将 `web/dist/` 下的所有文件嵌入到 Go 二进制
|
||||
- ✅ 通过 `WebAssets` 变量访问
|
||||
- ✅ 编译时打包,运行时直接读取内存
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 静态文件服务配置
|
||||
|
||||
**文件**: [`internal/api/server.go`](file://e:\Project\MeshRay\internal\api\server.go#L117-L160)
|
||||
|
||||
```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) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// API 请求返回 404
|
||||
if strings.HasPrefix(path, "/api/") {
|
||||
c.JSON(404, gin.H{"error": "API not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试直接提供文件
|
||||
filePath := strings.TrimPrefix(path, "/")
|
||||
if filePath == "" {
|
||||
filePath = "index.html"
|
||||
}
|
||||
|
||||
// 读取并返回文件内容
|
||||
file, err := staticFS.Open(filePath)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
content, _ := io.ReadAll(file)
|
||||
c.Data(200, getContentType(filePath), content)
|
||||
return
|
||||
}
|
||||
|
||||
// 其他请求都返回 index.html(SPA 路由支持)
|
||||
file, _ = staticFS.Open("index.html")
|
||||
if file != nil {
|
||||
defer file.Close()
|
||||
content, _ := io.ReadAll(file)
|
||||
c.Data(200, "text/html; charset=utf-8", content)
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
|
||||
1. **StaticFS 注册**: `/assets` 和 `/static` 目录
|
||||
2. **NoRoute 兜底**: 处理 Vue Router 的 SPA 路由
|
||||
3. **MIME 类型**: 通过 `getContentType()` 设置正确的 Content-Type
|
||||
|
||||
---
|
||||
|
||||
### Step 5: 实际文件验证
|
||||
|
||||
#### 编译后的前端文件:
|
||||
|
||||
```
|
||||
web/dist/
|
||||
├── embed.go (99 bytes) ✅ embed 定义
|
||||
├── index.html (639 bytes) ✅ 入口 HTML
|
||||
└── assets/ ✅ JS/CSS 资源
|
||||
├── index-AIAUiq89.js
|
||||
├── index-JLRO43JB.css
|
||||
├── vue-vendor-BBChLKcR.js
|
||||
├── element-plus-CWITzeOz.js
|
||||
├── Dashboard-DSsX0xA1.js
|
||||
└── ... (共 44 个文件)
|
||||
```
|
||||
|
||||
**生成时间**: 2026/3/24 20:44:21 ✅ 最新
|
||||
|
||||
---
|
||||
|
||||
### Step 6: 服务启动日志
|
||||
|
||||
**日志内容**:
|
||||
|
||||
```json
|
||||
{
|
||||
"level": "info",
|
||||
"time": "2026-03-24T20:44:55.330+0800",
|
||||
"caller": "api/server.go:103",
|
||||
"message": "使用内嵌的静态文件" ✅
|
||||
}
|
||||
{
|
||||
"level": "info",
|
||||
"time": "2026-03-24T20:44:55.330+0800",
|
||||
"caller": "api/server.go:394",
|
||||
"message": "Starting MeshRay",
|
||||
"address": ":9531" ✅
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 7: 实际访问测试
|
||||
|
||||
#### 测试 1: 访问根路径
|
||||
|
||||
```bash
|
||||
curl.exe http://localhost:9531/
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>MeshRay - 简单、高效异地组网</title>
|
||||
<script type="module" crossorigin src="/assets/index-AIAUiq89.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-BBChLKcR.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-JLRO43JB.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
✅ **HTML 正确返回,包含最新的 JS hash(AIAUiq89)**
|
||||
|
||||
---
|
||||
|
||||
#### 测试 2: 访问 JS 文件
|
||||
|
||||
```bash
|
||||
curl.exe http://localhost:9531/assets/index-AIAUiq89.js -I
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/javascript; charset=utf-8
|
||||
```
|
||||
|
||||
✅ **MIME 类型正确**
|
||||
|
||||
---
|
||||
|
||||
#### 测试 3: 服务器日志
|
||||
|
||||
```bash
|
||||
Get-Content logs\meshray.log -Tail 20
|
||||
```
|
||||
|
||||
**显示**:
|
||||
```
|
||||
status": 200, path: "/assets/index-AIAUiq89.js" ✅
|
||||
status": 200, path: "/assets/vue-vendor-BBChLKcR.js" ✅
|
||||
status": 200, path: "/assets/element-plus-CWITzeOz.js" ✅
|
||||
status": 200, path: "/assets/Dashboard-DSsX0xA1.js" ✅
|
||||
status": 200, path: "/" ✅
|
||||
```
|
||||
|
||||
✅ **所有静态资源返回 200 OK**
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ **架构设计总结**
|
||||
|
||||
### 1. 单文件部署策略
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ meshray.exe │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ Go 二进制代码 │ │
|
||||
│ │ ┌────────────┐ │ │
|
||||
│ │ │ embed.FS │ │ │
|
||||
│ │ │ 前端资源 │ │ │
|
||||
│ │ └────────────┘ │ │
|
||||
│ └──────────────────┘ │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- ✅ 无需单独部署前端
|
||||
- ✅ 避免 CORS 问题
|
||||
- ✅ 简化运维
|
||||
- ✅ 版本一致性
|
||||
|
||||
---
|
||||
|
||||
### 2. 路由优先级
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[请求到达] --> B{路径匹配?}
|
||||
B -->|/api/*| C[API 路由]
|
||||
B -->|/assets/*| D[StaticFS]
|
||||
B -->|/static/*| E[StaticFS]
|
||||
B -->|其他 | F[NoRoute]
|
||||
F --> G{是 SPA 路由?}
|
||||
G -->|是 | H[返回 index.html]
|
||||
G -->|否 | I[返回 404]
|
||||
```
|
||||
|
||||
**优先级顺序**:
|
||||
1. 中间件(认证、日志)
|
||||
2. 精确路由(`/api/v1/*`)
|
||||
3. StaticFS(`/assets/*`, `/static/*`)
|
||||
4. NoRoute(兜底,处理 SPA)
|
||||
|
||||
---
|
||||
|
||||
### 3. 数据流
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 用户浏览器
|
||||
participant Server as Gin 服务器
|
||||
participant Embed as embed.FS
|
||||
participant Vue as Vue 应用
|
||||
|
||||
User->>Server: GET /
|
||||
Server->>Embed: Open("index.html")
|
||||
Embed-->>Server: HTML 内容
|
||||
Server->>User: HTML + JS 引用
|
||||
|
||||
User->>Server: GET /assets/index.js
|
||||
Server->>Embed: Open("assets/index.js")
|
||||
Embed-->>Server: JS 内容
|
||||
Server->>User: application/javascript
|
||||
|
||||
User->>User: 执行 Vue 应用
|
||||
User->>Server: GET /api/v1/dashboard/stats
|
||||
Server->>Server: 业务逻辑处理
|
||||
Server->>User: JSON 数据
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ **验证清单**
|
||||
|
||||
完成以下检查确认前端服务正常:
|
||||
|
||||
- [x] ✅ `web/dist/embed.go` 存在
|
||||
- [x] ✅ `web/dist/index.html` 存在且最新
|
||||
- [x] ✅ `web/dist/assets/*.js` 存在且最新
|
||||
- [x] ✅ 后端编译成功(包含 embed)
|
||||
- [x] ✅ 服务启动日志显示"使用内嵌的静态文件"
|
||||
- [x] ✅ curl 测试 `/` 返回正确的 HTML
|
||||
- [x] ✅ curl 测试 `/assets/*.js` 返回 200 + application/javascript
|
||||
- [x] ✅ 服务器日志显示所有静态资源 200 OK
|
||||
- [ ] ⏳ 浏览器访问能看到登录页面(需手动测试)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **当前状态**
|
||||
|
||||
### 后端:
|
||||
```
|
||||
✅ 服务运行在 :9531
|
||||
✅ embed 静态资源已加载
|
||||
✅ 所有路由配置正确
|
||||
✅ MIME 类型设置正确
|
||||
```
|
||||
|
||||
### 前端:
|
||||
```
|
||||
✅ index.html 已编译(最新 hash: AIAUiq89)
|
||||
✅ JS 文件已编译(44 个文件)
|
||||
✅ CSS 文件已编译
|
||||
✅ Vue Router 配置正确
|
||||
✅ 登录页组件存在
|
||||
```
|
||||
|
||||
### 通信:
|
||||
```
|
||||
✅ 静态资源返回 200 OK
|
||||
✅ Content-Type 正确
|
||||
✅ SPA 路由支持正常
|
||||
✅ API 路由隔离正常
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 **关键文件清单**
|
||||
|
||||
| 文件 | 作用 | 状态 |
|
||||
|------|------|------|
|
||||
| `cmd/meshray/main.go` | 程序入口 | ✅ |
|
||||
| `internal/api/server.go` | 服务器配置 | ✅ |
|
||||
| `web/dist/embed.go` | embed 定义 | ✅ |
|
||||
| `web/dist/index.html` | 入口 HTML | ✅ |
|
||||
| `web/dist/assets/*.js` | Vue 应用 | ✅ |
|
||||
| `web/src/router/index.js` | 路由配置 | ✅ |
|
||||
| `web/src/views/Login.vue` | 登录组件 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🎉 **结论**
|
||||
|
||||
### 前端服务机制:**完全正常!** ✅
|
||||
|
||||
**证据**:
|
||||
1. ✅ embed.go 正确定义
|
||||
2. ✅ 前端已重新编译
|
||||
3. ✅ 后端成功嵌入静态资源
|
||||
4. ✅ 服务启动日志正常
|
||||
5. ✅ curl 测试返回正确
|
||||
6. ✅ 服务器日志显示所有资源 200 OK
|
||||
|
||||
**下一步**:
|
||||
- 请在浏览器中访问 http://localhost:9531
|
||||
- 应该能看到登录页面
|
||||
- 如果看不到,请截图错误信息
|
||||
|
||||
---
|
||||
|
||||
*MeshRay - 抽丝剥茧,真相大白!* ✨🔧
|
||||
Reference in New Issue
Block a user