Skip to content

Files

Latest commit

00d6687 · Oct 25, 2021

History

History
1090 lines (758 loc) · 54.1 KB

08.md

File metadata and controls

1090 lines (758 loc) · 54.1 KB

八、实时 Web 应用功能

在上一章中,我们考虑了如何通过 web 表单验证和处理用户生成的数据。当用户正确填写联系表时,它成功地通过了两轮验证,并向用户显示了一条确认消息。提交表单后,工作流程即告完成。如果我们想考虑一个更吸引人的工作流,那用户可能会以类似会话的方式参与服务器端应用呢?

今天的网络与蒂姆·伯纳斯·李(Tim Berners Lee)在 20 世纪 90 年代初设计的新生网络大不相同。当时,网络的重点是超链接连接的文档。客户端和服务器之间的 HTTP 事务总是意味着短暂的。

在 21 世纪初,这种情况开始改变。研究人员演示了服务器与客户端保持持久连接的方法。客户端的早期原型是使用 Adobe Flash 创建的,Adobe Flash 是当时唯一可用的技术之一,用于在 web 服务器和 web 客户端之间建立持久连接。

在这些早期尝试的同时,一个效率低下的时代以 AJAX(XHR)长轮询的形式诞生了。客户机将不断地调用服务器(类似于心跳检查),并检查客户机感兴趣的内容的状态是否已更改。服务器将返回相同的疲劳响应,直到客户机感兴趣的状态发生变化,并将其报告回客户机。这种方法的主要低效之处在于必须在 web 客户端和 web 服务器之间进行大量的网络调用。不幸的是,AJAX 长轮询的低效实践变得如此流行,以至于今天许多网站仍然广泛使用它。

实时 web 应用功能背后的思想是通过近实时地提供信息来提供更大的用户体验。请记住,由于网络延迟和物理定律对信号的限制,任何通信都不会在实时中进行,而是在近实时中进行。

实现实时 web 应用功能的主要成分是 WebSocket,这是一种允许 web 服务器和 web 客户端之间双向通信的协议。Go 是实现实时 web 应用的理想编程语言,因为它具有网络和 web 编程的内置功能。

在本章中,我们将构建一个实时聊天应用,演示实时 web 应用功能,这将允许网站用户与基本的聊天机器人对话。当用户向 bot 提问时,bot 将实时响应,用户和 bot 之间的所有通信将通过 web 浏览器和 web 服务器之间的 WebSocket 连接执行。

在本章中,我们将介绍以下主题:

  • 实时聊天功能
  • 实现实时聊天的服务器端功能
  • 实现实时聊天的客户端功能
  • 与代理交谈

实时聊天功能

如今,聊天机器人(也称为代理)为网站用户的需求提供各种各样的服务已经司空见惯,例如,从决定购买什么样的鞋,到提供关于客户投资组合中哪些股票看起来不错的提示。我们将构建一个基本的聊天机器人,它将为 IGWEB 用户提供一些关于同构 Go 的友好提示。

一旦激活实时聊天功能,用户可以继续访问网站的不同部分,而不会中断与机器人的对话,前提是用户使用网站上的导航菜单或在客户端路由的网站上找到的链接。在实际场景中,此功能对于产品销售和技术支持使用场景都是一个有吸引力的建议。例如,如果用户对网站上列出的产品有特定问题,用户可以自由浏览网站,而不必担心失去当前与代理的聊天对话。

请记住,我们将构建的代理具有较低的智能级别。此处介绍的代理仅用于说明目的,生产需要时应使用一种更健壮的人工智能AI)解决方案。根据您从本章中获得的知识,用一个更健壮的大脑替换当前代理的大脑应该是相当简单的,它将满足您在实时聊天功能中的特定需求。

设计实时聊天室

下图是描述 IGWEB 顶部条的线框设计。在最右边的图标,将激活即时聊天功能点击时:

图 8.1:描绘 IGWEB 顶条的线框设计

下图是描述实时聊天盒的线框设计。聊天盒由名为“Case”的代理的化身图像及其名称和标题组成。聊天室右上角有一个关闭按钮。用户可以在底部文本区域向代理输入他们的消息,该区域具有占位符文本,在此处键入您的消息。与人和机器人的对话将出现在聊天框的中间区域:

图 8.2:实时聊天盒的线框设计

实现实时聊天盒模板

