微件开发

微件是可复用的用户界面组件,是提供丰富用户体验的关键。GeoScene API for JavaScript 提供了一组即用型微件,为您创建自定义微件奠定了基础。

本指南主题讨论微件开发的基本原理。无论微件的预期功能如何,创建自定义微件的基础是一致的。其他信息部分提供了可帮助您入门的额外资源。

开发要求

在创建自己的自定义微件之前,您需要确保具备所需的要求。这些将根据您的微件要求而有所不同。下面列出的是微件开发的最低要求。

TypeScript

TypeScript 是 JavaScript 的超集。编写完成后,它可以编译成普通的 JavaScript。建议通过 TypeScript 进行微件发开。此处有一个出色的 TypeScript 设置指南页面,其中提供了一些使用 GeoScene API for JavaScript 设置 TypeScript 开发环境的基本步骤。还有大量的优秀在线资源,详细介绍了 TypeScript 是什么,为什么使用它,以及如何使用它。熟悉这些基础知识将使微件开发过程变得更加容易。

JSX

JSX 是一种 JavaScript 扩展语法,其允许以类似于 HTML 的方式描述微件 UI。它看起来类似于 HTML,因为它可以与 JavaScript 内联使用。

熟悉 geoscene/core/Accessor

Accessor 是 API 的核心功能之一,也是所有类(包括微件)的基础。有关其工作原理及其使用模式的其他详细信息,请参阅实现 Accessor主题。

微件生命周期

在开始开发之前,对微件生命周期有一个大致的了解非常重要。无论微件的类型如何,特定于其生命周期的一般概念是相同的。这些是:

  1. constructor (params) - 在设置任何所需属性时,这是最初创建微件的地方。由于微件派生自 Accessor,因此您可以访问获取、设置和监视属性,如使用属性主题中所述。
  2. postInitialize() - 此方法在创建微件之后,但在渲染 UI 之前调用。
  3. render() - 这是唯一必需的方法,用于渲染 UI。
  4. destroy() - 释放微件实例的方法

TypeScript 装饰器

微件开发可利用 TypeScript 装饰器。这允许我们在设计时定义和修改现有属性、方法和构造函数中的常见行为。我们将在下面讨论最常见的微件装饰器类型。

@subclass

这些装饰器可被认为是用于创建API类的底层黏合剂。

以下代码段导入并扩展了基本 Widget 类,并在渲染方法中定义了 UI。JSX 用于定义 UI。在此简单方案中,将创建一个以 John Smith 为其内容的 div 元素。

          
1
2
3
4
5
6
7
8
9
10
import Widget from "@geoscene/core/widgets/Widget";

@subclass("esri.widgets.HelloWorld")
class HelloWorld extends Widget {
  render() {
    return (
      <div>John Smith</div>
    );
  }
}

@property()

装饰器用于定义 Accessor 属性。现在可以 getset 使用此装饰器定义的任何属性。此外,您还可以 watch 任何属性更改。

  
1
2
@property()
name: string;

@aliasOf()

装饰器允许定义属性别名。这有助于保持代码整洁,以免重复现有属性(例如,已在 ViewModel 中实现)。上面提供的完整示例不使用此装饰器。如果存在与此文件关联的 HelloWorldViewModel,则可以通过此方法直接访问其属性,从而避免代码重复。

  
1
2
@aliasOf("viewModel.name")
name: string;

微件实现

以下步骤提供了实现您自定义微件时所需步骤的高度概述:

扩展微件

在最基本的级别上,您将首先通过从基本 Widget 类扩展来创建一个微件。

       
1
2
3
4
5
6
7
// 用于扩展基本 Widget 类的导入
import Widget from "@geoscene/core/widgets/Widget";

@subclass("geoscene.widgets.HelloWorld")
class HelloWorld extends Widget {

}

实现属性和方法

接下来,您可以实现特定于该微件的任何属性和/或方法。此代码片段演示了如何利用这些属性的装饰器

          
1
2
3
4
5
6
7
8
9
10
// 创建 name 属性
@property()
name: string = "John Smith";

// 创建 emphasized 属性
@property()
emphasized: boolean = false;

// 创建 private _onNameUpdate 方法
private _onNameUpdate(): string { return '${this.name}';}

默认情况下,元素中引用的函数将 this 设置为实际元素。您可以选择性地使用 bind 属性来更新 this。下面绑定了 _onNameUpdate 回调方法,以便在监听 name 属性更新时使用。这显示在以下 postInitialize 方法中。

        
1
2
3
4
5
6
7
8
class HelloWorld extends Widget {

  constructor(params?: any) {
    super(params);
    this._onNameUpdate = this._onNameUpdate.bind(this);
  }

}

当微件属性准备就绪时,可在渲染之前调用 postInitialize 方法。在以下代码片段中,我们正在监视 name 属性。更新后,它将调用 _onNameUpdate 回调方法。watchUtils.init() 调用将返回一个 WatchHandle 对象,然后将其传递到 own() 中。这有助于在微件被破坏后清理所有资源。

      
1
2
3
4
5
6
  postInitialize() {
    const handle = watchUtils.init(this, "name", this._onNameUpdate);

    // 用于在微件被破坏后清理资源的助手
    this.own(handle);
  }

