实现 Accessor

字数统计: 2k
阅读时长: 约 5 分钟
当前版本: 4.29

本指南提供了常见 Accessor 使用模式的一些示例。Accessor 旨在通过提供一种 getsetwatch 属性的机制来简化自定义 API 类的开发。

以下代码片段包括用于本地构建的 @geoscene/core ES 模块 (ESM),以及 vanilla JavaScript 中的 AMD 示例。

如果使用 AMD 模块TypeScript,则需安装 GeoScene Maps SDK for JavaScript4.x 类型定义。如果您使用的是 @geoscene/core ES 模块,则键入将包含在安装中。有关更多信息,请参阅设置 Typescript 环境

扩展 Accessor

API 中的许多类都可扩展 Accessor 类。这些类可以公开可能具有唯一特征的可观察属性,例如只读或计算。

创建简单子类

注: 使用 JavaScript 和 TypeScript 创建 API 子类之间存在重要差异。

当通过 JavaScript 构建应用程序时,则使用 createSubclass() 方法,该方法会自动调用 super()。当通过 TypeScript 构建应用程序时,则使用 @subclass() 装饰器和 extends 关键字。

此外,当使用 TypeScript 装饰器 (如 @subclass()) 时 ,为了向后兼容性,需要将 useDefineForClassFields 标志设置为 false。有关更多信息,请参阅 TSConfig 参考declaredClass 属性在构造函数中指定为字符串,它有助于 API 区分正在扩展的现有类和正在构建的自定义类。在 API 中,declaredClass只读的。此属性在 JavaScript 和 TypeScript 中提供相同的功能。但实现却是不同的,如下面的示例所示。

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass } from "@geoscene/core/core/accessorSupport/decorators.js";

    // declaredClass is "custom.Color"
    @subclass("custom.Color")
    class Color extends Accessor {
        // ...
    }

使用 Accessor 的 Mixins

GeoScene Maps SDK for JavaScript使用 mixins 来构建其类。mixin 是一个函数,用于在构建子类时帮助创建超类。请阅读此优秀文章,深入了解使用 TypeScript 的 mixins。

注:建议在构建 mixins 时使用 TypeScript,以利用静态类型和类型安全。

首先,定义 EventedMixin 以将事件系统添加到类中。

定义 Accessor mixin - ESM TS定义 Accessor mixin - ESM TS定义 Accessor mixin - AMD JS

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass } from "@geoscene/core/core/accessorSupport/decorators.js";

    // A type to represent a constructor function
    type Constructor<T = object> = new (...args: any[]) => T;

    // A type to represent a mixin function
    // See for more details https://www.bryntum.com/blog/the-mixin-pattern-in-typescript-all-you-need-to-know/
    type Mixin<T extends (...input: any[]) => any> = InstanceType<ReturnType<T>>;

    // TBase extends Constructor<Accessor> indicating that `EventedMixin`
    // expects the base class to extend `Accessor`, for example to be able to use the `watch` method.
    export const EventedMixin = <TBase extends Constructor<Accessor>>(Base: TBase) => {

        @subclass("custom.EventedMixin")
        return class Evented extends Base {

            // A first function defined by the mixin
            emit(type: string, event?: any): boolean {
                // ...
            }

            // Another function defined by the mixin
            on(type: string, listener: (event: any) => void): IHandle {
                // ...
            }
        }
    }

    // define the type of the mixin. This is useful to type properties that extends this mixin
    // eg: `myProperty: EventedMixin;`
    export type EventedMixinType = Mixin<typeof EventedMixin>;

在此示例中,我们创建了一个超类,它扩展了 Accessor 并添加了 EventedMixin 的功能。然后,Collection 类扩展了最终的子类。

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass } from "@geoscene/core/core/accessorSupport/decorators";

    // import the newly created custom mixin
    import { EventedMixin } from "custom/EventedMixin.ts";

    @subclass("custom.Collection")
    export class Collection extends EventedMixin(Accessor) {
        // Collection extends a super class composed of Accessor and EventedMixin.
    }

属性

定义简单属性

如果您希望创建一个简单的、可观察且不需要任何其他行为的属性,请应使用以下语法。您可以为基本属性值定义默认值和类型。如果使用 TypeScript,则可以在构造函数中设置默认属性值。

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass, property } from "@geoscene/core/core/accessorSupport/decorators.js";

    @subclass("custom.Color")
    class Color extends Accessor {
        @property()
        r: number = 255;

        @property()
        g: number = 255;

        @property()
        b: number = 255;

        @property()
        a: number = 1;
    }

定义自定义 getter 和 setter

有时可能需要确认、验证或转换属性上设置的值。设置属性时,可能还需要执行其他(同步)工作。this.set() 方法继承自 Accessor。以下片段显示了此情况。

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass, property } from "@geoscene/core/core/accessorSupport/decorators.js";

    @subclass("custom.Collection")
    class Collection extends Accessor {
        private _items: any[] = [];

        // Example: Define a custom property getter.
        // Accessor caches the values returned by the getters.
        // At this point `length` will never change.
        // See the "Notify a property change" section
        @property()
        get length(): number {
            return this._items.length;
        }

        set length(value: number) {
            // Example: perform validation
            if (value <= 0) {
                throw new Error(`value of length not valid: ${value}`);
            }

            // internally you can access the cached value of `length` using `_get`.
            const oldValue = this.get<number>("length");

            if (oldValue !== value) {
                // a setter has to update the value from the cache
                this.set("length", value);

                // Example: perform additional work when the length changes
                this._items.length = value;
            }
        }
    }

