Skip to content

Latest commit

 

History

History
1395 lines (926 loc) · 65.5 KB

03.md

File metadata and controls

1395 lines (926 loc) · 65.5 KB

三、与 GopherJS 一起去往前端

自创建以来,JavaScript 一直是 web 浏览器事实上的编程语言。因此,它在很长一段时间内垄断了前端 web 开发。它是镇上唯一一款能够操纵网页的文档对象模型DOM)并访问在现代网络浏览器中实现的各种应用编程接口API)的游戏。

由于这种排他性,JavaScript 是同构 web 应用开发的唯一可行选项。随着 GopherJS 的引入,我们现在能够在 web 浏览器中创建 Go 程序,这也使得使用 Go 开发同构 web 应用成为可能

GopherJS 允许我们在 Go 中编写程序,将其转换为等效的 JavaScript 表示形式,适合在任何支持 JavaScript 的 web 浏览器中运行。GopherJS 为我们提供了一种可行且有吸引力的替代 JavaScript 的方法,特别是当我们在服务器端使用 Go 时。随着 Go 在这两个方面的发展(前端和后端),我们有了新的机会来共享代码,并消除了由于必须在不同的环境中使用不同的编程语言而带来的心理环境变化。

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

  • 文档对象模型
  • 基本 DOM 操作
  • GopherJS 概述
  • GopherJS 示例
  • 内联模板渲染
  • 本地存储

文档对象模型

在深入研究 GopherJS 之前,重要的是要了解 JavaScript 以及 GopherJS 对我们的作用。JavaScript 的主要功能之一是能够访问和操作DOM(简称文档对象模型)。DOM 是一个树数据结构,它表示网页的结构以及网页中存在的所有节点(元素)。

DOM 的意义在于它充当 HTML 文档的编程接口,通过它,可以访问 DOM 的程序可以更改网页的样式、结构和内容。由于 DOM 树中的每个节点都是对象,因此可以将 DOM 视为给定网页的面向对象表示。因此,可以使用 JavaScript 访问和更改对象及其给定属性。

图 3.1描述了给定网页的 DOM 层次结构。网页上的所有元素都是html节点的子元素,由网页 html 源代码中的<html>标记表示:

图 3.1:网页的 DOM 层次结构

head节点是html节点的子节点,包含两个子元(在 html 中使用<meta>标记定义)和一个脚本节点(用于外部 CSS 或 JavaScript 源文件)。在头部节点的同一级别存在身体节点,身体节点使用<body>标记定义。

body 节点包含要在 web 页面上呈现的所有元素。在 body 节点的正下方,我们有一个子节点,它是标题节点(使用<h1>标记定义),即网页的标题。此节点没有子元素。

在标题节点的同一级别,我们还有一个 div 节点(使用<div>标记定义)。该节点包含一个 div 子节点,它有两个子节点,一个段落节点(使用<p>标记定义),在该节点的同一级别上存在一个图像节点(使用<img>标记定义)。

图像节点没有子元素,段落节点有一个子元素——跨度节点(使用<span>标记定义)。

web 浏览器中包含的 JavaScript 运行时为我们提供了访问 DOM 树中的各个节点及其相应值的功能。使用 JavaScript 运行时,我们可以访问单个节点,如果给定节点包含子节点,我们可以访问父节点的所有子节点的集合。

由于网页被表示为对象的集合,因此使用 DOM 我们可以访问任何给定 DOM 对象的事件、方法和属性。事实上,document对象表示网页文档本身。

以下是 MDN 网站上关于 DOM 的有用介绍: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction

访问和操作 DOM

如前所述,我们可以使用 JavaScript 访问和操作给定网页的 DOM。由于 GopherJS 可以转换为 JavaScript,我们现在可以在 Go 的范围内访问和操作 DOM。图 3.2描述了一个访问/操作 DOM 的 JavaScript 程序以及一个访问/操作 DOM 的 Go 程序:

图 3.2:DOM 可以通过 JavaScript 程序和/或 Go 程序(使用 GopherJS)访问和操作

现在,让我们来看看几个简单的编程片段,在这里我们可以使用 GO 访问 JavaScript 函数,然后使用 JavaScript 和在 GopherJS 中的等效指令执行一些基本 DOM 操作。目前,让我们预览一下使用 GopherJS 编码的效果。这些概念将在本章后面的部分详细解释,作为充分充实的示例。

基本 DOM 操作

在本节中,我们将查看一些基本 DOM 操作的集合。提供的每个 DOM 操作都包括在 JavaScript、GopherJS 和使用 DOM 绑定中执行的等效操作。

显示警报消息

JavaScript

alert("Hello Isomorphic Go!");

地鼠

js.Global.Call("alert", "Hello Isomorphic Go!")

DOM 绑定

dom.GetWindow().Alert("Hello Isomorphic Go!")

我们可以执行的最基本的操作之一是在模式对话框中显示alert消息。在 JavaScript 中,我们可以使用内置的alert功能显示alert消息:

alert("Hello Isomorphic Go!");

这行代码将在模式窗口对话框中打印出消息Hello Isomorphic Go!alert功能阻止进一步执行,直到用户关闭alert对话框。

当我们调用alert方法时,实际上是这样调用的:

window.alert("Hello Isomorphic Go!");

window对象是一个全局对象,表示 web 浏览器中打开的窗口。JavaScript 实现允许我们直接调用alert函数和其他内置函数,而无需显式引用它们作为窗口对象的方法,以方便使用。

我们使用js包通过 GopherJS 访问 JavaScript 功能。我们可以按如下方式将包导入我们的 Go 计划:

import "github.com/gopherjs/gopherjs/js"

js包为我们提供了与本机 JavaScript API 交互的功能。对js包中函数的调用直接转换为其等效的 JavaScript 语法。

我们可以使用 Go 和 GopherJS 以以下方式显示alert消息对话框:

js.Global.Call("alert", "Hello Isomorphic Go!")

在前面的代码片段中,我们使用了js.Global对象可用的Call方法。js.Global对象为我们提供了 JavaScript 的全局对象(window对象)。

以下是Call方法签名的样子:

func (o *Object) Call(name string, args ...interface{}) *Object

Call方法将使用提供的名称调用全局对象的方法。提供给方法的第一个参数是要调用的方法的名称。第二个参数是要传递给全局对象的方法的参数列表。Call方法被称为可变函数,因为它可以接受interface{}类型的可变数量的参数。

您可以通过查看位于的 GopherJS 文档来了解更多关于Call方法的信息 https://godoc.org/github.com/gopherjs/gopherjs/js#Object.Call

现在,我们已经看到了如何使用 AuthT2Ty 对象的 AuthT1 方法显示 AutoT0^对话框窗口,让我们看看 DOM 绑定。

dom包为我们提供了方便的到 JavaScript DOM API 的 GopherJS 绑定。与使用js.Global对象执行所有操作不同,使用此包的想法是 DOM 绑定为我们提供了一种调用公共 DOM API 功能的惯用方法。

如果您已经熟悉用于访问和操作 DOM 的 JavaScript API,那么使用dom包对您来说将是第二天性。我们可以使用GetWindow函数访问全局窗口对象,如下所示:

dom.GetWindow()

使用dom包,我们可以用以下代码显示警报对话框消息:

dom.GetWindow().Alert("Hello Isomorphic Go!")

这个代码片段的粗略视图显示,这感觉更接近于调用alert对话框的 JavaScript 方式:

window.alert("Hello Isomorphic Go!")

由于这种相似性,最好熟悉 JavaScript DOM API,因为它可以让您熟悉使用dom包的等效函数调用。

您可以在查看dom软件包的文档,了解更多有关该软件包的信息 https://godoc.org/honnef.co/go/js/dom

通过 ID 获取 DOM 元素

