65.9K
CodeProject 正在变化。 阅读更多。
Home

探索 Proxy 实现 DOM Intellisense,使用 TypeScript 进行元素绑定、双向数据绑定、事件等

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2020年7月5日

CPOL

20分钟阅读

viewsIcon

9981

downloadIcon

47

了解 Proxy 类如何用于消除元素 ID 字符串字面量,并与 TypeScript 结合,为 HTML 元素提供 Intellisense

目录

引言

我讨厌前端开发的两件事

  1. 元素 ID 是字符串字面量
  2. HTML 中的 JavaScript 代码
  3. JavaScript
  4. 实际上,任何与前端开发相关的事情,但这就是生活

哦,等等!那是四件事。

当我开始为这篇文章编写代码时,我最终经历了一些 U 理论 所描述的“从正在显现的未来中领导”的体验。没错。尽管如此,这就是我的经历。

所以我的“未来”是发现我将在这里展示的代码,嗯,不是我现在真正想用的,因为未来已经到来,当我写完这篇文章时,我意识到有很多事情我会做得不同!无论如何,我发现这是一个有用的探索,探讨如何利用 Proxy 类型将类属性绑定到模型,实现双向绑定,订阅 UI 事件等等,所有这些都使用实际的“编辑时”类型以实现类型安全和 Intellisense 支持——也就是说,没有字符串通过 ID 引用 DOM 元素。因此“IX”诞生了,它是“Interacx”的缩写,这是一个我很久以前创建的 WinForm 工具套件,用于自动化数据操作而无需使用 ORM。我决定重新利用这个名字,因为 WinForm 应用程序,嗯,过时了,现实是,我讨厌的,编写 Web 应用程序,才是真正的应用程序所在,我是说,重点。为了您的无尽乐趣,我决定使用一些 Vue 示例与我在这里使用代理开发的实现进行比较。

优点

使用我在这里开发的代码,我发现了一些优点:

  1. 我没有硬编码 DOM ID 字符串字面量。
  2. 我能够利用 TypeScript 的类型安全性。
  3. 能够将 DOM 元素作为对象属性引用,可以利用 Visual Studio 的 Intellisense。
  4. 连接事件和绑定非常容易。
  5. 编写单元测试非常容易——事实上,单元测试在我看来是这段代码中最有趣的方面之一。
  6. 我没有在 HTML 中放置“声明性代码”
    1. HTML 保持完全干净。
    2. 业务逻辑在代码中实现。
    3. 你不需要同时检查代码和 HTML 来弄清楚到底发生了什么。
  7. 第 6 点
  8. 第 6 点
  9. 第 6 点

我无法足够强调第 6 点的重要性,至少对我而言。对于大型 Web 应用程序,我曾因在代码和标记之间来回跳转以找出条件、循环和渲染而绞尽脑汁,这是一种令人沮丧的体验。对我来说,在 UI 级别包含由业务数据/规则驱动的声明性语法是一个糟糕的,不,是可怕的设计。这就是我不用 Razor 或类似渲染引擎的原因。我个人认为,HTML 中那些神秘的自定义标签,“if”和“loop”标签等,用于控制 UI 渲染,是所谓现代 Web 开发中最糟糕的想法之一。

缺点

所以让我们现实一点

  1. 语法要求 DOM 元素 ID 和对象属性名称之间有特定的映射。
  2. 代理更慢。
  3. 使用代理的代码高度专业化。
  4. 处理数组的代码很奇怪。
  5. 这里的代码在处理所有可能的 DOM 属性、特性和事件方面非常不完整。
  6. 我不知道这里的代码是否足够健壮以处理第 4 点。
  7. 我还没有探索这个概念是否能很好地与第三方小部件库配合使用,我最喜欢的是 jqWidgets。
  8. “未来”来得相当晚,基本上是我写完这篇文章的时候。

而且我真的怀疑有人会说:“哦,我们用 IX 来构建一个大型网站吧”,除了我可能!

那么为什么要费心呢?

  1. 我喜欢探索不同的方法来解决 Web 开发的缺点。
  2. 我没有遇到其他人尝试过这种方法。
  3. 学习代理非常有趣。
  4. 这很有趣!

什么是 Proxy?

至少在 JavaScript 中,`Proxy` 是一个替换你的对象并允许你拦截“`get`”和“`set`”方法的对象。在此处阅读更多关于 `Proxy` 对象的信息:这里

一个简单示例

一个简单的演示就足够了。首先,一个简单的代理存根,它只执行 `get`/`set` 操作并进行控制台日志记录

private myProxyHandler = {
  get: (obj, prop) => {
    console.log(`get ${prop}`);

    return obj[prop];
  },

  set: (obj, prop, val) => {
    console.log(`set ${prop} to ${val}`);
    obj[prop] = val;

    // Return true to accept change.
    return true;
  }
}

一个简单的测试用例

let proxy = new Proxy({}, this.myProxyHandler);
proxy.foo = 1;
let foo = proxy.foo;
console.log(`foo = ${foo}`);

以及输出

set foo to 1
get foo
foo = 1

啊哈,感受力量吧!世界现在是我的了!

一个 DOM 示例

现在让我们做一些更有趣的事情。我们将创建一个类,其中包含一个属性,其名称与 DOM 元素匹配。DOM 元素,带有一个标签,因为输入元素应该有标签

<div class="inline marginTop5">
  <div class="inline label">Name:</div>
  <div class="inline"><input id="name"/></div>
</div>

激动人心!

现在是这个类

class NameContainer {
  name: string;
}

以及新的代理

private valueProxy = {
  get: (obj, prop) => {
    console.log(`get ${prop}`);

    return obj[prop];
  },

  set: (obj, prop, val) => {
    console.log(`set ${prop} to ${val}`);

    let el = document.getElementById(prop) as HTMLInputElement;
    el.value = val;
    obj[prop] = val;

    // Return true to accept change.
    return true;
  }
}

注意,我只添加了这些

let el = document.getElementById(prop) as HTMLInputElement;
el.value = val;

这里,假设属性名就是元素 ID!

现在我们可以设置值,它会代理设置对象的 `name` 属性和 DOM `value` 属性

let nc = new Proxy(new NameContainer(), this.valueProxy);
nc.name = "Hello World!";

结果是:

如果我输入了一些东西,并且我想在“获取”`name` 属性时看到该值怎么办?很简单,getter 变为这样