为了让聊天盒出现在网站的所有部分,我们需要将聊天盒div容器放置在网页布局模板(layouts/webpage_layout.tmpl中的主要内容div容器的正下方:

<!doctype html>
<html>
  {{ template "partials/header_partial" . }}

    <div id="primaryContent" class="pageContent">
      {{ template "pagecontent" . }}
    </div>

 <div id="chatboxContainer" class="containerPulse">
 </div>

  {{ template "partials/footer_partial" . }}
</html>

聊天盒将在shared/templates/partials文件夹的chatbox_partial.tmpl源文件中作为部分模板实现:

<div id="chatbox">
  <div id="chatboxHeaderBar" class="chatboxHeader">
    <div id="chatboxTitle" class="chatboxHeaderTitle"><span>Chat with {{.AgentName}}</span></div>
    <div id="chatboxCloseControl">X</div>
  </div>

  <div class="chatboxAgentInfo">
    <div class="chatboxAgentThumbnail"><img src="{{.AgentThumbImagePath}}" height="81px"></div>
    <div class="chatboxAgentName">{{.AgentName}}</div>
    <div class="chatboxAgentTitle">{{.AgentTitle}}</div>
  </div>

  <div id="chatboxConversationContainer">

  </div>

  <div id="chatboxMsgInputContainer">
 <input type="text" id="chatboxInputField" placeholder="Type your message here...">

 </input>
  </div>

  <div class="chatboxFooter">
    <a href="http://www.isomorphicgo.org" target="_blank">Powered by Isomorphic Go</a>
  </div>
</div>

这是实现在线聊天框的图 8.2中描述的线框设计所需的 HTML 标记。请注意,input文本字段的 id 为"chatboxInputField"。这是input字段,用户可以在该字段中键入他们的消息。创建的每条消息,无论是用户编写的消息还是机器人编写的消息,都将使用livechatmsg_partial.tmpl模板:

<div class="chatboxMessage">
 <div class="chatSenderName">{{.Name}}</div>
 <div class="chatSenderMsg">{{.Message}}</div>
</div>

每个消息都位于自己的div容器中,该容器有两个div容器(以粗体显示),其中包含消息发送者的名称和消息本身。

live chat 功能中没有必要的按钮,因为我们将添加一个事件侦听器,以侦听是否按下 Enter 键,从而通过 WebSocket 连接将用户的消息提交到服务器。

现在,我们已经实现了用于呈现聊天框的 HTML 标记,让我们检查一下在服务器端实现实时聊天功能所需的功能。

实现实时聊天的服务器端功能

激活实时聊天功能后,我们将在 web 客户端和 web 服务器之间创建一个持久的 WebSocket 连接。Gorilla web Toolkit 在其websocket包中提供了 WebSocket 协议的出色实现,可在中找到 http://github.com/gorilla/websocket 。要获取websocket包,您可以发出以下命令:

$ go get github.com/gorilla/websocket

Gorilla web toolkit 还提供了一个有用的示例 web 聊天应用:

https://github.com/gorilla/websocket/tree/master/examples/chat

我们将重新利用 Gorilla 的示例 web 聊天应用来实现实时聊天功能,而不是重新发明轮子。网络聊天示例所需的源文件已复制到chat文件夹中。

为了使用 Gorilla 提供的示例聊天应用实现实时聊天功能,我们需要进行三项主要更改:

  • 来自聊天机器人(代理)的回复应该针对特定用户,而不是发送给每个连接的用户
  • 我们需要创建允许聊天机器人将消息发送回用户的功能
  • 我们需要在 Go 中实现聊天应用的前端部分

让我们更详细地考虑这三点中的每一点。

首先,Gorilla 的网络聊天示例是一个免费的聊天室。任何用户都可以进来,键入一条消息,连接到聊天服务器的所有其他用户都可以看到该消息。实时聊天功能的一个主要要求是聊天机器人和人之间的每次对话都应该是独占的。来自代理的答复必须针对特定用户,而不是所有连接的用户。

其次,Gorilla web toolkit 中的示例 web 聊天应用不会向用户发送任何消息。这就是自定义聊天机器人出现的地方。代理将通过已建立的 WebSocket 连接直接与用户通信。

第三,示例 web 聊天应用的前端部分实现为包含内联 CSS 和 JavaScript 的 HTML 文档。正如您可能已经猜到的,我们将在 Go 中实现实时聊天功能的前端部分,代码将驻留在client/chat文件夹中。

现在我们已经建立了我们的行动计划,以大猩猩网络聊天示例为基础来实现实时聊天功能。

我们将创建的修改后的 web 聊天应用包含两种主要类型:HubClient

轮毂类型

聊天中心负责维护客户端连接列表,并指示聊天机器人向相关客户端广播消息。例如,如果 Alice 问了一个问题“什么是同构 Go?,聊天机器人的答案应该是 Alice 而不是 Bob(Bob 可能还没有问过问题)。

*以下是[T0]结构的外观:

type Hub struct {
  chatbot bot.Bot
  clients map[*Client]bool
  broadcastmsg chan *ClientMessage
  register chan *Client
  unregister chan *Client
}

chatbot是实现Bot接口的聊天机器人(代理)。这是一个大脑,负责回答客户提出的问题。

clients映射用于注册客户端。map中存储的键值对由键、指向Client实例的指针组成,该值由设置为true的布尔值组成,以指示客户端已连接。客户通过broadcastmsgregisterunregister频道与集线器通信。register通道向集线器注册客户端。unregister通道,在集线器中注销客户端。客户端通过broadcastmsg通道发送用户输入的消息,该通道类型为ClientMessage。下面是我们介绍的[T11]结构:

type ClientMessage struct {
  client *Client
  message []byte
}

为了实现我们之前提出的第一个主要更改,即代理和用户之间对话的排他性,我们使用ClientMessage结构来存储指向Client实例的指针,该Client实例发送用户消息以及用户消息本身(一个byte片段)。

构造函数NewHub接受实现Bot接口的chatbot并返回一个新的Hub实例:

func NewHub(chatbot bot.Bot) *Hub {
  return &Hub{
    chatbot: chatbot,
    broadcastmsg: make(chan *ClientMessage),
    register: make(chan *Client),
    unregister: make(chan *Client),
    clients: make(map[*Client]bool),
  }
}

我们实现了一个导出的 getter 方法ChatBot,这样就可以从Hub对象访问chatbot

func (h *Hub) ChatBot() bot.Bot {
  return h.chatbot
}

当我们实现 RESTAPI 端点以将机器人的详细信息(名称、标题和化身图像)发送到客户端时,此操作将非常重要。

SendMessage方法负责向特定客户端广播消息:

func (h *Hub) SendMessage(client *Client, message []byte) {
  client.send <- message
}

该方法接收指向Client的指针和message(一个byte切片),该切片应发送给该特定客户机。消息将通过客户端的send通道发送。

调用Run方法启动聊天中心:

func (h *Hub) Run() {
  for {
    select {
    case client := <-h.register:
      h.clients[client] = true
      greeting := h.chatbot.Greeting()
      h.SendMessage(client, []byte(greeting))

    case client := <-h.unregister:
      if _, ok := h.clients[client]; ok {
        delete(h.clients, client)
        close(client.send)
      }
    case clientmsg := <-h.broadcastmsg:
      client := clientmsg.client
      reply := h.chatbot.Reply(string(clientmsg.message))
      h.SendMessage(client, []byte(reply))
    }
  }
}

我们在for循环中使用select语句来等待多个客户端操作。

如果指向Client的指针通过集线器的register通道进入,集线器将通过向客户端map添加client指针(作为密钥)来注册新客户端,并为其设置true值。我们将通过调用chatbot上的Greeting方法获取greeting消息返回给客户端。一旦我们得到问候语(一个字符串值),我们调用传入clientSendMessage方法,并将greeting转换为byte切片。

如果指向Client的指针通过集线器的unregister通道进入,集线器将删除给定clientmap中的条目,并关闭客户端的send通道,这意味着client将不再向服务器发送任何消息。

如果指向ClientMessage的指针通过集线器的broadcastmsg通道进入,集线器将把客户端的message(作为字符串值)传递给chatbot对象的Reply方法。一旦我们从代理获得reply(字符串值),我们调用SendMessage方法传入client并将reply转换为byte切片。

客户端类型

Client类型充当Hubwebsocket连接之间的中介。

以下是[T0]结构的外观:

type Client struct {
  hub *Hub
  conn *websocket.Conn
  send chan []byte
}

每个Client值包含一个指向Hub的指针、一个指向websocket连接的指针和一个缓冲通道send,用于出站消息。

readPump方法负责将通过websocket连接传入的入站消息中继到集线器:

func (c *Client) readPump() {
  defer func() {
    c.hub.unregister <- c
    c.conn.Close()
  }()
  c.conn.SetReadLimit(maxMessageSize)
  c.conn.SetReadDeadline(time.Now().Add(pongWait))
  c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
  for {
    _, message, err := c.conn.ReadMessage()
    if err != nil {
      if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
        log.Printf("error: %v", err)
      }
      break
    }
    message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
    // c.hub.broadcast <- message

    clientmsg := &ClientMessage{client: c, message: message}
 c.hub.broadcastmsg <- clientmsg

  }
}