我们可以使用document对象的getElementById方法访问给定id的元素。在这些示例中,我们访问主要内容div容器,其id"primaryContent"

JavaScript

element = document.getElementById("primaryContent");

地鼠

element := js.Global.Get("document").Call("getElementById", "primaryContent")

DOM 绑定

element := dom.GetWindow().Document().GetElementByID("primaryContent")

尽管dom包的方法调用与 JavaScript 方法调用非常相似,但可能会出现细微的差异。

例如,注意使用 JavaScript 对document对象的getElementById方法调用中的大小写,并将其与使用 DOM 绑定时的GetElementByID方法调用的大小写进行比较。

为了导出 Go 中的GetElementByID方法,我们必须大写第一个字母,这里是G。另外,请注意,使用 JavaScript 方式时,子字符串Id的大小写与使用 DOM 绑定时Id的大小写有细微的区别。

查询选择器

document对象的querySelector方法为我们提供了一种使用 CSS 查询选择器访问 DOM 元素的方法,其方式类似于 jQuery 库。我们可以使用 document 对象的querySelector方法访问 IGWEB 主页上包含欢迎消息的h2元素。

JavaScript

element = document.querySelector(".welcomeHeading");

地鼠

element := js.Global.Get("document").Call("querySelector", ".welcomeHeading")

DOMBINING

element := dom.GetWindow().Document().QuerySelector(".welcomeHeading")

更改元素的 CSS 样式属性

在前面的代码片段中,我们只考虑了访问 DOM 元素的例子。现在,让我们考虑一个例子,在这里我们改变元素的 CSS 样式属性。我们将通过更改div元素的display属性的值来隐藏主内容div容器中的内容。

我们可以通过将对js.Globaldom包的调用别名如下:

用于 GopherJS: JS := js.Global

用于dom包: D := dom.GetWindow().Document()来节省一些输入

为了更改主要内容 div 容器的显示属性,我们首先需要访问div元素,然后将其display属性更改为none值。

JavaScript

element = document.GetElementById("primaryContent");
element.style.display = "none"

地鼠

js := js.Global
element := js.Get("document").Call("getElementById"), "primaryContent")
element.Get("style").Set("display", "none")

DOM 绑定

d := dom.GetWindow().Document()
element := d.GetElementByID("welcomeMessage")
element.Style().SetProperty("display", "none", "")

您可以在的 GopherJS 游乐场体验与 GopherJS 的合作 https://gopherjs.github.io/playground/

GopherJS 概述

现在我们已经看到了使用 GopherJS 的预览,让我们考虑 GopHjs 如何工作的高级概述。图 3.3描述了一个同构的 Go 应用,它由一个 Go 前端 web 应用(使用 GopherJS)和一个 Go 后端 web 应用组成:

图 3.3:同构的 Go web 应用由 Go 前端 web 应用(使用 GopherJS)和 Go 后端 web 应用组成

图 3.3中,我们将通信方式说明为 HTTP 事务,但需要注意的是,这并不是客户端和 web 服务器通信的唯一方式。我们还可以使用 web 浏览器的 WebSocket API 建立持久连接,我们将在第 8 章实时 web 应用功能中介绍。

在上一节介绍的微示例中,我们介绍了 GopherJS DOM 绑定,它为我们提供了对 DOM API 的访问,DOM API 是在 web 浏览器中实现的 JavaScript API。除了 domapi 之外,还有其他 API,如 XHR(创建和发送 XMLHttpRequests)API 和 websocketapi(创建与 web 服务器的双向持久连接)。XHR 和 WebSocket API 也提供了 GopherJS 绑定。

图 3.4左侧为常见 JavaScript API,右侧为其等价的 GopherJS 绑定。有了 GopherJS 绑定,我们可以从 Go 编程语言本身访问 JavaScript API 功能:

图 3.4:常见 JavaScript API 及其等价的 GopherJS 绑定

地鼠传送器

我们使用 GopherJS transpiler 将 Go 程序转换为 JavaScript 程序。图 3.5描述了一个 Go 程序,该程序不仅使用 Go 标准库中的功能,还使用各种 JavaScript API 中的功能,使用等效的 GopherJS 绑定包:

图 3.5:使用标准库和 GopherJS 绑定传输到等效 JavaScript 程序的 Go 程序

我们使用gopherjs build命令将 Go 程序转换为等效的 JavaScript 表示形式。生成的 JavaSript 源代码并不意味着人类可以修改。JavaScript 程序可以访问嵌入在 web 浏览器中的底层 JavaScript 运行时,还可以访问常见的 JavaScript API。

要了解类型如何从 Go 转换为 JavaScript,请查看中的表格 https://godoc.org/github.com/gopherjs/gopherjs/js

关于 IGWEB,我们在client文件夹中组织了前端 Go web 应用项目代码。这使我们能够将前端 web 应用与后端 web 应用巧妙地分开。

图 3.6描述了包含大量 Go 源文件的客户项目文件夹:

图 3.6:客户机文件夹包含组成前端 Go Web 应用的 Go 源文件。GopherJS transpiler 生成一个 JavaScript 程序(client.js)和一个源映射(client.js.map)

client文件夹中的 GopherJS transpiler 源文件上运行 GopherJS transpiler 后,通过发出gopherjs build命令,创建两个输出文件。第一个输出文件是client.js文件,它表示等效的 JavaScript 程序。第二个输出文件是client.js.map文件,它是用于调试目的的源映射。当我们使用 web 浏览器的控制台追查 bug 时,这个源代码图为我们提供了关于产生的错误的详细信息。

The Appendix: Debugging Isomorphic Go, contains guidance and advice on debugging an isomorphic web application implemented in Go.

The gopherjs build command is synonymous in behavior with its go build counterpart. The client project folder can contain any number of subfolders, which may also contain Go source files. When we execute the gopherjs build command, a single JavaScript source program is created along with a source map file. This is analogous to the single static binary file that gets created when issuing a go build command.

可以通过在import语句中指定共享包的正确路径来共享服务器和客户端之间在客户端文件夹之外共享的代码。shared文件夹将包含跨环境共享的代码,如模型和模板。

我们可以使用<script>标记将 GopherJS 生成的 JavaScript 源文件作为外部javascript源文件包含在我们的网页中,如下所示:

<script type="text/javascript" src="/js/client.js"></script>

请记住,当我们发出一个gopherjs build命令时,我们不仅仅是在创建一个与我们正在编写的程序相当的 JavaScript,我们还带来了来自标准库的任何包或我们的程序所依赖的第三方包。因此,除了包括我们的前端 Go 程序外,GophjerJS 还包括我们的程序所依赖的任何依赖包。

并非 Go 标准库中的所有包都在 web 浏览器中工作。您可以参考 GopherJS 兼容性表,在查看 Go 标准库中支持的软件包列表 https://github.com/gopherjs/gopherjs/blob/master/doc/packages.md

这一事实的后果是,生成的 JavaScript 源代码的文件大小将与我们在 Go 程序中引入的依赖项的数量成比例增长。这一事实的另一个分支是,在同一网页上包含多个 GopherJS 生成的 JavaScript 文件是没有意义的,如图 3.7 所示,因为依赖包(如标准库中的公共包)将被多次包含,不必要地增加了脚本的总负载,却没有提供任何回报:

图 3.7:不要在一个网页中导入多个 GopherJS 生成的源文件

因此,一个网页最多只能包含一个 GopherJS 生成的源文件,如图 3.8所示:

图 3.8:网页中只应包含一个 GopherJS 生成的源文件

GopherJS 示例

在本章的前面,我们预览了使用 GopherJS 进行编码的情况。现在我们将看一些完全充实的例子,以巩固我们对一些基本概念的理解。

如前所述,前端 web 应用的源代码可以在client文件夹中找到。

如果要手动传输客户端目录中的 Go 代码,可以在client文件夹中发出gopherjs build命令:

