像Ajax一样异步更新Golang模板的方法有哪些?

像Ajax一样异步更新Golang模板的方法有哪些? 当页面加载时间较长时,会出现问题。在我的案例中,这个网站从数据库加载5000条记录,在所有内容加载完成之前需要较长时间。

我的意图是使用几个动态应用的主题。但这样做时,主题直到所有内容加载完成后才会加载,导致恼人的“闪烁”现象。

1. 无闪烁的静态方法 当我在根级别以“静态”方式应用主题时,没有闪烁,但更新所选按钮会有延迟。

<html data-theme="classic" lang="en">

http://94.237.92.101:2020/topics

2. 有闪烁的动态方法 当我在body级别以“即时”方式应用主题时,会出现恼人的闪烁。

<body data-theme="classic">
window.addEventListener("DOMContentLoaded", (event) => {
  document.body.dataset.theme = localStorage.theme
  if (localStorage.theme) {
     do nothing 
  } else {
    window.localStorage.setItem('theme', "classic")
    window.location.reload()
  }
})

http://94.237.92.101:3030/topics

我的问题是,在Go模板中是否有任何方法可以像Ajax一样异步加载数据?


更多关于像Ajax一样异步更新Golang模板的方法有哪些?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

这看起来不像是一个Go语言的问题,而是一个JavaScript和浏览器DOM的问题。

更多关于像Ajax一样异步更新Golang模板的方法有哪些?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


为什么要从数据库加载5000条记录?

为了展示闪烁问题 = 这个问题。 😊 如果页面因其他原因加载缓慢,它就会闪烁。所以我想找到能异步工作的方法来提高加载速度。

lutzhorn:

这看起来不像是Go的问题,而是JavaScript的问题。

Go从Postgresql获取数据并加载到Go模板中。JavaScript在哪里?据我所知,没有涉及JavaScript。请解释一下……

Sibert:

window.addEventListener(“DOMContentLoaded”, (event) => { document.body.dataset.theme = localStorage.theme if (localStorage.theme) { do nothing } else { window.localStorage.setItem(‘theme’, “classic”) window.location.reload() } })

这全是Javascript,你问Javascript在哪里是什么意思!?

window.addEventListener 在您的第二种动态方法中是 JavaScript。

我不太理解您所说的“在 Go 模板中异步加载数据”是什么意思。Go 运行在服务器端,它返回的是完全渲染后的模板扩展结果。它会将这个完整的结果发送给客户端,客户端可以对其进行任何操作。

因此,如果客户端出现任何闪烁,服务器端是无法对此进行任何处理的。

Sibert:

在我的案例中,这个网站从数据库加载了5000条记录,在所有内容加载完成之前需要很长时间。

你为什么从数据库加载5000条记录?你的例子看起来像是一个论坛页面。如果确实如此,我的建议是不要一次性加载5000条记录。也许可以加载前100条(即使这对我来说听起来也有些过度),然后提供一个搜索功能让人们来缩小范围。

所以,你在网页HTML中观察到的每一个闪烁或错误,都必须在将HTML表格插入现有HTML的JavaScript代码中处理。

你指的是这个吗?这是在将HTML表格插入到现有的HTML中。据我所见,没有JavaScript。还是我完全理解错了?

  <table id="table">
    <col style="width:100vw">
    {{range .}}
    <tr data-id{{.post_id}}>
      <td>
        <h5>{{.post_subject}}</h5><p>John Doe</p>
      </td>
    </tr>
    {{end}}
  </table>

lutzhorn:

我不太理解你所说的“在Go模板中异步加载数据”是什么意思。

我是个完全的新手,所以请见谅。

但我找到了这个,我认为它展示了ajax如何从文件(数据库)异步加载数据。

W3Schools在线HTML编辑器

W3Schools在线HTML编辑器

W3Schools在线代码编辑器允许您编辑代码并在浏览器中查看结果

这在Go中使用模板不是一样的吗?

layout.Execute(w, page)

但Go是同步执行的吗?我正在尝试理解。

不,这是服务器端执行的 Go 代码,就浏览器而言,它渲染的是静态 HTML。

在你链接的教程中,这是 JavaScript 代码,它在使用 AJAX 进行异步请求后插入 HTML 元素:

