借助Knockout.js创建交互式Web界面

当我在Daniweb论坛上度过时,我看到一些问题最终归结为操纵JavaScript数据和网站上的用户界面的问题。当构建允许用户查看,添加,编辑和删除数据的GUI时,一种常见的方法是花费大量时间在JavaScript变量和用户界面之间来回复制数据。此外,这通常涉及管理用户界面,例如在添加新数据时添加新元素。

但是,人们通常会执行不必​​要的额外工作。除了额外的时间,此类工作通常意味着编写额外的JavaScript代码,从而为更多的错误提供了空间。事实证明,有一些JavaScript库编写良好,经过严格测试,并且在简化我在此处描述的内容方面做得很好。

我个人喜欢的一种此类库称为Knockout,您可以在knockoutjs.com上找到它。其他图书馆也能很好地工作。我只是觉得这更适合我的口味。在本教程中,我将向您展示如何启动和运行它。

基本模型

首先,让我们构建基本的数据结构。我更喜欢从那里开始,进行数据建模。在此示例中,我们将创建一个简单的数组,其中包含有关美国总统的信息。数组中的每个项目都会有一个名字,一个姓氏和一个代表总统职位的数字,乔治·华盛顿为1:

{
  firstName: 'George',
  lastName: 'Washington',
  number: 1
};

我们将创建一个持有对象,如下所示:

var MyData = {
  presidents: []
};

这里的想法是,如果这是一个生产应用程序,那么除了总裁,我们以后还会在模型中放更多东西,即使在本教程中我们实际上并没有其他数据。

现在,我们来构建一个网页来进行尝试。如果我们不使用JavaScript框架,则可能会编写遍历数组的JavaScript代码,并在其中创建一个包含TR元素和三个TD元素的HTML字符串。然后,我们可以将其添加到现有表中。以后,如果我们要添加其他数据,则将数据添加到数组,然后创建另一个TR并将其添加到表中。在整个过程中,我们要做两件事:操纵数组和分别操纵屏幕上的元素。

但是相反,我们将使用剔除法来查看修改数据时自动创建和更改元素的难易程度。我们也可以走另一条路。如果我们在屏幕上有输入字段,则当我们在这些字段中输入数据时,数据可以自动更改。这大大简化了我们的工作。

首先,我们将使用CDN托管所需的外部库,jQuery和淘汰赛。 (过去几年来,使用云托管的库可以快速检索,并且不需要我们下载它们并将它们安装在我们的服务器上,生活变得更加轻松。)我最近偶然发现的一个CDN被称为CloudFlare CDN。它得到了不错的评价,似乎可以安全使用。 (我不隶属于他们。)在下面的代码中,您可以看到我在哪里添加了三个脚本行,这些脚本行从CloudFlare中访问了三个库,一个用于jQuery,两个用于敲除。

这是整个文件,我称为ko.html。 (这只是第一个版本,仅显示数据。我们将继续进行添加。)

<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
</head>
<script>

var MyData = {
  presidents: []
};

var datum1 = {
  firstName: 'George',
  lastName: 'Washington',
  number: 1
};

var datum2 = {
  firstName: 'Thomas',
  lastName: 'Jefferson',
  number: 3
};

var datum3 = {
  firstName: 'Abe',
  lastName: 'Lincoln',
  number: 1
};

MyData.presidents.push(datum1);
MyData.presidents.push(datum2);
MyData.presidents.push(datum3);

</script>
<body>

<table>
<thead>
  <tr><th>First Name</th><th>Last Name</th><th>Number</th></tr>
</thead>
<tbody data-bind="foreach: presidents">
  <tr>
    <td data-bind="html: firstName"></td>
    <td data-bind="html: lastName"></td>
    <td data-bind="html: number"></td>
  </tr>
</tbody>
</table>

<script>
ko.applyBindings(MyData);
</script>
</body>
</html>

