Private
Public Access
1
0
Files
u-desk/docs/03-模块文档/文件系统/html-preview-architecture.md

307 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HTML 预览架构优化
> 解决 Wails WebView 中 HTML 预览闪烁问题,优化资源路径处理
## 📋 目录
- [问题背景](#问题背景)
- [架构对比](#架构对比)
- [解决方案](#解决方案)
- [核心实现](#核心实现)
- [API 文档](#api-文档)
- [代码规范](#代码规范)
---
## 🔍 问题背景
### 现象
在 u-deskWails 桌面应用)中预览 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*