定义只读属性

以下语法演示了如何设置只读属性。this.set() 方法继承自 Accessor。

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass, property } from "@geoscene/core/core/accessorSupport/decorators.js";

    @subclass("custom.Person")
    class Person extends Accessor {
        // Example: read-only property may not be externally set
        @property({ readOnly: true })
        firstName: string;

        @property({ readOnly: true })
        lastName: string;

        updateName(firstName: string, lastName: string): void {
            // We may still update the read-only property internally, which will change
            // the property and notify changes to watchers
            this.set({
                firstName,
                lastName
            });
        }
    }

定义代理属性

以下代码片段演示了如何在内部属性上创建双向绑定。

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import Collection from "@geoscene/core/core/Collection.js";
    import { subclass, property, aliasOf } from "@geoscene/core/core/accessorSupport/decorators.js";

    @subclass("custom.GroupLayer")
    class GroupLayer extends Accessor {
        @property()
        sublayers: Collection = new Collection();

        // Define a property that reflects one in another object.
        @property({ aliasOf: "sublayers.length" })
        length: number;

        // Alternatively you can use the `@aliasOf` decorator
        // @aliasOf
        // length: number

        // You can also proxy a method from another object.
        @aliasOf("sublayers.add")
        add: (item: any) => void;
    }

计算属性

定义计算属性

当属性值依赖于许多其他属性时,可能需要使用此方法。这些属性始终是只读的。

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass, property } from "@geoscene/core/core/accessorSupport/decorators.js";

    @subclass()
    class Person extends Accessor {
        @property()
        firstName: string;

        @property()
        lastName: string;

        @property({
        readOnly: true
        })
        get fullName(): string {
            return `${this.firstName} ${this.lastName}`;
        }
    }

定义可写的计算属性

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass, property } from "@geoscene/core/core/accessorSupport/decorators.js";

    @subclass()
    class Person extends Accessor {
        @property()
        firstName: string;

        @property()
        lastName: string;

        @property()
        get fullName(): string {
            return `${this.firstName} ${this.lastName}`;
        }

        set fullName(value: string) {
            if (!value) {
                this.set("firstName", null);
                this.set("lastName", null);
                this.set("fullName", null);

                return;
            }

            const [firstName, lastName] = value.split(" ");
            this.set("firstName", firstName);
            this.set("lastName", lastName);
            this.set("fullName", value);
        }
    }

通知属性更改

有时属性发生更改时,无法通知。Accessor 具有一个内部方法来通知任何更改,这会将属性标记为 dirty。下次访问该属性时,将重新评估其值。

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass, property } from "@geoscene/core/core/accessorSupport/decorators.js";

    @subclass("custom.Collection")
    class Collection extends Accessor {
        private _items: any[] = [];

        @property({
            readOnly: true
        })
        get length(): number {
            return this._items.length;
        }

        add(item: any): void {
            this._items.push(item);

            // We know the value of `length` is changed.
            // Notify so that at next access, the getter will be invoked
            this.notifyChange("length");
        }
    }

自动转换

定义属性类型

可以为类属性定义类型。

js
    import Graphic from "@geoscene/core/Graphic.js";
    import Accessor from "@geoscene/core/core/Accessor.js";
    import Collection from "@geoscene/core/core/Collection.js";
    import { subclass, property } from "@geoscene/core/core/accessorSupport/decorators.js";

    @subclass()
    class GraphicsLayer extends Accessor {
        @property({
            // Define the type of the collection of Graphics
            // When the property is set with an array,
            // the collection constructor will automatically be called
            type: Collection.ofType(Graphic)
        })
        graphics: Collection<Graphic>;
    }

定义转换属性的方法

有时,您需要在设置属性时验证属性值的类型。一个很好的例子是为特定值设置熟知的预设名称,例如 map.basemap = "tianditu-vector"

如果尚未设置 type 元数据,则会自动为 Accessor 和基本类型创建适当的 cast

js
    import Accessor from "@geoscene/core/core/Accessor.js";
    import { subclass, property, cast } from "@geoscene/core/core/accessorSupport/decorators.js";

    @subclass()
    class Color extends Accessor {
        @property()
        r: number = 0;

        @property()
        g: number = 0;

        @property()
        b: number = 0;

        @property()
        a: number = 1;

        @cast("r")
        @cast("g")
        @cast("b")
        protected castComponent(value: number): number {
            // cast method to clamp the value that
            // will be set on r, g or b between 0 and 255
            return Math.max(0, Math.min(255, value));
        }

        @cast("a")
        protected castAlpha(value: number): number {
            // cast method to clamp the value that
            // will be set on a between 0 and 1
            return Math.max(0, Math.min(1, value));
        }
    }

其他信息

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