get: (obj, prop) => {
  console.log(`get ${prop}`);

  let el = document.getElementById(prop) as HTMLInputElement;
  let val = el.value;
  obj[prop] = val;

  return obj[prop];
},

我们可以通过模拟用户所做的更改来测试代码

let nc = new Proxy(new NameContainer(), this.valueProxy);
nc.name = "Hello World!";

// Simulate the user having changed the input box:
let el = document.getElementById("name") as HTMLInputElement;
el.value = "fizbin";

let newName = nc.name;
console.log(`The new name is: ${newName}`);

在控制台日志中,我们看到

set name to Hello World!
get name
The new name is: fizbin

需要注意的是,`obj[prop] = val` 会对非代理对象进行赋值,因此代理的 *setter* 不会被调用。

重点是...

  1. 我正在使用类型(因此也使用 Intellisense)来获取/设置 DOM 元素值。
  2. 我通过假设类属性的名称与元素 ID 相同,消除了名称的字符串字面量。

时髦!这是 Marc 的一小步,却是更好前端开发的一大步!不幸的是,登月需要更多的努力、基础设施、时间和一路上的许多灾难(这里停顿一下,以纪念在太空探索中失去的生命,我不想对“灾难”显得轻浮。)

那么,让我们开始沿着 U 型曲线下降的旅程吧!

关于代码

代码是 TypeScript,带有简单的 HTML 和 CSS,在 VS2017 解决方案中实现。

源代码也可以在 https://github.com/cliftonm/IX 找到。

有两个 HTML 页面可供玩耍

  1. index.html 是演示页面。
  2. Tests/IntegrationTests.html 运行集成测试。

Inner HTML 的简单数据绑定

我们从与 `DIV` 关联的 inner HTML 的简单数据绑定开始。

Vue 的方式

<div id="app">
  {{ message }}
</div>

var app = new Vue({
  el: '#app',
  data: {
   message: 'Hello Vue!'
  }
})

我不喜欢什么

  1. “Mustache”`{{ }}` 的用法。
  2. 那个 `#app`。
  3. 整个 `data` 对象的东西。

IX 的方式

<div id="app"></div>

let form = IX.CreateNullProxy(); // No associated view model.
form.app = "Hello Interacx!";

就是这样!

响应式行为

下一个例子是显示一些实时计算值作为 `SPAN` 标题的一部分。

Vue 的方式

<div id="app-2">
  <span v-bind:title="message">
    Hover your mouse over me for a few seconds
    to see my dynamically bound title!
  </span>
</div>

var app2 = new Vue({
  el: '#app-2',
  data: {
    message: 'You loaded this page on ' + new Date().toLocaleString()
  }
})

IX 的方式

<span id="mySpan">However your mouse over me for a few seconds 
to see the dynamically bound title!</span>

class HoverExample {
  mySpan = {
    attr: { title: "" }
  };

  onMySpanHover = new IXEvent();
}

let form = IX.CreateProxy(new HoverExample());
form
  .onMySpanHover
  .Add(() => 
    hform.mySpan.attr.title = `You loaded this page on ${new Date().toLocaleString()}`);

更冗长,但好处是你正在使用可重复的多播事件处理程序模式。我确实有一个实现,可以直接将标题设置为函数,但我并不喜欢这种实现所需的幕后一次性实现。

条件语句

我也不喜欢用声明性代码元素弄乱标记。

Vue 的方式

<div id="app-3">
  <span v-if="seen">Now you see me</span>
</div>

var app3 = new Vue({
  el: '#app-3',
  data: {
    seen: true
  }
})

IX 的方式

在 IX 中,条件行为通过事件机制实现,通常用于操作元素属性。与上面的 Vue 示例略有不同,请注意添加了两个按钮来切换 `SPAN` 的可见性

<span id="seen">Now you see me...</span>
 <!-- Two ways to declare a button -->
<button id="show">Show</button>
<input id="hide" type="button" value="Hide" />

class VisibilityExample {
  seen = {
    attr: { visible: true }
  };

  onShowClicked = new IXEvent().Add((_, p) => p.seen.attr.visible = true);
  onHideClicked = new IXEvent().Add((_, p) => p.seen.attr.visible = false);
}

IX.CreateProxy(new VisibilityExample());

这些连接到两个按钮,因此有事件处理程序。

这里

  1. 我们有一致的方式来操作元素属性。
  2. Intellisense 在 Visual Studio 中完美运行。
  3. 没有“`string`”元素名称。

循环

Vue 的方式

<div id="app-4">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>

var app4 = new Vue({
  el: '#app-4',
  data: {
    todos: [
      { text: 'Learn JavaScript' },
      { text: 'Learn Vue' },
      { text: 'Build something awesome' }
    ]
  }
})

IX 的方式

<ol id="someList"></ol>

class ListExample {
  someList: string[] = ["Learn Javascript", "Learn IX", "Wear a mask!"];
}

IX.CreateProxy(new ListExample());

结果

鉴于大多数列表来自数据源而不是硬编码

<ol id="someList"></ol>

class ListExample {
  someList: string[] = [];
}

let listForm = IX.CreateProxy(new ListExample());

listForm.someList.push("Learn Javascript");
listForm.someList.push("Learn IX");
listForm.someList.push("Wear a mask!");

或者

let listForm = IX.CreateProxy(new ListExample());
let items = ["Learn Javascript", "Learn IX", "Wear a mask!"];
listForm.someList = items;

按钮点击

Vue 的方式

<div id="app-5">
  <p>{{ message }}</p>
  <button v-on:click="reverseMessage">Reverse Message</button>
</div>

var app5 = new Vue({
  el: '#app-5',
  data: {
  message: 'Hello Vue.js!'
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('')
    }
  }
})

IX 的方式

<div>
  <p id="message"></p>
  <button id="reverseMessage">Reverse Message</button>
</div>

class ReverseExample {
  message = "Hello From Interacx!";
  onReverseMessageClicked = new IXEvent()
    .Add((_, p: ReverseExample) => p.message = p.message.split('').reverse().join(''));
}

IX.CreateProxy(new ReverseExample());

再次注意

  1. 无需“`Mustache`”`{{ }}` 语法。
  2. 没有“`#id`”`string` 来标识元素 ID。
  3. 事件机制是多播的,允许我们连接多个事件(未图示,但这是使用事件的重点。)

点击按钮后

数据转换

以下示例类似于 Vue 的 `.number` 属性,但实际实现更通用。

考虑这个 UI

以及标记(为便于阅读,删除了 CSS 和多余的 DIV)

