在本书的前五章中,我们着重于为 IGWEB 上的特定网页或特定功能开发功能,例如我们在上一章中实现的实时聊天功能。到目前为止,我们所提出的解决方案都达到了特定的目的。在促进特定用户界面特性的代码重用方面没有考虑太多因素,因为我们不需要创建它的多个实例。
可重用组件是用户界面小部件,提供了提高可重用性的方法。它们可以以即插即用的方式使用,因为每个组件都是一个独立的用户界面小部件,其中包含自己的一组 Go 源文件和静态资产,如 Go 模板文件,以及 CSS 和 JavaScript 源文件。
在本章中,我们将重点创建cogs——可在同构 Go web 应用中使用的可重用组件。术语cog
表示 Go 中的组件对象。cog 是可重用的用户界面小部件,可以专门在 Go 中实现(一种纯 cog**),也可以使用 Go 和 JavaScript 实现(一种混合 cog)。**
我们可以创建一个cog
的多个实例,并通过向cog
(称为道具提供输入参数(以键值对的形式)来控制 cog 的行为。当对道具进行后续更改时,cog
为反应性,这意味着它可以自动重新渲染自身。因此,齿轮有能力根据对其道具的更改来更改其外观。
也许,COG 最吸引人的特点是它们易于重用。cog 实现为独立的 Go 包,其中包含一个或多个 Go 源文件以及 cog 实现所需的任何静态资产。
在本章中,我们将介绍以下主题:
- 基本概念
- 实现纯齿轮
- 实现混合 cogs
Cogs(Go 中的组件对象)是在 Go 中实现的可重用组件。cogs 背后的指导思想是允许开发人员以惯用的方式在前端创建可重用组件。COG 是自包含的,定义为它们自己的 Go 包,这使得它们易于重用和维护。由于 COG 是自包含的,所以可以使用 COG 创建可组合的用户界面。
cog 遵循明确的关注点分离,其中cog
的表示层使用一个或多个 Go 模板实现,cog 的控制器逻辑在 Go 包中包含的一个或多个 Go 源文件中实现。这些 Go 源文件可以从标准库或第三方库导入 Go 包。当我们在本章的实现纯 cog部分中实现时间前 cog 时,我们将看到一个例子。
COG 还可能具有与之相关联的 CSS 样式表和 JavaScript 代码,允许cog
开发人员/维护人员根据需要利用预构建的 JavaScript 解决方案,而不是直接移植 JavaScript 小部件。这使得 cogs 可以与现有的 JavaScript 解决方案进行互操作,并防止出现开发人员不必重新发明众所周知的轮子就能节省宝贵时间的情况。例如,Pikaday(https://github.com/dbushell/Pikaday 是一个成熟的日历日期选择器 JavaScript 小部件。在本章的实现混合 cogs部分中,我们将学习如何实现利用 Pikaday JavaScript 小部件提供的功能的日期选择器cog
。使用日期选择器cog
的 Go 开发人员不需要任何 JavaScript 知识,只需要具备 Go 知识就可以使用它。
每个cog
都有一个虚拟 DOM 树,这是其实际 DOM 树的内存表示。操纵 cog 的内存中虚拟 DOM 树比操纵实际的 DOM 树本身要高效得多。图 9.1是一个维恩图,描绘了 cog 的虚拟 DOM 树、两棵树之间的差异以及实际的 DOM 树:
图 9.1:描述虚拟 DOM、差异和实际 DOM 的维恩图
随着 cog 属性的更改(道具),cog 的渲染引擎将利用其虚拟 DOM 树来确定更改,然后将更改与实际 DOM 树协调。这使得cog
可以反应,这意味着cog
可以在其道具更新时自动重新呈现自身。以这种方式,COG 降低了更新用户界面时所涉及的复杂性。
UX 工具包提供了在cog
包中实现 cogs 的功能,可使用以下go get
命令安装:
$ go get -u github.com/uxtoolkit/cog
所有 COG 必须实现Cog
接口:
type Cog interface {
Render() error
Start() error
}
Render
方法负责在网页上呈现cog
。如果渲染过程中出现任何错误,该方法将返回一个error
对象。
Start
方法负责激活cog
。如果cog
无法启动,该方法将返回一个error
对象。
cog
包包含两个重要的导出变量,ReactivityEnabled
和VDOMEnabled
。这两个导出变量的类型均为bool
,默认情况下,它们都设置为true
。
当变量ReactivityEnabled
设置为true
时,COG 会随着道具的更改而重新呈现。如果ReactivityEnabled
设置为false
,则必须显式调用 cog 的Render
方法来重新渲染 cog。
当变量VDOMEnabled
设置为true
时,将利用 cog 的虚拟 DOM 树呈现 cog。如果VDOMEnabled
设置为false
,则cog
将通过替换内部 HTML 操作使用实际的 DOM 树进行渲染。这可能是一个昂贵的操作,可以通过利用 cog 的虚拟 DOM 树来避免。
UXCog
类型实现Cog
接口的Render
方法。以下是UXCog struct
的外观:
type UXCog struct {
Cog
cogType reflect.Type
cogPrefixName string
cogPackagePath string
cogTemplatePath string
templateSet *isokit.TemplateSet
Props map[string]interface{}
element *dom.Element
id string
hasBeenRendered bool
parseTree *reconcile.ParseTree
cleanupFunc func()
}
UXCog
类型提供使齿轮工作的基本功能。这意味着为了实现我们自己的 COG,我们必须在我们创建的所有 COG 的类型定义中嵌入[T1]类型。我们特别感兴趣的是UXCog
类型的以下方法(为简洁起见,仅提供方法签名):
func (u *UXCog) ID() string
func (u *UXCog) SetID(id string)
func (u *UXCog) CogInit(ts *isokit.TemplateSet)
func (u *UXCog) SetCogType(cogType reflect.Type)
func (u *UXCog) SetProp(key string, value interface{})
func (u *UXCog) Render() error
ID
方法是一个 getter 方法,它返回 DOM 中 cog 的div
容器的 ID。cog 的div
容器称为其安装点。
SetID
方法是一种 setter 方法,用于在 DOM 中设置 cog 的div
容器的 ID。
CogInit
方法用于将cog
与应用的TemplateSet
对象关联。这种方法有两个重要目的。首先,该方法用于在服务器端注册一个cog
,使得给定cog
的所有模板都包含在isokit
内置的静态资产捆绑系统生成的模板捆绑中。其次,在客户端调用 cog 的CogInit
方法提供了对客户端应用TemplateSet
对象的cog
访问,允许cog
在网页上呈现自己。
SetCogType
方法允许我们通过对新实例化的cog
执行运行时反射来动态设置 cog 的类型。这提供了 isokit 的静态资产绑定系统所需的钩子,用于绑定与给定cog
关联的模板文件、CSS 源文件和 JavaScript 源文件。
SetProp
方法用于在 cog 的Props
映射中设置一个键值对,类型为map[string]interface{}
。地图的key
代表道具的名称,值代表道具的值。
Render
方法负责将cog
呈现给 DOM。如果在cog
被渲染后对其进行了更改(其属性值被更新),则cog
将被重新渲染。
您可以访问 UX 工具包网站了解有关 cogs 的更多信息:http://uxtoolkit.io 。
现在我们已经熟悉了UXCog
型,是时候检查cog
的解剖结构了。
对于 IGWEB 项目,我们将在$IGWEB_APP_ROOT/shared/cogs
文件夹中创建 COG。在阅读本节时,您可以浏览一下[T1]之前的版本,其实现可以在[T2]文件夹中找到,以查看本文所述概念的具体实现
仅出于说明目的,我们将引导您完成创建名为widget
的简单cog
的过程。
包含在widget
文件夹中的小部件cog
的项目结构按以下方式组织:
⁃ widget
⁃ widget.go
⁃ templates
⁃ widget.tmpl
widget.go
源文件将包含小部件cog
的实现。
templates
文件夹包含用于实现cog
的模板源文件。如果要在网页上呈现cog
,则必须至少存在一个模板源文件。模板源文件的名称必须与cog
的包名称匹配。例如,对于cog
包widget
,模板源文件的名称必须为widget.tmpl
。
COG 在命名包名和源文件时遵循约定而非策略。由于我们选择了名称widget
,我们必须在widget.go
源文件中声明一个名为widget
的 Go 包:
package widget
所有 COG 需要在其进口分组中包括errors
包、reflect
包和cog
包:
import (
"errors"
"reflect"
"github.com/uxtoolkit/cog"
)
我们必须声明一个名为cogType
的未导出的包范围变量:
var cogType reflect.Type
此变量表示 cog 的类型。我们调用reflect
包中的TypeOf
函数,传入新创建的cog
实例,在cog
包的init
函数中动态设置 cog 的类型:
func init() {
cogType = reflect.TypeOf(Widget{})
}
这为 isokit 的静态捆绑系统提供了一个钩子,让它知道在哪里寻找,以获取实现cog
功能所需的静态资产。
cog
实现了一个特定的类型。对于小部件,我们实现了Widget
类型。以下是Widget struct
:
type Widget struct {
cog.UXCog
}
为了实现cog
,我们必须嵌入cog.UXCog
类型,以便从cog.UxCog
类型中获得所需的所有功能。
struct
可能包含实现cog
所需的其他字段定义,具体取决于cog
的用途。
每个cog
实现都应该包含一个构造函数:
func NewWidget() *Widget {
w := &Widget{}
w.SetCogType(cogType)
return f
}
与任何典型的构造函数一样,其目的是创建 cog 的新实例Widget
。
cog 的构造函数必须包含调用SetCogType
方法的行(以粗体显示)。isokit 的自动静态资产捆绑系统将其用作挂钩,以捆绑 cog 所需的静态资产。
根据 cog 的实现,可以设置Widget
类型的附加字段来初始化cog
。
为了实现Cog
接口的实现,所有 COG 必须实现Start
方法:
func (w *Widget) Start() error {
var allRequiredConditionsHaveBeenMet bool = true
Start
方法负责激活cog
,包括将cog
初始呈现到网页。Start
方法返回error
对象,如果cog
启动失败,则返回nil
值。
为了便于说明,我们定义了一个[T0]条件块,其中包含一个名为[T1]的布尔变量:
if allRequiredConditionsHaveBeenMet == false {
return errors.New("Failed to meet all requirements, cog failed to start!")
}
如果满足启动cog
的所有条件,该变量将等于true
。否则,它将等于false
。如果是false
,则返回一个新的error
对象,表示cog
无法启动,因为没有满足所有要求。
我们可以通过调用SetProp
方法在 cog 的Props
映射中设置键值对:
w.SetProp("foo", "bar")
在本例中,我们已将名为foo
的道具设置为值bar
。Props
映射将自动用作数据对象,输入 cog 的模板。这意味着 cog 的模板可以访问Props
地图中定义的所有道具。
按照惯例,cog 的模板源文件名必须命名为widget.tmpl
,以匹配 cog 的包名widget
,并且模板文件应位于 cog 文件夹widget
中的templates
文件夹中。
让我们快速查看一下 Type T0.源文件的外观:
<p>Value of Foo is: {{.foo}}</p>
请注意,我们可以打印出模板中键为foo
的道具的值。
让我们回到小部件 cog 的Start
方法。我们称 cog 的Render
方法在 web 浏览器中呈现cog
:
err := w.Render()
if err != nil {
return err
}
如果在渲染cog
时遇到错误,Render
方法返回error
对象,否则返回值nil
表示cog
渲染成功。
如果cog
渲染成功,cog 的Start
方法返回nil
值,表示cog
已成功启动:
return nil
为了将我们的cog
呈现给真实的 DOM,我们需要一个地方将cog
呈现给。容纳cog
渲染内容的div
容器称为其装入点。挂载点是在 DOM 中呈现[T4]的位置。为了在主页上呈现小部件cog
,我们将向主页的内容模板添加以下标记:
<div data-component="cog" id="widgetContainer"></div>
通过将data-component
属性设置为"cog"
,我们表示div
元素将用作 cog 的挂载点,cog 的渲染内容将包含在此元素中。
在客户端应用中,小部件cog
可以如下实例化:
w := widget.NewWidget()
w.CogInit(env.TemplateSet)
w.SetID("widgetContainer")
w.Start()
w.SetProp("foo", "bar2")
我们创建一个新的Widget
实例并将其分配给变量w
。我们必须调用cog
的CogInit
方法将应用的TemplateSet
对象与cog
关联起来。cog
使用TemplateSet
以便获取呈现cog
所需的相关模板。我们称 cog 的SetID
方法,将id
传递给div
元素,作为 cog 的安装点。我们调用 cog 的Start
方法来激活cog
。由于Start
方法调用 cog 的Render
方法,cog 将在指定的装入点呈现,即 id 为"widgetContainer"
的div
元素。最后,当我们调用SetProp
方法并将"foo"
道具的值更改为"bar2"
时,cog
将自动重新渲染。
现在我们已经研究了一个 Ty0 T0 的基本解剖结构,让我们考虑如何使用虚拟 DOM 渲染 COG。
每个cog
实例都有一个与之关联的虚拟 DOM 树。这个虚拟 DOM 树是由 cog 的div
容器的所有子级组成的解析树。
图 9.2是一个流程图,描述了将cog
重新呈现(通过应用对账)到 DOM 的过程:
图 9.2:绘制和重新绘制 cog 的流程图
当在 DOM 中首次呈现[T0]时,将执行替换内部 HTML 操作。替换 DOM 中元素的内部 HTML 内容的操作是一项昂贵的操作。因此,它不会在cog
的后续渲染上执行。
对 cog 的Render
方法的所有后续调用都将利用 cog 的虚拟 DOM 树。cog 的虚拟 DOM 树用于跟踪 cog 的当前虚拟 DOM 树和 cog 的新虚拟 DOM 树之间的更改。更新 cog 的 prop 值后,cog
将有一个新的虚拟 DOM 树与当前的虚拟 DOM 树进行比较。
让我们考虑一个带有小部件 COG 的示例场景。调用小部件 cog 的Start
方法将执行cog
的初始渲染(因为 cog 的Render
方法在Start
方法中被调用)。cog
将有一个虚拟 DOM 树,该树将是保存 cog 呈现内容的div
容器的解析树。如果我们通过调用cog
上的SetProp
方法来更新"foo"
道具(在 cog 的模板中呈现),那么Render
方法将自动被调用,因为cog
是被动的。在cog
上执行后续渲染操作后,cog 的当前虚拟 DOM 树将与 cog 的新虚拟 DOM 树(更新 cog 道具后创建的虚拟 DOM 树)不同。
如果当前虚拟 DOM 树和新虚拟 DOM 树之间没有更改,则无需执行任何操作。但是,如果当前虚拟 DOM 树和新虚拟 DOM 树之间存在差异,那么我们必须将构成差异的更改应用于实际 DOM。应用这些变更的过程称为对账。执行对账可以避免执行昂贵的替换内部 HTML 操作。成功应用协调后,cog 的新虚拟 DOM 树将被视为 cog 的当前虚拟 DOM 树,为下一个渲染周期准备cog
:
图 9.3:cog 的现有虚拟 DOM 树(左)和 cog 的新虚拟 DOM 树(右)
图 9.3在左侧描绘了 cog 的现有虚拟 DOM 树,在右侧描绘了 cog 的新虚拟 DOM 树。在对两个虚拟 DOM 树(新的和现有的)执行diff
操作后,确定最右边的div
元素(包含ul
元素)及其子元素已更改,并且协调操作将仅更新实际 DOM 中的div
元素及其子元素。
图 9.4描述了一个cog
的生命周期,它从服务器端开始,我们首先在服务器端注册cog
。cog 的类型必须在服务器端注册,以便 cog 的关联模板以及其他静态资产可以自动绑定并提供给客户端应用:
图 9.4:cog 的生命周期
cog
生命周期中的后续步骤在客户端进行。我们通过引入一个数据组件属性等于"cog"
的div
元素来声明cog
的装入点,以指示div
元素是cog
的装入点。
下一步是通过调用cog
的构造函数来创建cog
的新实例。我们通过调用cog
的CogInit
方法初始化cog
并传入客户端应用的TemplateSet
对象。初始化cog
还包括调用 cog 的SetID
方法将挂载点关联到cog
(以便cog
知道渲染到哪里)。Cog
初始化还包括在调用Start
方法之前通过调用 cog 的Props map
方法来设置道具。
请注意,在调用 cog 的Start
方法之前调用 cog 的SetProp
方法不会呈现cog
。cog
只有在cog
通过调用其Start
方法呈现到挂载点后,才会在调用其SetProp
方法时重新呈现。
调用 Cog 的Start
方法将激活cog
并将 Cog 的内容呈现到指定的装入点。
对 cog 的SetProp
方法的任何后续调用都将导致cog
的重新呈现。
当用户导航到网站上的另一个页面时,cog
所在的容器被移除,有效地破坏了cog
。用户可以指定一个清理函数,该函数应该在销毁[T2]之前调用。这可以在 cog 销毁之前以负责任的方式释放资源。我们将在本章后面看到一个实现清理功能的示例。
现在我们已经对 cogs 有了基本的了解,是时候在实践中实现一些 cogs 了。尽管 COG 在客户端运行,但需要注意的是,服务器端应用需要通过注册它们来确认它们的存在。出于这个原因,COG 的代码被战略性地放在shared/cogs
文件夹中
纯齿轮仅在 Go 中实现。正如您将看到的,我们可以利用现有 Go 包的功能来实现 cogs。
在igweb.go
源文件的 main 函数中,我们调用传入应用模板集的initailizeCogs
函数:
initializeCogs(env.TemplateSet)
initializeCogs
函数负责初始化将在同构 Go web 应用中使用的所有 COG:
func initializeCogs(ts *isokit.TemplateSet) {
timeago.NewTimeAgo().CogInit(ts)
liveclock.NewLiveClock().CogInit(ts)
datepicker.NewDatePicker().CogInit(ts)
carousel.NewCarousel().CogInit(ts)
notify.NewNotify().CogInit(ts)
isokit.BundleStaticAssets()
}
注意,initializeCogs
函数只接受一个输入参数ts
,即TemplateSet
对象。我们调用 cog 的构造函数来创建cog
的新实例,并立即调用cog
的CogInit
方法,将TemplateSet
对象ts
作为该方法的输入参数传入。这允许cog
将其模板包括在应用的模板集中,以便后续生成的模板包将包括与cog
关联的模板。
我们调用BundleStaticAssets
方法来生成每个cog
所需的静态资产(CSS 和 JavaScript 源文件)。将生成两个文件。第一个文件是cogimports.css
,它将包含所有 COG 所需的 CSS 源代码,第二个文件是cogimports.js
,它将包含所有 COG 所需的 JavaScript 源代码。
现在,我们已经看到了 COG 是如何在服务器端初始化的,现在是时候来看看制作cog
的原因了。我们将首先制作一个非常简单的cog
,一段时间前的cog
,它以人类可以理解的格式显示时间。
是时候在 about 页面上重新访问 gopher bios 了。在第 3 章中的自定义模板函数部分与 GopherJS一起进入前端,我们学习了如何使用自定义模板函数以 Ruby 格式显示 gopher 的开始日期时间。
我们将更进一步,通过实现一个 time agocog
,以人类可以理解的格式显示开始日期时间。图 9.5以默认 Go 格式、Ruby 格式和人类可理解格式显示 Molly 的开始日期:
图 9.5:描述时间间隔 cog 的插图,这是以人类可读格式显示时间的最后一行
Molly 于 2017 年 5 月 24 日加入了 IGWEB 团队,这是 6 个月前以人类可读的格式出现的(在撰写本文时)。
在about_content.tmpl
模板源文件中,我们为之前的cog
引入了一个div
容器:
<h1>About</h1>
<div id="gopherTeamContainer">
{{range .Gophers}}
<div class="gopherContainer">
<div class="gopherImageContainer">
<img height="270" src="{{.ImageURI}}">
</div>
<div class="gopherDetailsContainer">
<div class="gopherName"><h3><b>{{.Name}}</b></h3></div>
<div class="gopherTitle"><span>{{.Title}}</span></div>
<div class="gopherBiodata"><p>{{.Biodata}}</p></div>
<div class="gopherStartTime">
<p class="standardStartTime">{{.Name}} joined the IGWEB team on <span class="starttime">{{.StartTime}}.</p>
<p class="rubyStartTime">That's <span class="starttime">{{.StartTime | rubyformat}}</span> in Ruby date format.</p>
<div class="humanReadableGopherTime">That's
<div id="Gopher-{{.Name}}" data-starttimeunix="{{.StartTime | unixformat}}" data-component="cog" class="humanReadableDate starttime"></div>
in Human readable format.
</div>
</div>
</div>
</div>
{{end}}
</div>
请注意,我们已经为名为data-component
的属性分配了一个值cog
。这表示该div
容器将用作装入点,容纳cog
的渲染内容。我们将容器的id
属性设置为地鼠的名字,前缀为"Gopher-"
。
稍后您将看到,当我们实例化一个cog
时,我们必须为一个 cog 的div
容器提供一个 ID,以便cog
实例知道它的装入点是cog
应该向其提供输出的位置。我们定义了另一个自定义数据属性starttimeunix
,并将其设置为 Gopher 开始为 IGWEB 工作时的 Unix 时间戳值。
回想一下,该值是通过调用模板操作获得的,该操作将通过将StartTime
属性管道化到自定义模板函数unixformat
获得的值放入其中。
unixformat
自定义模板函数是shared/templatefuncs/funcs.go
源文件中定义的UnixTime
函数的别名:
func UnixTime(t time.Time) string {
return strconv.FormatInt(t.Unix(), 10)
}
对于给定的Time
实例,此函数将以 Unix 格式以string
值的形式返回时间。
返回到about_content.tmpl
源文件,注意提供给div
容器的humanReadableDate
CSSclassName
。稍后,我们将使用此 CSSclassName
获取 About 页面上的所有timeago
cogdiv
容器。
现在,我们已经看到了如何在大约页上声明 COG 的 Tyt0}容器,让我们来看看如何实现前一次的 Tyl T1。
前一次cog
是一次纯粹的围棋cog
。这意味着它只使用 Go 实现。Go 包go-humanize
为我们提供了以人类可读格式显示时间所需的功能。我们将利用此软件包来实现时间之前的cog
。以下是go-humanize
包的 GitHub 页面的 URL:https://github.com/dustin/go-humanize 。
让我们检查一下shared/cogs/timeago/timeago.go
源文件。我们首先将包名声明为timeago
:
package timeago
在我们的导入分组中,我们包括github.com/uxtoolkit/cog
,该软件包为我们提供了实现cog
的功能(以粗体显示)。我们将go-humanize
分组包含在我们的导入分组中,并将其别名为"humanize"
(粗体显示):
import (
"errors"
"reflect"
"time"
humanize "github.com/dustin/go-humanize"
"github.com/uxtoolkit/cog"
)
所有 COG 必须声明一个名为cogType
的未报告变量,其类型为reflect.Type
:
var cogType reflect.Type
在init
函数中,我们通过对新创建的TimeAgo
实例调用reflect.TypeOf
函数返回的值来分配cogType
变量:
func init() {
cogType = reflect.TypeOf(TimeAgo{})
}
我们实现的每个cog
都需要初始化cogType
变量。正确设置cogType
允许静态资产绑定系统在 web 应用中考虑 cog 的静态资产依赖关系。cogType
将用于收集实现cog
功能所需的所有模板和静态资产。
以下是我们用来定义TimeAgo cog
的struct
:
type TimeAgo struct {
cog.UXCog
timeInstance time.Time
}
请注意,我们在struct
定义中嵌入了ux.UXCog
。如前所述,cog.UXCog
类型将为我们提供必要的功能,使我们能够呈现cog
。除了嵌入ux.UXCog
之外,我们还声明了一个名为timeInstance
的未报告字段,类型为time.Time
。这将包含我们将转换为人类可读格式的time.Time
实例。
我们创建了一个名为NewTimeAgo
的构造函数,它返回一个新的TimeAgo cog
实例:
func NewTimeAgo() *TimeAgo {
t := &TimeAgo{}
t.SetCogType(cogType)
return t
}
我们这里的构造函数遵循与 Go 中实现的任何其他构造函数相同的模式。注意,我们将cogType
传递给新创建的TimeAgo
实例的SetCogType
方法。这是必需的,以便 cog 的静态资产包含在由 isokit 的静态资产捆绑系统生成的静态资产捆绑中。
我们为TimeAgo
结构的timeInstance
字段创建一个名为SetTime
的 setter 方法:
func (t *TimeAgo) SetTime(timeInstance time.Time) {
t.timeInstance = timeInstance
}
客户端应用将使用此 setter 方法设置TimeAgo
cog 的时间。我们将使用SetTime
方法设置 gopher 加入 IGWEB 团队的开始日期。
为了实现Cog
接口,cog
必须定义Start
方法。Start
方法是cog
中的动作发生的地方。通过阅读cog
的Start
方法,您应该能够大致了解cog
的功能。以下是TimeAgo
cog 的Start
方法:
func (t *TimeAgo) Start() error {
if t.timeInstance.IsZero() == true {
return errors.New("The time instance value has not been set!")
}
t.SetProp("timeAgoValue", humanize.Time(t.timeInstance))
err := t.Render()
if err != nil {
return err
}
return nil
}
Start
方法返回一个错误对象,通知调用方cog
是否正确启动。在执行任何活动之前,检查是否设置了timeInstance
值。我们使用一个if
条件语句来检查timeInstance
值是否为零值,表明它尚未设置。如果出现这种情况,该方法将返回一个新创建的error
对象,指示尚未设置时间值。如果设置了timeInstance
值,我们继续前进。
我们调用 cog 的SetProp
方法,用人类可以理解的时间值设置timeAgoValue
属性。我们通过从go-humanize
包(别名为humanize
)调用Time
函数并将 cog 的timeInstance
值传递给它,从而获得人类可以理解的时间值。
我们调用 cog 的Render
方法来呈现cog
。如果在尝试呈现cog
时出错,Start
方法将返回error
对象。否则,将返回一个值nil
,表示启动cog
时没有错误。
此时,我们已经实现了[T0]cog 的 Go 部分。为了使人类可读的时间出现在网页上,我们必须实现 cog 的模板。
timeago.tmpl
文件(位于shared/cogs/timeago/templates
目录中)是一个简单的单行模板。我们声明以下span
元素,并且我们有一个模板操作来呈现timeAgoValue
属性:
<span class="timeagoSpan">{{.timeAgoValue}}</span>
按照惯例,我们必须将在cog
包的templates
文件夹中找到的cog
的主模板命名为与 cog 包同名。例如,对于timeago
包,cog
的主模板将是timeago.tmpl
。您可以自由定义和使用任何自定义模板函数,该函数已与cog
模板一起注册到应用的模板集。您还可以自由创建任意数量的子模板,这些子模板将由 cog 的主模板调用。
现在我们有了TimeAgo
cog 的模板,我们就有了在 About 页面上实例化cog
所需的一切。
让我们检查一下client/handlers/about.go
源文件中的InitializeAboutPage
函数:
func InitializeAboutPage(env *common.Env) {
humanReadableDivs := env.Document.GetElementsByClassName("humanReadableDate")
for _, div := range humanReadableDivs {
unixTimestamp, err := strconv.ParseInt(div.GetAttribute("data-starttimeunix"), 10, 64)
if err != nil {
log.Println("Encountered error when attempting to parse int64 from string:", err)
}
t := time.Unix(unixTimestamp, 0)
humanTime := timeago.NewTimeAgo()
humanTime.CogInit(env.TemplateSet)
humanTime.SetID(div.ID())
humanTime.SetTime(t)
err = humanTime.Start()
if err != nil {
println("Encountered the following error when attempting to start the timeago cog: ", err)
}
}
}
由于 About 页面上列出了三个 gopher,因此页面上将总共运行三个TimeAgo
cog 实例。我们使用env.Document
对象上的GetElementByClassName
方法收集 COG 的div
容器,并提供一个类名称humanReadableDate
。然后我们循环遍历每个div
元素,这就是实例化cog
的所有操作发生的地方。
首先,我们从div
容器中包含的自定义数据属性中提取 Unix 时间戳值。回想一下,我们已经使用自定义模板函数unixformat
用地鼠开始时间的 Unix 时间戳填充了starttimeunix
自定义数据属性。
然后,我们使用time
包中可用的Unix
函数创建一个新的time.Time
对象,并提供我们从div
容器的自定义数据属性中提取的unixTimestamp
。实例化和设置TimeAgo
cog 的代码以粗体显示。我们首先通过调用构造函数NewTimeAgo
并将其分配给humanTime
变量来实例化一个新的TimeAgo
cog。
然后我们在humanTime
对象上调用CogInit
方法,并为其提供env.TemplateSet
对象。我们调用SetID
方法注册div
容器的id
属性,将其与cog
实例关联。然后我们调用TimeAgo
cog 上的SetTime
方法,传入time.Time
对象t
,该对象是我们使用从div
容器中提取的unixTimestamp
创建的。
我们现在已经准备好了通过调用Start
方法启动cog
的一切。我们将Start
方法返回的error
对象分配给err
。如果err
不等于nil
,则表示启动cog
时发生错误,在这种情况下,我们会在 web 控制台中打印出一条有意义的消息。如果没有错误,cog
将被呈现到网页上。图 9.6以人类可读的格式显示了 Molly 开始时间的屏幕截图。
图 9.6:运行中的时间间隔
当我们在时间cog
之前调用Start
方法时,时间使用虚拟 DOM 呈现在网页上,而不是进行替换内部 HTML 操作。由于时间在cog
之前,只更新一次时间,调用cog
的Start
方法,很难理解 cog 的虚拟 DOM 在运行。
在本例中,我们将构建一个实时时钟Cog
,它能够显示世界上任何地方的当前时间。由于我们将以秒为单位显示时间,因此我们将每秒执行一次SetProp
操作,以重新渲染实时时钟Cog
。
图 9.7为实时时钟示意图:
图 9.7:描绘实时时钟 cog 的图示
我们将渲染四个地方的当前时间:您当前所在的地方、金奈、新加坡和夏威夷。在shared/templates/index_content.tmpl
模板源文件中,我们声明了四个div
容器,作为我们将实例化的四个实时时钟齿轮的安装点:
<div data-component="cog" id="myLiveClock" class="liveclockTime"></div>
<div data-component="cog" id="chennaiLiveClock" class="liveclockTime"></div>
<div data-component="cog" id="singaporeLiveClock" class="liveclockTime"></div>
<div data-component="cog" id="hawaiiLiveClock" class="liveclockTime"></div>
请再次注意,我们通过声明包含属性"data-component"
的div
容器并将其值设置为"cog"
,定义了实时时钟的挂载点。我们为所有四个cog
容器分配唯一 ID。我们在div
容器中声明的类名liveclockTime
用于样式设计。
现在我们已经为四个活时钟 COGS 设置了安装点,让我们来看看如何实现实时时钟 To0T0。
实时时钟Cog
的实现可以在shared/cogs/liveclock
文件夹中的liveclock.go
源文件中找到。
我们为 cog 的包名声明名称liveclock
:
package liveclock
请注意,在我们的进口分组中,我们包含了github.com/uxtoolkit/cog
包:
import (
"errors"
"reflect"
"time"
"github.com/uxtoolkit/cog"
)
我们定义cogType
未报告的包变量:
var cogType reflect.Type
在init
函数中,我们通过对新创建的LiveClock
实例调用reflect.TypeOf
函数返回的值来分配cogType
变量:
func init() {
cogType = reflect.TypeOf(LiveClock{})
}
这是实施cog
的必要步骤。
此时,我们已经确定,声明和初始化cog
的cogType
是实现cog
必须执行的基线要求的一部分。
以下是LiveClock
cog 的结构:
type LiveClock struct {
cog.UXCog
ticker *time.Ticker
}
我们在 cog 的结构定义中嵌入了[T0]类型。我们引入一个ticker
字段,它是指向time.Ticker
的指针。我们将使用这个ticker
为实时时钟每秒滴答作响。
以下是LiveClock
cog 的构造函数:
func NewLiveClock() *LiveClock {
liveClock := &LiveClock{}
liveClock.SetCogType(cogType)
liveClock.SetCleanupFunc(liveClock.Cleanup)
return liveClock
}
NewLiveClock
函数作为实时时钟cog
的构造函数。我们声明liveClock
变量并将其初始化为一个新的LiveClock
实例。我们调用liveClock
对象的SetCogType
方法并传递cogType
。回想一下,这是一个必需的步骤(以粗体显示),必须出现在 cog 的构造函数中。
然后我们调用liveClock
对象的SetCleanupFunc
方法,并为其提供一个清理函数liveClock.Cleanup
。SetCleanUp
方法包含在cog.UXCog
类型中。它允许我们指定在从 DOM 中删除[T5]之前应该调用的清理函数。最后,我们返回LiveClock cog
的新实例。
让我们检查一下Cleanup
函数:
func (lc *LiveClock) Cleanup() {
lc.ticker.Stop()
}
这个函数非常简单。我们只需在 cog 的ticker
对象上调用Stop
方法来停止ticker
。
以下是 cog 的Start
方法,其中ticker
将启动:
func (lc *LiveClock) Start() error {
我们首先声明时间布局常量layout
,并将其设置为RFC1123Z
时间格式。我们声明一个location
变量,一个指向time.Location
类型的指针:
const layout = time.RFC1123
var location *time.Location
在启动LiveClock
cog 之前,cog
的用户必须设置两个重要道具"timezoneName"
和"timezoneOffset"
:
if lc.Props["timezoneName"] != nil && lc.Props["timezoneOffset"] != nil {
location = time.FixedZone(lc.Props["timezoneName"].(string), lc.Props["timezoneOffset"].(int))
} else {
return errors.New("The timezoneName and timezoneOffset props need to be set!")
}
这些值用于初始化位置变量。如果没有提供这些道具中的任何一种,将返回一个error
。
如果两个道具都存在,我们继续将实时时钟cog
的ticker
属性分配给一个新创建的time.Ticker
实例,该实例将每秒打勾:
lc.ticker = time.NewTicker(time.Millisecond * 1000)
当一个值到达时,我们在股票代码的通道上range
每一秒迭代一次,我们设置currentTime
属性,为它提供一个格式化的时间值(以粗体显示):
go func() {
for t := range lc.ticker.C {
lc.SetProp("currentTime", t.In(location).Format(layout))
}
}()
请注意,我们使用位置和时间布局来设置时间格式。一旦cog
被呈现,每秒对SetProp
的调用将自动调用Render
方法重新呈现cog
。
我们调用 cog 的Render
方法将cog
呈现到网页:
err := lc.Render()
if err != nil {
return err
}
在方法的最后一行中,我们返回一个nil
值,表示没有发生错误:
return nil
我们已经在liveclock.tmpl
源文件中为cog
定义了模板:
<p>{{.timeLabel}}: {{.currentTime}}</p>
我们打印出时间标签和当前时间。timeLabel
道具用于向cog
提供时间标签,并且将是我们想要知道当前时间的地点的名称。
现在我们已经了解了制作实时时钟cog
的过程,以及它如何显示时间,让我们继续在主页上添加一些实时时钟齿轮。
以下是源文件index.go
的InitializeIndexPage
函数中的代码部分,我们在其中实例化了本地时区的实时时钟 cog:
// Localtime Live Clock Cog
localZonename, localOffset := time.Now().In(time.Local).Zone()
lc := liveclock.NewLiveClock()
lc.CogInit(env.TemplateSet)
lc.SetID("myLiveClock")
lc.SetProp("timeLabel", "Local Time")
lc.SetProp("timezoneName", localZonename)
lc.SetProp("timezoneOffset", localOffset)
err = lc.Start()
if err != nil {
println("Encountered the following error when attempting to start the local liveclock cog: ", err)
}
为了实例化本地时间的 cog,我们首先获取本地时区名称和本地时区偏移量。然后我们创建一个名为lc
的LiveClock cog
的新实例。我们调用CogInit
方法来初始化 cog。我们调用SetID
方法来注册 cog 安装点的id
,即cog
将输出到的div
容器。我们调用SetProp
方法来设置"timeLabel"
、"timezoneName"
和"timezoneOffset"
道具。最后,我们调用Start
方法来启动LiveClock
cog。像往常一样,我们检查cog
是否正确启动,如果没有,我们在 web 控制台中打印error
对象。
以类似的方式,我们为金奈、新加坡和夏威夷实例化了LiveClock
齿轮,与我们对当地时间的实例化方式基本相同,除了一件事。对于其他地方,我们明确提供每个地方的时区名称和 GMT 时区偏移:
// Chennai Live Clock Cog
chennai := liveclock.NewLiveClock()
chennai.CogInit(env.TemplateSet)
chennai.SetID("chennaiLiveClock")
chennai.SetProp("timeLabel", "Chennai")
chennai.SetProp("timezoneName", "IST")
chennai.SetProp("timezoneOffset", int(+5.5*3600))
err = chennai.Start()
if err != nil {
println("Encountered the following error when attempting to start the chennai liveclock cog: ", err)
}
// Singapore Live Clock Cog
singapore := liveclock.NewLiveClock()
singapore.CogInit(env.TemplateSet)
singapore.SetID("singaporeLiveClock")
singapore.SetProp("timeLabel", "Singapore")
singapore.SetProp("timezoneName", "SST")
singapore.SetProp("timezoneOffset", int(+8.0*3600))
err = singapore.Start()
if err != nil {
println("Encountered the following error when attempting to start the singapore liveclock cog: ", err)
}
// Hawaii Live Clock Cog
hawaii := liveclock.NewLiveClock()
hawaii.CogInit(env.TemplateSet)
hawaii.SetID("hawaiiLiveClock")
hawaii.SetProp("timeLabel", "Hawaii")
hawaii.SetProp("timezoneName", "HDT")
hawaii.SetProp("timezoneOffset", int(-10.0*3600))
err = hawaii.Start()
if err != nil {
println("Encountered the following error when attempting to start the hawaii liveclock cog: ", err)
}
现在,我们将能够看到活动的时钟齿轮。图 9.8是主页上显示的实景木屐的屏幕截图。
图 9.8:运行中的实时时钟 cog
每过一秒,每个实时时钟都会更新为新的时间值。虚拟 DOM 启动并只渲染更改内容的差异,每秒高效地重新渲染实时时钟。
到目前为止,我们已经实现的前两个 COG 是完全在 Go 中实现的纯 COG。如果我们想利用现有的 JavaScript 解决方案来提供特定的功能,该怎么办?这种情况需要实现一个混合 cog,一个用 Go 和 JavaScript 实现的[T0]。
JavaScript 已经存在了二十多年。在这段时间内,使用该语言创建了许多健壮的、生产就绪的解决方案。同构 Go 不能存在于它自己的岛上,我们必须承认,JavaScript 生态系统中有许多有用的现成解决方案。在许多场景中,我们可以通过创建利用现有 JavaScript 解决方案的解决方案来节省大量时间和精力,而不是以纯粹的方式重新实现整个解决方案。
混合 COG 是使用 Go 和 JavaScript 实现的。混合 cogs 的主要目的是利用现有 JavaScript 解决方案中的功能,并将该功能公开为一个cog
。这意味着cog
实现者需要同时了解 Go 和 JavaScript 才能实现混合 COG。记住,混合 cogs 的用户只需要知道 Go,因为 JavaScript 的使用是cog
的内部实现细节。这使得可能不熟悉 JavaScript 的 Go 开发人员可以随时使用 COG。
让我们考虑一个场景,保证实现混合式 To0T0。Molly,IGWEB 事实上的产品经理,想出了一个杀手锏来提供更好的客户支持。她向技术团队提出的功能要求是允许网站用户在联系人表单上提供可选的优先日期,用户可以通过该日期从 IGWEB 团队的地鼠那里得到回复
Molly 发现了一个独立的日期选择器小部件,它是用香草 JavaScript(无框架/库依赖项)实现的,名为 Pikaday:[T0]https://github.com/dbushell/Pikaday 。
Pikaday 是 JavaScript 日期选择器小部件,它突出显示了本节开头介绍的事实。JavaScript 并没有消失,已经有很多有用的解决方案使用它。这意味着,如果有必要,我们必须能够利用现有的 JavaScript 解决方案。Pikaday 日期选取器是一个特殊的用例,在这个用例中,利用这个现有的 JavaScript 日期选取器小部件比实现一个纯cog
的小部件更为有利:
图 9.9:描述时间敏感日期输入字段和日历日期选择器小部件的线框设计
图 9.9是一个线框设计,描绘了带有时间敏感输入字段的联系人表单,点击该字段将显示日历日期选择器。让我们看看如何通过实现日期选择器 cog 来满足 Molly 的请求,这是一个混合 cog,由 Go 和 JavaScript 制成
我们首先将 Pikaday、日期选择器小部件所需的 JavaScript 和 CSS 源文件分别放在 cog 的static
文件夹中的js
和css
文件夹中。
在shared/templates/partials/contactform_partial.tmpl
源文件中,我们声明日期选择器 cog 的装入点(以粗体显示):
<fieldset class="pure-control-group">
<div data-component="cog" id="sensitivityDate"></div>
</fieldset>
div
容器满足所有cog
悬置点的两个基本要求:我们设置了"data-component"
属性,值为"cog"
,我们为cog
容器指定了id
为"sensitivityDate"
。
让我们逐节检查shared/cogs/datepicker/datepicker.go
源文件中定义的日期选择器 cog 的实现。首先,我们首先声明包名:
package datepicker
以下是 cog 的导入分组:
import (
"errors"
"reflect"
"time"
"github.com/gopherjs/gopherjs/js"
"github.com/uxtoolkit/cog"
)
请注意,我们在导入分组中包含了gopherjs
包(以粗体显示)。我们需要gopherjs
的功能来查询 DOM。
在我们声明了cogType
之后,我们立即将JS
变量初始化为js.Global
:
var cogType reflect.Type
var JS = js.Global
您可能还记得,这为我们节省了一点打字时间。我们可以直接将js.Global
称为JS
。
从 Pikaday 项目网页,https://github.com/dbushell/Pikaday ,我们可以学习日期选择器小部件接受的所有输入参数。输入参数作为单个 JavaScript 对象提供。日期选择器cog
将公开这些输入参数的子集,仅足以满足 Molly 的特征请求。我们创建一个名为DatePickerParams
的struct
,作为日期选择器小部件的输入参数:
type DatePickerParams struct {
*js.Object
Field *js.Object `js:"field"`
FirstDay int `js:"firstDay"`
MinDate *js.Object `js:"minDate"`
MaxDate *js.Object `js:"maxDate"`
YearRange []int `js:"yearRange"`
}
我们嵌入了*js.Object
来表示这是一个 JavaScript 对象。然后,我们为 JavaScript 输入对象的各个属性声明struct
的各个 Go 字段。例如,名为Field
的字段用于field
属性。我们为每个字段提供的"js"``struct
标记允许 GopherJS 将struct
及其字段从指定的 Go 名称转换为等效的 JavaScript 名称。正如我们声明了名为 field 的字段一样,我们还声明了FirstDay
(firstDay
)、MinDate
(minDate
)、MaxDate
(maxDate
)和YearRange
(yearRange
的字段。
阅读 Pikaday 文档,[T0]https://github.com/dbushell/Pikaday ,我们可以了解这些输入参数的用途:
Field
-用于将日期选择器绑定到表单字段。FirstDay
-用于指定一周的第一天。(周日 0,周一 1 等)。MinDate
-可在日期选择器小部件中选择的最早日期。MaxDate
-可在日期选择器小部件中选择的最新日期。YearRange
-要显示的年份范围。
现在我们已经定义了日期选择器的输入参数结构DatePickerParams
,现在是实现日期选择器cog
的时候了。我们首先声明[T2]结构:
type DatePicker struct {
cog.UXCog
picker *js.Object
}
像往常一样,我们嵌入了cog.UXCog
以提供我们所需的所有 UXCog 功能。我们还声明了一个字段picker
,它是指向js.Object
的指针。picker
属性将用于引用 Pikaday 日期选择器 JavaScript 对象。
然后,我们为日期选择器cog
实现一个名为NewDatePicker
的构造函数:
func NewDatePicker() *DatePicker {
d := &DatePicker{}
d.SetCogType(cogType)
return d
}
现在,cog 构造函数对您来说应该很熟悉了。它的职责是返回一个DatePicker
的新实例并设置 cog 的cogType
。
现在我们的构造函数已经就位,是时候检查日期选择器 cog 的Start
方法了:
func (d *DatePicker) Start() error {
if d.Props["datepickerInputID"] == nil {
return errors.New("Warning: The datePickerInputID prop need to be set!")
}
err := d.Render()
if err != nil {
return err
}
我们首先检查"datepickerInputID"
道具是否已设置。这是输入字段元素的id
,将用作DatePickerParams``struct
中的Field
值。这是一个硬要求,在启动cog
之前,该道具必须由调用者设置。未能设置此道具将导致错误。
如果设置了"datepickerInputID"
道具,我们调用 cog 的Render
方法来渲染 cog。这将呈现日期选择器 JavaScript 小部件对象所依赖的输入字段的 HTML 标记。
然后,我们继续声明并实例化,params
,输入参数 JavaScript 对象,该对象将发送到日期选择器 JavaScript 小部件:
params := &DatePickerParams{Object: js.Global.Get("Object").New()}
日期选择器输入参数对象params
是一个 JavaScript 对象。Pikaday JavaScript 对象将使用params
对象进行初始配置。
我们使用 cog 的Props
属性来确定 cog 属性的范围。对于每个迭代,我们获取属性的名称(propName
)和属性的值(propValue
:
for propName, propValue := range d.Props {
我们声明的[T0]块对于可读性非常重要:
switch propName {
case "datepickerInputID":
inputFieldID := propValue.(string)
dateInputField := JS.Get("document").Call("getElementById", inputFieldID)
params.Field = dateInputField
case "datepickerLabel":
// Do nothing
case "datepickerMinDate":
datepickerMinDate := propValue.(time.Time)
minDateUnix := datepickerMinDate.Unix()
params.MinDate = JS.Get("Date").New(minDateUnix * 1000)
case "datepickerMaxDate":
datepickerMaxDate := propValue.(time.Time)
maxDateUnix := datepickerMaxDate.Unix()
params.MaxDate = JS.Get("Date").New(maxDateUnix * 1000)
case "datepickerYearRange":
yearRange := propValue.([]int)
params.YearRange = yearRange
default:
println("Warning: Unknown prop name provided: ", propName)
}
}
switch
块中的每个case
语句都告诉我们日期选择器cog
接受的所有属性作为输入参数,这些参数将传递给 Pikaday JavaScript 小部件。如果无法识别道具名称,我们将在 web 控制台中打印一条警告,指出道具未知。
第一个案例,处理"datepickerInputID"
道具。它将用于指定激活 Pikaday 小部件的输入元素的id
。在这个case
中,我们通过调用document
对象上的getElementById
方法并将inputFieldID
传递给该方法来获取输入元素字段。我们将 inputparams
属性Field
设置为从getElementById
方法调用获得的 input field 元素。
第二个案例处理"datepickerLabel"
道具。"datepickerLabel"
道具的值将在 cog 的模板源文件中使用。因此,不需要处理这一特殊情况。
第三个案例处理"datepickerMinDate"
道具。它将用于获取 Pikaday 小部件应显示的最小日期。我们将调用方提供的type time.Time
的"datepickerMinDate"
值转换为其 Unix 时间戳表示形式。然后,我们使用 Unix 时间戳创建一个新的 JavaScriptdate
对象,该对象适用于minDate
输入参数。
第四个案例处理"datepickerMaxDate"
道具。它将用于获取日期选择器小部件应显示的最大日期。我们在这里遵循相同的策略,就像我们在minDate
参数中所做的一样。
第五个案例处理"datepickerYearRange"
道具。它将用于指定显示的日历将覆盖的年份范围。年份范围是一个切片,我们使用 prop 的值填充输入参数对象的YearRange
属性。
如前所述,default``case
处理调用方提供未知道具名称的场景。如果我们到达default``case
,我们将在 web 控制台中打印一条警告消息。
现在我们可以实例化 Pikaday 小部件,并向其提供输入参数 obect,params
,如下所示:
d.picker = JS.Get("Pikaday").New(params)
最后,通过返回一个nil
值,我们表明启动 cog 时没有错误:
return nil
现在我们已经实现了日期选择器 COG,让我们来看看在损坏的 To0t0 源文件中定义的 COG 的主要模板,看起来是这样的:
<label class="datepickerLabel" for="datepicker">{{.datepickerLabel}}</label>
<input class="datepickerInput" type="text" id="{{.datepickerInputID}}" name="{{.datepickerInputID}}">
我们声明一个label
元素,以使用 prop"datepickerLabel"
显示日期选择器 cog 的标签。我们声明一个input
元素,它将作为输入元素字段,与 Pikaday 小部件一起使用。我们使用"datepickerInputID"
属性指定输入元素字段的id
属性。
现在我们已经实现了日期选择器 cog,是时候开始使用它了。我们在InitializeContactPage
函数中实例化cog
,在client/handlers/contact.go
源文件中找到:
byDate := datepicker.NewDatePicker()
byDate.CogInit(env.TemplateSet)
byDate.SetID("sensitivityDate")
byDate.SetProp("datepickerLabel", "Time Sensitivity Date:")
byDate.SetProp("datepickerInputID", "byDateInput")
byDate.SetProp("datepickerMinDate", time.Now())
byDate.SetProp("datepickerMaxDate", time.Date(2027, 12, 31, 23, 59, 0, 0, time.UTC))
err := byDate.Start()
if err != nil {
println("Encountered the following error when attempting to start the datepicker cog: ", err)
}
首先,我们创建一个新的DatePicker cog
实例。然后我们调用 cog 的CogInit
方法来注册应用的模板集。我们调用SetID
方法来设置 cog 的安装点。我们调用 cog 的SetProp
方法来设置datePickerLabel
、datepickerInputID
、datepickerMinDate
和datepickerMaxDate
道具。我们称之为 cog 的Start
方法来激活它。如果启动cog
时出现任何错误,我们会将错误消息打印到 web 控制台。
这就是全部!我们可以使用 date picker hybridcog
利用 Pikaday 小部件所需的功能。这种方法的优点是,使用日期选择器cog
的 Go 开发人员不需要了解 Pikaday 小部件的内部工作(JavaScript)就可以使用它。相反,他们可以使用日期选择器cog
在 Go 范围内向他们公开的功能。
图 9.10显示了日期选择器cog
的屏幕截图:
图 9.10:运行中的日历日期选择器小部件
即使 cog 用户没有提供任何道具,除了所需的datepickerInputID
来自定义配置日期选择器cog
,Pikaday 小部件也可以正常启动。但是,如果我们需要为cog
提供一组默认参数,该怎么办?在下一个示例中,我们将构建另一个混合cog
,一个旋转木马(图像滑块)cog
,在其中我们将定义默认参数。
在本例中,我们将创建一个图像旋转齿轮,如图 9.11 中的线框设计所示。
图 9.11:描绘旋转木马齿轮的线框设计
转盘齿轮将由小型滑块小部件提供动力,该小部件使用香草 JavaScript 实现。以下是小滑块项目的 URL:[T0]https://github.com/ganlanyuan/tiny-slider 。
我们将小滑块小部件的 JavaScript 源文件tiny-slider.min.js
放在 cog 的static/js
文件夹中。我们将与小滑块小部件相关联的 CSS 文件tiny-slider.css
和styles.css
放在static/css
文件夹中。
我们将构建的转盘齿轮将公开 tiny slider 小部件提供的以下输入参数:
container Node | String Default: document.querySelector('.slider').
container
参数表示滑块容器元素或选择器:
items Integer Default: 1.
items
参数表示正在显示的幻灯片数量:
slideBy Integer | 'page' Default: 1.
slideBy
参数表示一次“点击”的幻灯片数量:
autoplay Boolean Default: false.
autoplay
参数切换幻灯片的自动更改:
autoplayText Array (Text | Markup) Default: ['start', 'stop'].
autoplayText
参数控制自动播放开始/停止按钮中显示的文本或标记。
controls Boolean Default: true.
controls
参数用于切换控件(上一个/下一个按钮)的显示和功能
图像转盘将显示 IGWEB 上提供的一组特色产品。我们已经在shared/templates/index_content.tmpl
源文件中声明了 cog 的安装点:
<div data-component="cog" id="carousel"></div>
我们已经声明了div
容器将作为转盘齿轮的安装点。我们已经声明了属性"data-component"
,并将其赋值为"cog"
。我们还声明了"carousel"
的id
属性。
转盘 cog 在shared/cogs/carousel
文件夹中找到的carousel.go
源文件中实现。以下是程序包声明和导入分组:
package carousel
import (
"errors"
"reflect"
"github.com/gopherjs/gopherjs/js"
"github.com/uxtoolkit/cog"
)
小滑块小部件是用一个输入参数 JavaScript 对象实例化的。我们将使用CarouselParams struct
对输入参数对象建模:
type CarouselParams struct {
*js.Object
Container string `js:"container"`
Items int `js:"items"`
SlideBy string `js:"slideBy"`
Autoplay bool `js:"autoplay"`
AutoplayText []string `js:"autoplayText"`
Controls bool `js:"controls"`
}
嵌入指向js.Object
的指针后,struct
中定义的每个字段都对应于其等价的 JavaScript 参数 object 属性。例如,Container
字段映射到输入参数对象的container
属性。
下面是定义carousel
cog 的结构:
type Carousel struct {
cog.UXCog
carousel *js.Object
}
像往常一样,我们嵌入了cog.UXCog
类型以借用UXCog
的功能。carousel
字段将用于引用小滑块小部件,它是一个 JavaScript 对象。
到现在为止,您应该能够猜出旋转齿轮的构造函数是什么样子的:
func NewCarousel() *Carousel {
c := &Carousel{}
c.SetCogType(cogType)
return c
}
除了创建对Carousel
实例的新引用外,构造函数还设置 cog 的cogType
。
现在是时候检查旋转木马 cog 实现的最大部分了,这可以在 cog 的Start
方法中找到:
func (c *Carousel) Start() error {
我们首先检查cog
的用户是否设置了contentItems
和carouselContentID
道具。contentItems
属性是应该出现在转盘中的图像的服务器相对图像路径的字符串片段。carouselContentID
属性是保存旋转木马内容的div
容器的id
属性的值。
如果这两个道具中有一个没有设置,我们返回一个error
,指示这两个道具都必须设置。如果设置了两个道具,我们将继续渲染 cog:
if c.Props["contentItems"] == nil || c.Props["carouselContentID"] == nil {
return errors.New("The contentItems and carouselContentID props need to be set!")
}
err := c.Render()
if err != nil {
return err
}
我们在此时呈现[T0],因为网页上需要存在 HTML 标记,才能使[T1]正常工作。值得注意的是,存放转盘内容的div
容器,我们使用所需的carouselContentID
道具提供其id
。如果有error
呈现cog
,我们返回error
表示cog
无法启动。如果在呈现cog
时没有遇到error
,我们继续实例化输入参数对象:
params := &CarouselParams{Object: js.Global.Get("Object").New()}
这个struct
表示我们将在微小滑块对象实例化时提供给它的输入参数。
代码的下一部分很重要,因为这是我们定义默认参数的地方:
// Set the default parameter values
params.Items = 1
params.SlideBy = "page"
params.Autoplay = true
params.AutoplayText = []string{PLAYTEXT, STOPTEXT}
params.Controls = false
当 cog 维护人员查看这段代码时,他们可以很容易地确定 cog 的默认行为是什么。通过查看默认参数,可以看出滑块一次只显示一个项目。滑块设置为“按页面滑动”模式,滑块将自动启动幻灯片放映。我们为AutoplayText
属性提供了一个字符串片段,分别使用PLAYTEXT
和STOPTEXT
常量为播放和停止按钮提供文本符号。我们已将Controls
属性设置为false
,因此默认情况下,图像转盘上不会出现“下一个”和“上一个”按钮。
我们继续迭代cog
用户提供的所有属性,访问每个道具,包括propName
(string
)和propValue
(interface{}
):
for propName, propValue := range c.Props {
我们在propName
上声明switch
块:
switch propName {
case "carouselContentID":
if propValue != nil {
params.Container = "#" + c.Props["carouselContentID"].(string)
}
case "contentItems":
// Do nothing
case "items":
if propValue != nil {
params.Items = propValue.(int)
}
case "slideBy":
if propValue != nil {
params.SlideBy = c.Props["slideBy"].(string)
}
case "autoplay":
if propValue != nil {
params.Autoplay = c.Props["autoplay"].(bool)
}
case "autoplayText":
if propValue != nil {
params.AutoplayText = c.Props["autoplayText"].([]string)
}
case "controls":
if propValue != nil {
params.Controls = c.Props["controls"].(bool)
}
default:
println("Warning: Unknown prop name provided: ", propName)
}
}
使用switch
块可以很容易地看到定义的每个case
语句中所有有效道具的名称。如果道具名称未知,则属于default
情况,我们在 web 控制台中打印警告消息。
第一个case
处理所需的"carouselContentID"
道具。用于指定包含转盘内容项的div
容器。
第二个case
处理所需的"contentItems"
道具。这个道具是一个string
切片,它将用于 cog 的模板中,因此我们无需对其执行任何操作。
第三个case
处理"items"
道具。这是处理 tns slider 对象的items
参数的道具,该参数显示在给定时间要显示的幻灯片数量。如果道具值不是nil
,我们将道具值的int
值指定给params.Items
属性。
第四个case
处理slideBy
道具。如果 prop 值不是nil
,我们将 prop 值(断言为string
的类型)分配给params
对象的SlideBy
属性。
第五个case
操作"autoplay"
道具。如果 prop 值不是nil
,我们将 prop 值(断言为bool
的类型)分配给params
对象的Autoplay
属性。
第六个case
操作"autoplayText"
道具。如果 prop 值不是nil
,我们将 prop 值(断言为[]string
的类型)分配给params
对象的AutoplayText
属性。
第七个case
处理"controls"
道具。如果 prop 值不是nil
,我们将属性值(类型断言为bool
)分配给params
对象的Controls
属性。
如果物业名称不属于上述七种情况中的任何一种,则由default case
处理。回想一下,如果我们到达这个case
,它表示cog
的用户提供了一个未知的道具名称。
我们现在可以实例化小滑块小部件并将其分配给 cog 的carousel
属性:
c.carousel = JS.Get("tns").New(params)
Start
方法返回nil
值,表示启动cog
时没有遇到错误:
return nil
shared/cogs/carousel/templates/carousel.tmpl
源文件定义转盘cog
的模板:
<div id="{{.carouselContentID}}" class="carousel">
{{range .contentItems}}
<div><img src="{{.}}"></div>
{{end}}
</div>
我们申报了一个div
容器来存放旋转木马的图像。contentItems``string
切片中的每个项目都是一个与图像相关的服务器路径。我们使用range
模板操作来迭代contentItems
属性(一个string
切片)以打印出每个图像的地址,这些图像位于其自己的div
容器中。注意,我们提供了点(.
模板动作作为img
元素的src
属性的值。点模板动作表示迭代contentItems
切片时的当前值。
现在我们已经实现了旋转木马cog
并创建了它的模板,现在是在主页上实例化并启动cog
的时候了。我们将在client/handlers/index.go
源文件中的InitializeIndexPage
函数开头添加转盘cog
的代码:
c := carousel.NewCarousel()
c.CogInit(env.TemplateSet)
c.SetID("carousel")
contentItems := []string{"/statimg/products/watch.jpg", "/statimg/products/shirt.jpg", "/statimg/products/coffeemug.jpg"}
c.SetProp("contentItems", contentItems)
c.SetProp("carouselContentID", "gophersContent")
err := c.Start()
if err != nil {
println("Encountered the following error when attempting to start the carousel cog: ", err)
}
我们首先创建一个新的旋转木马cog``c
,调用构造函数NewCarousel
。我们调用CogInit
方法将应用的模板集与cog
相关联。我们调用SetID
方法将cog
与其装入点相关联,即cog
将向其输出的div
容器。我们使用包含图像文件路径的string
切片文本创建string
切片。我们调用SetProp
方法来设置所需的contentItems
和所需的carouselContent
道具。我们不设置任何其他道具,因为我们对旋转齿轮的默认行为感到满意。我们启动cog
并检查在执行此操作时是否遇到任何错误。如果遇到任何错误,我们将在 web 控制台中打印错误消息。
图 9.12是渲染旋转木马齿轮的屏幕截图:
图 9.12:旋转木马齿轮的作用
现在,我们已经完成了转盘齿轮,我们将在下一节中创建一个通知齿轮,在网页上显示动画通知消息。
到目前为止,我们考虑的所有cog
实现都已将输出呈现到 web 页面。让我们考虑一个不提供任何输出到 Web 页面的 Oracle T1。我们将要实现的 notifycog
将利用 Alertify JavaScript 库在网页上显示动画通知消息。
图 9.13是一幅插图,描绘了当用户向购物车添加商品时,出现在网页右下角的通知消息:
图 9.13:描述通知的图示
由于cog
将完全依赖 JavaScript 库来满足其呈现需求,因此我们不必为cog
实现模板,也不必为 cog 声明装入点。
我们将利用 AlertifyJavaScript 库的功能来显示通知。以下是 Alertify 项目的 URL:[T0]https://github.com/MohammadYounes/AlertifyJS 。
查看shared/cogs/notify
文件夹内部,注意不存在模板文件夹。我们已经将 Alertify 的 CSS 和 JavaScript 源文件的静态资产分别放在了shared/cogs/notify/static/css
和shared/cogs/notify/static/js
文件夹中。
通知cog
在shared/cogs/notify
文件夹中找到的notify.go
源文件中实现。由于客户端 web 应用只有一个 notifycog
提供的通知系统是有意义的,因此只应启动一次cog
实例。为了跟踪并确保只能启动一个 notifycog
实例,我们将声明alreadyStarted
布尔变量:
var alreadyStarted bool
Notify
结构定义通知cog
的字段:
type Notify struct {
cog.UXCog
alertify *js.Object
successNotificationEventListener func(*js.Object)
errorNotificationEventListener func(*js.Object)
}
我们键入嵌入的cog.UXCog
是为了提供实现Cog
接口所需的功能。alertify
字段用于引用alertify
JavaScript 对象。
我们正在构建的通知cog
是事件驱动的。例如,当从客户端应用的任何页面触发自定义成功通知事件时,将显示成功通知。我们定义了两个字段,successNotificationEventListener
和errorNotificationEventListener
,这两个字段都是将 JavaScript 对象指针作为输入变量的函数。我们已经定义了这些字段,这样我们就可以跟踪自定义事件侦听器函数,我们设置这些函数来侦听成功和错误通知。当需要删除事件监听器时,访问它们变得很容易,因为它们是 notifycog
实例的属性。
NewNotify
函数用作构造函数:
func NewNotify() *Notify {
n := &Notify{}
n.SetCogType(cogType)
n.SetCleanupFunc(n.Cleanup)
return n
}
请注意,我们已经注册了一个清理函数(以粗体显示),该函数将在销毁[T0]之前调用。
让我们检查 cog 的Start
方法:
func (n *Notify) Start() error {
if alreadyStarted == true {
return errors.New("The notification cog can be instantiated only once.")
}
我们首先通过检查alreadyStarted
布尔变量的值来检查 notifycog
实例是否已经启动。如果alreadyStarted
的值为true
,则表示之前的 notifycog
实例已经启动,所以我们返回一个error
,表示 notifycog
无法启动。
如果cog
尚未启动,我们继续实例化 Alertify JavaScript 对象:
n.alertify = js.Global.Get("alertify")
我们调用 cog 的StartListening
方法来设置侦听自定义成功和错误通知消息事件的事件侦听器:
n.StartListening()
return nil
以下是 cog 的StartListening
方法:
func (n *Notify) StartListening() {
alreadyStarted = true
D := dom.GetWindow()
n.successNotificationEventListener = D.AddEventListener("displaySuccessNotification", false, func(event dom.Event) {
message := event.Underlying().Get("detail").String()
n.notifySuccess(message)
})
n.errorNotificationEventListener = D.AddEventListener("displayErrorNotification", false, func(event dom.Event) {
message := event.Underlying().Get("detail").String()
n.notifyError(message)
})
}
如果我们达到了这个方法,则表示cog
启动成功,所以我们将alreadyStarted
布尔变量设置为true
。我们设置了一个事件侦听器,它将侦听displaySuccessNotification
自定义事件。我们通过将正在创建的事件侦听器函数分配给cog
实例的successNotificationEventListener
属性来跟踪它。我们声明并实例化message
变量,并将其设置为event
对象的detail
属性,该属性将包含应在网页上显示给用户的string``message
。然后我们调用 cog 的notifySuccess
方法在网页上显示成功通知消息。
我们按照类似的过程为displayErrorNotification
设置事件侦听器。我们将事件侦听器函数分配给 cog 的errorNotificationEventListener
属性。我们从event
对象中提取detail
属性,并将其分配给message
变量。我们调用 cog 的notifyError
方法在网页上显示错误通知消息。
notifySuccess
方法负责在网页上显示成功通知消息:
func (n *Notify) notifySuccess(message string) {
n.alertify.Call("success", message)
}
我们调用 alertify 对象的success
方法来显示成功通知消息。
notifyError
方法负责在网页上显示错误通知消息:
func (n *Notify) notifyError(message string) {
n.alertify.Call("error", message)
}
我们调用 alertify 对象的error
方法来显示错误通知消息。
cog 的CleanUp
方法只是调用 cog 的StopListening
方法:
func (n *Notify) Cleanup() {
n.StopListening()
}
StopListening
方法用于在cog
被销毁之前移除事件侦听器:
func (n *Notify) StopListening() {
D := dom.GetWindow()
if n.successNotificationEventListener != nil {
D.RemoveEventListener("displaySuccessNotification", false, n.successNotificationEventListener)
}
if n.errorNotificationEventListener != nil {
D.RemoveEventListener("displayErrorNotification", false, n.errorNotificationEventListener)
}
}
我们调用 DOM 对象的RemoveEventListener
方法来删除处理displaySuccessNotification
和displayErrorNotification
自定义事件的事件侦听器函数。
notify
包的导出Success
功能用于广播自定义成功事件通知消息:
func Success(message string) {
var eventDetail = js.Global.Get("Object").New()
eventDetail.Set("detail", message)
customEvent := js.Global.Get("window").Get("CustomEvent").New("displaySuccessNotification", eventDetail)
js.Global.Get("window").Call("dispatchEvent", customEvent)
}
在函数内部,我们创建了一个名为eventDetail
的新 JavaScript 对象。我们将应该显示在网页上的string``message
分配给eventDetail
对象的detail
属性。然后我们创建一个名为customEvent
的新自定义event
对象。我们将自定义事件的名称displaySuccessNotification
与eventDetail
对象一起作为输入参数传递给CustomEvent
类型的构造函数。最后,为了调度事件,我们在window
对象上调用dispatchEvent
方法并提供customEvent
。
notify 包的导出Error
功能用于广播自定义错误事件通知消息:
func Error(message string) {
var eventDetail = js.Global.Get("Object").New()
eventDetail.Set("detail", message)
customEvent := js.Global.Get("window").Get("CustomEvent").New("displayErrorNotification", eventDetail)
js.Global.Get("window").Call("dispatchEvent", customEvent)
}
此函数的实现与Success
函数几乎相同。唯一的区别是我们发送了一个displayErrorNotification
定制事件。
我们在client/handlers/initpagelayoutcontrols.go
源文件中找到的InitializePageLayoutControls
函数中实例化并启动 notifycog
(粗体显示):
func InitializePageLayoutControls(env *common.Env) {
n := notify.NewNotify()
err := n.Start()
if err != nil {
println("Error encountered when attempting to start the notify cog: ", err)
}
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)
})
}
将商品添加到购物车的通知消息(成功或错误)可在client/handlers/shoppingcart.go
源文件内的addToCart
函数中找到:
func addToCart(productSKU string) {
m := make(map[string]string)
m["productSKU"] = productSKU
jsonData, _ := json.Marshal(m)
data, err := xhr.Send("PUT", "/restapi/add-item-to-cart", jsonData)
if err != nil {
println("Encountered error: ", err)
notify.Error("Failed to add item to cart!")
return
}
var products []*models.Product
json.NewDecoder(strings.NewReader(string(data))).Decode(&products)
notify.Success("Item added to cart")
}
如果无法将商品添加到购物车,则调用notify.Error
函数(粗体显示)。如果商品成功添加到购物车,则调用notify.Success
函数(以粗体显示)。
在client/handlers/shoppingcart.go
源文件的removeFromCart
函数中可以找到从购物车中移除物品的通知消息:
func removeFromCart(env *common.Env, productSKU string) {
m := make(map[string]string)
m["productSKU"] = productSKU
jsonData, _ := json.Marshal(m)
data, err := xhr.Send("DELETE", "/restapi/remove-item-from-cart", jsonData)
if err != nil {
println("Encountered error: ", err)
notify.Error("Failed to remove item from cart!")
return
}
var products []*models.Product
json.NewDecoder(strings.NewReader(string(data))).Decode(&products)
renderShoppingCartItems(env)
notify.Success("Item removed from cart")
}
如果商品无法从购物车中移除,则调用notify.Error
函数(粗体显示)。如果商品成功从购物车中移除,则调用notify.Success
函数(粗体显示)。
图 9.14是当我们向购物车添加产品时,通知 cog 的截图:
图 9.14:运行中的通知 cog
在本章中,我们介绍了 cogs 可重用组件,这些组件可以专门在 Go(纯 cogs)中实现,也可以使用 Go 和 JavaScript(混合 cogs)实现。齿轮有很多好处。我们可以以即插即用的方式使用它们,创建它们的多个实例,由于它们是自包含的,因此易于维护它们,并且可以轻松地重用它们,因为它们可以作为自己的 Go 包及其所需的静态资产(模板文件、CSS 和 JavaScript 源文件)存在。
我们向您介绍了 UX 工具包,它为我们提供了实现 cogs 的技术。我们研究了 cog 的解剖结构,并探索了 cog 的文件结构在 Go、CSS、JavaScript 和模板文件的放置方面可能是什么样子。我们考虑了 COG 如何利用虚拟 DOM 来呈现其内容,而不是执行昂贵的替换内部 HTML 操作。我们介绍了 cog 生命周期的各个阶段。我们向您展示了如何实现我们散布在整个 IGWEB 上的各种齿轮,包括纯齿轮和混合齿轮。
在第 10 章测试同构的 Go Web 应用中,我们将学习如何执行 IGWEB 的自动化端到端测试。这将包括在服务器端和客户端实施测试以实现功能。