$ gopherjs build

如前所述,将生成两个源文件,client.jsJavaScript 源文件和client.js.map源映射文件。

要手动运行 web 服务器,您可以进入igweb文件夹并运行以下命令:

$ go run igweb.go

一个更方便的替代方法是使用kick使用以下命令编译 Go 代码和 GopherJS 代码:

$ kick --appPath=$IGWEB_APP_ROOT --gopherjsAppPath=$IGWEB_APP_ROOT/client --mainSourceFile=igweb.go

使用kick的优点是,它将自动监视对 Go 后端 web 应用或 GopherJS 前端 web 应用所做的更改。如前一章所述,kick将在检测到更改时执行即时启动,这将加快迭代开发周期。

一旦您运行了igweb程序,您可以通过以下 URL 访问 GopherJS 示例: http://localhost:8080/front-end-examples-demo

前端示例演示将包含一些基本的 GopherJS 示例。让我们打开igweb文件夹中的igweb.go源文件,看看一切是如何工作的。

registerRoutes功能中,我们注册了以下路径:

r.Handle("/front-end-examples-demo", handlers.FrontEndExamplesHandler(env)).Methods("GET")
r.Handle("/lowercase-text", handlers.LowercaseTextTransformHandler(env)).Methods("POST")

/front-end-examples-demo路径用于显示我们的前端示例网页。/lowercase-text路由用于将文本转换为小写。稍后我们将更详细地介绍第二条路线;首先,让我们来看看处理 Type T3 路由的处理函数(在 OutT2E.Soad 文件中找到的):

package handlers

import (
  "net/http"
  "github.com/EngineerKamesh/igb/igweb/common"
  "github.com/isomorphicgo/isokit"
)

func FrontEndExamplesHandler(env *common.Env) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    env.TemplateSet.Render("frontend_examples_page", &isokit.RenderParams{Writer: w, Data: nil})
  })
}

在这里,我们定义了处理函数FrontEndExamplesHandler,它接受指向env对象的指针作为输入参数,并返回http.Handler函数。我们定义了一个闭包来返回http.HandlerFunc,它接受http.ResponseWriter*http.Request作为输入参数。

我们调用TemplateSet对象上的Render方法来呈现前端示例页面。该方法的第一个输入参数是模板的名称,即frontend_examples_page。第二个输入参数是要使用的渲染参数。因为我们是从服务器端呈现模板,所以我们传递了whttp.ResponseWriter,它负责写出网页响应(呈现的模板)。因为我们没有向模板传递任何数据,所以我们将一个值nil分配给RenderParams结构的Data字段。

第 4 章同构模板中,我们将解释模板集是如何工作的,以及如何使用isokit包提供的同构模板呈现器在服务器端和客户端呈现模板。

client.go源文件中找到的initializePage函数的部分源代码列表中,我们包含了以下代码行来初始化 GopherJS 代码示例(以粗体显示):