渲染微件

实现属性后,使用 JSX 渲染微件的 UI。这在微件的渲染方法中处理,这是微件实现所需的唯一必需方法。

请注意,尚不支持作为 JSX 元素创建的微件。例如,以下代码段将不起作用。

const search = <Search view={view} />;

       
1
2
3
4
5
6
7
render() {
  return (
    <div>
      {this._onNameUpdate()}
    </div>
  );
}

最后,在微件上调用 destroy 将处理微件并释放使用下面的 postInitialize 中引用的 own() 方法注册的所有资源。

      
1
2
3
4
5
6
postInitialize() {
  const handle = watchUtils.init(this, "name", this._onNameUpdate);

  // 用于在微件被破坏后清理资源的助手
  this.own(handle);
}

导出模块

在代码页的最末尾,添加一行以导出对象。

 
1
export default HelloWorld;

微件渲染

下面列出的属性可用于渲染微件:

  • classes:用于构建微件的类属性的值。这有助于简化 CSS 类设置。
  • styles:允许动态更改样式。
  • afterCreate:此回调方法在将节点添加到 DOM 后执行。任何子节点和属性都已应用。在渲染器中使用此方法访问真实 DOM 节点。也可使用每个元素。
  • afterUpdate:每次更新节点时,都会执行此回调方法。
  • bind:此属性用于为事件处理程序设置 this 的值。
  • key:这用于唯一标识在其同级中的 DOM 节点。如果同级元素具有相同选择器,并且这些元素是动态添加/删除的,这一点很重要。
classesstylesafterCreateafterUpdatebindkey
                        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 使用微件的类助手方法的较新方法。
render() {
  const dynamicClass = {
    [CSS.bold]: this.isBold,
    [CSS.italic]: this.isItalic
  }

  return {
    <div class={this.classes(CSS.base, dynamicClass)}>Hello World!</div>
  };
}

// 使用classes属性的较旧方法。
// 如果使用 4.7 之前的版本,请使用此选项。
render() {
  const dynamicClass = {
    [CSS.bold]: this.isBold,
    [CSS.italic]: this.isItalic
  };

  return (
    <div class={CSS.base} classes={dynamicClass}>Hello World!</div>
  );
}

除了上面提到的方法,还有一个便利的 storeNode 方法。您可以使用它来分配引用变量的 HTMLElement DOM 节点。这将使用自定义数据属性 data-node-ref 来存储对元素 DOM 节点的引用。为使其正常工作,还必须将其绑定到微件实例,例如 bind={this},如以下代码片段所示。

           
1
2
3
4
5
6
7
8
9
10
11
// 将 data-node-ref 属性分配给 DOM 节点值。
// 它应该与 “bind” 属性结合使用,
// 并在使用 storeNode 便利方法时使用。

rootNode: HTMLElement = null;

render() {
  return (
    <div afterCreate={storeNode} bind={this} data-node-ref="rootNode" />
  );
}

ViewModel 模式

使用微件框架有两个部分。即:1)微件,以及 2)微件的 ViewModel。微件(即视图)部分负责处理微件的用户界面(UI),这意味着微件如何通过 DOM 显示和处理用户交互。ViewModel 部分负责微件的基础功能,或者更确切地说,负责其业务逻辑。

为什么要将微件框架分为这两个独立的部分?其中一个原因是,通过将微件的视图与其 ViewModel 分离,我们可以提高其可重用性的效率。通过关注微件的 ViewModel,您可以移除 UI 部分,并可专注于其核心逻辑。此逻辑可以重用,并通过具有不同 UI 实现的各种微件进行扩展。此外,从测试中移除 DOM/UI 也可加快速度,因为等式中少了一个因素。此外,由于 ViewModels 是从 geoscene/core/Accessor 扩展而来的,因此它们利用了 Accessor 的所有功能。这有助于保持 JavaScript API 各部分之间的一致性,因为许多其他模块也从此类派生而来。ViewModel 公开了支持视图所需功能所需的 API 属性和方法,而视图包含 DOM 逻辑。

那么这两个部分是如何协同工作的呢?当微件渲染时,将渲染其 state。此状态派生自视图和 ViewModel 的属性。在微件生命周期的某个时刻,视图会调用 ViewModel 的方法/属性。反过来,这会导致属性或结果的更改。触发更改后,视图将收到通知,并将在 UI 端进行更新。

使用搜索微件SearchViewModel 的示例:

              
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建搜索微件
const searchWidget = new Search({
  view: view
});
// 在视图左上角的
// 其他元素下方添加搜索微件
view.ui.add(searchWidget, {
  position: "top-left",
  index: 2
});
// 使用 SearchViewMode 的”search-start”事件
searchWidget.viewModel.on("search-start", (event) => {
  console.log("SearchViewModel says: 'Search started'.");
});

其他信息

有关详细信息,请参阅以下附加链接:

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.

Navigated to 微件开发