X:
<input id="x" class="fieldInputSmall" />
Y:
<input id="y" class="fieldInputSmall" />

这里,我们不希望字符串“`1`”和“`2`”相加得到“`12`”,所以我们实现转换器

class InputForm {
  x: number;
  y: number;

  onXChanged = new IXEvent();
  onYChanged = new IXEvent();

  // Converters, so 1 + 2 != '12'
  onConvertX = x => Number(x);
  onConvertY = y => Number(y);

  Add = () => this.x + this.y;
}

class OutputForm {
  sum: number;
}

事件像这样连接

let inputForm = IX.CreateProxy(new InputForm());
let outputForm = IX.CreateProxy(new OutputForm());

inputForm.onXChanged.Add(() => outputForm.sum = inputForm.Add());
inputForm.onYChanged.Add(() => outputForm.sum = inputForm.Add());

在幕后,输入框文本通过 `onConvertX` 和 `onConvertY` 转换器转换为 `Number`,其余部分由属性的标准数据绑定处理,将 `sum` 设置为 `x` 和 `y` 的值。

另外,请注意如何创建类作为 HTML 片段的容器。我们可以很容易地将 `sum` 放入 `InputForm` 中,但我希望通过使用单独的容器对象 `OutputForm` 来演示如何将属性 compartmentalize 到单独的容器中。

双向绑定

我们已经在上面的示例中看到了视图和模型之间的绑定。Vue 的一个示例是根据输入元素的实时更新直接更新一个元素。虽然我想不出一个实际生活中需要这样做的例子,但实时更新(例如筛选条件)肯定是有用的,所以我们从 Vue 示例开始

Vue 的方式

<div id="app-6">
  <p>{{ message }}</p>
    <input v-model="message">
  </div>

var app6 = new Vue({
  el: '#app-6',
  data: {
    message: 'Hello Vue!'
  }
})

IX 的方式

这已经很容易通过事件实现

First Name:
<p id="message2">/p>
<input id="input2"/>

class BidirectionalExample {
  message2: string = "";
  input2: string = "";

  onInput2KeyUp = new IXEvent().Add((v, p: BidirectionalExample) => p.message2 = v);
}

IX.CreateProxy(new BidirectionalExample());

然而,为了使其更“Vue 化”,我们可以这样做

class BidirectionalExample {
  message2 = new IXBinder({ input2: null });
  input2: string = "";

这里,我们将“`from`”元素指定为键,以及键的任何“`value`”。令我不悦的是,键无法以利用 Intellisense 和类型检查的方式实现。我们能做的最好的事情是运行时检查“`from`”绑定元素是否存在。因此,在这一点上,将“`bind from`”属性指定为 `string` 几乎说得通。相反,我选择了这种实现方式

class BidirectionalExample {
  input2: string = "";
  message2 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
  onInput2KeyUp = new IXEvent().Add((v, p: BidirectionalExample) => p.message2 = v);
}

这也有点糟糕,但它具有支持 Intellisense 的优势,尽管你绑定的属性必须已经事先声明。在幕后,我们有一个非常简单的实现来提取名称,通过将函数转换为 `string`

public static nameof<TResult>(name: () => TResult): string {
  let ret = IX.RightOf(name.toString(), ".");

  return ret;
}

遗憾的是,除非你想使用像 ts-nameof 这样的东西,否则 JavaScript 能做的最好也就是这样了,我不想用它,因为 `ts-nameof` 是一个编译时转换,我不想让使用这个库的开发者为了让它工作而费尽周折。

我们还可以将同一个源绑定到不同的目标

<p>
  <label id="message2"></label>
  <label id="message3"></label>
</p>
<input id="input2" />

class BidirectionalExample {
  input2: string = "";
  message2 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
  message3 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
}

以及不同的源到相同的目标

<p>
  <label id="message2"></label>
  <label id="message3"></label>
</p>
<input id="input2" />
<input id="input3" />

class BidirectionalExample {
  input2: string = "";
  input3: string = "";
  message2 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
  message3 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) })
               .Add({ bindFrom: IX.nameof(() => this.input3) });
}

这里,在左侧编辑框中输入会设置消息 2 和 3

在右侧编辑框中输入会设置消息 3

但正如我之前所说,进行这种绑定并没有多大意义。通常,转换会做一些“有用”的事情,所以我们有这个设计好的例子

class BidirectionalExample {
  input2: string = "";
  input3: string = "";
  message2 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) });
  message3 = new IXBinder({ bindFrom: IX.nameof(() => this.input2) }).Add({
    bindFrom: IX.nameof(() => this.input3), 
    op: v => v.split('').reverse().join('') 
  });

// onInput2KeyUp = new IXEvent().Add((v, p: BidirectionalExample) => p.message2 = v);
}

于是我们得到

复选框

绑定复选框状态

Vue 的方式

Vue 有一个优雅的演示,将 `checkbox` 状态绑定到标签

<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>

IX 的方式

鉴于

<input id="checkbox" type="checkbox" />
<label id="ckLabel" for="checkbox"></label>

我们继续遵循使用 TypeScript 类和属性的模式

class CheckboxExample {
  checkbox: boolean = false;
  ckLabel = new IXBinder({ bindFrom: IX.nameof(() => this.checkbox) });
}

IX.CreateProxy(new CheckboxExample());

或者,因为上面的 `nameof` 语法很笨拙,而且代码转译后 JavaScript 中没有真正的“`nameof`”运算符,所以在这种情况下我们不得不回归到字符串字面量

class CheckboxExample {
  checkbox: boolean = false;
  ckLabel = new IXBinder({ bindFrom: "checkbox" });
}

IX.CreateProxy(new CheckboxExample());

或者我们可以连接 `click` 事件

class CheckboxExample {
  checkbox: boolean = false;
  ckLabel: string = "Unchecked";

  onCheckboxClicked = 
    new IXEvent().Add(
      (_, p: CheckboxExample) => 
          p.ckLabel = p.checkbox ? "Checked" : "Unchecked");
}

IX.CreateProxy(new CheckboxExample());

绑定复选框值

View 的方式

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<br>
<span>Checked names: {{ checkedNames }}</span>

请注意,`span` 文本包含方括号

IX 的方式

鉴于

<input id="jane" value="Jane" type="checkbox" />
<label for="jane">Jane</label>
<input id="mary" value="Mary" type="checkbox" />
<label for="mary">Mary</label>
<input id="grace" value="Grace" type="checkbox" />
<label for="grace">Grace</label>
<br />
<label id="ckNames"></label>