func initializePage(env *common.Env) {

  l := strings.Split(env.Window.Location().Pathname, "/")
  routeName := l[1]

  if routeName == "" {
    routeName = "index"
  }

  if strings.Contains(routeName, "-demo") == false {
    handlers.InitializePageLayoutControls(env)
  }

  switch routeName {

  case "front-end-examples-demo":
    gopherjsprimer.InitializePage()

gopherjsprimer.InitializePage函数负责将事件侦听器添加到前端示例网页上的元素中。在注册任何事件之前,我们首先检查带有/front-end-examples路由的页面是否已被访问。如果用户使用不同的路径访问页面,如/index,则无需为前端示例页面设置事件处理程序。如果用户已经访问了/front-end-examples路由,那么控制流将到达指定"front-end-examples-demo"值的case语句,我们将通过调用gopherjsprimer.InitializePage函数为网页上的 UI 元素设置所有事件处理程序。

让我们仔细看看在 OUT1 T1 源文件中发现的函数 T0。

func InitializePage() {

  d := dom.GetWindow().Document()

  messageInput := d.GetElementByID("messageInput").(*dom.HTMLInputElement)

  alertButtonJS := d.GetElementByID("alertMessageJSGlobal").(*dom.HTMLButtonElement)
  alertButtonJS.AddEventListener("click", false, func(event dom.Event) {
 DisplayAlertMessageJSGlobal(messageInput.Value)
 })

  alertButtonDOM := d.GetElementByID("alertMessageDOM").(*dom.HTMLButtonElement)
 alertButtonDOM.AddEventListener("click", false, func(event dom.Event) {
 DisplayAlertMessageDOM(messageInput.Value)
 })

  showGopherButton := d.GetElementByID("showGopher").(*dom.HTMLButtonElement)
 showGopherButton.AddEventListener("click", false, func(event dom.Event) {
 ShowIsomorphicGopher()
 })

  hideGopherButton := d.GetElementByID("hideGopher").(*dom.HTMLButtonElement)
 hideGopherButton.AddEventListener("click", false, func(event dom.Event) {
 HideIsomorphicGopher()
 })

  builtinDemoButton := d.GetElementByID("builtinDemoButton").(*dom.HTMLButtonElement)
 builtinDemoButton.AddEventListener("click", false, func(event dom.Event) {
 builtinDemo(event.Target())
 })

  lowercaseTransformButton := d.GetElementByID("lowercaseTransformButton").(*dom.HTMLButtonElement)
 lowercaseTransformButton.AddEventListener("click", false, func(event dom.Event) {
 go lowercaseTextTransformer()
 })

}

InitializePage函数负责使用元素的AddEventListener方法(以粗体显示)将事件侦听器添加到前端示例网页中的元素中

显示警报消息

让我们从一个显示警报对话框的示例开始。在本章前面,我们看到了如何使用js.Global对象的Call方法和 GopherJS DOM 绑定来显示警报对话框。图 3.9描述了我们第一个示例的用户界面:

图 3.9:显示警报消息示例

用户界面由输入文本字段组成,用户可以在其中输入自定义消息以显示在警报对话框中。文本字段后面有两个按钮:

  • 第一个按钮将使用js.Global对象上的Call方法显示警报对话框
  • 第二个按钮将显示使用 GopherJS DOM 绑定的警报对话框

前端示例的 HTML 标记可在位于shared/templates/frontend_examples_page.tmpl的模板文件中找到。

以下是警报消息示例的 HTML 标记:

<div class="example">
<form class="pure-form">
  <fieldset class="pure-group">
  <h2>Example: Display Alert Message</h2>
  </fieldset>
  <fieldset class="pure-control-group">
  <label for="messageInput">Alert Message: </label>
  <input id="messageInput" type="text" value="Hello Gopher!" />
  </fieldset>
  <fieldset class="pure-group">
 <button id="alertMessageJSGlobal" type="button" class="pure-button pure-button-primary">Display Alert Message using js.Global</button>
 <button id="alertMessageDOM" type="button" class="pure-button pure-button-primary">Display Alert Message using dom package</button>
</fieldset>
</form>
</div>

在这里,我们声明了两个按钮(以粗体显示),并为它们分配了唯一的 ID。使用js.Global.Call功能显示警报对话框的按钮的idalertMessageJSGlobal。使用 GopherJS DOM 绑定显示警报对话框的按钮的idalertMessageDOM

The following code snippet from the InitializePage function, defined in the initpage.go source file, is responsible for setting up the event handlers for the Display Alert Message buttons that will be displayed in the example:

  alertButtonJS := d.GetElementByID("alertMessageJSGlobal").(*dom.HTMLButtonElement)
  alertButtonJS.AddEventListener("click", false, func(event dom.Event) {
    DisplayAlertMessageJSGlobal(messageInput.Value)
  })

  alertButtonDOM := d.GetElementByID("alertMessageDOM").(*dom.HTMLButtonElement)
  alertButtonDOM.AddEventListener("click", false, func(event dom.Event) {
    DisplayAlertMessageDOM(messageInput.Value)
  })

我们通过调用document对象上的GetElementByID函数来获取第一个按钮,将按钮的id作为输入参数传递给函数。然后,我们调用按钮上的AddEventListener方法来创建一个新的事件侦听器,它将侦听单击事件。我们在单击第一个按钮时调用DisplayAlertMessagesJSGlobal函数,并传入messageInput文本字段的值,其中包含用户可以输入的自定义警报消息。

我们以类似的方式为第二个按钮设置事件监听器,除了在按钮上检测到单击事件时调用的函数是DisplayAlertMessageDOM,该函数调用该函数以使用 GopherJS DOM 绑定显示警报对话框。同样,我们将messageInput文本字段的值传递给函数。

现在,如果您要单击任一按钮,您应该能够看到警报对话框。将警报消息更改为其他内容,请注意,对警报消息文本字段所做的更改将反映在警报对话框中。图 3.10描述了警报对话框,其中有一条自定义消息“Hello Isomorphic Gopher!”:

图 3.10:显示带有自定义警报消息的警报对话框的示例

更改元素的 CSS 样式属性

现在我们来看一个例子,在这个例子中,我们通过更改元素的 CSS 样式属性来实际操作 DOM。本例的用户界面由同构的地鼠图像组成,其右下方有两个按钮,如图 3.11所示。单击第一个按钮时,将显示同构的地鼠图像(如果隐藏)。单击第二个按钮时,将隐藏同构的地鼠图像(如果显示)。图 3.11显示了可视的同构地鼠:

图 3.11:同构地鼠图像可见时的用户界面

图 3.12描绘了当同构地鼠图像不可见时的用户界面:

图 3.12:同构地鼠图像不可见时的用户界面

以下是生成本例用户界面的 HTML 标记:

<div class="example">
  <form class="pure-form">
  <fieldset class="pure-group">
    <h2>Example: Change An Element's CSS Style Property</h2>
  </fieldset>
  <fieldset class="pure-group">
    <div id="igRacer">
      <img id="isomorphicGopher" border="0" src="/statimg/isomorphic_go_logo.png">
    </div>
  </fieldset>
  <fieldset class="pure-group">
 <button id="showGopher" type="button" class="pure-button pure-button-primary">Show Isomorphic Gopher</button>
 <button id="hideGopher" type="button" class="pure-button pure-button-primary">Hide Isomorphic Gopher</button>
  </fieldset>
  </form>
</div>

在这里,我们声明一个图像标记,它表示同构的 Go 图像,并将其分配为isomorphicGopherid。我们声明两个按钮(以粗体显示):

  • 第一个按钮的idshowGopher,单击后将显示同构的地鼠图像
  • 第二个按钮的idhideGopher,点击后将隐藏同构的地鼠图像

InitializePage函数中的以下代码片段负责为显示和隐藏同构地鼠图像的两个按钮设置事件处理程序:

  showGopherButton := d.GetElementByID("showGopher").(*dom.HTMLButtonElement)
  showGopherButton.AddEventListener("click", false, func(event dom.Event) {
    ShowIsomorphicGopher()
  })

  hideGopherButton := d.GetElementByID("hideGopher").(*dom.HTMLButtonElement)
  hideGopherButton.AddEventListener("click", false, func(event dom.Event) {
    HideIsomorphicGopher()
  })

如果单击“显示同构地鼠”按钮,我们将调用ShowIsomorphicGopher函数。如果单击隐藏同构地鼠按钮,我们将调用HideIsomorphicGopher函数。

让我们检查一下在client/gopherjsprimer/cssexample.go源文件中定义的ShowIsomorphicGopherHideIsomorphicGopher函数:

package gopherjsprimer

import "honnef.co/go/js/dom"

func toggleIsomorphicGopher(isVisible bool) {

  d := dom.GetWindow().Document()
  isomorphicGopherImage := d.GetElementByID("isomorphicGopher").(*dom.HTMLImageElement)

  if isVisible == true {
    isomorphicGopherImage.Style().SetProperty("display", "inline", "")
  } else {
    isomorphicGopherImage.Style().SetProperty("display", "none", "")
  }

}

func ShowIsomorphicGopher() {
  toggleIsomorphicGopher(true)
}

func HideIsomorphicGopher() {
  toggleIsomorphicGopher(false)
}

ShowIsomorphicGopherHideIsomorphicGopher函数都调用toggleIsomorphicGopher函数。唯一不同的是,ShowIsomorphicGopher函数调用输入参数为 true 的toggleIsomorphicGopher函数,HideIsomorphicGopher函数调用输入参数为falsetoggleIsomorphicGopher函数。

The toggleIsomorphicGopher function takes in a single argument, which is a Boolean variable indicating whether or not the IsomorphicGopher image should be shown, or not.

如果我们向函数传递一个值true,则应显示同构的地鼠图像,如图 3.11所示。如果我们将一个值false传递给函数,则不应显示同构的地鼠图像,如图 3.12所示。我们将Document对象的值赋给d变量。我们调用Document对象的GetElementByID方法来获得同构的地鼠图像。请注意,我们已经执行了一个类型断言(以粗体显示),以断言d.GetElementByID("isomorphicGopher")返回的值具有一个具体类型*dom.HTMLImageElement

我们声明了一个if条件块,用于检查isVisible布尔变量的值是否为true,如果是,我们将图像元素的Style对象的display属性设置为inline。这将导致出现同构的地鼠图像,如图 3.11所示。

如果isVisible布尔变量的值为false,则我们到达else块,并将图像元素的Style对象的display属性设置为none,这将阻止显示同构的地鼠图像,如图 3.12所示。

操作员功能的 JavaScript 类型

JavaScripttypeof运算符用于返回给定操作数的类型。例如,让我们考虑下面的 JavaScript 代码:

typeof 108 === "number"

此表达式将计算为布尔值true。在类似的注释中,现在考虑这个 JavaScript 代码:

typeof "JavaScript" === "string"

此表达式也将计算为布尔值true

所以你可能想知道,我们如何使用 Go 使用 JavaScripttypeof操作符?答案是,我们需要jsbuiltin包,即用于内置 JavaScript 功能的 GopherJS 绑定,其中包括typeof操作符。

在本例中,我们将使用 JavaScript 的typeof操作符和jsbuiltin包。图 3.13描述了本例的用户界面:

图 3.13:JavaScript 类型示例的用户界面

以下是实现本例用户界面的 HTML 标记:

<div class="example">
  <h2>Example: JavaScript Builtin Functionality for typeof operation</h2>
  <p>Note: The message should appear in the web console after clicking the button below.</p>
 <button id="builtinDemoButton" type="button" class="pure-button pure-button-primary">Builtin Demo</button>
</div>

我们已经申报了一个按钮,其idbultinDemoButton。现在,让我们为InitializePage函数内的内置演示按钮设置一个事件监听器,以处理点击事件:

  builtinDemoButton := d.GetElementByID("builtinDemoButton").(*dom.HTMLButtonElement)
  builtinDemoButton.AddEventListener("click", false, func(event dom.Event) {
    builtinDemo(event.Target())
  })

我们通过调用Document对象d上的GetElementID方法来获取button元素。我们将返回的button元素分配给builtinDemoButton变量。然后,我们在button元素中添加一个事件监听器,以检测单击它的时间。如果检测到点击事件,我们调用builtinDemo函数并传入button元素的值,该元素恰好是事件目标。

让我们检查一下在client/gopherjsprimer文件夹中找到的builtindemo.go源文件:

package gopherjsprimer

import (
  "github.com/gopherjs/jsbuiltin"
  "honnef.co/go/js/dom"
)

func builtinDemo(element dom.Element) {

  if jsbuiltin.TypeOf(element) == "object" {
    println("Using the typeof operator, we can see that the element that was clicked, is an object.")
  }

}

bulitindemo函数接受dom.Element类型的输入参数。在这个函数中,我们通过调用jsbuiltin包中的TypeOf函数(以粗体显示),对传递到函数中的元素执行 JavaScripttypeof操作。我们检查传入的元素是否是对象。如果它是一个对象,我们将向 web 控制台打印一条消息,确认传递到函数中的元素是一个对象。图 3.14描述了打印在 web 控制台上的消息:

图 3.14:单击内置演示按钮后在 web 控制台上打印的消息

表面上看,这是一个相当平凡的例子。然而,它强调了一个非常重要的概念,在 Go 的范围内,我们仍然可以访问内置的 JavaScript 功能。

使用 XHR post 将文本转换为小写

现在我们将创建一个简单的小写文本转换器。用户输入的任何文本都将转换为小写。我们的小写文本转换器解决方案的用户界面如图 3.15所示。在图像中,输入文本是 GopherJS。当用户点击小写字母时就可以了!按钮,文本字段中的文本将转换为其小写等效文本,即 gopherjs:

图 3.15:小写文本转换器示例

事实上,我们可以在客户端应用文本转换;然而,更有趣的是,我们可以看到这样一个示例:我们将输入文本以XHR Post的形式发送到 web 服务器,然后在服务器端执行小写转换。服务器将文本转换为小写后,输入将发送回客户端,文本字段将使用输入文本的小写版本进行更新。

以下是用户界面的 HTML 标记的外观:

<div class="example">
  <form class="pure-form">
  <fieldset class="pure-group">
    <h2>Example: XHR Post</h2>
  </fieldset>
  <fieldset class="pure-control-group">
    <label for="textToLowercase">Enter Text to Lowercase: </label>
    <input id="textToLowercase" type="text" placeholder="Enter some text here to lowercase." value="GopherJS" />
  </fieldset>
  <fieldset class="pure-group">
    <button id="lowercaseTransformButton" type="button" class="pure-button pure-button-primary">Lowercase It!</button>
  </fieldset>
  </form>
</div>

我们声明了一个input文本字段,用户可以在其中输入他们想要转换为小写的文本。我们为input文本字段分配idtextToLowercase。然后我们声明一个按钮,其idlowercaseTransformButton。点击此按钮后,我们将向服务器发起XHR Post。服务器将文本转换为小写,并将输入文本的小写版本发回。

以下是InitializePage函数的代码,用于设置按钮的事件侦听器:

  lowercaseTransformButton := d.GetElementByID("lowercaseTransformButton").(*dom.HTMLButtonElement)
  lowercaseTransformButton.AddEventListener("click", false, func(event dom.Event) {
    go lowercaseTextTransformer()
  })

我们将button元素分配给lowercaseTransformButton变量。然后我们调用button元素上的AddEventListener方法来检测点击事件。当检测到点击事件时,我们调用lowercaseTextTransformer函数。

下面是在client/gopherjsprimer/xhrpost.go源文件中定义的lowercaseTextTransformer函数:

func lowercaseTextTransformer() {
  d := dom.GetWindow().Document()
  textToLowercase := d.GetElementByID("textToLowercase").(*dom.HTMLInputElement)

  textBytes, err := json.Marshal(textToLowercase.Value)
  if err != nil {
    println("Encountered error while attempting to marshal JSON: ", err)
    println(err)
  }

  data, err := xhr.Send("POST", "/lowercase-text", textBytes)
  if err != nil {
    println("Encountered error while attempting to submit POST request via XHR: ", err)
    println(err)
  }

  var s string
  err = json.Unmarshal(data, &s)

  if err != nil {
    println("Encountered error while attempting to umarshal JSON data: ", err)
  }
  textToLowercase.Set("value", s)
}

我们首先获取文本输入元素并将其分配给textToLowercase变量。然后,我们使用json包中的Marshal函数将输入到文本输入元素的文本值封送到其 JSON 表示形式。我们将封送的值分配给textBytes变量。

我们使用 GopherJS XHR 绑定将XHR Post发送到 web 服务器。XHR 绑定通过xhr包提供给我们。我们调用xhr包中的Send函数来提交XHR Post。函数的第一个参数是我们将用来提交数据的 HTTP 方法。这里我们指定了POST作为 HTTP 方法。第二个输入参数是指向POST的数据的路径。这里我们已经指定了/lowercase-text路由,这是我们在igweb.go源文件中设置的。第三个也是最后一个参数是通过XHR Post发送的数据,即textBytes——JSON 封送数据。

来自XHR Post的服务器响应将存储在data变量中。我们调用json包中的Unmarshal函数来解组服务器的响应,并将解组值分配给string类型的s变量。然后,我们使用textToLowercase对象的Set方法将文本输入元素的值设置为s变量的值。

现在,让我们看一看服务器端处理程序,它负责在 ToR.T0EY 源文件中的小写转换:

package handlers

import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "log"
  "net/http"
  "strings"

  "github.com/EngineerKamesh/igb/igweb/common"
)

func LowercaseTextTransformHandler(env *common.Env) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    var s string

    reqBody, err := ioutil.ReadAll(r.Body)
    if err != nil {
      log.Print("Encountered error when attempting to read the request body: ", err)
    }

    reqBodyString := string(reqBody)

    err = json.Unmarshal([]byte(reqBodyString), &s)
    if err != nil {
      log.Print("Encountered error when attempting to unmarshal JSON: ", err)
    }

    textBytes, err := json.Marshal(strings.ToLower(s))
    if err != nil {
      log.Print("Encountered error when attempting ot marshal JSON: ", err)
    }
    fmt.Println("textBytes string: ", string(textBytes))
    w.Write(textBytes)

  })

}

