160 lines
4.8 KiB
Go
160 lines
4.8 KiB
Go
|
|
// 仅读取 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 <raw.json> -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 := `<script>(function(){var v0=null;function tick(){fetch("/__version?="+Date.now(),{cache:"no-store"}).then(function(r){return r.text();}).then(function(v){if(v==="")return;if(v0===null)v0=v;else if(v0!==v){v0=v;location.reload();}}).catch(function(){});}setInterval(tick,600);tick();})();</script>`
|
|||
|
|
closing := []byte("</body>")
|
|||
|
|
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 <built.json>")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
}
|
|||
|
|
}
|