返回博客首页
← 所有文章

NativeScript Observable 魔术字符串属性名,滚蛋!

2017年12月13日 — 作者:Alexander Ziskind

引言

如今,JavaScript API 中一种流行的做法是将与属性名匹配的字符串作为参数传递给某些函数。这些有时被称为“魔术字符串”,它们非常容易导致代码异味。

为什么我们不应该使用它们?毕竟,JavaScript 允许我们通过以下两种方式访问对象属性

obj.property
obj[‘property’]

使用字符串表示属性名极易出错,也不利于重构。在保持代码属性和标记中绑定属性同步方面,我们已经有很多需要担心的事情了,至少让我们在代码方面消除这种担忧。

在 NativeScript Core 应用程序中,支持双向数据绑定的常见模式是创建一个扩展 NativeScript 自身的 Observable 的视图模型。通过使用 Observable 的 get()set() 方法,我们可以免费获得 UI 更新。不幸的是,这些方法接受魔术字符串来让 API 知道更新了哪个属性名。

借助 TypeScript,我们可以使用以下三种方法中的一种或两种来解决此问题。选择您最喜欢的或最适合您情况的方法。

设置

我们可以用一个简单的“Hello World”应用程序来演示这一点。我们可以使用这个简单的 CLI 命令快速生成一个带有常见“Hello World”模板和 TypeScript 的 NativeScript 应用程序

tns create magic-strings-be-gone --tsc

在代码编辑器中打开项目文件夹,并打开 main-view-model.ts 文件,该文件定义了 HelloWorldModel 类。我们在这里看到的带有 getter 和 setter 的 message 属性绑定到标记中的一个标签。对于此练习,我们不需要 getter 和 setter,一个简单的公共属性就足够了。删除 message 属性的 getter 和 setter,只需添加一个 string 类型的公共 message 属性。如果需要,您还可以删除私有 _message 字段,但它并没有打扰任何人,对吧?哦,直接删除它吧,你知道你想要这么做。

此时,我们的类应该如下所示

export class HelloWorldModel extends Observable {
    private _counter: number;
    constructor() {
        super();
        this._counter = 42;
        this.updateMessage();
    }

    public message: string;

    public onTap() {
        this._counter--;
        this.updateMessage();
    }

    private updateMessage() {
        if (this._counter <= 0) {
            this.message = 'Hoorraaay! You unlocked the NativeScript clicker achievement!';
        } else {
            this.message = `${this._counter} taps left`;
        }
    }
}

这里的问题是,当我们更新 message 属性时,我们不会收到通知,并且我们的 UI 不会更新。为了获得免费的 UI 更新,我们必须使用 Observable 的 set() 函数来设置 message 属性,该函数会在内部触发通知。因此,将我们的 updateMessage() 方法更改为此

private updateMessage() {
    if (this._counter <= 0) {
        this.set('message', 'Hoorraaay! You unlocked the NativeScript clicker achievement!');
    } else {
        this.set('message', `${this._counter} taps left`);
    }
}

那些魔术字符串出现了!Observable 的 API 要求将字符串发送到 set() 方法,而我们的问题就从这里开始。如果我们重构代码并更改属性名或魔术字符串(或者更糟糕的是,其中一个魔术字符串),那么我们将遇到非常难以诊断的运行时问题。

让我们修复它,好吗?

方法 1

TypeScript 有一个方便的操作符,允许我们创建一个类的公共属性名的“列表”。好吧,这并不完全准确。我们可以创建一个 type,它包含类中所有公共属性的字符串形式。

我们可以使用神奇的 keyof 操作符来做到这一点。

在我们的 HelloWorldModel 类定义上方,定义一个新类型并使用 keyof 操作符

type MessageType = keyof HelloWorldModel;

如果我们的编辑器(如 Visual Studio Code)提供了 TypeScript 智能感知,当我们悬停在 MessageType 标识符上时,我们将看到我们创建的新类型是 HelloWorldModel 类所有公共属性的字符串形式。

这很完美,因为现在我们可以创建一个常量,它只包含其中一个属性,特别是 message 属性

const messageType: MessageType = 'message';

此常量看起来像一个字符串,但它不是字符串。它的类型为 MessageType。如果我们尝试拼写错误 message,我们将立即看到编译错误。由于我们现在有一个强类型常量,因此我们只需在 updateMessage() 方法的 set() 方法调用中使用它即可

private updateMessage() {
    if (this._counter <= 0) {
        this.set(messageType, 'Hoorraaay! You unlocked the NativeScript clicker achievement!');
    } else {
        this.set(messageType, `${this._counter} taps left`);
    }
}

现在,如果我们更改属性名称,我们将得到编译错误,这比得到运行时错误好得多。

方法 2

如果我们想要一种更通用的方法,并且不想为要更新的每个属性添加一个常量,那么我们可以使用下一种方法,它类似于我们仍然使用 keyof 操作符。