LowercaseTextTransformHandler函数中,我们调用ioutil包中的ReadAll函数来读取请求主体。我们将reqBody的字符串值保存到reqBodyString变量中。然后,我们将 JSON 解组该字符串,并将解组后的值存储到s变量中,该变量为string类型。

我们使用strings包中的ToLower函数将s字符串变量的值转换为小写,并将该值封送到其 JSON 表示形式中。然后我们调用http.ResponseWriter``w上的Write方法,以小写形式写出字符串的 JSON 封送值。

当我们点击小写的时候就可以了!按钮,将字符串 GopherJS 转换为其小写表示 GopherJS,如图 3.16所示:

图 3.16:点击按钮后,文本“GopherJS”将转换为小写“GopherJS”

内联模板渲染

在本节中,您将学习如何使用 GopherJS 在 Go 中执行客户端模板渲染。我们可以使用html/template包在 web 浏览器中直接渲染模板。我们将使用内联模板渲染汽车表的各个行。

汽车上市演示

在 cars 清单演示中,我们将使用从内联客户端 Go 模板呈现的行填充一个表。在我们的示例中,该表将是一个汽车列表,我们将从汽车切片中获取要在表中显示的汽车。然后,我们将使用gob编码对汽车片段进行编码,并通过 XHR 调用将数据传输到 web 服务器实例。

客户端模板呈现有许多好处:

  • 呈现 web 服务器上的 CPU 使用情况,这是由服务器端模板呈现引起的
  • 呈现客户端模板不需要重新加载整页
  • 通过在客户端渲染模板,可以减少带宽消耗

让我们打开shared/templates/carsdemo_page.tmpl目录中的cars.html源文件:

{{ define "pagecontent" }}
<table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
  <thead>
    <tr>
      <th class="mdl-data-table__cell--non-numeric">Model Name</th>
      <th class="mdl-data-table__cell--non-numeric">Color</th>
      <th class="mdl-data-table__cell--non-numeric">Manufacturer</th>
    </tr>
  </thead>
 <tbody id="autoTableBody">
 </tbody>
</table>
{{end}}
{{template "layouts/carsdemolayout" . }}

