// 仅读取 build 后的报告 JSON,本地渲染 qiye.html(不执行 BuildReportFromRawSources)。 // // go run ./cmd/qygl_report_preview -in resources/dev-report/built.json // go run ./cmd/qygl_report_preview -in built.json -addr :8899 -watch // // 每次打开/刷新页面都会重新读取 -in 文件;加 -watch 后保存 JSON 会自动刷新浏览器。 package main import ( "bytes" "encoding/json" "flag" "fmt" "html/template" "log" "net/http" "os" "path/filepath" ) func parseBuiltReport(data []byte) (map[string]interface{}, error) { var root map[string]interface{} if err := json.Unmarshal(data, &root); err != nil { return nil, err } if _, ok := root["jiguangFull"]; ok { return nil, fmt.Errorf("检测到 raw 字段 jiguangFull,请先执行: go run ./cmd/qygl_report_build -in -out built.json") } if k, _ := root["kind"].(string); k == "full" { r, ok := root["report"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("kind=full 时缺少 report 对象") } return r, nil } if r, ok := root["report"].(map[string]interface{}); ok { return r, nil } if root["entName"] != nil || root["basic"] != nil || root["reportTime"] != nil { return root, nil } return nil, fmt.Errorf("不是有效的 build 后报告(根级应有 entName、basic、reportTime 之一,或 {\"report\":{...}} / kind=full)") } func fileVersionTag(path string) (string, error) { st, err := os.Stat(path) if err != nil { return "", err } return fmt.Sprintf("%d-%d", st.ModTime().UnixNano(), st.Size()), nil } func renderPage(tmpl *template.Template, report map[string]interface{}, injectLive bool) ([]byte, error) { reportBytes, err := json.Marshal(report) if err != nil { return nil, err } var buf bytes.Buffer if err := tmpl.Execute(&buf, map[string]interface{}{ "ReportJSON": template.JS(reportBytes), }); err != nil { return nil, err } b := buf.Bytes() if !injectLive { return b, nil } script := `` closing := []byte("") idx := bytes.LastIndex(b, closing) if idx < 0 { return append(b, []byte(script)...), nil } out := make([]byte, 0, len(b)+len(script)) out = append(out, b[:idx]...) out = append(out, script...) out = append(out, b[idx:]...) return out, nil } func main() { addr := flag.String("addr", ":8899", "监听地址") root := flag.String("root", ".", "项目根目录(含 resources/qiye.html)") inPath := flag.String("in", "", "build 后的 JSON(由 qygl_report_build 生成,或 fixture.full 中的 report 形态)") watch := flag.Bool("watch", false, "监听 -in 文件变化并自动刷新浏览器(轮询)") flag.Parse() if *inPath == "" { log.Fatal("请指定 -in ") } rootAbs, err := filepath.Abs(*root) if err != nil { log.Fatalf("解析 root: %v", err) } tplPath := filepath.Join(rootAbs, "resources", "qiye.html") if _, err := os.Stat(tplPath); err != nil { log.Fatalf("未找到模板 %s: %v", tplPath, err) } var inAbs string if filepath.IsAbs(*inPath) { inAbs = *inPath } else { inAbs = filepath.Join(rootAbs, *inPath) } if _, err := os.Stat(inAbs); err != nil { log.Fatalf("读取 %s: %v", inAbs, err) } tmpl, err := template.ParseFiles(tplPath) if err != nil { log.Fatalf("解析模板: %v", err) } http.HandleFunc("/__version", func(w http.ResponseWriter, r *http.Request) { tag, err := fileVersionTag(inAbs) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte(tag)) }) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } raw, err := os.ReadFile(inAbs) if err != nil { http.Error(w, "读取报告文件失败: "+err.Error(), http.StatusInternalServerError) return } report, err := parseBuiltReport(raw) if err != nil { http.Error(w, "解析 JSON 失败: "+err.Error(), http.StatusInternalServerError) return } html, err := renderPage(tmpl, report, *watch) if err != nil { http.Error(w, "渲染失败: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(html) }) log.Printf("报告预览: http://127.0.0.1%s/ (每请求重读 %s)", *addr, inAbs) if *watch { log.Printf("已启用 -watch:保存 JSON 后约 0.6s 内自动刷新页面") } if err := http.ListenAndServe(*addr, nil); err != nil { log.Fatal(err) } }