我们使用特殊的数组绑定来实现容器对象(因为属性在类中不存在,我不能使用“`nameof`”笨拙的方法,所以“`ID`”遗憾地是字符串字面量。)当然,在下一个示例中,我确实有复选框的属性,但我仍然使用了字符串字面量!

class CheckboxListExample {
  ckNames = IXBinder.AsArray(items => items.join(", "))
    .Add({ bindFrom: "jane", attribute: "value" })
    .Add({ bindFrom: "mary", attribute: "value" })
    .Add({ bindFrom: "grace", attribute: "value" });
}

IX.CreateProxy(new CheckboxListExample());

然后我们得到

请注意,我们没有用复选框状态初始化属性!如果我们这样做

class CheckboxListExample {
  jane: boolean = false;
  mary: boolean = false;
  grace: boolean = false;
  ckNames = IXBinder.AsArray(items => items.join(", "))
    .Add({ bindFrom: "jane", attribute: "value" })
    .Add({ bindFrom: "mary", attribute: "value" })
    .Add({ bindFrom: "grace", attribute: "value" });
}

let ckListExample = IX.CreateProxy(new CheckboxListExample());

我们可以通过编程方式设置检查状态

ckListExample.jane = true;
ckListExample.mary = true;

我们看到

因此,我们这里注意到的一件事是,引用 HTML 元素的属性与*元素的 checked 属性相关联*。这是 IX 编码方式的一个产物,实际上指出了一个有趣的问题——对象属性只映射到 DOM 元素的一个属性,而且 IX 对该 DOM 元素应该是什么,取决于元素是什么,非常有主见!

单选按钮

View 的方式

此示例将 `radio` 按钮的值绑定到 `span`

<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<br>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<br>
<span>Picked: {{ picked }}</span>

IX 的方式

鉴于

<input id="marc" value="Marc" type="radio" name="group1" />
<label for="marc">Marc</label>
<input id="chris" value="Chris" type="radio" name="group1" />
<label for="chris">Chris</label>
<br />
<label id="rbPicked"></label>

我们添加了两个绑定器,无论点击哪个,其绑定器事件都会触发。再次注意,在这个例子中,我没有使用“`nameof`”语法,因为在这种情况下属性不存在!

class RadioExample {
  rbPicked = new IXBinder({ bindFrom: "marc", attribute: "value" })
    .Add({ bindFrom: "chris", attribute: "value" });
}

IX.CreateProxy(new RadioExample());

从而更新到当前选中的单选按钮

如果我们想通过编程方式设置单选按钮的状态,请定义属性

class RadioExample {
  marc: boolean = false;
  chris: boolean = false;
  rbPicked = new IXBinder({ bindFrom: "marc", attribute: "value" })
    .Add({ bindFrom: "chris", attribute: "value" });
}

在代理初始化后,设置状态

let rbExample = IX.CreateProxy(new RadioExample());
rbExample.chris = true;

组合框

Vue 的方式

<select v-model="selected">
  <option disabled value="">Please select one</option>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>
<span>Selected: {{ selected }}</span>

IX 的方式

鉴于

<select id="selector">
  <option selected disabled>Please select one</option>
  <option value="1">A</option>
  <option value="2">B</option>
  <option value="3">C</option>
</select>
<br />
<span id="selection"></span>

以及容器类

class ComboboxExample {
  selector = new IXSelector();
  selection: string = "";

  onSelectorChanged = 
    new IXEvent().Add((_, p) => 
      p.selection = `Selected: ${p.selector.text} with value ${p.selector.value}`);
}

IX.CreateProxy(new ComboboxExample());

我们接着看到

选择后

请注意,作为 `IXSelector` 实现的 `selector` 属性,包含两个属性:`text` 和 `value`,用于选定的项目。

我们还可以通过编程方式初始化选项。给定

<select id="selector2"></select>
<br />
<span id="selection2"></span>

class ComboboxInitializationExample {
  selector2 = new IXSelector().Add
     ({ selected:true, disabled: true, text: "Please select one" })
    .Add({ value: 12, text: "AAA" })
    .Add({ value: 23, text: "BBB" })
    .Add({ value: 34, text: "CCC" });

  selection2: string = "";

  onSelector2Changed = new IXEvent().Add((_, p) => 
  p.selection2 = `Selected: ${p.selector2.text} with value ${p.selector2.value}`);
}

 let cb = IX.CreateProxy(new ComboboxInitializationExample());

我们看到

并使用选项值以编程方式设置选择

cb.selector2.value = 34;

或使用选项文本

cb.selector2.text = "AAA";
<img border="0" height="45" src="5272881/select5.png" width="182" />

或添加到选项列表

cb.selector2.options.push({ text: "DDD", value: 45 });

或删除选项项

cb.selector2.options.pop();

或更改选项的文本和值

cb.selector2.options[2] = { text: "bbb", value: 999 };

实现模式

IX 要求类属性与 DOM 元素 ID 匹配,并且事件处理程序具有特定的签名。

ID 和 Class 属性名称

KeyUp、Changed 和 Convert 事件

Notice:

事件名称使用属性名,首字母大写,所以 `firstName` 变为 `FirstName`。

支持的事件

KeyUp

`on[Prop]KeyUp` - 实时按键弹起事件

Changed

`on[Prop]Changed` - 元素失去焦点

此事件适用于文本、单选和复选框输入以及“`select`”(`combobox`)元素。

转换

`onConvert[Prop]` - 如果已定义,则在 `KeyUp` 和 `Changed` 事件触发之前执行该函数。

悬停

`on[Prop]Hover` - 如果已定义且属性具有签名

{
  attr: { title: "" }
};

这将鼠标悬停时设置元素的标题。

集成测试

我们可以通过在模型更改后直接检查 DOM 元素来轻松测试 IX 的行为,反之亦然。我更喜欢使用“集成测试”而不是“单元测试”这个词,因为我们不是在测试 IX 库中的低级函数——我们正在测试 DOM 元素与对象属性的集成。

测试用例的 HTML 很简单

<div id="testResults" class="inline" style="min-width:600px">
  <ol id="tests"></ol>
</div>

<div id="testDom"></div>

我们有一个有序列表用于测试结果,以及一个 `div` 用于放置每个测试所需的 HTML。

测试运行器

这些测试实际上使用 IX 来操作测试结果,并直接操作 DOM 以模拟 UI 更改。运行器看起来像这样