这个 HTML 源文件包含我们示例的网页内容,一个汽车表,我们将使用内联模板呈现表中的每一行。

我们已经声明了将使用table标记在网页上显示的表。我们已经为每一列声明了标题。因为我们将显示一个汽车表,所以每辆汽车有三列;我们有一列表示型号名称,一列表示颜色,一列表示制造商。

我们将要添加到表中的每一新行都将附加到tbody元素(以粗体显示)。

注意,我们使用carsdemolayout.tmpl布局模板来布局 cars 演示页面。让我们打开位于shared/templates/layouts目录中的文件:

<html>
  {{ template "partials/carsdemoheader" }}
<body>
    <div class="pageContent" id="primaryContent">
      {{ template "pagecontent" . }}
    </div>
<script src="/js/client.js"></script>
</body>
</html>

布局模板不仅负责呈现pagecontent模板,还负责呈现位于templates/shared/partials目录中的标题模板carsdemoheader.tmpl。布局模板还负责导入 GopherJS 生成的client.js外部 JavaScript 源文件。

让我们看一看{ To.t0}源文件:

<head>
  <link rel="icon" type="image/png" href="/statimg/isomorphic_go_icon.png">
  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
  <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
  <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
</head>

在这个头模板文件中,我们为 Material Design 库导入 CSS 样式表和 JavaScript 源文件。我们将使用材质设计库,使用默认材质设计样式使表格看起来漂亮。

client.go源文件的initializePage函数中,我们在登录到 cars demo 网页时,包含了以下代码行来初始化 cars demo 代码示例:

carsdemo.InitializePage()

client/carsdemo目录的cars.go源文件中,我们声明了用于呈现给定汽车信息的内联模板:

const CarItemTemplate = `
  <td class="mdl-data-table__cell--non-numeric">{{.ModelName}}</td>
  <td class="mdl-data-table__cell--non-numeric">{{.Color}}</td>
  <td class="mdl-data-table__cell--non-numeric">{{.Manufacturer}}</td>
`

我们声明了CarItemTemplate常量,这是一个包含内联模板的多行字符串。在模板的第一行中,我们呈现包含模型名称的列。在模板的第二行中,我们渲染汽车的颜色。最后,在模板的第三行中,我们呈现了汽车的制造商。

我们用Document对象声明并初始化D变量,如下所示:

var D = dom.GetWindow().Document()

InitializePage函数(在client/carsdemo/cars.go源文件中找到)负责调用cars函数:

func InitializePage() {
  cars()
}

cars函数中,我们创建了一个nano、一个ambassador和一个omni——三个Car类型的实例。紧接着,我们使用汽车对象填充cars切片:

nano := models.Car{ModelName: "Nano", Color: "Yellow", Manufacturer: "Tata"}
ambassador := models.Car{ModelName: "Ambassador", Color: "White", Manufacturer: "HM"}
omni := models.Car{ModelName: "Omni", Color: "Red", Manufacturer: "Maruti Suzuki"}
cars := []models.Car{nano, ambassador, omni}

现在我们有了一个cars片段来填充表,现在是时候用以下代码生成表的每一行了:

  autoTableBody := D.GetElementByID("autoTableBody")
  for i := 0; i < len(cars); i++ {
    trElement := D.CreateElement("tr")
    tpl := template.New("template")
    tpl.Parse(CarItemTemplate)
    var buff bytes.Buffer
    tpl.Execute(&buff, cars[i])
    trElement.SetInnerHTML(buff.String())
    autoTableBody.AppendChild(trElement)
  }

在这里,我们声明并初始化了autoTableBody变量,它是表的tbody元素。这是我们将用于向表中追加新行的元素。我们循环通过cars切片,对于每个Car结构,我们使用Document对象的CreateElement方法动态创建一个tr元素。然后,我们创建一个新模板,并解析 car 项模板的内容。

我们声明一个名为buff的缓冲区变量,用于保存已执行模板的结果。我们在模板对象tpl上调用Execute函数,传入buffcars切片的i索引处的当前Car记录,这将是提供给内联模板的数据对象

然后我们调用tr元素对象上的SetInnerHTML方法,并传入buff变量的字符串值,该变量将包含呈现的模板内容。

这是 cars 表在填充所有行时的外观:

图 3.17:cars 表

此示例对于说明非常有用,但在现实场景中并不实用。随着项目代码库的扩展,在 Go 源文件中混合使用 HTML 编写的内联模板可能会成为无法维护的混乱。除此之外,如果我们能够在客户端访问服务器可以访问的面向用户的网页的所有模板,那就太好了。事实上,我们可以,这将是我们在第 4 章同构模板中的重点。

现在,我们已经看到了如何渲染内联模板,让我们考虑一下如何将 AutoT0.A.切片传输为服务器,作为二进制数据,以 AuthT1 格式编码。

传输 gob 编码数据

encoding/gob包为我们提供了管理 GOB 流的功能,GOB 流是编码器和解码器之间交换的二进制值。您使用编码器将值编码为gob编码数据,并使用解码器解码gob编码数据。

通过服务器端和客户端的 Go,我们创建了一个特定于 Go 的环境,如图 3.18所示。这是使用encoding/gob包作为客户端和服务器之间数据交换手段的理想环境:

图 3.18:特定于围棋的环境

我们将要传输的数据由cars切片组成。Car结构可以被认为是同构的,因为我们可以在客户端和服务器端使用Car结构。

请注意,在cars.go源文件中,我们已将encoding/gob包(以粗体显示)包含在我们的导入分组中:

import (
  "bytes"
  "encoding/gob"
  "html/template"

  "github.com/EngineerKamesh/igb/igweb/shared/models"

  "honnef.co/go/js/dom"
  "honnef.co/go/js/xhr"
)

我们使用以下代码将cars切片编码为gob格式:

  var carsDataBuffer bytes.Buffer
  enc := gob.NewEncoder(&carsDataBuffer)
  enc.Encode(cars)

这里我们声明了一个名为carsDataBuffer的字节缓冲区,它将包含gob编码的数据。我们创建了一个新的gob编码器,并指定要将编码数据存储到carsDataBuffer中。然后我们在gob编码器对象上调用Encode方法,并传入cars切片。此时,我们已将cars切片编码为carsDataBuffer

现在我们已经将cars切片编码为gob格式,我们可以使用HTTP POST方法通过 XHR 调用将gob编码数据传输到服务器:

  xhrResponse, err := xhr.Send("POST", "/cars-data", carsDataBuffer.Bytes())

  if err != nil {
    println(err)
  }

  println("xhrResponse: ", string(xhrResponse))

我们调用xhr包中的Send函数,并指定要使用POST方法,并将数据发送到/cars-dataURL。我们在carsDataBuffer上调用Bytes方法,以将缓冲区表示为字节片。我们将发送给服务器的就是这个字节片,它是gob编码的car片。

来自服务器的响应将存储在xhrResponse变量中,我们将在 web 控制台中打印此变量。 现在我们已经看到了程序的客户端,现在是时候来看看为/cars-data路由提供服务的服务器端处理程序函数了。让我们检查 handlers 目录中的carsdata.go源文件中定义的CarsDataHandler函数:

func CarsDataHandler(env *common.Env) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

    var cars []models.Car
    var carsDataBuffer bytes.Buffer

    dec := gob.NewDecoder(&carsDataBuffer)
    body, err := ioutil.ReadAll(r.Body)
    carsDataBuffer = *bytes.NewBuffer(body)
    err = dec.Decode(&cars)

    w.Header().Set("Content-Type", "text/plain")

    if err != nil {
      log.Println(err)
      w.Write([]byte("Something went wrong, look into it"))

    } else {
      fmt.Printf("Cars Data: %#v\n", cars)
      w.Write([]byte("Thanks, I got the slice of cars you sent me!"))
    }

  })
}

