# 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(`]*)>`) htmlScriptTagRegex = regexp.MustCompile(`]*)>`) // ... 其他标签 // 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 ``` --- ## 📚 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(`]*)>`) ) ``` ❌ **避免**:在函数内部动态编译 ```go // 不要这样做 - 每次调用都重新编译 func process(html string) { regex := regexp.MustCompile(`]*)>`) } ``` ### 日志规范 - 保留关键操作的日志(请求开始/结束) - 移除详细的调试日志 - 使用结构化日志格式 ```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*