为了满足实时聊天功能的要求,我们不得不对该功能做一些细微的更改。在 Gorilla 网络聊天示例中,仅消息被转发到Hub。由于我们正在将聊天机器人响应定向回发送它们的客户端,因此不仅需要将消息发送到集线器,还需要发送消息的客户端(以粗体显示)。我们通过创建一个ClientMessage结构来实现:

type ClientMessage struct {
  client *Client
  message []byte
}

ClientMessage结构包含用于保存指向客户端的指针以及message``byte切片的字段。

回到client.go源文件中的readPump函数,以下两行有助于Hub知道哪个客户端发送了消息:

    clientmsg := &ClientMessage{client: c, message: message}
    c.hub.broadcastmsg <- clientmsg

writePump方法负责通过websocket连接中继来自客户端send通道的出站消息:

func (c *Client) writePump() {
  ticker := time.NewTicker(pingPeriod)
  defer func() {
    ticker.Stop()
    c.conn.Close()
  }()
  for {
    select {
    case message, ok := <-c.send:
      c.conn.SetWriteDeadline(time.Now().Add(writeWait))
      if !ok {
        // The hub closed the channel.
        c.conn.WriteMessage(websocket.CloseMessage, []byte{})
        return
      }

      w, err := c.conn.NextWriter(websocket.TextMessage)
      if err != nil {
        return
      }
      w.Write(message)

      // Add queued chat messages to the current websocket message.
      n := len(c.send)
      for i := 0; i < n; i++ {
        w.Write(newline)
        w.Write(<-c.send)
      }

      if err := w.Close(); err != nil {
        return
      }
    case <-ticker.C:
      c.conn.SetWriteDeadline(time.Now().Add(writeWait))
      if err := c.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
        return
      }
    }
  }
}

ServeWS方法将由 web 应用注册为 HTTP 处理程序:

func ServeWs(hub *Hub) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
      log.Println(err)
      return
    }
    client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
    client.hub.register <- client
    go client.writePump()
    client.readPump()
  })
}

此方法执行两项重要任务。该方法将普通 HTTP 连接升级为websocket连接,并将客户端注册到集线器。

现在我们已经为我们的 web 聊天服务器设置了代码,是时候在我们的 web 应用中激活它了。

激活聊天服务器

igweb.go源文件中,我们包含了一个名为startChatHub的函数,负责启动Hub

func startChatHub(hub *chat.Hub) {
  go hub.Run()
}

我们在main函数中添加以下代码来创建一个新的聊天机器人,将其与Hub关联,并启动Hub

  chatbot := bot.NewAgentCase()
  hub := chat.NewHub(chatbot)
  startChatHub(hub)

当我们调用registerRoutes函数来注册服务器端 web 应用的所有路由时,请注意,我们还将hub值传递给该函数:

  r := mux.NewRouter()
  registerRoutes(&env, r, hub)

registerRoutes函数中,我们需要hub为返回代理信息的 Rest API 端点注册路由处理程序:

r.Handle("/restapi/get-agent-info", endpoints.GetAgentInfoEndpoint(env, hub.ChatBot()))

我们将在一节中讨论这个端点,将代理的信息公开给客户

hub还用于注册 WebSocket 路由/ws的路由处理程序。我们注册ServeWS处理函数,传入hub

  r.Handle("/ws", chat.ServeWs(hub))

现在,我们已经准备好了激活聊天服务器的一切,现在是时候关注实时聊天功能的明星聊天代理了。

特工的大脑

我们将用于实时聊天功能的聊天机器人类型AgentCase将实现以下Bot interface

type Bot interface {
  Greeting() string
  Reply(string) string
  Name() string
  Title() string
  ThumbnailPath() string
  SetName(string)
  SetTitle(string)
  SetThumbnailPath(string)
}

Greeting方法将用于向用户发送初始问候语,诱使他们与聊天机器人互动。

Reply方法接受一个问题(一个字符串)并返回给定问题的答案(也是一个字符串)。

实施的其他方法纯粹是出于心理原因,让人们产生一种错觉,以为他们在与某人交流,而不是与某物交流。

Name方法是返回聊天机器人名称的 getter 方法。

Title方法是返回聊天机器人标题的 getter 方法。

ThumbnailPath方法是一种 getter 方法,它返回聊天机器人化身图像的路径。

每个 getter 方法都有相应的 setter 方法:SetNameSetTitleSetThumbnailPath

通过定义Bot接口,我们清楚地说明了聊天机器人的期望。这使我们能够在将来扩展聊天机器人解决方案。例如,Case所展示的智力可能过于初级和有限。在不久的将来,我们可能想要实现一个名为 Molly 的机器人,它的智能可能会使用一个更强大的算法来实现。只要 Molly 聊天机器人实现了Bot接口,新的聊天机器人就可以很容易地插入我们的 web 应用。

事实上,从服务器端 web 应用的角度来看,这只是一行代码的更改。我们将实例化一个AgentMolly实例,而不是实例化一个AgentCase实例。除了智能上的差异,新的聊天机器人 Molly 将有自己的名字、头衔和头像,因此人类可以将其与Case区分开来。

这是AgentCase结构:

type AgentCase struct {
 Bot
 name string
 title string
 thumbnailPath string
 knowledgeBase map[string]string
 knowledgeCorpus []string
 sampleQuestions []string
}

我们已经将Bot接口嵌入到struct定义中,表示AgentCase类型将实现Bot接口。字段name表示代理的名称。title字段为代理人的头衔。字段thumbnailPath用于指定聊天机器人化身图像的路径。

knowledgeBase字段为map[string]string类型的map。这基本上是特工的大脑。map中的键是在特定问题中找到的常用术语。map中的值就是问题的答案。