CarsDataHandler函数中,我们声明cars变量,它是Car对象的切片。就在下面,我们有carsDataBuffer,它将包含gob编码的数据,我们从客户端 web 应用发送的 XHR 调用中接收到这些数据。

我们创建了一个新的gob解码器,并指定gob数据将存储在carsDataBuffer中。然后,我们使用ioutil包中的ReadAll函数读取请求正文,并将所有内容保存到body变量中。

然后,我们创建一个新的字节缓冲区,并将body变量作为输入参数传递给NewBuffer函数。carsDataBuffer现在包含通过 XHR 调用传输的gob编码数据。最后,我们调用dec对象的Decode函数,将gob编码的数据转换回Car对象的切片。

如果我们没有收到任何错误,我们将cars切片打印到标准输出:

Cars Data: []models.Car{models.Car{ModelName:"Nano", Color:"Yellow", Manufacturer:"Tata"}, models.Car{ModelName:"Ambassador", Color:"White", Manufacturer:"HM"}, models.Car{ModelName:"Omni", Color:"Red", Manufacturer:"Maruti Suzuki"}}

除了将cars切片打印到标准输出之外,我们还向 web 客户端写回一个响应,指示cars的切片已成功接收。我们可以在 web 浏览器控制台中查看此消息:

图 3.19:服务器对 web 客户端的响应

本地存储

您知道 web 浏览器附带内置的键值数据库吗?这个数据库的名称是本地存储,在 JavaScript 中,我们可以访问localStorage对象作为window对象的属性。本地存储允许我们在 web 浏览器中本地存储数据。本地存储是按域和协议进行的,这意味着来自同一来源的页面可以访问和修改共享数据。

本地存储的一些好处如下:

  • 它提供安全的数据存储
  • 它的存储限制远大于 cookies(至少 5 MB)
  • 它提供低延迟数据访问
  • 这对需要脱机运行的 web 应用很有帮助(不需要 internet 连接)
  • 它可以用作本地缓存

通用本地存储操作

我们将向您展示如何使用 JavaScript 代码对localStorage对象执行一些常见操作。这些行动包括:

  1. 设置键值对
  2. 获取给定键的值
  3. 获取所有键值对
  4. 清除所有条目

在下一节中,我们将在一个完全充实的示例中向您展示如何使用 GopherJS 执行相同的操作。

设置键值对

要将项目存储到本地存储中,我们调用localStorage对象的setItem方法,并将键和值作为参数传递给该方法:

localStorage.setItem("foo", "bar"); 

这里我们提供了一个"foo"键,带有"bar"值。

获取给定键的值

要从本地存储中获取项目,我们调用localStorage对象的getItem方法,并将键作为单个参数传递给该方法:

var x = localStorage.getItem("foo");

这里我们提供了"foo"键,我们希望x变量的值等于"bar"

获取所有键值对

我们可以使用for循环从本地存储器中检索所有键值对,并使用localStorage对象的keygetItem方法访问键值的值:

for (var i = 0; i < localStorage.length; i++) {
  console.log(localStorage.key(i)); // prints the key
  console.log(localStorage.getItem(localStorage.key(i))); // prints the value
}

我们在localStorage对象上使用key方法,传入数字索引i,得到存储器中的 ith键。类似地,我们将i数字索引传递给localStorage对象的key方法,以获取存储中第 i 处的密钥名称。请注意,localStorage.key(i)方法调用获取密钥的名称,并将其传递给getItem方法以检索给定密钥的值。

清除所有条目

通过调用localStorage对象上的clear方法,可以轻松删除本地存储中的所有条目:

localStorage.clear();

建立本地存储检查器

有了上一节中介绍的关于如何使用localStorage对象的信息,让我们继续构建一个本地存储检查器。本地存储检查员将允许我们执行以下操作:

  • 查看当前存储在本地存储器中的所有键值对
  • 向本地存储添加新的键值对
  • 清除本地存储器中的所有键值对

图 3.20描述了本地存储检查器的用户界面:

图 3.20:本地存储演示用户界面

LocalStorage 演示标题下的方框是一个div容器,负责保存当前存储在本地存储器中的键值对列表。键输入文本字段是用户为键值对输入键的位置。值输入文本字段是用户输入键值对值的位置。单击 Save(保存)按钮将新的键值条目保存到本地存储器中。单击全部清除按钮,将清除本地存储器中的所有键值条目。

创建用户界面

我们已经在shared/templates/layouts文件夹中找到的localstorage_layout.tmpl源文件中定义了本地存储演示页面的布局:

<!doctype html>
<html>
  {{ template "partials/localstorageheader_partial" }}
  <body>
    <div class="pageContent" id="primaryContent">
      {{ template "pagecontent" . }}
    </div>

  <script type="text/javascript" src="/js/client.js"></script>

  </body>
</html>

此布局模板定义本地存储演示网页的布局。我们使用模板操作(以粗体显示)呈现partials/localstorageheader_partial标题模板和pagecontent页面内容模板。

请注意,在网页的底部,我们包含了 JavaScript 源文件client.js,它是 GopherJS 使用script标记生成的(以粗体显示)。

我们已经在shared/templates/partials文件夹中找到的localstorageheader_partial.tmpl源文件中为本地存储演示页面定义了头模板:

<head>
  <title>LocalStorage Demo</title> 
  <link rel="icon" type="image/png" href="/statimg/isomorphic_go_icon.png">
 <link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css" integrity="sha384-nn4HPE8lTHyVtfCBi5yW9d20FjT8BJwUXyWZT9InLYax14RDjBj46LmSztkmNP9w" crossorigin="anonymous">
 <link rel="stylesheet" type="text/css" href="/static/css/igweb.css">
 <link rel="stylesheet" type="text/css" href="/static/css/localstoragedemo.css">
</head>

此标题模板用于呈现head标记,其中我们使用link标记(以粗体显示)包含外部 CSS 样式表。

我们已经在shared/templates文件夹中找到的localstorage_example_page.tmpl源文件中为本地存储演示的用户界面定义了 HTML 标记:

{{ define "pagecontent" }}

<h1>LocalStorage Demo</h1>

    <div id="inputFormContainer">
      <form class="pure-form">
      <fieldset class="pure-group" style="min-height: 272px">
      <div id="storageContents">
 <dl id="itemList">
 </dl>
 </div>
      </fieldset>

      <fieldset class="pure-control-group">
      <label for="messageInput">Key: </label>
      <input id="itemKey" type="text" value="" />
      <label for="messageInput">Value: </label>
      <input id="itemValue" type="text" value="" />

      </fieldset>

      <fieldset class="pure-control-group">
      </fieldset>

      <fieldset class="pure-group">
        <button id="saveButton" type="button" class="pure-button pure-button-primary">Save</button>
 <button id="clearAllButton" type="button" class="pure-button pure-button-primary">Clear All</button>
      </fieldset>
      </form>
    </div>

{{end}}
{{template "layouts/localstorage_layout" . }}

div元素的id"storageContents"将用于在本地存储数据库中存储条目列表。事实上,我们将使用 dl(description list)元素和id"itemList"来显示所有键值对。

我们已经定义了一个输入文本字段供用户输入键,我们还定义了一个输入文本字段供用户输入值。我们还为Save按钮定义了标记,在它的正下方,我们为Clear All按钮定义了标记。

设置服务器端路由

我们已经在igweb.go源文件中找到的registerRoutes函数中注册了/localstorage-demo路由:

r.Handle("/localstorage-demo", handlers.LocalStorageDemoHandler(env)).Methods("GET")

我们已经定义了LocalStorageDemoHandler服务器端处理函数来服务handlers文件夹中的localstoragedemo.go源文件中的/localstorage-demo服务器端路由:

package handlers

import (
  "net/http"

  "github.com/EngineerKamesh/igb/igweb/common"
  "github.com/isomorphicgo/isokit"
)

