在上一章中,我们考虑了如何通过 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 聊天应用包含两种主要类型:Hub
和Client
。
聊天中心负责维护客户端连接列表,并指示聊天机器人向相关客户端广播消息。例如,如果 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
的布尔值组成,以指示客户端已连接。客户通过broadcastmsg
、register
和unregister
频道与集线器通信。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
消息返回给客户端。一旦我们得到问候语(一个字符串值),我们调用传入client
的SendMessage
方法,并将greeting
转换为byte
切片。
如果指向Client
的指针通过集线器的unregister
通道进入,集线器将删除给定client
的map
中的条目,并关闭客户端的send
通道,这意味着client
将不再向服务器发送任何消息。
如果指向ClientMessage
的指针通过集线器的broadcastmsg
通道进入,集线器将把客户端的message
(作为字符串值)传递给chatbot
对象的Reply
方法。一旦我们从代理获得reply
(字符串值),我们调用SendMessage
方法传入client
并将reply
转换为byte
切片。
Client
类型充当Hub
和websocket
连接之间的中介。
以下是[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 方法:SetName
、SetTitle
和SetThumbnailPath
。
通过定义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
变量,设置name
、title
和thumbnailPath
字段。然后我们调用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)
现在我们已经有了lsi
和queryVector
,是时候找到与查询条件最匹配的文档了。我们通过根据查询计算语料库中每个文档的余弦相似度来实现这一点:
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.ResponseWriter
、w
写出 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 功能已经激活,chatbox
div 元素将已经存在,也就是说,它将是一个非 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]string
的map
。然后我们通过频道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
方法来获取input
message 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 对象[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
对象message
和sender
字符串作为输入值传递给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
字段设置为map
、m
。我们将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 根据需要重新发布其内容。*