function myFunction(xml) {
  var i;
  var xmlDoc = xml.responseXML;
  var table="<tr><th>Artist</th><th>Title</th></tr>";
  var x = xmlDoc.getElementsByTagName("CD");
  for (i = 0; i <x.length; i++) { 
    table += "<tr><td>" +
    x[i].getElementsByTagName("ARTIST")[0].childNodes[0].nodeValue +
    "</td><td>" +
    x[i].getElementsByTagName("TITLE")[0].childNodes[0].nodeValue +
    "</td></tr>";
  }
  document.getElementById("demo").innerHTML = table;
}

也许你应该向我们展示一个最小但完整的示例,包括你使用的服务器端 Go 代码以及客户端 HTML 和 JavaScript。

不,这是在浏览器中执行的JavaScript,而不是在服务器上执行的Go或任何其他代码。

客户端JavaScript代码基本上是对一个XML文件发起HTTP GET请求(xhttp.open("GET", "cd_catalog.xml", true);)。这个请求确实是在浏览器中异步执行的,这意味着即使在请求XML文件时,您仍然可以与HTML网站进行交互。一旦HTTP GET请求完成并且完整的XML文件加载完毕,它会在JavaScript函数myFunction中被解析,然后一个HTML表格会被添加到现有的HTML中。

关键点在于,所有这些操作都是在浏览器中完成的。提供XML文件的服务器并不关心这些。它接收对该文件的HTTP GET请求并返回该文件。也许这个文件已经存在于服务器磁盘上的某个位置,或者是从数据库中读取的。也许这个文件是动态生成的,甚至可能在此过程中使用了Go模板。所有这些对于等待HTTP GET请求响应的浏览器来说都是不可见的。一旦服务器完成了它正在做的任何事情,它就会返回完整的XML文件,仅此而已。

因此,您在网页HTML中观察到的任何闪烁或错误都必须在将HTML表格插入现有HTML的JavaScript代码中处理。

// 注意:原始HTML内容中未包含Go代码。
// 此处代码块仅为遵循格式要求而保留。

在Go模板中实现类似Ajax的异步更新,可以通过以下几种技术方案:

1. 使用Server-Sent Events (SSE)

SSE允许服务器主动向客户端推送数据,适合实时更新场景。

服务端代码:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

func sseHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")
    w.Header().Set("Access-Control-Allow-Origin", "*")

    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
        return
    }

    // 模拟数据加载
    for i := 1; i <= 100; i++ {
        data := map[string]interface{}{
            "progress": i,
            "message":  fmt.Sprintf("已加载 %d/5000 条记录", i*50),
        }
        
        jsonData, _ := json.Marshal(data)
        fmt.Fprintf(w, "data: %s\n\n", jsonData)
        flusher.Flush()
        
        time.Sleep(100 * time.Millisecond)
    }
    
    // 发送完成事件
    fmt.Fprintf(w, "event: complete\ndata: {\"status\":\"done\"}\n\n")
    flusher.Flush()
}

客户端代码:

const eventSource = new EventSource('/sse');

eventSource.addEventListener('message', (event) => {
    const data = JSON.parse(event.data);
    document.getElementById('progress').textContent = data.message;
    document.getElementById('progress-bar').style.width = data.progress + '%';
});

eventSource.addEventListener('complete', (event) => {
    const data = JSON.parse(event.data);
    if (data.status === 'done') {
        eventSource.close();
        // 加载完整数据
        loadFullContent();
    }
});

2. 使用WebSocket双向通信

适合需要双向数据交换的场景。

服务端代码:

package main

import (
    "github.com/gorilla/websocket"
    "log"
    "net/http"
    "time"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("升级WebSocket失败:", err)
        return
    }
    defer conn.Close()

    // 分批发送数据
    for i := 0; i < 5000; i += 100 {
        batch := fetchBatchFromDB(i, 100)
        
        err := conn.WriteJSON(map[string]interface{}{
            "type": "data_batch",
            "data": batch,
            "progress": (i + 100) / 5000 * 100,
        })
        
        if err != nil {
            log.Println("发送数据失败:", err)
            break
        }
        
        time.Sleep(50 * time.Millisecond)
    }
    
    conn.WriteJSON(map[string]interface{}{
        "type": "complete",
        "message": "所有数据加载完成",
    })
}