注意创建数据模型的顶部的脚本部分。然后,我用三个主席结构填充它。 (并且我故意给了Abe Lincoln错误的数字1;我们将添加通过GUI修改数据的功能,然后对其进行修复。)

数据结构只是存储在数组中的几个简单的JavaScript对象。现在,查看主体部分中的表格元素。第一部分只是一个简单的THEAD元素。但是主体是我们使用knockout.js的地方。 Knockout.js使用名为data-bind的自定义HTML属性。 (HTML规范允许我们创建以data-开头的自定义属性。尽管我们不在这里使用jQuery,但jQuery使得通过data()方法访问这些东西变得容易。)TBODY标记具有以下数据绑定:

data-bind="foreach: presidents"

这只是说我们将遍历根对象的Presidents成员中的项目。我们对每个项目做什么?我们将创建一个表行,并在其中填充包含名字,姓氏和数字的TD元素。这是我们的方法:

<td data-bind="html: firstName"></td>

这表示我们将使用从当前对象的firstName成员获取的HTML填充TD。

您可以看到我们如何创建三个TD元素,并用当前对象中的命名成员填充每个TD元素:firstName,lastName和number。外部元素tbody循环遍历JavaScript数组中的对象,敲除将为数组中的每个元素创建内部内容TR和TDs。这是我们加载页面时的外观:

但是为了使所有这些工作正常,我们在文件末尾的元素之后放了一行关键代码:

ko.applyBindings(MyData);

(我们也可以将其放入jQuery文档加载处理程序中。)这将调用敲除,并告诉它在处理数据绑定信息时要使用哪些数据。然后,执行以下步骤:敲除使用MyData加载根对象。我们的TBODY元素访问MyData的Presidents成员,并在Presidents数组中的每个项目上执行循环。对于每个项目,基因剔除都会创建一个由该项目的成员组成的表格行-名字,姓氏和编号。淘汰赛实际上是为我们生成元素的难点。考虑一下我们提供的模板。然后,当我们打开页面时,敲除运行并根据现有数据为我们创建表。非常好!而且几乎不需要编码。实际上,我们的代码包括填充JavaScript数据结构和调用applyBindings。淘汰赛为我们完成了其余的工作。

修改数据

最后一个示例只是基于现有JavaScript数据创建了一个表,该表并不十分复杂。但是,关于淘汰赛的一件好事是,它提供了一种机制,如果您修改数据,屏幕元素将为我们自动更改。这使工作变得更轻松,因为在设计出模板之后,如果我们修改数据,则无需对用户界面做任何事情。我们要做的就是修改我们的数据。也就是说,我们可以专注于数据模型,而不必担心更改用户界面以容纳新数据。但是,要使此工作有效,我们必须首先对代码进行一些调整。敲除可以将我们的数据转换为特殊类型的成员,敲除库随后可以“观察”或关注更改,从而在数据更改时更新用户界面。通过将我们的数据成员(例如firstName)转换为可以调用以设置或检索数据的函数,可以实现此目的。让我们尝试一下。本示例起初会有些麻烦,但是在下一个示例中,我们将看到淘汰赛如何提供一种更简单的方法。

为了转换数据,我们使每个成员成为可观察对象的实例。可观察对象可以容纳对象或简单类型,例如字符串或数字。这是修改后的代码。 (请记住,这有点混乱,但是稍后我们将撤消这些更改,并看到一种更简单的方法。)

<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
</head>
<script>

var MyData = {
  presidents: []
};

var datum1 = {
  firstName: ko.observable('George'),
  lastName: ko.observable('Washington'),
  number: ko.observable(1)
};

var datum2 = {
  firstName: ko.observable('Thomas'),
  lastName: ko.observable('Jefferson'),
  number: ko.observable(3)
};

var datum3 = {
  firstName: ko.observable('Abe'),
  lastName: ko.observable('Lincoln'),
  number: ko.observable(1)
};

MyData.presidents.push(datum1);
MyData.presidents.push(datum2);
MyData.presidents.push(datum3);

</script>
<body>