knowledgeCorpus字段是一个字符串byte片段,是机器人将被询问的问题中可能存在的术语的知识库。我们使用knowledgeBase地图的键来构建knowledgeCorpus。语料库是用于进行语言分析的文本集合。在我们的例子中,我们将根据人类用户向 bot 提供的问题(查询)进行语言分析。

sampleQuestions字段是一个字符串byte片段,它将包含一个示例问题列表,用户可以向聊天机器人提问。聊天机器人在与用户打招呼时会向用户提供一个示例问题,以诱使人类用户进行对话。可以理解的是,人类用户可以根据自己的偏好自由地解释示例问题或提出完全不同的问题。

initializeIntelligence方法用于初始化病例大脑:

func (a *AgentCase) initializeIntelligence() {

  a.knowledgeBase = map[string]string{
    "isomorphic go isomorphic go web applications": "Isomorphic Go is the methodology to create isomorphic web applications using the Go (Golang) programming language. An isomorphic web application, is a web application, that contains code which can run, on both the web client and the web server.",
    "kick recompile code restart web server instance instant kickstart lightweight mechanism": "Kick is a lightweight mechanism to provide an instant kickstart to a Go web server instance, upon the modification of a Go source file within a particular project directory (including any subdirectories). An instant kickstart consists of a recompilation of the Go code and a restart of the web server instance. Kick comes with the ability to take both the go and gopherjs commands into consideration when performing the instant kickstart. This makes it a really handy tool for isomorphic golang projects.",
    "starter code starter kit": "The isogoapp, is a basic, barebones web app, intended to be used as a starting point for developing an Isomorphic Go application. Here's the link to the github page: https://github.com/isomorphicgo/isogoapp",
    "lack intelligence idiot stupid dumb dummy don't know anything": "Please don't question my intelligence, it's artificial after all!",
    "find talk topic presentation lecture subject": "Watch the Isomorphic Go talk by Kamesh Balasubramanian at GopherCon India: https://youtu.be/zrsuxZEoTcs",
    "benefits of the technology significance of the technology importance of the technology": "Here are some benefits of Isomorphic Go: Unlike JavaScript, Go provides type safety, allowing us to find and eliminate many bugs at compile time itself. Eliminates mental context-shifts between back-end and front-end coding. Page loading prompts are not necessary.",
    "perform routing web app register routes define routes": "You can implement client-side routing in your web application using the isokit Router preventing the dreaded full page reload.",
    "render templates perform template rendering": "Use template sets, a set of project templates that are persisted in memory and are available on both the server-side and the client-side",
    "cogs reusable components react-like react": "Cogs are reuseable components in an Isomorphic Go web application.",
  }

  a.knowledgeCorpus = make([]string, 1)
  for k, _ := range a.knowledgeBase {
    a.knowledgeCorpus = append(a.knowledgeCorpus, k)
  }

  a.sampleQuestions = []string{"What is isomorphic go?", "What are the benefits of this technology?", "Does isomorphic go offer anything react-like?", "How can I recompile code instantly?", "How can I perform routing in my web app?", "Where can I get starter code?", "Where can I find a talk on this topic?"}

}

此方法中有三项重要任务:

  • 首先,我们建立了案例的知识库。
  • 其次,建立案例知识库。
  • 第三,我们设置了示例问题,在问候人类用户时将使用哪种情况。

我们必须注意的第一项任务是建立案例的知识库。这包括设置AgentCase实例的knowledgeBase属性。如前所述,map中的键表示问题中的术语,map中的值就是问题的答案。例如,"isomorphic go isomorphic go web applications"键可以处理以下问题:

  • 什么是同构 Go?
  • 关于同构围棋你能告诉我什么?

它还可以为非问题的陈述提供服务:

  • 告诉我关于同构围棋的事
  • 给我同构围棋的详细资料

由于knowledgeBase地图的地图文字声明中包含大量文本,我建议您在计算机上查看源文件agentcase.go

我们必须注意的第二个任务是设置 Case 的语料库,即用于针对用户问题进行语言分析的文本集合。语料库是根据knowledgeBase地图的键构建的。我们使用内置的make函数将AgentCase实例的knowledgeCorpus字段属性设置为新创建的字符串byte切片。使用for循环,我们迭代knowledgeBase map中的所有条目,并将每个键附加到knowledgeCorpus字段切片。

我们必须注意的第三个也是最后一个任务是设置Case将呈现给人类用户的示例问题。我们只需填充AgentCase实例的sampleQuestions属性。我们使用字符串文字声明填充字符串byte片段中包含的所有示例问题。

以下是AgentCase类型的 getter 和 setter 方法:

func (a *AgentCase) Name() string {
  return a.name
}

func (a *AgentCase) Title() string {
  return a.title
}

func (a *AgentCase) ThumbnailPath() string {
  return a.thumbnailPath
}

func (a *AgentCase) SetName(name string) {
  a.name = name
}

func (a *AgentCase) SetTitle(title string) {
  a.title = title
}

func (a *AgentCase) SetThumbnailPath(thumbnailPath string) {
  a.thumbnailPath = thumbnailPath
}

这些方法用于获取和设置AgentCase对象的名称、标题和thumbnailPath字段。

下面是用于创建新AgentCase实例的构造函数:

func NewAgentCase() *AgentCase {
  agentCase := &AgentCase{name: "Case", title: "Resident Isomorphic Gopher Agent", thumbnailPath: "/statimg/chat/Case.png"}
  agentCase.initializeIntelligence()
  return agentCase
}

我们用一个新的AgentCase实例声明并初始化agentCase变量,设置nametitlethumbnailPath字段。然后我们调用initializeIntelligence方法来初始化 Case 的大脑。最后,我们返回新创建并初始化的AgentCase实例。

问候人类

Greeting方法用于在激活实时聊天功能时向用户提供第一次问候:

func (a *AgentCase) Greeting() string {

  sampleQuestionIndex := randomNumber(0, len(a.sampleQuestions))
  greeting := "Hi there! I'm Case. You can ask me a question on Isomorphic Go. Such as...\"" + a.sampleQuestions[sampleQuestionIndex] + "\""
  return greeting

}

由于问候语将包含一个随机选择的样本问题,可以询问该问题的具体情况,因此会调用randomNumber函数来获取样本问题的索引号。我们将最小值和最大值传递给randomNumber函数,以指定生成的随机数应在的范围。

以下是用于在给定范围内生成随机数的randomNumber函数:

func randomNumber(min, max int) int {
  rand.Seed(time.Now().UTC().UnixNano())
  return min + rand.Intn(max-min)
}