func fetchBatchFromDB(offset, limit int) []interface{} {
    // 模拟从数据库获取批次数据
    return make([]interface{}, limit)
}

3. 使用Fetch API分页加载

结合Go模板和JavaScript实现分页异步加载。

服务端路由:

func main() {
    http.HandleFunc("/topics", topicsHandler)
    http.HandleFunc("/api/topics", apiTopicsHandler)
    http.ListenAndServe(":8080", nil)
}

func topicsHandler(w http.ResponseWriter, r *http.Request) {
    tmpl := template.Must(template.ParseFiles("topics.html"))
    tmpl.Execute(w, nil)
}

func apiTopicsHandler(w http.ResponseWriter, r *http.Request) {
    page := r.URL.Query().Get("page")
    limit := r.URL.Query().Get("limit")
    
    // 根据page和limit查询数据库
    data := fetchPaginatedData(page, limit)
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

HTML模板:

<!DOCTYPE html>
<html data-theme="{{.Theme}}">
<head>
    <script>
        let currentPage = 1;
        const pageSize = 100;
        
        async function loadTopics(page) {
            const response = await fetch(`/api/topics?page=${page}&limit=${pageSize}`);
            const data = await response.json();
            
            const container = document.getElementById('topics-container');
            data.forEach(topic => {
                const div = document.createElement('div');
                div.className = 'topic-item';
                div.innerHTML = `
                    <h3>${topic.title}</h3>
                    <p>${topic.content}</p>
                `;
                container.appendChild(div);
            });
            
            if (data.length === pageSize) {
                currentPage++;
                // 继续加载下一页
                setTimeout(() => loadTopics(currentPage), 100);
            }
        }
        
        // 立即加载主题避免闪烁
        document.documentElement.dataset.theme = localStorage.getItem('theme') || 'classic';
        
        // 页面加载后开始异步加载数据
        document.addEventListener('DOMContentLoaded', () => {
            loadTopics(1);
        });
    </script>
</head>
<body>
    <div id="topics-container"></div>
    <div id="loading">正在加载数据...</div>
</body>
</html>

4. 使用模板片段和hx-target(HTMX)

结合HTMX实现部分模板更新。

服务端代码:

func topicsHandler(w http.ResponseWriter, r *http.Request) {
    if r.Header.Get("HX-Request") == "true" {
        // 返回部分HTML
        data := fetchPartialData()
        tmpl := template.Must(template.ParseFiles("topics_partial.html"))
        tmpl.Execute(w, data)
    } else {
        // 返回完整页面
        tmpl := template.Must(template.ParseFiles("topics_full.html"))
        tmpl.Execute(w, nil)
    }
}

HTML使用HTMX:

<div hx-get="/topics" hx-trigger="load" hx-target="#content">
    <div id="content">
        <!-- 异步加载的内容将出现在这里 -->
    </div>
</div>

5. 解决主题闪烁问题的具体方案

针对你的主题切换问题,建议:

// 在渲染模板时注入主题
func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
    // 从cookie或session获取主题
    theme := getThemeFromRequest(r)
    
    templateData := map[string]interface{}{
        "Theme": theme,
        "Data":  data,
    }
    
    tmpl, err := template.ParseFiles(tmpl)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    tmpl.Execute(w, templateData)
}
<!DOCTYPE html>
<html data-theme="{{.Theme}}">
<head>
    <style>
        [data-theme="dark"] { /* 暗色主题样式 */ }
        [data-theme="light"] { /* 亮色主题样式 */ }
    </style>
    <script>
        // 立即设置主题,无需等待DOMContentLoaded
        (function() {
            const savedTheme = localStorage.getItem('theme');
            if (savedTheme) {
                document.documentElement.dataset.theme = savedTheme;
            }
        })();
    </script>
</head>
<body>
    <!-- 页面内容 -->
    <div hx-get="/api/topics" hx-trigger="load" hx-swap="innerHTML">
        正在加载数据...
    </div>
</body>
</html>

这些方法都可以实现Go模板的异步更新,避免页面加载时的闪烁问题。SSE和WebSocket适合实时性要求高的场景,而Fetch API分页加载更适合大数据量的分批处理。

回到顶部