<table>
<thead>
  <tr><th>First Name</th><th>Last Name</th><th>Number</th></tr>
</thead>
<tbody data-bind="foreach: presidents">
  <tr><td data-bind="html: firstName"></td>
  <td data-bind="html: lastName"></td>
  <td data-bind="html: number"></td></tr>
</tbody>
</table>

<script>
ko.applyBindings(MyData);
</script>
</body>
</html>

我们修改了总统数据,如下所示:

firstName: ko.observable('George'),

以前只是一个简单的任务:

firstName: 'George'

试试看。将此另存为ko2.html。在Chrome或您喜欢用于开发工作的任何浏览器中将其打开。然后按F12打开Chrome的JavaScript开发人员工具。在控制台中,让我们手动修改数据。首先,让我们将林肯的数字固定为16。在JavaScript控制台中键入此数字,然后按Enter:

MyData.presidents[2].number()

这将检索总统索引2中数字的当前值,该值是列表中的第三位。注意,由于我们现在使用的是可观察对象,因此我们将其称为函数。我们将看到数字1出现。因为林肯是第16任总统,所以应该是16岁。在控制台中键入以下内容以更改数据:

MyData.presidents[2].number(16)

键入时,请注意,UI会立即更改。显示林肯的行立即更改为16,而无需我们触摸用户界面。如果再次检索该数据值,我们将看到它显示16:

MyData.presidents[2].number()

换句话说,我们只更改了数据。我们不必手动更改UI。我们不必使用jQuery搜索表,然后使用包含Lincoln的行和包含数字的TD进行搜索,也不必更改TD中的数据以匹配数据模型。用户界面会自动更新,除了更改数据的代码外,我们无需编码。让我们稍微调整一下此代码,看看如何在不使用JavaScript控制台的情况下做到这一点。我们将创建一个标记为“修复”的按钮,它将为我们更改项目。首先,在标记后添加一个按钮:

<button>Fix</button>

然后在ko2.html底部找到脚本部分,并将其更改为如下所示:

<script>
ko.applyBindings(MyData);
$('button').on('click', function() {
    MyData.presidents[2].number(16);
});
</script>

我们正在使用jQuery将处理程序附加到按钮。该代码与我们在控制台中键入的代码相同。现在重新加载页面。查看林肯行,该行显示1.单击按钮,数字将更改为16。有效!

让我们进行下一步。我们不仅要更改现有总裁中的数据,还让我们包括一种添加其他总裁的方法。我们有一个总统列表,因此在列表中添加另一个元素很容易。但是,要使它起作用,我们必须进行快速更改。在主数据模型中找到行长数组的行:

var MyData = {
  presidents: []
};

让我们也将其更改为可观察的。但这将是一个特殊类型,其中包含一个称为可观察数组的数组。更改它看起来像这样,注意在单词observable之后添加了Array这个单词:

var MyData = {
  presidents: ko.observableArray([])
};

这就是全部。现在我们可以将元素添加到数组中,并且接口将自动更新。将其另存为ko3.html,然后在浏览器中将其打开。然后从JavaScript控制台键入以下内容:

MyData.presidents.push({firstName:'John', lastName:'Adams', number: 2})

这段代码从Presidents数组开始,然后调用push将新对象插入到该数组中。当您按Enter键时,您将看到自动添加到列表的新项目,而无需我们手动更改UI。同样,我们只是更改了数据。用户界面会自动更新。

让我们构建使用此方法的最终代码集,而无需打开JavaScript控制台。我们将在页面上放置三个文本输入框,我们可以在其中输入新总统的数据。然后,我们将包含一个添加按钮,该按钮可读取这些文本框,创建数据结构并将该新数据结构插入到总裁列表中。但是,我们需要阅读那些文本框吗?为什么不对这些文本框也使用剔除?为此,我们将另一个成员添加到MyData结构中。

