Skip to content

Latest commit

 

History

History
1548 lines (1067 loc) · 73.5 KB

09.md

File metadata and controls

1548 lines (1067 loc) · 73.5 KB

九、Cogs——可复用组件

在本书的前五章中,我们着重于为 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包包含两个重要的导出变量,ReactivityEnabledVDOMEnabled。这两个导出变量的类型均为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的项目结构按以下方式组织:

widgetwidget.gotemplateswidget.tmpl

widget.go源文件将包含小部件cog的实现。

templates文件夹包含用于实现cog的模板源文件。如果要在网页上呈现cog,则必须至少存在一个模板源文件。模板源文件的名称必须与cog的包名称匹配。例如,对于cogwidget,模板源文件的名称必须为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的道具设置为值barProps映射将自动用作数据对象,输入 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。我们必须调用cogCogInit方法将应用的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。

虚拟 DOM 树

每个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的新实例。我们通过调用cogCogInit方法初始化cog并传入客户端应用的TemplateSet对象。初始化cog还包括调用 cog 的SetID方法将挂载点关联到cog(以便cog知道渲染到哪里)。Cog初始化还包括在调用Start方法之前通过调用 cog 的Props map方法来设置道具。

请注意,在调用 cog 的Start方法之前调用 cog 的SetProp方法不会呈现cogcog只有在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的新实例,并立即调用cogCogInit方法,将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容器的humanReadableDateCSSclassName。稍后,我们将使用此 CSSclassName获取 About 页面上的所有timeagocogdiv容器。

现在,我们已经看到了如何在大约页上声明 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 cogstruct

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 方法设置TimeAgocog 的时间。我们将使用SetTime方法设置 gopher 加入 IGWEB 团队的开始日期。

为了实现Cog接口,cog必须定义Start方法。Start方法是cog中的动作发生的地方。通过阅读cogStart方法,您应该能够大致了解cog的功能。以下是TimeAgocog 的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 的主模板调用。

现在我们有了TimeAgocog 的模板,我们就有了在 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,因此页面上将总共运行三个TimeAgocog 实例。我们使用env.Document对象上的GetElementByClassName方法收集 COG 的div容器,并提供一个类名称humanReadableDate。然后我们循环遍历每个div元素,这就是实例化cog的所有操作发生的地方。

首先,我们从div容器中包含的自定义数据属性中提取 Unix 时间戳值。回想一下,我们已经使用自定义模板函数unixformat用地鼠开始时间的 Unix 时间戳填充了starttimeunix自定义数据属性。

然后,我们使用time包中可用的Unix函数创建一个新的time.Time对象,并提供我们从div容器的自定义数据属性中提取的unixTimestamp。实例化和设置TimeAgocog 的代码以粗体显示。我们首先通过调用构造函数NewTimeAgo并将其分配给humanTime变量来实例化一个新的TimeAgocog。

然后我们在humanTime对象上调用CogInit方法,并为其提供env.TemplateSet对象。我们调用SetID方法注册div容器的id属性,将其与cog实例关联。然后我们调用TimeAgocog 上的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之前,只更新一次时间,调用cogStart方法,很难理解 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的必要步骤。

此时,我们已经确定,声明和初始化cogcogType是实现cog必须执行的基线要求的一部分。

以下是LiveClockcog 的结构:

type LiveClock struct {
  cog.UXCog
  ticker *time.Ticker
}

我们在 cog 的结构定义中嵌入了[T0]类型。我们引入一个ticker字段,它是指向time.Ticker的指针。我们将使用这个ticker为实时时钟每秒滴答作响。

以下是LiveClockcog 的构造函数:

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.CleanupSetCleanUp方法包含在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

在启动LiveClockcog 之前,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

如果两个道具都存在,我们继续将实时时钟cogticker属性分配给一个新创建的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.goInitializeIndexPage函数中的代码部分,我们在其中实例化了本地时区的实时时钟 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,我们首先获取本地时区名称和本地时区偏移量。然后我们创建一个名为lcLiveClock cog的新实例。我们调用CogInit方法来初始化 cog。我们调用SetID方法来注册 cog 安装点的id,即cog将输出到的div容器。我们调用SetProp方法来设置"timeLabel""timezoneName""timezoneOffset"道具。最后,我们调用Start方法来启动LiveClockcog。像往常一样,我们检查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]。

实现混合 cogs

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文件夹中的jscss文件夹中。

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 的特征请求。我们创建一个名为DatePickerParamsstruct,作为日期选择器小部件的输入参数:

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 的字段一样,我们还声明了FirstDayfirstDay)、MinDateminDate)、MaxDatemaxDate)和YearRangeyearRange的字段。

阅读 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方法来设置datePickerLabeldatepickerInputIDdatepickerMinDatedatepickerMaxDate道具。我们称之为 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.cssstyles.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属性。

下面是定义carouselcog 的结构:

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的用户是否设置了contentItemscarouselContentID道具。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属性提供了一个字符串片段,分别使用PLAYTEXTSTOPTEXT常量为播放和停止按钮提供文本符号。我们已将Controls属性设置为false,因此默认情况下,图像转盘上不会出现“下一个”和“上一个”按钮。

我们继续迭代cog用户提供的所有属性,访问每个道具,包括propNamestring)和propValueinterface{}):

 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/cssshared/cogs/notify/static/js文件夹中。

通知cogshared/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字段用于引用alertifyJavaScript 对象。

我们正在构建的通知cog是事件驱动的。例如,当从客户端应用的任何页面触发自定义成功通知事件时,将显示成功通知。我们定义了两个字段,successNotificationEventListenererrorNotificationEventListener,这两个字段都是将 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方法来删除处理displaySuccessNotificationdisplayErrorNotification自定义事件的事件侦听器函数。

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对象。我们将自定义事件的名称displaySuccessNotificationeventDetail对象一起作为输入参数传递给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 的自动化端到端测试。这将包括在服务器端和客户端实施测试以实现功能。