回到Greeting方法,我们使用随机索引从sampleQuestions字符串切片中获取样本问题。然后,我们将样本问题分配给greeting变量并返回greeting

回答人类的问题

现在,我们已经初始化了聊天机器人的智能,并准备好迎接人类用户,现在是时候指导聊天机器人如何思考用户的问题,以便聊天机器人可以提供合理的回答。

聊天机器人发送给人类用户的回复仅限于在AgentCase结构的knowledgeBase映射中找到的值。如果人类用户提出的问题超出了聊天机器人知道的范围(知识库),它将简单地回复消息"I don't know the answer to that one."

为了分析用户的问题并提供最佳答案,我们将使用nlp软件包,其中包含一组可用于基本自然语言处理的机器学习算法。

您可以通过发出以下go get命令来安装nlp包:

$ go get github.com/james-bowman/nlp

让我们从方法声明开始,逐段回顾一下Reply方法:

func (a *AgentCase) Reply(query string) string {

函数接受一个问题字符串并返回给定问题的答案字符串。

我们声明表示用户问题答案的result变量:

  var result string

result变量将通过Reply方法返回。

使用nlp包,我们创建了一个新的vectoriser和一个新的transformer

  vectoriser := nlp.NewCountVectoriser(true)
  transformer := nlp.NewTfidfTransformer()

**vectoriser**将知识库中的查询词编码成术语文档矩阵,每列表示语料库中的一个文档,每行表示一个术语。它用于跟踪在特定文档中找到的术语的频率。对于我们的使用场景,您可以将文档视为在 Type T1 的字符串切片中找到的唯一条目。

transformer将用于消除knowledgeCorpus中频繁出现的术语的偏差。例如,在knowledgeCorpus中重复出现的单词,例如之间重复出现的单词,以及网页上重复出现的单词,其权重会更小。变压器为**TFIDF(术语频率逆文件频率)**变压器。

然后我们继续创建reducer,这是一个新的TruncatedSVD实例:

  reducer := nlp.NewTruncatedSVD(4)

我们刚才声明的reducer意义重大,因为我们将执行潜在语义分析LSA),也称为潜在语义索引LSI),以搜索和检索用户查询词的适当文档。LSA 帮助我们根据术语的共现情况找到语料库中存在的语义属性。它假设经常出现在一起的单词一定有某种语义关系。

reducer用于查找可能隐藏在文档特征向量中术语频率下的语义。

以下代码是将语料库转换为潜在语义索引的管道,该索引将模型与文档相匹配:

  matrix, _ := vectoriser.FitTransform(a.knowledgeCorpus...)
  matrix, _ = transformer.FitTransform(matrix)
  lsi, _ := reducer.FitTransform(matrix)

我们必须通过相同的管道运行用户查询,以便将其投影到相同的维度空间:

  matrix, _ = vectoriser.Transform(query)
  matrix, _ = transformer.Transform(matrix)
  queryVector, _ := reducer.Transform(matrix)

现在我们已经有了lsiqueryVector,是时候找到与查询条件最匹配的文档了。我们通过根据查询计算语料库中每个文档的余弦相似度来实现这一点:

  highestSimilarity := -1.0
  var matched int
  _, docs := lsi.Dims()
  for i := 0; i < docs; i++ {
    similarity := nlp.CosineSimilarity(queryVector.(mat.ColViewer).ColView(0), lsi.(mat.ColViewer).ColView(i))
    if similarity > highestSimilarity {
      matched = i
      highestSimilarity = similarity
    }
  }

余弦相似度计算两个数值向量的角度差。

语料库中与用户查询具有最高相似度的文档将被匹配为反映用户问题的最佳文档。余弦相似性的可能值可能介于 0 到 1 之间。0 表示完全正交,1 表示完全匹配。余弦相似性值也可以是**NaN(不是数字)**值。NaN 值表示根本没有匹配项。

如果未找到匹配项,highestSimilarity值将为-1;否则,它将是介于 0 和 1 之间的值:

  if highestSimilarity == -1 {
    result = "I don't know the answer to that one."
  } else {
    result = a.knowledgeBase[a.knowledgeCorpus[matched]]
  }

  return result

if条件块中,检查highestSimilarity值是否为-1;如果是,则给用户的答案是"I don't know the answer to that one."

如果我们到达else块,则表明highestSimilarity是介于 0 和 1 之间的值,表明找到了匹配项。回想一下,我们的knowledgeCorpus中的文档在knowledgeBase``map中有一个对应的键。用户问题的答案是带有提供键的knowledgeBase``map中的值,我们将result字符串设置为该值。在方法的最后一行代码中,我们返回[T8]变量。

实现聊天机器人智能的逻辑来自 James Bowman 的文章,在 Go中使用机器学习对网页进行语义分析 http://www.jamesbowman.me/post/semantic-analysis-of-webpages-with-machine-learning-in-go/ )。

向客户端公开代理的信息

现在,我们已经实现了聊天代理AgentCase,我们需要一种向客户端公开案例信息的方法,特别是它的名称、标题和到它的化身图像的路径。

我们创建了一个新的 Rest API 端点GetAgentInfoEndpoint,以向客户端 web 应用公开聊天代理的信息:

func GetAgentInfoEndpoint(env *common.Env, chatbot bot.Bot) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

    m := make(map[string]string)
    m["AgentName"] = chatbot.Name()
    m["AgentTitle"] = chatbot.Title()
    m["AgentThumbImagePath"] = chatbot.ThumbnailPath()
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(m)
  })

注意,在GetAgentInfoEndpoint函数的签名中,我们接受env对象和chatbot对象。注意,chatbot属于bot.Bot类型,是一种接口类型,而不是AgentCase类型。这为我们提供了灵活性,可以方便地在另一个 bot 中进行交换,例如将来使用AgentMolly而不是AgentCase

我们只需创建一个类型为map[string]string的地图m,其中包含机器人的名称、标题和化身图像路径。我们设置了一个头来指示服务器响应将采用 JSON 格式。最后,我们使用http.ResponseWriterw写出 JSON 编码的map

实现实时聊天的客户端功能

现在,我们已经介绍了实现聊天机器人所需的服务器端功能,现在是时候从客户端 web 应用的角度关注实时聊天功能了。