现在我要在这里提些什么。在这一点上,我们开始违反一些在计算机科学课程中学到的好的原则。我将创建一个单独的数据结构,该结构仅用于帮助管理用户界面,而不是我们要存储在数据库中的数据。因此,为了避免混乱,让我们将主数据模型分为两部分:主数据和辅助数据。我们将总裁列表放在主数据中,并将与用户输入字段进行交互的数据放在助手数据中。然后,如果我们将其存储回服务器,则将仅存储主数据,而不存储辅助数据。

<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
</head>
<script>

var MyData = {
  main: {
    presidents: ko.observableArray([])
  },
  helpers: {
    newitem: {
      firstName: ko.observable(),
      lastName: ko.observable(),
      number: ko.observable()
    }
  }
};

var datum1 = {
  firstName: 'George',
  lastName: 'Washington',
  number: 1
};

var datum2 = {
  firstName: 'Thomas',
  lastName: 'Jefferson',
  number: 3
};

var datum3 = {
  firstName: 'Abe',
  lastName: 'Lincoln',
  number: 16
};

MyData.main.presidents.push(ko.mapping.fromJS(datum1));
MyData.main.presidents.push(ko.mapping.fromJS(datum2));
MyData.main.presidents.push(ko.mapping.fromJS(datum3));

</script>
<body>

<table data-bind="with:main">
<thead>
  <tr><th>First Name</th><th>Last Name</th><th>Number</th></tr>
</thead>
<tbody data-bind="foreach: presidents">
  <tr>
    <td data-bind="html: firstName"></td>
    <td data-bind="html: lastName"></td>
    <td data-bind="html: number"></td>
  </tr>
</tbody>
</table>

<br />
<div data-bind="with:helpers.newitem">
    First: <input type="text" data-bind="value:firstName" /><br />
    Last: <input type="text" data-bind="value:lastName" /><br />
    Number: <input type="text" data-bind="value:number" />
</div>
<button>Add</button>

<script>
ko.applyBindings(MyData);
$('button').on('click', function() {
    MyData.main.presidents.push(ko.mapping.fromJS({
        firstName: MyData.helpers.newitem.firstName(),
        lastName: MyData.helpers.newitem.lastName(),
        number: MyData.helpers.newitem.number()
    }));
});

</script>
</body>
</html>

注意我如何将MyData分为两部分。但这意味着要访问Presidents数组,我们必须将其限定为MyData.main.presidents,而以前只是MyData.presidents。这意味着要么像这样修改foreach:foreach:main.presidents,要么使用稍微不同的技术,即剔除“ with”语句,这就是我所做的。查看开始表格标签。我添加了此数据绑定到它:

data-bind="with:main"

这意味着在表内部,剔除将在主数据成员的上下文中进行。因此,当我们在此处访问总裁列表时:

<tbody data-bind="foreach: presidents">

我们实际上将访问main.presidents。

现在看一下我在表格后面添加的div部分。再次,我使用了with,但是这次我想直接接触newitem成员。因此,我使用了:helpers.newitem。另外,查看输入框。为了将它们附加到数据,我使用了一个称为value的敲除命令。这会将数据附加到输入元素的value属性。由于我使用的是可观察对象,因此这两种方法都会起作用:例如,通过代码更改MyData.helpers.newitem.firstName时,UI将立即使用文本框更新以显示该值。您可以在JavaScript控制台中输入以下命令进行尝试:

MyData.helpers.newitem.firstName('Dani')

然后,您会看到FirstName旁边的文本框立即被Dani归档。同样,这意味着要更改UI,例如填充文本框,我们不需要深入研究jQuery并找到文本框。我们只是通过更改模型中的数据来更改它。而且很酷的事情是,如果我们将屏幕上的多个元素附加到该数据项,则所有项都会更新。很酷。

类似地,要读取数据,我们只调用函数而不传递参数,如下所示:

MyData.helpers.newitem.firstName()

这意味着,当我们要添加新总裁时,我们不必手动检查输入值。我们只是从数据中获取它们,再次允许我们不触摸用户界面。