创建一个名为 observable-extensions.ts 的新文件,并将以下代码添加到该文件

import { Observable } from 'data/observable';

export function getObservableProperty<T extends Observable, K extends keyof T>(obj: T, key: K) {
    return obj.get(key);
}

export function setObservableProperty<T extends Observable, K extends keyof T>(obj: T, key: K, value: T[K]) {
    obj.set(key, value);
}

我们在这里导出两个作用于 Observable 的函数。看看泛型 setObservableProperty() 函数。它指定第一个参数的类型为 T,它必须继承自 Observable,而我们的 HelloWorldModel 就是这样的一个类。第二个参数的类型为 K,它扩展了 keyof T 的类型,也就是扩展 Observable 的类的所有公共属性的列表。那里有很多东西需要解包,但相信我,它有效。

我们现在可以在 main-vew-model.ts 文件中导入 setObservableProperty() 函数

import { setObservableProperty } from './observable-extensions';

并再次修改 updateMessage() 方法以使用新函数

private updateMessage() {
    if (this._counter <= 0) {
        setObservableProperty(this, 'message', 'Hoorraaay! You unlocked the NativeScript clicker achievement!');
    } else {
        setObservableProperty(this, 'message', `${this._counter} taps left`);
    }
}

这看起来非常简单。它甚至看起来像我们又有了我们的魔术字符串,但实际上,message 参数根本不是字符串。它是一个 type。如果我们尝试拼写错误,我们将立即得到 TypeScript 编译错误,这是我们在本文开头没有得到的。

方法 3

我们在方法 2 中看到的扩展函数方法很棒,它不允许我们弄乱属性名称。但是,调用 observable 属性扩展函数的语法不如我们希望的那样直观。如果我们能够像往常一样设置我们的 message 属性并忘记它,那不是很好吗?

属性装饰器来救援!

感谢 Peter Staev 提供了一种可以帮助我们做到这一点的装饰器方法。以下是如何使我们的视图模型代码更简洁。

创建另一个文件,我们称之为 observable-decorator.ts,并添加此代码

import { Observable } from "data/observable";

export function ObservableProperty() {
    return (obj: Observable, key: string) => {
        let storedValue = obj[key];

        Object.defineProperty(obj, key, {
            get: function () {
                return storedValue;
            },
            set: function (value) {
                if (storedValue === value) {
                    return;
                }
                storedValue = value;
                this.notify({
                    eventName: Observable.propertyChangeEvent,
                    propertyName: key,
                    object: this,
                    value,
                });
            },
            enumerable: true,
            configurable: true
        });
    };
}

此文件提供了一个名为 ObservableProperty 的装饰器,可以应用于您希望与 UI 保持同步的视图模型中的属性。在此处阅读有关 JavaScript 装饰器的更多信息 此处此处

就我们的示例而言,每次您在视图模型中更新 message 属性时,您都希望运行一段代码来通知 UI。如果您查看上面代码中的 set: 函数定义,您会注意到我们将更新的值存储在局部字段中,然后调用 notify 方法来做到这一点。

为了将此装饰器应用于视图模型类中的 message 属性,只需像这样将装饰器添加到属性即可

@ObservableProperty()
public message: string;

现在您的 updateMessage() 方法可以简化为

private updateMessage() {
    if (this._counter <= 0) {
        this.message = 'Hoorraaay! You unlocked the NativeScript clicker achievement!';
    } else {
        this.message = `${this._counter} taps left`;
    }
}

并且一切仍然有效。

以下是包含方法 3 实现的完整视图模型文件,以供参考

import { Observable } from 'data/observable';
import { ObservableProperty } from './observable-decorator';

export class HelloWorldModel extends Observable {
    private _counter: number;

    constructor() {
        super();
        this._counter = 42;
        this.updateMessage();
    }

    @ObservableProperty()
    public message: string;

    public onTap() {
        this._counter--;
        this.updateMessage();
    }

    private updateMessage() {
        if (this._counter <= 0) {
            this.message = 'Hoorraaay! You unlocked the NativeScript clicker achievement!';
        } else {
            this.message = `${this._counter} taps left`;
        }
    }
}

此方法确实使您能够保持模型代码的简洁,但您会失去使用 keyof 操作符获得的一些强类型。因此,选择哪种方法取决于您,但所有三种方法都肯定有助于使我们的代码更安全。

TypeScript 为我们提供了一些强大的工具来帮助我们避免出错,而 keyof 操作符是一个非常棒的工具。虽然这些技术是在 NativeScript Core 应用程序的上下文中演示的,但我们可以在任何 API 需要魔术字符串作为属性名的应用程序中使用它们。

您可能也喜欢本文的视频版本,此处提供。如果您喜欢视频学习,并且对更多从初学者到高级的 NativeScript 开发技巧感兴趣,请查看 NativeScripting.com 获取视频课程。

编码愉快。