InitialPageLayoutControls函数中,我们将click事件上的event侦听器添加到顶部栏上的实时聊天图标中:

  liveChatIcon := env.Document.GetElementByID("liveChatIcon").(*dom.HTMLImageElement)
  liveChatIcon.AddEventListener("click", false, func(event dom.Event) {

    chatbox := env.Document.GetElementByID("chatbox")
    if chatbox != nil {
      return
    }
    go chat.StartLiveChat(env)
  })

如果 live chat 功能已经激活,chatboxdiv 元素将已经存在,也就是说,它将是一个非 nil 值。在这个场景中,我们从函数返回。

但是,如果 live chat 功能尚未激活,我们将位于chat包中的StartLiveChat函数称为 goroutine,传入env对象。调用此功能将激活实时聊天功能。

创建实时聊天客户端

我们将使用gopherjs/websocket/websocketjs包创建一个 WebSocket 连接,用于连接到 web 服务器实例。

您可以使用以下go get命令安装此软件包:

$ go get -u github.com/gopherjs/websocket

实时聊天功能的客户端实现可以在client/chat/chat.go源文件中找到。我们定义了websocketjs.WebSocket类型的ws变量和map[string]string类型的agentInfo变量:

var ws *websocketjs.WebSocket
var agentInfo map[string]string

我们还声明了一个常量,该常量表示 Enter 键的键代码:

const ENTERKEY int = 13

GetAgentInfoRequest功能用于从/restapi/get-agent-info端点获取代理信息:

func GetAgentInfoRequest(agentInfoChannel chan map[string]string) {
  data, err := xhr.Send("GET", "/restapi/get-agent-info", nil)
  if err != nil {
    println("Encountered error: ", err)
  }
  var agentInfo map[string]string
  json.NewDecoder(strings.NewReader(string(data))).Decode(&agentInfo)
  agentInfoChannel <- agentInfo
}

一旦我们从服务器检索到 JSON 编码的数据,我们将其解码为类型为map[string]stringmap。然后我们通过频道agentInfoChannel发送agentInfo map

getServerPort函数是获取服务器运行端口的辅助函数:

func getServerPort(env *common.Env) string {

  if env.Location.Port != "" {
    return env.Location.Port
  }

  if env.Location.Protocol == "https" {
    return "443"
  } else {
    return "80"
  }

}

此函数用于在StartLiveChat函数中构造serverEndpoint字符串变量,该变量表示我们将要进行 WebSocket 连接的服务器端点。

当用户点击顶部栏中的实时聊天图标时,StartLiveChat功能将被称为 goroutine:

func StartLiveChat(env *common.Env) {

  agentInfoChannel := make(chan map[string]string)
  go GetAgentInfoRequest(agentInfoChannel)
  agentInfo = <-agentInfoChannel

我们首先通过调用[T0]函数作为 goroutine 获取代理的信息。代理的信息将作为类型为map[string]string的地图通过agentInfoChannel频道发送。agentInfo``map将被用作数据对象,传递给partials/chatbox_partial模板以显示代理的详细信息(名称、标题和化身图像)。

然后,我们继续创建新的 WebSocket 连接并连接到服务器端点:

  var err error
  serverEndpoint := "ws://" + env.Location.Hostname + ":" + getServerPort(env) + "/ws"
  ws, err = websocketjs.New(serverEndpoint)
  if err != nil {
    println("Encountered error when attempting to connect to the websocket: ", err)
  }

我们使用 helpergetServerPort函数获取服务器运行的端口。在构造serverEndpoint字符串变量时使用服务器端口值,该变量表示我们将要连接到的服务器端点的 WebSocket 地址。

我们使用env.Document对象的GetElementByID方法通过提供"chatboxContainer"的 ID 来获取聊天容器div元素。我们还添加了 CSS 动画样式,以使聊天盒容器在聊天机器人可用于回答问题时产生戏剧性的效果:

  chatContainer := env.Document.GetElementByID("chatboxContainer").(*dom.HTMLDivElement)
  chatContainer.SetClass("containerPulse")

  env.TemplateSet.Render("partials/chatbox_partial", &isokit.RenderParams{Data: agentInfo, Disposition: isokit.PlacementReplaceInnerContents, Element: chatContainer})

我们调用模板集对象的Render方法,渲染"partials/chatbox_partial"模板并提供模板渲染参数。我们指定要提供给模板的数据对象是agentInfo映射。我们指定呈现的配置应该是用呈现的模板输出替换关联元素的内部 HTML 内容。最后,我们指定要渲染到的关联元素是chatContainer元素。

当实时聊天功能正常且连接到服务器的 WebSocket 连接时,聊天盒标题栏(包含聊天盒标题的条带)chatboxHeaderBar将变为绿色。如果 WebSocket 连接已断开或存在错误,则条带将显示为红色。默认情况下,当我们将chatboxHeaderBar的默认 CSS 类设置为"chatboxHeader"时,条带将显示为绿色:

  chatboxHeaderBar := env.Document.GetElementByID("chatboxHeaderBar").(*dom.HTMLDivElement)
  chatboxHeaderBar.SetClass("chatboxHeader")

初始化事件侦听器

最后,我们调用InitializeChatEventHandlers函数,传入env对象,初始化实时聊天功能的事件处理程序:

  InitializeChatEventHandlers(env)

InitializeChatEventHandlers功能负责设置实时聊天功能所需的所有事件侦听器。有两个控件需要用户交互。第一个是 messageinput字段,用户在该字段中键入并通过按 Enter 键发送问题。第二个是关闭按钮,X,位于聊天框的右上角,用于关闭实时聊天功能。

为了处理用户与消息input字段的交互,我们设置了keypress事件监听器,它将检测消息input文本字段元素中的keypress事件:

func InitializeChatEventHandlers(env *common.Env) {

  msgInput := env.Document.GetElementByID("chatboxInputField").(*dom.HTMLInputElement)
  msgInput.AddEventListener("keypress", false, func(event dom.Event) {
    if event.Underlying().Get("keyCode").Int() == ENTERKEY {
      event.PreventDefault()
      go ChatSendMessage(env, msgInput.Value)
      msgInput.Value = ""
    }

  })

我们通过调用env.Document对象上的GetElementByID方法来获取inputmessage textfield 元素。然后,我们将keypress事件侦听器函数附加到元素。如果用户按下的键是 Enter 键,那么我们将阻止keypress事件的默认行为,并调用ChatSendMessage函数,作为 goroutine,传入env对象和msgInput元素的Value属性。最后,我们通过将消息输入字段的Value属性设置为空字符串值来清除该字段中的文本。

关闭聊天控件

为了在单击 X 控件以关闭 live chat 功能时处理用户交互,我们设置了一个事件侦听器来处理 close 控件的单击事件:

  closeControl := env.Document.GetElementByID("chatboxCloseControl").(*dom.HTMLDivElement)
  closeControl.AddEventListener("click", false, func(event dom.Event) {
    CloseChat(env)
  })

我们通过调用env.Document对象上的GetElementByID方法,指定 ID"chatboxCloseControl",得到表示 close 控件的div元素。我们在click事件的 close 控件上附加了一个事件侦听器,它将调用CloseChat函数。

为 WebSocket 对象设置事件侦听器

现在我们已经为用户交互设置了事件监听器,我们必须在 WebSocket 对象[T0]上设置事件监听器。我们首先在message事件上添加一个事件侦听器:

  ws.AddEventListener("message", false, func(ev *js.Object) {
    go HandleOnMessage(env, ev)
  })

当新消息通过 WebSocket 连接时,message事件侦听器将被触发。这表示代理将消息发送回用户。在这种情况下,我们调用HandleOnMessage函数,将env对象和事件对象ev传递给函数。

我们必须从 WebSocket 对象监听的另一个事件是close事件。此事件可以从正常操作场景触发,例如用户使用 close 控件关闭 live chat 功能。该事件也可以从异常操作场景中触发,例如 web 服务器实例突然停机,导致 WebSocket 连接中断。我们的代码必须足够智能,只能在异常连接关闭情况下触发:

  ws.AddEventListener("close", false, func(ev *js.Object) {6

    chatboxContainer := env.Document.GetElementByID("chatboxContainer").(*dom.HTMLDivElement)
    if len(chatboxContainer.ChildNodes()) > 0 {
      go HandleDisconnection(env)
    }
  })

我们首先获取 chatbox 容器div元素来实现这一点。如果 chatbox 容器中的子节点数大于零,则表示用户在使用 live chat 功能时连接异常关闭,我们必须调用HandleDisconnection函数,作为 goroutine,将env对象传递给该函数。

在某些情况下,关闭事件可能不会触发,例如当我们失去互联网连接时。即使 internet 连接已断开,WebSocket 连接正在通过的 TCP 连接仍可能被视为活动的。为了使我们的实时聊天功能能够灵活地处理这种情况,我们需要监听env.Window对象的offline事件,该事件将在网络连接丢失时触发:

  env.Window.AddEventListener("offline", false, func(event dom.Event) {
    go HandleDisconnection(env)
  })

}

我们执行与之前处理此事件相同的操作。我们将HandleDisconnection函数称为 goroutine,将env对象传递给该函数。请注意,最后一个右括号}表示InitializeChatEventHandlers功能结束。

