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

9.2 KiB
Raw Blame History

HTML 预览架构优化

解决 Wails WebView 中 HTML 预览闪烁问题,优化资源路径处理

📋 目录


🔍 问题背景

现象

在 u-deskWails 桌面应用)中预览 HTML 文件时,点击链接切换到另一个 HTML 文件有明显闪烁,而在普通浏览器中不明显。

根因分析

  1. 双重更新周期selectedFileItemfileContent 分开更新,导致两次 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. 后端路由

// 注册路由
mux.HandleFunc("/localfs/html-preview", handleHtmlPreviewRequest)

2. 预编译正则表达式

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. 统一路径解析

// resolveHtmlPathToUrl 统一处理相对路径和绝对路径
func resolveHtmlPathToUrl(baseDir string, path string) string {
    // 处理以 / 开头的绝对路径
    if strings.HasPrefix(path, "/") {
        path = path[1:]
    }
    // ... 解析并转换为 /localfs/ URL
}

4. 文件类型处理

// CSS 文件:转换内容中的相对路径
if ext == ".css" {
    transformedContent := transformCssContent(string(content), basePath)
}

// JS 文件:转换动态 import 路径
if ext == ".js" || ext == ".mjs" {
    transformedContent := transformJsDynamicImports(string(content), basePath)
}

5. 前端调用

<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 处理所有路径

// 路径处理统一在这个函数内部完成
newUrl := resolveHtmlPathToUrl(baseDir, path)

避免:在多处重复判断 / 开头

// 不要这样做
if strings.HasPrefix(path, "/") {
    newUrl = resolveHtmlPathToUrl(baseDir, path[1:])
} else {
    newUrl = resolveHtmlPathToUrl(baseDir, path)
}

正则表达式预编译

正确做法:在 var 块中预编译

var (
    htmlLinkTagRegex = regexp.MustCompile(`<link\s+([^>]*)>`)
)

避免:在函数内部动态编译

// 不要这样做 - 每次调用都重新编译
func process(html string) {
    regex := regexp.MustCompile(`<link\s+([^>]*)>`)
}

日志规范

  • 保留关键操作的日志(请求开始/结束)
  • 移除详细的调试日志
  • 使用结构化日志格式
// 保留
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 构建产物正确加载

验证命令

# 构建
go build -o u-desk.exe .

# 测试文件
# E:/wk-lab/lab-admin/dist/index.html

📊 收益总结

指标 优化前 优化后
前端代码行数 ~230 行 ~10 行
闪烁问题 明显
路径转换 仅前端 前后端统一
可维护性

文档版本: 1.0 创建日期: 2026-02-28 作者: Claude Code