func LocalStorageDemoHandler(env *common.Env) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    env.TemplateSet.Render("localstorage_example_page", &isokit.RenderParams{Writer: w, Data: nil})
  })
}

LocalStorageDemoHandler函数负责将网页响应写入客户端。它调用应用的TemplateSet对象的Render方法来呈现localstorage_example_page模板。您将在第 4 章同构模板中了解有关呈现同构模板的更多信息

实现客户端功能

实现本地存储检查器的客户端功能包括以下步骤:

  1. 正在初始化本地存储检查器网页
  2. 实施本地存储检查器

正在初始化本地存储检查器网页

为了初始化本地存储检查器网页上的事件处理程序,我们需要在本地存储演示case中,在client.go源文件中找到的initializePage函数中添加以下代码行:

localstoragedemo.InitializePage()

调用localstoragedemo包中定义的InitializePage函数将为保存和清除所有按钮添加事件侦听器。

实施本地存储检查器

本地存储检查器的实现可以在client/localstoragedemo目录下的localstorage.go源文件中找到。

import分组中,我们包括jsdom套餐(以粗体显示):

package localstoragedemo

import (
 "github.com/gopherjs/gopherjs/js"
 "honnef.co/go/js/dom"
)

我们已经定义了localStorage变量,并为其分配了连接到window对象的localStorage对象的值:

var localStorage = js.Global.Get("localStorage")

像往常一样,我们用D变量给Document对象加上别名,以节省一些输入:

var D = dom.GetWindow().Document().(dom.HTMLDocument)

InitializePage功能负责设置保存和清除所有按钮的事件侦听器:

func InitializePage() {
 saveButton := D.GetElementByID("saveButton").(*dom.HTMLButtonElement)
 saveButton.AddEventListener("click", false, func(event dom.Event) {
 Save()
 })

 clearAllButton := D.GetElementByID("clearAllButton").(*dom.HTMLButtonElement)
 clearAllButton.AddEventListener("click", false, func(event dom.Event) {
 ClearAll()
 })

 DisplayStorageContents()
}

我们通过调用Document对象的GetElementByID方法获取saveButton元素,并提供id"saveButton"作为该方法的唯一输入参数。在这下面,我们在 click 事件上添加了一个事件监听器来调用Save函数。调用Save函数将保存一个新的键值对条目。

我们还通过调用Document对象的GetElementByID方法并提供id"clearAllButton"作为该方法的唯一输入参数来获取clearAllButton元素。就在下面,我们在 click 事件上添加了一个事件监听器来调用ClearAll函数。调用ClearAll函数将清除当前存储在本地存储器中的所有键值对。

Save功能负责将键值对保存到 web 浏览器的本地存储器中:

func Save() {

 itemKey := D.GetElementByID("itemKey").(*dom.HTMLInputElement)
 itemValue := D.GetElementByID("itemValue").(*dom.HTMLInputElement)

  if itemKey.Value == "" {
    return
  }

  SetKeyValuePair(itemKey.Value, itemValue.Value)
  itemKey.Value = ""
  itemValue.Value = ""
  DisplayStorageContents()
}

我们使用Document对象的GetElementByID方法获取键和值的文本输入字段(以粗体显示)。在if条件块中,我们检查用户是否没有为键输入文本字段输入值。如果它们没有输入值,我们将从函数返回。

如果用户在键输入文本字段中输入了一个值,我们将继续前进。我们调用SetKeyValuePair函数,并提供itemKeyitemValue的值作为该函数的输入参数

We then set the Value property of both itemKey and itemValue to an empty string, to clear the input text field, so that the user can easily add new entries later without having to manually clear the text in these fields.

最后我们调用DisplayStorageContents函数,它负责显示本地存储器中的所有当前条目。

让我们看一下函数的函数:

func SetKeyValuePair(itemKey string, itemValue string) {
  localStorage.Call("setItem", itemKey, itemValue)
}

在这个函数中,我们只需调用localStorage对象的setItem方法,将itemKeyitemValue作为输入参数传递给函数。此时,键值对条目将保存到 web 浏览器的本地存储器中。

DisplayStorageContents功能负责显示itemList元素中本地存储的所有键值对,这是一个dl(描述列表)元素:

func DisplayStorageContents() {

  itemList := D.GetElementByID("itemList")
  itemList.SetInnerHTML("")

  for i := 0; i < localStorage.Length(); i++ {

    itemKey := localStorage.Call("key", i)
    itemValue := localStorage.Call("getItem", itemKey)

    dtElement := D.CreateElement("dt")
    dtElement.SetInnerHTML(itemKey.String())

    ddElement := D.CreateElement("dd")
    ddElement.SetInnerHTML(itemValue.String())

    itemList.AppendChild(dtElement)
    itemList.AppendChild(ddElement)
  }

}

我们调用输入值为空字符串的SetInnerHTML方法来清除列表的内容。

我们使用for循环遍历本地存储中的所有条目。对于存在的每个键值对,我们分别通过调用localStorage对象的keygetItem方法获得itemKeyitemValue

我们使用一个dt元素(dtElement来显示键。dt元素用于定义描述列表中的术语。我们使用一个dd元素(ddElement来显示该值。dd元素用于描述描述列表中的术语。使用描述列表及其相关元素来显示键值对,我们使用语义友好的方法在网页上显示键值对。我们通过调用AppendChild方法将dtdd元素附加到itemList对象。

ClearAll功能用于删除所有保存在本地存储器中的键值对:

func ClearAll() {
  localStorage.Call("clear")
  DisplayStorageContents()
}

我们调用localStorage对象的clear方法,然后调用DisplayStorageContents函数。如果一切正常,则应清除所有项目,单击“全部清除”按钮后,itemList元素中不会出现任何值。

运行本地存储演示

您可以在http://localhost:8080/localstorage-demo访问本地存储演示。

让我们向本地存储添加一个新的键值对。在键输入文本字段中,我们添加"foo"键,在值输入文本字段中,我们添加"bar"值。单击 Save 按钮将新的键值对添加到本地存储。

图 3.21显示点击保存按钮后,新创建的键值对出现:

图 3.21:显示新添加的键值对的本地存储检查器

尝试刷新网页,然后尝试重新启动 web 浏览器并返回网页。请注意,在这些场景中,本地存储仍然保留保存的键值对。点击全部清除按钮,您会注意到itemList已被清除,如图 3.20所示,因为本地存储已清空所有键值对。

我们刚刚创建的本地存储检查器特别便于检查由第三方 JavaScript 解决方案填充的键值对,该解决方案由我们的客户端 web 应用使用。如果您登陆本地存储演示页面,在查看 IGWEB 主页上的图像转盘后,您会注意到 itemList 中填充了键-值对,如图 3.22 所示:

图 3.22:显示由图像转盘填充的键值对的本地存储演示

这些键值对由图像转盘填充,我们将在第 9 章Cogs–可重用组件中将其作为可重用组件实现。

总结

在本章中,我们介绍了使用 GopherJS 在前端使用 Go 进行编程。我们向您介绍了 DOM,并展示了如何使用 GopherJS 访问和操作它。我们向您介绍了几个微示例,让您了解使用 GopherJS 进行编码的情况。然后,我们继续向您展示完全充实的示例。

我们向您展示了如何显示警报对话框和显示自定义消息。我们还向您展示了如何更改元素的 CSS 样式属性。我们继续向您展示如何使用jsbuiltin包在 Go 的范围内调用 JavaScript 的typeof操作符。我们向您展示了如何创建一个简单的小写文本转换器,并演示了如何使用xhr包发送XHR Post。我们还向您展示了如何呈现内联 Go 模板,最后,我们向您展示了如何构建本地存储检查器。

第 4 章同构模板中,我们将介绍同构模板,即可以在服务器端或客户端呈现的模板