现在我们已经为 live chat 功能设置了所有必要的事件侦听器,现在是时候检查我们刚刚设置的事件侦听器调用的每个函数了。

在用户点击消息input文本字段内的 Enter 键后调用ChatSendMessage函数:

func ChatSendMessage(env *common.Env, message string) {
  ws.Send([]byte(message))
  UpdateChatBox(env, message, "Me")
}

我们调用 WebSocket 对象的Send方法ws,将用户的问题发送到 web 服务器。然后我们调用UpdateChatBox函数将用户的消息呈现到聊天盒的对话容器中。我们将用户编写的env对象messagesender字符串作为输入值传递给UpdateChatBox函数。sender字符串是发送消息的人;在这种情况下,由于用户发送了它,sender字符串将是"Me"sender字符串帮助用户区分用户发送的消息和聊天机器人回复的消息。

UpdateChatBox功能用于更新聊天盒对话容器区域:

func UpdateChatBox(env *common.Env, message string, sender string) {

  m := make(map[string]string)
  m["Name"] = sender
  m["Message"] = message
  conversationContainer := env.Document.GetElementByID("chatboxConversationContainer").(*dom.HTMLDivElement)
  env.TemplateSet.Render("partials/livechatmsg_partial", &isokit.RenderParams{Data: m, Disposition: isokit.PlacementAppendTo, Element: conversationContainer})
  scrollHeight := conversationContainer.Underlying().Get("scrollHeight")
  conversationContainer.Underlying().Set("scrollTop", scrollHeight)
}

我们创建了一个新的map[string]string类型的映射,该映射将用作数据对象,该数据对象将被馈送到partials/livechatmsg_partial模板。地图由一个带有"Name"键的条目表示sender,一个带有"Message"键的条目表示message"Name""Message"的值都将显示在聊天盒的对话容器区域中。

我们通过调用env.Document对象的GetElementByID方法并指定"chatboxConversationContainer"id值来获取conversationContainer的元素。