let testForm = IX.CreateProxy(new TestResults());
let idx = 0;

tests.forEach(test => {
  // Get just the name of the test function.
  let testName = IX.LeftOf(test.testFnc.toString(), "(");

  // The ID will start with a lowercase letter
  let id = IX.LowerCaseFirstChar(testName);

  // Push a template to OL, where the template value is simply the test name, 
  // to the test results ordered list.
  testForm.tests.push(IXTemplate.Create({ value: testName, id: id }));

  // Create an object with the id and proxy it. 
  // This will match the id of the template we just created, so we can set its style.
  // This is a great example of not actually needing to create a class, which is really
  // just a dictionary.
  let obj = {};

  // The classList here allows us to set the test LI element style class 
  // to indicate success/failure of the test.
  obj[id] = { classList: new IXClassList() }; 
  let testProxy = IX.CreateProxy(obj);

  // Create the DOM needed for the test.
  this.CreateTestDom(testForm, test.dom);

  // Run the test and indicate the result.
  this.RunTest(testForm, idx, testProxy, test, id);

  // Remove the DOM needed for the test.
  this.RemoveTestDom(testForm);

  ++idx;
});

我们有这三个辅助函数

CreateTestDom(testForm: TestResults, testDom: string): void {
  testForm.testDom = testDom || "";
}

RemoveTestDom(testForm: TestResults, ): void {
  testForm.testDom = "";
}

RunTest(testForm: TestResults, idx:number, testProxy: object, test, id: string): void {
  let passFail = "pass";

  try {
    test.testFnc(test.obj, id);
  } catch (err) {
    passFail = "fail";
    let template = testForm.tests[idx];
    template.SetValue(`${template.value} => ${err}`);
  }

  testProxy[id].classList.Add(passFail);
}

通过的测试显示为绿色,失败的测试显示为红色,并附有错误消息。

.pass {
  color: green;
}

.fail {
  color: red;
}

例如,我们可以测试故障处理

static ShouldFail(obj): void {
  throw "Failed!!!";
}

我们看到

定义测试

测试被定义为指定以下内容的对象数组

  1. 要运行的测试。
  2. 测试中正在操作的“对象”。
  3. 支持测试的 HTML。

像这样