但是现在有一个棘手的部分。用户输入新信息并单击“添加”按钮后,我们要创建一个新的总裁对象。我们不想只接受MyData.helpers.newitem对象并直接添加它。这就是自动填充的数据。但是,如果我们将对象本身直接添加到数组中,那么我们将拥有同一对象的多个副本,这将是一团糟。 (作为练习,我鼓励您考虑那里的含义,甚至尝试一下以查看会发生什么。)相反,我们想要获取对象的完整副本,并将该副本添加到数组中。

我有一些技巧可用于复制JavaScript对象。一种是使用JSON2进行字符串化,然后解析该字符串。如果我们有包含许多成员的复杂对象,而这些成员又是具有成员的对象,则此方法很好。它节省了很多代码,它像这样:

second = JSON.parse(JSON.stringify(first))

首先是我们要复制的原始对象。除了一个轻微的问题,这在这里将部分起作用:我们的对象充满了可观察对象。 ko.mapping提供了一个方便的函数,用于从具有可观察对象的JavaScript对象中检索一个JavaScript对象。在上面的代码中,您可以看到我只是手动复制。我通过调用observable函数访问每个成员以获取该成员的数据,并将其存储回我的新对象中:

firstName: MyData.helpers.newitem.firstName()

对于像我们这里这样的小型数据结构,这很好用。但是,一种更优雅的方法是在剔除中使用另一个函数fromJS。 (从技术上讲,我们先前使用的toJS和fromJS是一个单独的库,即剔除映射库的一部分。我们将该库作为C​​loudFlare CDN的第三行脚本包含在内。)

让我们对代码进行最后的更改,我们将完成。用下面这段代码中的代码替换按钮的处理程序,我们有了最终的应用程序(是的,“ app”。我们不仅在此处构建网页;还在构建交互式Web应用程序!)

<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
</head>
<script>

var MyData = {
  main: {
    presidents: ko.observableArray([])
  },
  helpers: {
    newitem: {
      firstName: ko.observable(),
      lastName: ko.observable(),
      number: ko.observable()
    }
  }
};

var datum1 = {
  firstName: 'George',
  lastName: 'Washington',
  number: 1
};

var datum2 = {
  firstName: 'Thomas',
  lastName: 'Jefferson',
  number: 3
};

var datum3 = {
  firstName: 'Abe',
  lastName: 'Lincoln',
  number: 16
};

MyData.main.presidents.push(ko.mapping.fromJS(datum1));
MyData.main.presidents.push(ko.mapping.fromJS(datum2));
MyData.main.presidents.push(ko.mapping.fromJS(datum3));

</script>
<body>

<table data-bind="with:main">
<thead>
  <tr><th>First Name</th><th>Last Name</th><th>Number</th></tr>
</thead>
<tbody data-bind="foreach: presidents">
  <tr>
    <td data-bind="html: firstName"></td>
    <td data-bind="html: lastName"></td>
    <td data-bind="html: number"></td>
  </tr>
</tbody>
</table>

<br />
<div data-bind="with:helpers.newitem">
    First: <input type="text" data-bind="value:firstName" /><br />
    Last: <input type="text" data-bind="value:lastName" /><br />
    Number: <input type="text" data-bind="value:number" />
</div>
<button>Add</button>

<script>
ko.applyBindings(MyData);
$('button').on('click', function() {
    MyData.main.presidents.push(
      ko.mapping.fromJS(ko.mapping.toJS(MyData.helpers.newitem))
    );
});

</script>
</body>
</html>

结论

这很酷,但是我们还有很多事情可以做。在下一个教程中,我将进一步介绍它,并说明另外两个功能如何:

  • 当用户单击一行时,该行变为可使用文本框编辑。然后用户可以保存更改。
  • 用户可以删除行

您会发现这很容易做到。我们将在表中对HTML进行一些小的调整,但调整很少。淘汰赛将处理困难的部分,让我们专注于数据结构和JavaScript代码。然后,我们将最后看看将数据保存回服务器所需要做的工作。回头见!