我们调用env.TemplateSet对象的Render方法,并指定要呈现partials/livechatmsg_partial模板。在渲染参数(RenderParams对象中,我们将Data字段设置为mapm。我们将Disposition字段设置为isokit.PlacementAppendTo,以指定处置操作将是相对于关联元素的操作的追加。我们将Element字段设置为conversationContainer,因为这是添加聊天信息的元素。

在呈现新消息时,功能的最后两行将自动滚动conversationContainer到底部,以便始终向用户显示最新消息。

除了ChatSendMessage功能外,UpdateChatBox功能的另一个实用程序是HandleOnMessage功能:

func HandleOnMessage(env *common.Env, ev *js.Object) {

  response := ev.Get("data").String()
  UpdateChatBox(env, response, agentInfo["AgentName"])
}

回想一下,当 WebSocket 连接触发"message"事件时,将调用此函数。我们通过获取event对象的data属性的字符串值,从通过 WebSocket 连接进行通信的聊天机器人获取响应。然后我们调用传入env对象的UpdateChatBox函数、response字符串和sender字符串agentInfo["AgentName"]。请注意,我们已经传递了代理的名称,agentInfo``map中的值是使用"AgentName"键获得的,作为sender字符串。

CloseChat功能用于关闭 web 套接字连接,并从用户界面中关闭聊天框:

func CloseChat(env *common.Env) {
  ws.Close()
  chatboxContainer := env.Document.GetElementByID("chatboxContainer").(*dom.HTMLDivElement)
  chatboxContainer.RemoveChild(chatboxContainer.ChildNodes()[0])

}

我们首先对 WebSocket 对象调用Close方法。我们获取chatboxContainer元素并移除其第一个子节点,随后将移除第一个子节点的所有子节点。

请记住,当用户点击聊天框中的 X 控件时,或在实时聊天功能打开时遇到异常 WebSocket 连接终止的情况下,将调用此函数。

处理断开连接事件

这就引出了最后一个函数HandleDisconnection,该函数在异常 WebSocket 连接关闭事件或 internet 连接已断开时调用,即wenv.Window对象触发offline事件时调用:

func HandleDisconnection(env *common.Env) {

  chatContainer := env.Document.GetElementByID("chatboxContainer").(*dom.HTMLDivElement)
  chatContainer.SetClass("")

  chatboxHeaderBar := env.Document.GetElementByID("chatboxHeaderBar").(*dom.HTMLDivElement)
  chatboxHeaderBar.SetClass("chatboxHeader disconnected")

  chatboxTitleDiv := env.Document.GetElementByID("chatboxTitle").(*dom.HTMLDivElement)
  if chatboxTitleDiv != nil {
    titleSpan := chatboxTitleDiv.ChildNodes()[0].(*dom.HTMLSpanElement)
    if titleSpan != nil {
      var countdown uint64 = 6
      tickerForCountdown := time.NewTicker(1 * time.Second)
      timerToCloseChat := time.NewTimer(6 * time.Second)
      go func() {
        for _ = range tickerForCountdown.C {
          atomic.AddUint64(&countdown, ^uint64(0))
          safeCountdownValue := atomic.LoadUint64(&countdown)
          titleSpan.SetInnerHTML("Disconnected! - Closing LiveChat in " + strconv.FormatUint(safeCountdownValue, 10) + " seconds.")
        }
      }()
      go func() {
        <-timerToCloseChat.C
        tickerForCountdown.Stop()
        CloseChat(env)
      }()
    }
  }
}

我们首先使用SetClass方法将chatContainer的 CSSclassname值设置为空字符串,以禁用chatContainer元素的脉动效应,以指示连接已断开。

然后我们通过使用SetClass方法将chatboxHeaderBar元素的 CSSclassname值设置为"chatboxHeader disconnected",将chatboxHeaderBar的背景色更改为红色

剩余的代码将向用户显示一条消息,指示连接已断开,实时聊天功能将自动启动倒计时。当实时聊天功能自动关闭时,chatboxHeaderBar将以秒为单位显示倒计时,5-4-3-2-1。我们使用两个 goroutine,一个用于倒计时计时器,另一个用于倒计时计时器。当倒计时计时器过期时,表示倒计时结束,我们调用传入env对象的CloseChat函数关闭直播聊天功能。

与代理交谈

现在,我们已经实现了服务器端和客户端功能,以实现实时聊天功能,展示了实时 web 应用功能。现在是时候开始与聊天代理的对话(问答会话)。

单击网站顶部栏上的实时聊天图标后,我们会看到网页右下角的聊天框。下面的屏幕截图显示了聊天室中聊天代理的问候语:

图 8.3:聊天框打开,显示聊天代理的问候语

我们可以使用聊天室右上角的 X 控件关闭实时聊天室窗口。我们可以通过再次单击顶部栏中的 live chat 图标来重新激活 live chat 功能。我们不需要问聊天代理一个问题,比如什么是同构 Go?,我们实际上可以提供一个声明,比如告诉我更多关于同构 Go 的信息,如下面的屏幕截图所示:

图 8.4:聊天代理理解信息请求,即使它不是问题

如下一个屏幕截图所示,用户和聊天代理之间的问答会话可以根据用户的意愿持续多久。这也许是聊天代理最大的优势,它在与人类打交道时有无限的耐心。

图 8.5:问答环节可以继续,只要人类愿意

我们实现的聊天代理的智能范围非常狭窄且有限。当人类用户提出超出其智能范围的问题时,聊天代理会承认自己不知道答案,如下所示:

图 8.6:聊天代理对于超出其智能范围的问题没有答案

一些人类用户可能对聊天代理很粗鲁。这与聊天代理提供的面向公众的角色一起出现。如果我们把语料库调整得恰到好处,我们可以让聊天代理展示一个机智的回答。

图 8.7:显示机智回答的聊天代理

如前所述,我们策略性地将聊天盒容器放置在网页布局的主要内容区域之外。完成此操作后,聊天盒和与聊天代理的对话可以继续,因为我们可以自由浏览 IGWEB 的链接,如下所示:

图 8.8:当用户浏览 IGWEB 时,聊天对话将被保留

例如,即使在单击咖啡杯产品图像以进入产品详细信息页面后,聊天对话仍将继续,如下所示:

图 8.9:当用户访问咖啡杯的产品详细信息页面时,聊天对话被保留

实时 web 应用依赖于与 Internet 的持久连接。让我们看看 live chat 功能如何优雅地处理断开 Internet 连接的情况,如下所示:

图 8.10:关闭互联网连接

一旦互联网连接关闭,我们会立即收到聊天盒标题栏中的断开通知,如图 8.11所示。聊天盒标题栏的背景色变为红色,并开始倒计时以关闭实时聊天功能。倒计时完成后,实时聊天功能会自动关闭:

图 8.11:关闭实时聊天功能的倒计时显示在聊天框的标题栏中

在实现实时 Web 应用功能时,考虑持久的 WebSoSk 连接被中断的场景总是很重要的。通过使现场聊天关闭,当 Web 客户端和 Web 服务器之间的持久连接中断时,我们有办法提供一个提示。发送给用户,以与聊天代理断开连接

总结

在本章中,我们以 IGWEB 的实时聊天功能的形式实现了实时 web 应用功能。您学习了如何使用 WebSocket 在 web 服务器和 web 客户端之间建立持久连接。在服务器端,我们向您介绍了 Gorilla toolkit 项目中的websocket包。在客户端,我们向您介绍了 GopherJS 项目的gopherjs/websocket/websocketjs包。

我们创建了一个简单、基本的聊天机器人,实时回答用户提出的问题,通过已建立的 WebSocket 连接转发人与机器人之间的对话。由于实时 web 应用功能依赖于持久连接,我们还添加了代码,以便在互联网连接中断时自动关闭实时聊天功能

我们使用nlp包来实现基本聊天代理的大脑,以便它能够回答一些与同构 Go 相关的问题。我们使聊天代理解决方案具有可扩展性,将来可以通过定义Bot接口添加不同智能的新机器人。

第 9 章Cogs–可重用组件中,我们将探讨如何在整个 IGWEB 上实现可重用的接口小部件。可重用组件提供了一种提高可重用性的方法,它们可以即插即用的方式使用。正如您将了解到的,COG 也是高效的,它利用虚拟 DOM 根据需要重新发布其内容。*