新增:文档体系重构+CHANGELOG补充+发布产物清理
This commit is contained in:
306
docs/03-模块文档/文件系统/html-preview-architecture.md
Normal file
306
docs/03-模块文档/文件系统/html-preview-architecture.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# HTML 预览架构优化
|
||||
|
||||
> 解决 Wails WebView 中 HTML 预览闪烁问题,优化资源路径处理
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [问题背景](#问题背景)
|
||||
- [架构对比](#架构对比)
|
||||
- [解决方案](#解决方案)
|
||||
- [核心实现](#核心实现)
|
||||
- [API 文档](#api-文档)
|
||||
- [代码规范](#代码规范)
|
||||
|
||||
---
|
||||
|
||||
## 🔍 问题背景
|
||||
|
||||
### 现象
|
||||
|
||||
在 u-desk(Wails 桌面应用)中预览 HTML 文件时,点击链接切换到另一个 HTML 文件有明显闪烁,而在普通浏览器中不明显。
|
||||
|
||||
### 根因分析
|
||||
|
||||
1. **双重更新周期**:`selectedFileItem` 和 `fileContent` 分开更新,导致两次 Vue 渲染
|
||||
2. **srcdoc 机制**:每次内容变化,iframe 完全重新解析 HTML
|
||||
3. **WebView 差异**:Wails WebView2 对 srcdoc 处理比 Chrome 慢
|
||||
|
||||
### 技术债务
|
||||
|
||||
| 问题 | 影响 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 前端路径转换逻辑复杂 | 维护困难 | P1 |
|
||||
| 重复渲染导致闪烁 | 用户体验差 | P0 |
|
||||
| 代码分散在前端 | 架构不清晰 | P2 |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构对比
|
||||
|
||||
### 优化前
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端处理 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────┐ ┌──────────────────┐ ┌───────────────┐ │
|
||||
│ │ 读取文件 │ → │ convertHtmlPaths │ → │ htmlContent │ │
|
||||
│ │ fileContent│ │ 处理相对路径 │ │ WithTheme │ │
|
||||
│ └──────────┘ └──────────────────┘ └───────┬───────┘ │
|
||||
│ ↓ │
|
||||
│ ┌───────────┐ │
|
||||
│ │ srcdoc │ │
|
||||
│ └───────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- srcdoc 每次变化都重新解析
|
||||
- 前端逻辑复杂(200+ 行)
|
||||
- 主题切换需要重新渲染
|
||||
|
||||
### 优化后
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端 后端 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────┐ ┌─────────────┐ │
|
||||
│ │ 预览URL │ → /localfs/html-preview │ 读取HTML │ │
|
||||
│ │ 生成 │ ?path=xxx │ 转换路径 │ │
|
||||
│ └─────┬────┘ │ 注入脚本 │ │
|
||||
│ ↓ └──────┬──────┘ │
|
||||
│ ┌───────────┐ ↓ │
|
||||
│ │ iframe │ ←──────────────────────── 返回处理后的HTML │
|
||||
│ │ src │ │
|
||||
│ └───────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- iframe 导航而非重建,无闪烁
|
||||
- 浏览器可缓存
|
||||
- 前端代码简化(减少 200+ 行)
|
||||
|
||||
---
|
||||
|
||||
## ✅ 解决方案
|
||||
|
||||
### 核心变更
|
||||
|
||||
| 变更 | 说明 |
|
||||
|------|------|
|
||||
| 使用 `:src` 替代 `:srcdoc` | iframe 导航而非重建 |
|
||||
| 后端统一处理路径转换 | 前端逻辑移至后端 |
|
||||
| 支持 CSS/JS 文件路径转换 | 动态 import 也正确解析 |
|
||||
|
||||
### 修改文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `internal/filesystem/asset_handler.go` | 新增路由、路径转换函数 |
|
||||
| `frontend/.../FileEditorPanel.vue` | 改用 `:src`,移除前端处理逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 核心实现
|
||||
|
||||
### 1. 后端路由
|
||||
|
||||
```go
|
||||
// 注册路由
|
||||
mux.HandleFunc("/localfs/html-preview", handleHtmlPreviewRequest)
|
||||
```
|
||||
|
||||
### 2. 预编译正则表达式
|
||||
|
||||
```go
|
||||
var (
|
||||
// CSS 相关
|
||||
cssImportRegex = regexp.MustCompile(`@import\s+(?:url\s*\(\s*)?["']([^"']+)["']\s*\)?\s*;`)
|
||||
cssUrlRegex = regexp.MustCompile(`url\(\s*["']?([^"')]+)["']?\s*\)`)
|
||||
|
||||
// HTML 标签
|
||||
htmlLinkTagRegex = regexp.MustCompile(`<link\s+([^>]*)>`)
|
||||
htmlScriptTagRegex = regexp.MustCompile(`<script\s+([^>]*)>`)
|
||||
// ... 其他标签
|
||||
|
||||
// ES6 模块语句
|
||||
es6ImportFromRegex = regexp.MustCompile(`import\s+([\s\S]*?)\s+from\s+["']([^"']+)["']`)
|
||||
es6DynamicImport = regexp.MustCompile(`import\s*\(\s*["']([^"']+)["']\s*\)`)
|
||||
es6BareImport = regexp.MustCompile(`(?m)^\s*import\s+["']([^"']+)["']`)
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 统一路径解析
|
||||
|
||||
```go
|
||||
// resolveHtmlPathToUrl 统一处理相对路径和绝对路径
|
||||
func resolveHtmlPathToUrl(baseDir string, path string) string {
|
||||
// 处理以 / 开头的绝对路径
|
||||
if strings.HasPrefix(path, "/") {
|
||||
path = path[1:]
|
||||
}
|
||||
// ... 解析并转换为 /localfs/ URL
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 文件类型处理
|
||||
|
||||
```go
|
||||
// CSS 文件:转换内容中的相对路径
|
||||
if ext == ".css" {
|
||||
transformedContent := transformCssContent(string(content), basePath)
|
||||
}
|
||||
|
||||
// JS 文件:转换动态 import 路径
|
||||
if ext == ".js" || ext == ".mjs" {
|
||||
transformedContent := transformJsDynamicImports(string(content), basePath)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 前端调用
|
||||
|
||||
```vue
|
||||
<iframe :src="htmlPreviewUrl"></iframe>
|
||||
|
||||
<script setup>
|
||||
const htmlPreviewUrl = computed(() => {
|
||||
if (!props.config.currentFileFullPath || !props.config.isHtmlFile) {
|
||||
return ''
|
||||
}
|
||||
const encodedPath = encodeURIComponent(props.config.currentFileFullPath)
|
||||
return `http://localhost:18765/localfs/html-preview?path=${encodedPath}`
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 API 文档
|
||||
|
||||
### HTML 预览接口
|
||||
|
||||
**路径**:`GET /localfs/html-preview`
|
||||
|
||||
**参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `path` | string | 是 | HTML 文件绝对路径(URL 编码) |
|
||||
| `theme` | string | 否 | 主题(`light` / `dark`),默认 `light` |
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
GET /localfs/html-preview?path=E%3A%2Fdocs%2Fpreview.html&theme=dark
|
||||
```
|
||||
|
||||
**返回**:
|
||||
|
||||
- Content-Type: `text/html; charset=utf-8`
|
||||
- 处理后的 HTML 内容(资源路径已转换为 `/localfs/` URL)
|
||||
|
||||
### 处理流程
|
||||
|
||||
1. 读取 HTML 文件
|
||||
2. 转换静态资源路径(link, script, img, video 等)
|
||||
3. 转换内联样式中的 url()
|
||||
4. 转换 ES6 import 语句
|
||||
5. 注入链接点击拦截脚本
|
||||
6. 返回处理后的 HTML
|
||||
|
||||
---
|
||||
|
||||
## 📐 代码规范
|
||||
|
||||
### DRY 原则
|
||||
|
||||
✅ **正确做法**:统一使用 `resolveHtmlPathToUrl` 处理所有路径
|
||||
|
||||
```go
|
||||
// 路径处理统一在这个函数内部完成
|
||||
newUrl := resolveHtmlPathToUrl(baseDir, path)
|
||||
```
|
||||
|
||||
❌ **避免**:在多处重复判断 `/` 开头
|
||||
|
||||
```go
|
||||
// 不要这样做
|
||||
if strings.HasPrefix(path, "/") {
|
||||
newUrl = resolveHtmlPathToUrl(baseDir, path[1:])
|
||||
} else {
|
||||
newUrl = resolveHtmlPathToUrl(baseDir, path)
|
||||
}
|
||||
```
|
||||
|
||||
### 正则表达式预编译
|
||||
|
||||
✅ **正确做法**:在 `var` 块中预编译
|
||||
|
||||
```go
|
||||
var (
|
||||
htmlLinkTagRegex = regexp.MustCompile(`<link\s+([^>]*)>`)
|
||||
)
|
||||
```
|
||||
|
||||
❌ **避免**:在函数内部动态编译
|
||||
|
||||
```go
|
||||
// 不要这样做 - 每次调用都重新编译
|
||||
func process(html string) {
|
||||
regex := regexp.MustCompile(`<link\s+([^>]*)>`)
|
||||
}
|
||||
```
|
||||
|
||||
### 日志规范
|
||||
|
||||
- 保留关键操作的日志(请求开始/结束)
|
||||
- 移除详细的调试日志
|
||||
- 使用结构化日志格式
|
||||
|
||||
```go
|
||||
// 保留
|
||||
log.Printf("[HtmlPreview] 处理完成: %s (%d -> %d bytes)", filePath, len(content), len(finalContent))
|
||||
|
||||
// 移除
|
||||
// log.Printf("[replaceHtmlTagAttribute] 找到属性 %s=%s", attrName, relativePath)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试验证
|
||||
|
||||
### 测试场景
|
||||
|
||||
1. **基础 HTML 预览**:打开包含 CSS/JS 的 HTML 文件
|
||||
2. **资源路径解析**:验证相对路径和绝对路径正确转换
|
||||
3. **链接点击**:点击 HTML 内的链接,验证正确打开新文件
|
||||
4. **Vite 构建产物**:验证 `/assets/` 路径的 Vue 构建产物正确加载
|
||||
|
||||
### 验证命令
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
go build -o u-desk.exe .
|
||||
|
||||
# 测试文件
|
||||
# E:/wk-lab/lab-admin/dist/index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 收益总结
|
||||
|
||||
| 指标 | 优化前 | 优化后 |
|
||||
|------|--------|--------|
|
||||
| 前端代码行数 | ~230 行 | ~10 行 |
|
||||
| 闪烁问题 | 明显 | 无 |
|
||||
| 路径转换 | 仅前端 | 前后端统一 |
|
||||
| 可维护性 | 中 | 高 |
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 1.0*
|
||||
*创建日期: 2026-02-28*
|
||||
*作者: Claude Code*
|
||||
Reference in New Issue
Block a user