let tests = [
  // { testFnc: IntegrationTests.ShouldFail },
  { testFnc: IntegrationTests.InputElementSetOnInitializationTest, 
    obj: { inputTest: "Test" }, dom: "<input id='inputTest'/>" },
  { testFnc: IntegrationTests.InputElementSetOnAssignmentTest, 
    obj: { inputTest: "" }, dom: "<input id='inputTest'/>" },
  { testFnc: IntegrationTests.InputSetsPropertyTest, 
    obj: { inputTest: "" }, dom: "<input id='inputTest'/>" },
  { testFnc: IntegrationTests.ListInitializedTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  { testFnc: IntegrationTests.ReplaceInitializedTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  { testFnc: IntegrationTests.ChangeListItemTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  { testFnc: IntegrationTests.PushListItemTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  { testFnc: IntegrationTests.PopListItemTest, 
    obj: { list: ["A", "B", "C"] }, dom: "<ol id='list'></ol>" },
  {
    testFnc: IntegrationTests.ButtonClickTest,
    obj: { clicked: false, onButtonClicked : new IXEvent().Add((_, p) => p.clicked = true)},
    dom: "<button id='button'></button>"
  },
  {
    testFnc: IntegrationTests.OnlyOneClickEventTest,
    obj: { clicked: 0, onButtonClicked: new IXEvent().Add((_, p) => p.clicked += 1) },
    dom: "<button id='button'></button>"
  },
  {
    testFnc: IntegrationTests.CheckboxClickTest,
    obj: { clicked: false, checkbox: false, 
    onCheckboxClicked: new IXEvent().Add((_, p) => p.clicked = p.checkbox)},
    dom: "<input id='checkbox' type='checkbox'/>"
  },
  {
    testFnc: IntegrationTests.RadioButtonClickTest,
    obj: { clicked: false, checkbox: false, 
    onRadioClicked: new IXEvent().Add((_, p) => p.clicked = p.radio) },
    dom: "<input id='radio' type='radio'/>"
  },
  {
    testFnc: IntegrationTests.ConvertTest,
    obj: { inputTest: "", onConvertInputTest: s => `${s} Converted!` },
    dom: "<input id='inputTest'/>"
  },
  { testFnc: IntegrationTests.VisibleAttributeTest, 
    obj: { inputTest: { attr: { visible: true } } }, dom: "<input id='inputTest'/>" },
  { testFnc: IntegrationTests.ControlBindingTest, 
    obj: { input: "123", output: new IXBinder({ bindFrom: "input" }) }, 
    dom: "<input id='input'><p id='output'>" },
  { testFnc: IntegrationTests.ControlBindingWithOperationTest, 
    obj: { input: "123", output: new IXBinder({ bindFrom: "input", 
    op: v => `${v} Operated!` }) }, dom: "<input id='input'><p id='output'>" },
  { testFnc: IntegrationTests.ControlBindingAssignmentTest, 
    obj: { input: "", output: new IXBinder({ bindFrom: "input" }) }, 
    dom: "<input id='input'><p id='output'>" },
];

我不会用实际的测试来烦你,但我会指出,在某些情况下我们必须模拟点击,因此测试必须分发适当的事件,例如

static ButtonClickTest(obj): void {
  let test = IX.CreateProxy(obj);
  let el = document.getElementById("button") as HTMLButtonElement;
  el.dispatchEvent(new Event('click')); 
  IXAssert.Equal(test.clicked, true);
}

IXAssert 辅助函数

这个类只是将 `if` 语句包装成一行代码,因为我相当不喜欢用于断言的 `if` 语句。

export class IXAssert {
  public static Equal(got: any, expected: any): void {
    let b = got == expected;

    if (!b) {
      throw `Expected ${expected}, got ${got}`;
    }
  }

  public static IsTrue(b: boolean): void {
    if (!b) {
      throw "Not true";
    }
  }
}

你不需要 TypeScript 类

你应该从测试的实现方式中意识到,你不需要实际的 TypeScript 类,你只需要一个对象,比如 `obj: { inputTest: "Test" }` —— 毕竟,TypeScript 类纯粹是用于 IDE 类型检查和 Intellisense 的开发端构造。即使是 JavaScript 类也只是“JavaScript 现有基于原型的继承的语法糖”。(JavaScript 类)

幕后

TypeScript 不意味着运行时类型反射

TypeScript 在*编写*代码时对于确保类型安全非常出色。然而,当代码被转译为 JavaScript 时,IDE 使用的所有类型信息当然会丢失。这很不幸,因为有时我真的希望代码中有类型信息。有一些变通方法,例如对于原生类型和类

let a = 1;
let b = "foo";
let c = true;
let d = [];
let e = new SomeClass();
[a, b, c, d, e].forEach(q => console.log(q.constructor.name));

let listForm = IX.CreateProxy(new ListExample());

你得到

这很有用。然而,鉴于这个类

class SomeClass {
  a: number;
  b: string;
}

这被转译成一个空对象 `{}。所以,`Object.keys(new SomeClass())` 返回一个空数组 `[]`。要确定类的属性,属性必须被初始化,它们甚至可以被初始化为 `null` 或 undefined

class SomeClass {
  a: number = null;
  b: string = undefined;
}

因此,IX 中存在一个约束,即你必须初始化属性,否则无法在类属性和具有属性名称 ID 的元素之间建立连接。

Proxy 初始化

public static CreateProxy<T>(container: T): T {
  let proxy = new Proxy(container, IX.uiHandler);
  IX.CreatePropertyHandlers(container, proxy);
  IX.CreateButtonHandlers(container, proxy);
  IX.CreateBinders(container, proxy);
  IX.Initialize(container, proxy);

  return proxy;
}

除了实例化代理,我们还可以看到还需要几个其他步骤

  1. 特殊属性处理器
  2. 按钮处理程序
  3. 绑定器
  4. 最终初始化

CreatePropertyHandlers

此代码旨在处理属性、类列表和事件连接。事件只连接一次,以防在类属性赋值后重新初始化代理。为了使事情复杂一些,这里处理了特定情况,例如将 `attr` 键代理以适应将属性分配给关联 DOM 元素的自定义语法。`class` 属性的处理方式类似,为 `classList` 键创建了一个代理。在结论中讨论了更好的实现。否则,该函数的最初目的仅是处理 `mouseover`、`change` 和 `keyup` 事件。

private static CreatePropertyHandlers<T>(container: T, proxy: T) {
  Object.keys(container).forEach(k => {
  let el = document.getElementById(k);
  let anonEl = el as any;

  // If element exists and we haven't assigned a proxy to the container's field, 
  // then wire up the events.
  if (el && !anonEl._proxy) {
    anonEl._proxy = this;

    if (container[k].attr) {
      // Proxy the attributes of the container so we can intercept the setter for attributes
      console.log(`Creating proxy for attr ${k}`);
      container[k].attr = IXAttributeProxy.Create(k, container[k].attr);
    }

    if (container[k].classList) {
      console.log(`Creating proxy for classList ${k}`);
      container[k].classList = IXClassListProxy.Create(k, container[k].classList);
    }

    let idName = IX.UpperCaseFirstChar(el.id);

    // TODO: create a dictionary to handle this.
    let changedEvent = `on${idName}Changed`;
    let hoverEvent = `on${idName}Hover`;
    let keyUpEvent = `on${idName}KeyUp`;

    if (container[hoverEvent]) {
      IX.WireUpEventHandler(el, container, proxy, null, "mouseover", hoverEvent);
    }

    // Change event is always wired up so we set the container's value 
    // when the UI element value changes.
    switch (el.nodeName) {
      case "SELECT":
      case "INPUT":
        // TODO: If this is a button type, then what?
        IX.WireUpEventHandler(el, container, proxy, "value", "change", changedEvent);
      break;
    }

    if (container[keyUpEvent]) {
      switch (el.nodeName) {
        case "INPUT":
          // TODO: If this is a button type, then what?
          IX.WireUpEventHandler(el, container, proxy, "value", "keyup", keyUpEvent);
          break;
        }
      }
    }
  });
}

很明显,这是一个非常不完整的实现,仅足以用于概念验证。

WireUpEventHandler

附加到事件监听器的事件处理程序对 `SELECT` HTML 元素实现了一个自定义检查,并假设类属性已使用 `IXSelector` 实例初始化。这样做是为了在选择时将选定项目的文本和值分配给 `IXSelector` 实例。否则,事件处理程序会更新类的属性(即被代理的类)。因为“按钮”没有属性,而只是一个事件,所以我们检查 DOM 上是否确实存在需要读取并设置到相应类属性上的属性。最后,如果类实现了事件处理程序,则会触发任何多播事件。如果类中定义了自定义转换器,则会首先为非按钮事件调用它。

private static WireUpEventHandler<T>(el: HTMLElement, container: T, proxy: T, 
  propertyName: string, eventName: string, handlerName: string) {
  el.addEventListener(eventName, ev => {
    let el = ev.srcElement as HTMLElement;
    let oldVal = undefined;
    let newVal = undefined;
    let propName = undefined;
    let handler = container[handlerName];

    switch (el.nodeName) {
      case "SELECT":
        let elSelector = el as HTMLSelectElement;
        let selector = container[el.id] as IXSelector;
        selector.value = elSelector.value;
        selector.text = elSelector.options[elSelector.selectedIndex].text;
        break;

      default:
        // buttons are click events, not change properties.
        if (propertyName) {
          oldVal = container[el.id];
          newVal = el[propertyName];
          propName = el.id;
        }

        let ucPropName = IX.UpperCaseFirstChar(propName ?? "");

        if (propertyName) {
          newVal = IX.CustomConverter(proxy, ucPropName, newVal);
          container[propName] = newVal;
        }

        break;
    }

    if (handler) {
      (handler as IXEvent).Invoke(newVal, proxy, oldVal);
    }
  });
}

同样,足以实现概念验证。

CustomConverter

private static CustomConverter<T>(container: T, ucPropName: string, newVal: string): any {
  let converter = `onConvert${ucPropName}`;

  if (container[converter]) {
    newVal = container[converter](newVal);
  }

  return newVal;
}

CreateButtonHandlers

按钮(以及类似按钮的元素,如复选框和单选按钮)有其独特的要求。复选框和单选按钮(它们是 `INPUT` HTML 元素)有一个 `checked` 属性,而按钮没有。代理的类必须实现预期的“on....”,并且必须将其分配给 `IXEvent` 以支持多播事件。

private static CreateButtonHandlers<T>(container: T, proxy: T) {
  Object.keys(container).forEach(k => {
    if (k.startsWith("on") && k.endsWith("Clicked")) {
      let elName = IX.LeftOf(IX.LowerCaseFirstChar(k.substring(2)), "Clicked");
      let el = document.getElementById(elName);
      let anonEl = el as any;

      if (el) {
        if (!anonEl._proxy) {
        anonEl._proxy = this;
      }

      if (!anonEl._clickEventWiredUp) {
        anonEl._clickEventWiredUp = true;

        switch (el.nodeName) {
          case "BUTTON":
            IX.WireUpEventHandler(el, container, proxy, null, "click", k);
            break;

          case "INPUT":
            // sort of not necessary to test type but a good idea, 
            // especially for checkboxes and radio buttons.
            let typeAttr = el.getAttribute("type");

            if (typeAttr == "checkbox" || typeAttr == "radio") {
              IX.WireUpEventHandler(el, container, proxy, "checked", "click", k);
            } else {
              IX.WireUpEventHandler(el, container, proxy, null, "click", k);
            }

            break;
          }
        }
      }
    }
  });
}

CreateBinders

绑定器处理实时事件,例如按键抬起以及输入元素失去焦点。增加复杂性的是,绑定器可能与多个复选框或单选按钮相关联,并绑定当前选定项目的列表。这是一段令人困惑的代码,因为数组和非数组属性都可以绑定。假设如果绑定的是数组,则该数组填充了选定的复选框或单选按钮(尽管从技术上讲,单选按钮应该是排他的)。否则,属性本身会设置为选中状态或元素的值。最后,可以在值设置到*代理*上之前定义一个可选的“op”(操作)。在代理上设置值而不是在*代理*对象上设置值会调用代理的 setter,它可以定义更多行为,但最终也会将值分配给原始容器对象。

// We assume binders are created on input elements. Probably not a great assumption.
private static CreateBinders<T>(container: T, proxy: T): void {
  Object.keys(container).forEach(k => {

    if (container[k].binders?.length ?? 0 > 0) {
      let binderContainer = container[k] as IXBinder;
      let binders = binderContainer.binders as IXBind[];

      if (binderContainer.asArray) {
        binders.forEach(b => {
          let elName = b.bindFrom;
          let el = document.getElementById(elName);

          let typeAttr = el.getAttribute("type");

          // Limited support at the moment.
          if (typeAttr == "checkbox" || typeAttr == "radio") {
            el.addEventListener("click", ev => {
              let values: string[] = [];

              // Get all the items currently checked
                binders.forEach(binderItem => {
                  let boundElement = (document.getElementById(binderItem.bindFrom) 
                                      as HTMLInputElement);
                  let checked = boundElement.checked;

                  if (checked) {
                    values.push(boundElement[binderItem.attribute]);
                  }
                });

                let ret = binderContainer.arrayOp(values);
                proxy[k] = ret;
              });
            }
          });
        } else {
          binders.forEach(b => {
            let elName = b.bindFrom;
            let el = document.getElementById(elName);
            console.log(`Binding receiver ${k} to sender ${elName}`);

            let typeAttr = el.getAttribute("type");
  
            if (typeAttr == "checkbox" || typeAttr == "radio") {
              el.addEventListener("click", ev => {
                let boundAttr = b.attribute ?? "checked";
                let v = String((ev.currentTarget as HTMLInputElement)[boundAttr]);
                v = b.op === undefined ? v : b.op(v);
                proxy[k] = v;
              });
            } else {
              // Realtime typing
              el.addEventListener("keyup", ev => {
                let v = (ev.currentTarget as HTMLInputElement).value;
                // proxy[elName] = v; --- why?
                v = b.op === undefined ? v : b.op(v);
                proxy[k] = v;
              });

              // Lost focus, or called when value is set programmatically in the proxy setter.
              el.addEventListener("changed", ev => {
                let v = (ev.currentTarget as HTMLInputElement).value;
                v = b.op === undefined ? v : b.op(v);
                proxy[k] = v;
              });
          }
        });
      }
    }
  });
}

初始化

最后一个函数开始很简单,但最终变得更复杂,因为它不仅需要处理原生非数组类型,还需要处理数组和 DOM 元素,如“`select`”

private static Initialize<T>(container: T, proxy: T): void {
  Object.keys(container).forEach(k => {
    let name = container[k].constructor?.name;

    switch (name) {
      case "String":
      case "Number":
      case "Boolean":
      case "BigInt":
        proxy[k] = container[k]; // Force the proxy to handle the initial value.

        break;

      case "Array":
        // Special handling of arrays that have an initial set of elements 
        // so we don't duplicate the elements.
        // At this point, container[k] IS the proxy (IXArrayProxy) 
        // so we have the issue that the proxy is set to 
        // the array but the UI elements haven't been created. If we just do: 
        // proxy[k] = container[k]; 
        // This will initialize the UI list but push duplicates of the into the array.

        // So, for arrays, we want to create the array proxy 
        // as an empty array during initialization instead,
        // then set the empty proxy to the container, then the container to the proxy.
        if (container[k]._id != k) {
          let newProxy = IXArrayProxy.Create(k, container);
          newProxy[k] = container[k];
          container[k] = newProxy;
        }

        break;

      case "IXSelector":
        // Similar to "Array" above, except we are proxying the IXSelector.options array, 
        // not the container itself.
        if (container[k]._id != k) {
          // Set the element that this IXSelector manages 
          // so we know what to do when value and text are assigned.
          container[k]._element = document.getElementById(k);
          let selector = container[k] as IXSelector;

          // Proxy the options array so we can initialize it as well as push/pop.
          if (selector.options.length > 0) {
          let newProxy = IXArrayProxy.Create(k, container);
            newProxy[k] = selector.options;
            selector.options = newProxy;
          }
        }

        break;
    }
  });
}

数组

数组有点像一场噩梦。数组函数,如 `push`、`pop` 和 `length`,实际上是通过代理的 getter 进行向量化处理的(就像代理对象上的任何其他函数一样)

static ArrayChangeHandler = {
  get: function (obj, prop, receiver) {
  // return true for this special property, 
  // so we know that we're dealing with a ProxyArray object.
  if (prop == "_isProxy") {
    return true;
  }

  // Setup for push and pop, preserve state when the setter is called.
  // Very kludgy but I don't know of any other way to do this.
    if (prop == "push") {
    receiver._push = true;
  }

  if (prop == "pop") {
    receiver._pop = true;
  }

  if (prop == "length") {
    return obj[receiver._id].length;
  }

  return obj[prop];
},

请注意,正在设置一个标志,指示在 setter 中即将执行的操作是 `push` 还是 `pop`!此信息用于确定当长度改变时,数组在 setter 中应如何调整。事实证明,弹出数组元素只会改变数组的长度

set: function (obj, prop, val, receiver) {
  // we're looking for this pattern:
  // "setting 0 for someList with value Learn Javascript"
  let id = receiver._id;
  console.log('setting ' + prop + ' for ' + id + ' with value ' + val);

  if (prop == "length" && receiver._pop) {
    let el = document.getElementById(id);
    let len = obj[id].length;

    for (let i = val; i < len; i++) {
      el.childNodes[val].remove();
      obj[id].pop();
    }

    receiver._pop = false;
  } else {

如果 setter 不是 pop,那么它正在更新数组中的现有项

// We might be setting an array item, or we might be doing a push, 
// in either case "prop" is the index value.
if (!isNaN(prop)) {
  let el = document.getElementById(id);
  switch (el.nodeName) {
    // TODO: "UL"!
    case "OL": {
    let n = Number(prop);
    let ol = el as HTMLOListElement;

    if (n < ol.childNodes.length && !receiver._push) {
      // We are replacing a node
      // innerText or innerHTML?
      (ol.childNodes[n] as HTMLLIElement).innerText = val;

或者我们正在向数组中添加一个项目

    } else {
      let li = document.createElement("li") as HTMLLIElement;
      let v = val;

      if (val._isTemplate) {
        let t = val as IXTemplate;
        // innerText or innerHTML?
        li.innerText = t.value;
        li.id = t.id;
        v = t.value;
      } else {
        li.innerText = val;
      }

      (el as HTMLOListElement).append(li);
      obj[id].push(v);
      receiver._push = false;
    }

最后,数组属性可能会被设置为一个全新的数组

} else if (val.constructor.name == "Array") {
  let el = document.getElementById(id);

  // TODO: remove all child elements?

  switch (el.nodeName) {
    case "SELECT":
      (val as IXOption[]).forEach(v => {
        let opt = document.createElement("option") as HTMLOptionElement;
        opt.innerText = v.text;
        opt.value = String(v.value);
        opt.disabled = v.disabled;
        opt.selected = v.selected;
        (el as HTMLSelectElement).append(opt);
      });
    break;

    case "OL":
    case "UL":
      (val as []).forEach(v => {
        let li = document.createElement("li") as HTMLLIElement;
        li.innerText = v;
        (el as HTMLOListElement).append(li);
      });
    break;
  }
}

IXEvent

`IXEvent`(及其辅助类 `IXSubscriber`)是实现多播事件的包装器

export class IXSubscriber {
  subscriber: (obj: any, oldVal: string, newVal: string) => void;

  constructor(subscriber: (obj: any, oldVal: string, newVal: string) => void) {
  this.subscriber = subscriber;
}

  Invoke(obj: any, oldVal: string, newVal: string): void {
    this.subscriber(obj, oldVal, newVal);
  }
}

import { IXSubscriber } from "./IXSubscriber"

export class IXEvent {
  subscribers: IXSubscriber[] = [];

  // We probably only usually want the new value, followed by the container, 
  // followed by the old value.
  Add(subscriber: (newVal: string, obj: any, oldVal: string) => void) : IXEvent {
    this.subscribers.push(new IXSubscriber(subscriber));

    return this;
  }

  Invoke(newVal: string, obj: any, oldVal: string): void {
    this.subscribers.forEach(s => s.Invoke(newVal, obj, oldVal));
  }
}

IXTemplate

这个类是我为模板提供功能的蹩脚尝试。

export class IXTemplate {
  public _isTemplate: boolean = true;

  public value?: string;
  public id?: string;

  public static Create(t: any): IXTemplate {
    let template = new IXTemplate();
    template.value = t.value;
    template.id = t.id;

    return template;
  }

  public SetValue(val: string): void {
    document.getElementById(this.id).innerText = val;
  }
}

这充其量是可疑的,因为它设置了 `innerText` 而不是 `innerHtml`,而且我真的不确定它的用处,除了它在集成测试中使用。

结论

走到 U 的另一端,我现在正在重新考虑整个实现。

我需要 Proxy 吗?

当我实现 `combobox` 的一个示例时,并使用这种结构

selector = new IXSelector();

我想到,嗯,也许所有映射到 DOM 元素的类属性都应该被一个实际的“helper”包装起来。这将允许包装器直接实现元素的 DOM 属性和特性,从而有效地消除了对 Proxy 的需求!它还将消除“自己创造语法”的情况,比如

.Add({ bindFrom: "jane", attribute: "value" })

mySpan = {
  attr: { title: "" }
};

“初始化”过程将仅仅遍历类属性(它们仍然必须存在)并用 DOM ID 初始化属性,因此特定实现可以直接操作 DOM,而不是通过代理。而且我仍然会有 Intellisense,因为包装器实现会包含我将要接触的 DOM 属性和特性。

当然,这需要大量的工作——理想情况下,每个元素的 DOM 都必须重新实现,而这仅仅适用于原生的 HTML 元素。那么第三方 UI 库呢?一种方法是逐步完成,根据我在使用该框架编写的 Web 应用程序中的需求进行。此外,我可以从已经存在的 HTML....接口(例如 HTMLButtonElement)派生此类包装器类。这会奏效,但我也非常喜欢这种美妙之处

nc.name = "Hello World!";

Proxy 和 DOM 包装器

没有理由实现不能通过检查构造函数名称来同时支持两者,就像我已经在初始化过程中所做的那样。因此,这将由开发人员决定

  1. 对于原生类型,代理将封装基本行为。
  2. 对于包装器类型,将不使用代理,从而使开发人员能够更精细地控制元素。

这也将消除开发人员需要学习的任何晦涩的语法,因为包装器将实现已经存在的 HTML 接口,并且人们已经熟悉这些接口。

此外,所有处理数组 push、pop 和赋值的繁琐代码都会变得更加简洁!

未来正在显现

因此,我只能得出结论,在经历了 U 过程之后,我现在知道了未来会是什么样子,至少对于 *我* 想要使用的框架来说!

历史

  • 2020年7月5日:初始版本
探索 Proxy 实现 DOM Intellisense,使用 TypeScript 进行元素绑定、双向数据绑定、事件等 - CodeProject - 代码之家
© . All rights reserved.