三、优化应用并使用组件显示数据

第 2 章显示、循环、搜索和过滤数据中,我们的 Vue 应用显示了我们的人员目录,我们可以借此机会优化我们的代码并将其分离为组件。这使代码更易于管理,更易于理解,并使其他开发人员(或您,在几个月后回来查看代码时)更容易计算出数据流。

本章将涵盖:

  • 通过减少重复和逻辑组织代码来优化 Vue.js 代码
  • 如何创建 Vue 组件并与 Vue 一起使用
  • 如何与组件一起使用道具和插槽
  • 利用事件在组件之间传输数据

优化代码

当我们在解决问题的同时编写代码时,出现了一个问题,您需要后退一步,查看代码以优化它。这可能包括减少变量和方法的数量或创建方法,以减少重复功能。我们当前的 Vue 应用如下所示:

      const app = new Vue({
        el: '#app',
        data: {
          people: [...],
          currency: '$',
          filterField: '',
          filterQuery: '',
          filterUserState: ''
        },
        methods: {
          activeStatus(person) {
            return (person.isActive) ? 'Active' : 
             'Inactive';
          },
          activeClass(person) {
            return person.isActive ? 'active' : 
            'inactive';
          },
          balanceClass(person) {
            let balanceLevel = 'success';
            if(person.balance < 2000) {
              balanceLevel = 'error';
            } else if (person.balance < 3000) {
              balanceLevel = 'warning';
            }
            let increasing = false,
            balance = person.balance / 1000;
            if(Math.round(balance) == 
             Math.ceil(balance)) {
              increasing = 'increasing';
            }
            return [balanceLevel, increasing];
          },
          formatBalance(balance) {
            return this.currency + balance.toFixed(2);
          },
          formatDate(date) {
            let registered = new Date(date);
            return registered.toLocaleString('en-US');
          },
          filterRow(person) {
            let result = true;
            if(this.filterField) {
              if(this.filterField === 'isActive') {
                result = (typeof this.filterUserState 
                 === 'boolean') ? (this.filterUserState 
                 === person.isActive) : true;
              } else {
                let query = this.filterQuery,
                    field = person[this.filterField];
                if(typeof field === 'number') {
                  query.replace(this.currency, '');
                  try {
                    result = eval(field + query);
                  } catch(e) {}
                } else {
                  field = field.toLowerCase();
                  result =        
            field.includes(query.toLowerCase());
                }
              }
            }
            return result;
          },
          isActiveFilterSelected() {
            return (this.filterField === 'isActive');
          }
        }
      });

看看前面的代码,我们可以做一些改进。这些措施包括:

  • 减少筛选变量的数量并进行逻辑分组
  • 组合格式函数
  • 减少硬编码变量和属性的数量
  • 将方法重新排序为更符合逻辑的顺序

我们将分别讨论这些要点,这样我们就有了一个干净的代码库,可以用它来构建组件。

减少筛选变量的数量并进行逻辑分组

过滤当前使用了三个变量,filterFieldfilterQueryfilterUserState。当前唯一链接这些变量的是名称,而不是它们自己的对象中系统地链接它们。这样做可以避免对它们是否与同一组件相关或只是巧合地相同产生歧义。在数据对象中,创建一个名为filter的新对象,并将每个变量移到内部:

      data: {
        people: [..],
        currency: '$',
        filter: {
          field: '',
          query: '',
          userState: '',
        }
      }

要访问数据,请将任何引用的filterField更新为this.filter.field。请注意额外的点,表示它是过滤器对象的键。别忘了更新filterQueryfilterUserState参考文献。例如,isActiveFilterSelected方法将变为:

      isActiveFilterSelected() {
        return (this.filter.field === 'isActive');
      }

您还需要更新视图中的v-modelv-show属性。各种变量共出现五次。

在更新过滤变量时,我们可以借此机会删除一个。使用当前的过滤器,一次只能有一个过滤器处于活动状态。这意味着queryuserState变量只在任何时候使用,这使我们有机会组合这两个变量。要做到这一点,我们需要更新视图和应用代码以满足这一需求。

从过滤器数据对象中删除userState变量,并将视图中出现的filter.userState更新为filter.query。现在在 Vue JavaScript 代码中为filter.userState查找并替换,再次将其替换为filter.query

在浏览器中查看您的应用时,它将显示为初始工作状态,能够按字段筛选用户。但是,如果按状态筛选,然后切换到任何其他字段,则不会显示查询字段。这是因为使用单选按钮将值设置为布尔值,而在尝试将查询字段转换为小写时,布尔值无法转换为小写。为了解决这个问题,我们可以使用本机 JavaScriptString()函数将filter.query变量中的任何值转换为字符串。这确保了我们的过滤功能可以处理任何过滤输入:

      if(this.filter.field === 'isActive') {
        result = (typeof this.filter.query ===        
       'boolean') ? (this.filter.query ===             
        person.isActive) : true;
         } else {
        let query = String(this.filter.query),
            field = person[this.filter.field];
           if(typeof field === 'number') {
           query.replace(this.currency, '');
          try {
            result = eval(field + query);
          } catch(e) {}
        } else {
          field = field.toLowerCase();
          result = field.includes(query.toLowerCase());
        }

现在,将其添加到我们的代码中可以确保无论值是多少,查询数据都是可用的。现在的问题是当用户在要筛选的字段之间切换时。如果您选择了活动用户并选择了单选按钮,则过滤将按预期工作。但是,如果您现在切换到电子邮件或其他字段,则输入框中会预先填充有truefalse。这会立即过滤,通常不会返回任何结果。在两个文本过滤字段之间切换时也会发生这种情况,这不是期望的效果。

我们想要的是,无论何时更新选择框,过滤器查询都应该清除。无论是单选按钮还是输入框,选择新字段都应重置过滤器查询,这确保可以开始新的搜索。

这是通过删除选择框和filter.field变量之间的链接并创建我们自己的方法来处理更新来完成的。然后,当选择框更改时,我们触发该方法。然后,此方法将清除query变量,并将field变量设置为选择框值。

删除选择框上的v-model属性并添加新的v-on:change属性。我们将向其中传递一个方法名称,该名称将在每次更新选择框时触发。

v-on是我们以前从未遇到过的新 Vue 绑定。它允许您将动作从元素绑定到 Vue 方法。例如,v-on:click是最常用的一种,它允许您将click函数绑定到元素。我们将在本书的下一节中对此进行详细介绍。

其中 v-bind 可以abbreviated表示一个冒号,v-on可以缩短为@符号,允许您使用@click="",例如:

      <select v-on:change="changeFilter($event)"     
       id="filterField">
        <option value="">Disable filters</option>
        <option value="isActive">Active user</option>
        <option value="name">Name</option>
        <option value="email">Email</option>
        <option value="balance">Balance</option>
        <option value="registered">Date 
         registered</option>
      </select>

该属性在每次更新时触发changeFilter方法,并将更改的$event数据传递给它。这个默认的 Vue 事件对象包含了很多我们可以利用的信息,但是target.value数据是我们需要的关键

在 Vue 实例中创建一个新方法,该方法接受事件参数并更新queryfield变量。query变量需要清除,因此将其设置为空字符串,而field变量可以设置为选择框的值:

      changeFilter(event) {
        this.filter.query = '';
        this.filter.field = event.target.value;
      }

现在查看应用时,应清除任何筛选器查询,同时仍按预期操作。

组合格式函数

我们下一步的优化将是在我们的 Vue 实例中结合formatBalanceformatDate方法。然后,这将允许我们扩展格式函数,而不会使用具有类似功能的多个方法使代码膨胀。有两种方法可以实现格式样式函数,我们可以自动检测输入的格式,或者将所需的格式选项作为第二个选项传入。两者都有各自的优点和缺点,但我们将逐一讨论。

自动检测格式化

当传递到函数中时,自动检测变量类型对于更干净的代码非常有用。在您的视图中,您可以调用函数并传递一个要格式化的参数。例如:

      {{ format(person.balance) }}

然后,该方法将包含一个switch语句,并根据typeof值格式化变量。switch语句可以对单个表达式求值,然后根据输出执行不同的代码。Switch语句可以非常强大,因为它们允许根据结果使用几个不同的代码位构建子句。更多关于 MDN 上的switch声明的信息可以阅读

如果您比较相同的表达式,那么Switch语句是if语句的最佳替代。对于一个代码块,您还可以有多个案例,如果前面的案例都不满足,甚至可以包含一个默认案例。作为一个正在使用的示例,我们的格式化方法可能如下所示:

      format(variable) {
        switch (typeof variable) {
          case 'string':
          // Formatting if the variable is a string
          break;
          case 'number':
          // Number formatting
          break;
          default:
          // Default formatting
          break;
        }
      }

重要的是要注意break;行。这些完成了每个switch案例。如果省略了中断,代码将继续并执行以下情况,这有时是期望的效果。

自动检测变量类型和格式是简化代码的好方法。然而,对于我们的应用来说,这不是一个合适的解决方案,因为我们正在格式化日期,当输出typeof时,它会产生一个字符串,并且无法从我们希望格式化的其他字符串中识别出来。

传入第二个变量

上述自动检测的替代方法是将第二个变量传递到format函数中。如果我们希望格式化其他字段,这将为我们提供更大的灵活性和可伸缩性。对于第二个变量,我们可以传入与switch语句中预选列表匹配的固定字符串,也可以传入字段本身。视图中的固定字符串方法示例如下:

      {{ format(person.balance, 'currency') }}

如果我们有几个不同的字段,所有这些字段都需要像balance当前那样进行格式化,那么这将非常有效,但在使用balance键和currency格式时似乎有一些轻微的重复

作为折衷方案,我们将传递person对象作为第一个参数,这样我们就可以访问所有数据,并将字段的名称作为第二个参数。然后,我们将使用它来识别所需的格式方法和返回特定数据。

创建方法

在您看来,将formatDateformatBalance函数替换为单数格式函数,将person变量作为第一个参数传入,并将包含引号的字段作为第二个参数传入:

      <td v-bind:class="balanceClass(person)">
        {{ format(person, 'balance') }}
      </td>
      <td>
        {{ format(person, 'registered') }}
      </td>

在 Vue 实例中创建一个新的格式化方法,该方法接受两个参数:personkey。第一步,使用 person 对象和key变量检索字段:

      format(person, key) {
        let field = person[key],
            output = field.toString().trim();      
        return output;
      }

我们还在函数中创建了第二个变量,名为output——这将是函数末尾返回的变量,默认设置为field。这确保了如果我们的格式化键与传入的不匹配,将返回未触及的字段数据。但是,我们会将字段转换为字符串,并从变量中删除任何空格。现在运行应用将返回没有任何格式的字段。

添加一个switch语句,将表达式设置为仅为key。在switch语句中添加两个案例,一个是balance,另一个是registered。由于我们不希望我们的输入与案例不匹配时发生任何事情,因此我们无需有default声明:

      format(person, key) {
        let field = person[key],
            output = field.toString().trim();

        switch(key) {
 case 'balance':
 break;
 case 'registered':
 break;
 }
        return output;
      }

现在,我们只需要将代码从原始格式化函数复制到各个案例中:

      format(person, key) {
        let field = person[key],
            output = field.toString().trim();

        switch(key) {
          case 'balance':
            output = this.currency + field.toFixed(2);
            break;

          case 'registered':
           let registered = new Date(field);
 output = registered.toLocaleString('en-US');
          break;
        }
        return output;
      }

这个格式函数现在灵活多了。我们可以添加更多的switch案例,以满足更多字段的需要(例如,处理name字段),或者我们可以向现有代码添加新案例。这方面的一个例子是,如果我们的数据包含一个字段,该字段详细说明了用户deactivated的帐户日期,我们可以轻松地以与注册相同的格式显示该字段:

      case 'registered':
 case 'deactivated':
        let registered = new Date(field);
        output = registered.toLocaleString('en-US');
        break;

减少硬编码变量和属性的数量,减少冗余

当查看 Vue JavaScript 时,很快就会发现可以通过引入全局变量和在函数中设置更多局部变量来优化它,从而使其更具可读性。我们也可以使用现有的功能来停止重复我们自己。

第一个优化是在我们的filterRow()方法中,我们检查filter.field是否处于活动状态。这在我们用来显示和隐藏单选按钮的isActiveFilterSelected方法中也有重复。更新if语句,改为使用此方法,代码如下:

      ...

    if(this.filter.field === 'isActive') {
    result = (typeof this.filter.query === 'boolean') ?       
    (this.filter.query === person.isActive) : true;
      } else {

      ...

前面的代码删除了this.filter.field === 'isActive'代码,并替换为isActiveFilterSelected()方法。现在应该是这样的:

      ...

    if(this.isActiveFilterSelected()) {
    result = (typeof this.filter.query === 'boolean') ?     
     (this.filter.query === person.isActive) : true;
     } else {

      ...

当我们使用filterRow方法时,我们可以通过在方法开始时将queryfield存储为变量来减少代码。result也不是这个的正确关键字,所以我们把它改为visible。首先,在开始时创建并存储两个变量,并将result重命名为visible

      filterRow(person) {
        let visible = true,
 field = this.filter.field,
 query = this.filter.query;      ...

替换该变量函数中的所有实例,例如,方法的第一部分如下所示:

      if(field) {
          if(this.isActiveFilterSelected()) {
            visible = (typeof query === 'boolean') ?   
            (query === person.isActive) : true;
          } else {

          query = String(query),
          field = person[field];

保存文件并在浏览器中打开应用,以确保优化不会破坏功能。

最后一个阶段是将方法重新排序为对您有意义的顺序。可以随意添加注释来区分不同的方法类型,例如,与 CSS 类或过滤相关的方法类型。我也删除了activeStatus方法,因为我们可以利用format方法格式化这个字段的输出。经过优化后,JavaScript 代码现在看起来如下所示:

      const app = new Vue({
        el: '#app',
         data: {
          people: [...],
          currency: '$',
          filter: {
            field: '',
            query: ''
          }
        },
        methods: {
          isActiveFilterSelected() {
            return (this.filter.field === 'isActive');
          },
          /**
           * CSS Classes
           */
          activeClass(person) {
             return person.isActive ? 'active' : 
             'inactive';
          },
           balanceClass(person) {
            let balanceLevel = 'success';
            if(person.balance < 2000) {
              balanceLevel = 'error';
            } else if (person.balance < 3000) {
              balanceLevel = 'warning';
            }
                let increasing = false,
                balance = person.balance / 1000;
            if(Math.round(balance) == 
             Math.ceil(balance)) {
              increasing = 'increasing';
            }
            return [balanceLevel, increasing];
          },
          /**
           * Display
           */
          format(person, key) {
            let field = person[key],
            output = field.toString().trim();
            switch(key) {
              case 'balance':
                output = this.currency + 
              field.toFixed(2);
                break;
              case 'registered':
          let registered = new Date(field);
          output = registered.toLocaleString('en-US');
          break;  
        case 'isActive':
          output = (person.isActive) ? 'Active' : 
          'Inactive';
            }
        return output;
          },  
          /**
           * Filtering
           */
          changeFilter(event) {
            this.filter.query = '';
            this.filter.field = event.target.value;
          },
          filterRow(person) {
            let visible = true,
                field = this.filter.field,
                query = this.filter.query; 
            if(field) {  
              if(this.isActiveFilterSelected()) {
                visible = (typeof query === 'boolean') ?
               (query === person.isActive) : true;
              } else { 
                query = String(query),
                field = person[field];
                if(typeof field === 'number') {
                  query.replace(this.currency, '');  
                  try {
                    visible = eval(field + query);
                  } catch(e) {}  
                } else {  
                  field = field.toLowerCase();
                  visible = 
                  field.includes(query.toLowerCase());         
                }
              }
            }
            return visible;
          }
        }
      });

创建 Vue 组件

现在我们确信我们的代码更干净,我们可以继续为应用的各个部分制作 Vue 组件。现在先把代码放在一边,打开一个新文档,同时开始处理组件。

Vue 组件功能极其强大,是任何 Vue 应用的绝佳补充。它们允许您制作包含自己的数据、方法和计算值的可重用代码包。

对于我们的应用,我们有机会创建两个组件:一个用于每个人,另一个用于我们应用的过滤部分。我鼓励您始终考虑在可能的情况下将应用分解为组件,这有助于将代码分组到相关功能中。

组件看起来像迷你 Vue 实例,因为每个实例都有自己的数据、方法和计算对象,以及一些特定于组件的选项,我们稍后将介绍这些选项

注册组件后,您将创建一个自定义 HTML 元素以在视图中使用,例如:

      <my-component></my-component>

命名组件时,可以使用 kebab 大小写(连字符)、PascalCase(无标点符号,但每个单词都大写)或 camelCase(类似于 Pascal,但第一个单词不大写)。Vue 组件不受 W3C web 组件/自定义元素规则的限制,也不与之关联,但遵循使用 kebab case 的惯例是一种很好的做法。

创建和初始化组件

Vue 组件使用Vue.component(tagName, options)语法注册。每个组件必须有一个关联的标记名。Vue.component注册必须在初始化 Vue 实例之前进行。作为最低要求,每个组件都应该有一个template属性,指示在使用组件时应该显示什么。模板必须始终具有单个包装元素;这样就可以用父容器替换自定义 HTML 标记。

例如,您不能将以下内容作为模板:

      <div>Hello</div><div>Goodbye</div>

如果您确实传递了此格式的模板,Vue 将在浏览器的 JavaScript 控制台中抛出一个错误,警告您。

使用简单的固定模板,自己创建 Vue 组件:

 Vue.component('my-component', {
 template: '<div>hello</div>'
 });

      const app = new Vue({
        el: '#app',

       // App options
      });

声明了这个组件后,它现在将为我们提供一个<my-component></my-component>HTML 标记,供我们在视图中使用。

还可以在 Vue 实例本身上指定组件。如果您在一个站点上有多个 Vue 实例,并且希望包含一个实例的组件,则可以使用此选项。为此,将组件创建为简单对象,并在 Vue 实例的components对象中指定tagName

      let Child = {
        template: '<div>hello</div>'
      }

      const app = new Vue({
        el: '#app',

        // App options

        components: {
          'my-component': Child
        }
      });

不过,对于我们的应用,我们将坚持使用Vue.component()方法初始化组件。

使用您的组件

在视图中,添加自定义 HTML 元素组件:

      <div id="app">
        <my-component></my-component>
      </div>

在浏览器中查看此内容时,应将<my-component>HTML 标记替换为<div>和 hello 消息。

在某些情况下,可能无法解析和接受自定义 HTML 标记-这些情况通常位于<table><ol><ul><select>元素中。如果是这种情况,您可以在标准 HTML 元素上使用is=""属性:

      <ol>
        <li is="my-component"></li>
      </ol>

使用组件数据和方法

由于 Vue 组件是 Vue 应用的自包含元素,因此每个组件都有自己的数据和功能。这有助于在同一页面上重新使用组件,因为每个组件实例的信息都是自包含的。methodscomputed函数的声明与您在 Vue 应用上的声明相同,但是,数据键应该是返回对象的函数。

The data object of a component must be a function. This is so that each component has its own self-contained data, rather than getting confused and sharing data between different instances of the same component. The function must still return an object as you would in your Vue app.

创建一个名为balance的新组件,在组件中添加data函数和computed对象,并在template属性中添加一个空的<div>

      Vue.component('balance', {
        template: '<div></div>',
        data() {
          return {

          }
        },
        computed: {

        }
      });

接下来,使用整数向cost数据对象添加一个键/值对,并将变量添加到模板中。将<balance></balance>自定义 HTML 元素添加到视图中,您将看到整数:

      Vue.component('balance', {
        template: '<div>{{ cost }}</div>',
        data() {
          return {
            cost: 1234
          }
        },
        computed: {

        }
      });

与我们在第 1 章中的 Vue 实例一样,开始使用 Vue.js时,在computed对象中添加一个函数,将货币符号附加到整数,并确保小数点后有两位。不要忘记将货币符号添加到数据函数中

更新模板以输出计算值而不是原始成本:

      Vue.component('balance', {
        template: '<div>{{ formattedCost }}</div>',
        data() {
          return {
            cost: 1234,
            currency: '$'
          }
        },
        computed: {
          formattedCost() {
 return this.currency + this.cost.toFixed(2);
 }
        }
      });

这是组件的一个基本示例,但是,组件本身的固定cost非常有限。

向组件传递数据–道具

将平衡作为一个组件是很好的,但如果平衡是固定的,则不是很好。当您添加通过 HTML 属性传入参数和属性的功能时,组件就真正成为了自己的组件。在 Vue 世界中,这些被称为道具。道具可以是静态的,也可以是可变的。为了使组件能够获得这些属性,您需要使用props属性在组件上创建一个数组

例如,如果我们想制作一个heading组件:

      Vue.component('heading', {
        template: '<h1>{{ text }}</h1>',

        props: ['text']
      });

然后在视图中使用该组件,如下所示:

      <heading text="Hello!"></heading>

使用 props,我们不需要在数据对象中定义text变量,因为在 props 数组中定义它会自动使其在模板中可用。props 数组还可以采取进一步的选项,允许您定义预期输入的类型,无论它是必需的还是省略时使用的默认值。

向 balance 组件添加一个道具,以便我们可以将成本作为 HTML 属性传递。您的视图现在应该如下所示:

      <balance cost="1234"></balance> 

现在,我们可以将成本道具添加到 JavaScript 中的组件中,并从数据函数中删除固定值:

      template: '<div>{{ formattedCost }}</div>',
 props: ['cost'],
      data() {
        return {
          currency: '$'
        }
      },

然而,在我们的浏览器中运行它会在我们的 JavaScript 控制台中抛出一个错误。这是因为,从本质上讲,传入的道具被解释为字符串。我们可以通过两种方式解决这一问题;我们可以将道具转换为formatCost()函数中的数字,也可以使用v-bind:HTML 属性告诉 Vue 接受输入。

如果您还记得的话,我们对truefalse值的过滤器使用了这种技术,允许它们作为布尔值而不是字符串使用。在您的costHTML 属性前面添加v-bind:

      <balance v-bind:cost="15234"></balance> 

我们还可以做一个额外的步骤来确保 Vue 知道预期的输入类型,并告知其他用户您的代码应该传递给组件的内容。这可以在组件本身中完成,并与格式一起允许您指定默认值以及是否需要道具。

将你的props数组转换成一个对象,以cost为键。如果只是定义字段类型,则可以通过将值设置为字段类型,使用 Vue 速记来声明该字段类型。它们可以是字符串、数字、布尔值、函数、对象、数组或符号。由于我们的成本属性应该是一个数字,请将其添加为键:

      props: {
 cost: Number
 },

如果我们的组件呈现了$0.00,而不是在未定义任何内容时抛出错误,那就太好了。我们可以通过将默认值设置为0来实现这一点。要定义默认值,我们需要将道具转换为对象本身,其中包含一个值为Numbertype键。然后我们可以定义另一个default键并将该值设置为0

      props: {
        cost: {
          type: Number,
 default: 0
 }
      },

在浏览器中呈现组件应显示传递到成本属性的任何值,但删除该值将呈现$0.00

总而言之,我们的组件如下所示:

      Vue.component('balance', {
        template: '<div>{{ formattedCost }}</div>',

        props: {
          cost: {
            type: Number,
            default: 0
          }
        },

        data() {
          return {
            currency: '$'
          }
        },

        computed: {
          formattedCost() {
            return this.currency +       
            this.cost.toFixed(2);
          }
        }
      });

当我们制作清单应用的person组件时,我们应该能够扩展这个例子。

将数据传递到组件–插槽

有时,您可能需要向组件传递未存储在属性中的 HTML 块,或者在出现在组件中之前要格式化的 HTML 块。您可以在组件中使用插槽,而不是尝试在计算变量或类似变量中预格式化

插槽类似于占位符,允许您将内容放置在组件的开始标记和结束标记之间,并确定它们将显示的位置

一个完美的例子就是模态窗口。这些标签通常有几个标签,如果您希望在应用中多次使用这些标签,通常会包含大量要复制和粘贴的 HTML。相反,您可以创建一个modal-window组件,并用插槽传递 HTML。

创建一个名为modal-window的新组件。它接受一个属性visible,该属性接受一个布尔值,默认为false。对于模板,我们将使用引导模式中的 HTML 作为一个很好的示例,说明使用插槽的组件如何轻松简化应用。为确保组件具有样式,请确保在文档中包含引导资产文件

      Vue.component('modal-window', {
        template: `<div class="modal fade">
          <div class="modal-dialog" role="document">
            <div class="modal-content">
              <div class="modal-header">
               <button type="button" class="close" 
               data-dismiss="modal" aria-label="Close">
               <span aria-hidden="true">&times;</span>
              </button>
             </div>
          <div class="modal-body">
          </div>
           <div class="modal-footer">
            <button type="button" class="btn btn-  
             primary">Save changes</button>
            <button type="button" class="btn btn-      
             secondary" data-dismiss="modal">Close
            </button>
            </div>
          </div>
         </div>
      </div>`,

      props: {
        visible: {
          type: Boolean,
          default: false
        }
       }
    });

我们将使用 visible 道具来确定模态窗口是否打开。向接受visible变量的外部容器添加一个v-show属性:

      Vue.component('modal-window', {
          template: `<div class="modal fade" v-
            show="visible">
          ...
        </div>`,

        props: {
          visible: {
            type: Boolean,
            default: false
          }
        }
      });

将您的modal-window组件添加到应用中,现在将visible指定为true,这样我们就可以了解并看到发生了什么:

      <modal-window :visible="true"></modal-window>

现在,我们需要将一些数据传递到模式框。在两个标记之间添加标题和一些段落:

      <modal-window :visible="true">
        <h1>Modal Title</h1>
 <p>Lorem ipsum dolor sit amet, consectetur                
         adipiscing elit. Suspendisse ut rutrum ante, a          
         ultrices felis. Quisque sodales diam non mi            
         blandit dapibus. </p>
 <p>Lorem ipsum dolor sit amet, consectetur             
          adipiscing elit. Suspendisse ut rutrum ante, a             
          ultrices felis. Quisque sodales diam non mi             
          blandit dapibus. </p>
       </modal-window>

在浏览器中按 refresh 不会做任何事情,因为我们需要告诉组件如何处理数据。在模板中,添加一个<slot></slot>HTML 标记,您希望在其中显示内容。用modal-body类将其添加到div中:

      Vue.component('modal-window', {
        template: `<div class="modal fade" v-      
        show="visible">
          <div class="modal-dialog" role="document">
            <div class="modal-content">
              <div class="modal-header">
          <button type="button" class="close" data-              
              dismiss="modal" aria-label="Close">
               <span aria-hidden="true">&times;</span>
             </button>
              </div>
              <div class="modal-body">
                <slot></slot>
              </div>
              <div class="modal-footer">
              <button type="button" class="btn btn-  
             primary">Save changes</button>
             <button type="button" class="btn btn-                   
               secondary" data-
            dismiss="modal">Close</button>
           </div>
           </div>
        </div>
        </div>`,

         props: {
          visible: {
            type: Boolean,
            default: false
          }
        }
      });

查看您的应用将显示您在模式窗口中传递的内容。这个新组件已经让应用看起来更干净了。

查看引导 HTML,我们可以看到页眉、正文和页脚都有空间。我们可以用命名的插槽标识这些部分。这允许我们将特定内容传递到组件的特定区域。

在模式窗口的页眉和页脚中创建两个新的<slot>标记。为这些新名称赋予名称属性,但将现有名称保留为空:

      template: `<div class="modal fade" v-              
      show="visible">
        <div class="modal-dialog" role="document">
          <div class="modal-content">
            <div class="modal-header">
              <slot name="header"></slot>
              <button type="button" class="close" data-
               dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
             </button>
          </div>
           <div class="modal-body">
            <slot></slot>
          </div>
          <div class="modal-footer">
            <slot name="footer"></slot>
            <button type="button" class="btn btn-  
            primary">Save changes</button><button type="button" class="btn btn-
           secondary" data-
           dismiss="modal">Close</button>
           </div>
        </div>
       </div>
     </div>`,

在我们的应用中,我们现在可以通过在 HTML 中指定slot属性来指定内容的去向。这可以放在特定的标签上,也可以放在多个标签周围的容器上。没有slot属性的任何 HTML 也将默认为您的未命名插槽:

      <modal-window :visible="true">
        <h1 slot="header">Modal Title</h1>

        <p>Lorem ipsum dolor sit amet, consectetur             
        adipiscing elit. Suspendisse ut rutrum ante, a 
        ultrices felis. Quisque sodales diam non mi 
         blandit dapibus. </p>

        <p slot="footer">Lorem ipsum dolor sit amet,            
         consectetur adipiscing elit. Suspendisse ut 
         rutrum ante, a ultrices felis. Quisque sodales 
           diam non mi blandit dapibus. </p>
      </modal-window>

我们现在可以指定内容并将其定向到特定位置。

您对插槽所能做的最后一件事是指定一个默认值。例如,您可能希望大部分时间都在页脚中显示按钮,但希望能够在需要时替换它们。使用<slot>,除非在应用中指定组件时被覆盖,否则标签之间的任何内容都将显示。

创建一个名为buttons的新插槽,并将按钮放在页脚内侧。尝试用其他内容替换它们。

模板变为:

      template: `<div class="modal fade" v-
      show="visible">
        <div class="modal-dialog" role="document">
          <div class="modal-content">
            <div class="modal-header">
              <slot name="header"></slot>
              <button type="button" class="close" data-
              dismiss="modal" aria-label="Close">
                <span aria-hidden="true">&times;</span>
              </button>
            </div>
            <div class="modal-body">
              <slot></slot>
            </div>
            <div class="modal-footer">
              <slot name="footer"></slot>
              <slot name="buttons">
                <button type="button" class="btn btn-
                 primary">Save changes</button>
                <button type="button" class="btn btn-
                 secondary" data-
                 dismiss="modal">Close</button>
              </slot>
            </div>
          </div>
        </div>
      </div>`,

以及 HTML:

     <modal-window :visible="true">
     <h1 slot="header">Modal Title</h1>
      <p>Lorem ipsum dolor sit amet, consectetur 
      adipiscing elit. Suspendisse ut rutrum ante, a 
      ultrices felis. Quisque sodales diam non mi blandit 
      dapibus. </p>

        <p slot="footer">Lorem ipsum dolor sit amet, 
       consectetur adipiscing elit. Suspendisse ut rutrum 
       ante, a ultrices felis. Quisque sodales diam non mi 
       blandit dapibus. </p>

        <div slot="buttons">
 <button type="button" class="btn btn-      
           primary">Ok</button> </div>
       </modal-window>

虽然我们不会在人员列表应用中使用插槽,但最好了解 Vue 组件的功能。如果您希望使用这样的模式框,可以将可见性设置为默认为 false 的变量。然后,您可以添加一个带有点击方法的按钮,将变量从false更改为true——显示模式框。

创建可重复的组件

组件的美妙之处在于能够在同一视图中多次使用它们。这使您能够为该数据的布局提供一个单一的“真相来源”。我们将为我们的人员列表创建一个可重复的组件,并为过滤部分创建一个单独的组件

打开您在前两章中创建的人员列表代码,并创建一个名为team-member的新组件。不要忘记在 Vue 应用初始化之前定义组件。在组件中添加一个prop以允许传入 person 对象。出于验证目的,仅指定它可以是一个Object

      Vue.component('team-member', {
        props: {
          person: Object
        }
      });

我们现在需要将模板集成到组件中,组件是我们视图中tr内部(包括)的所有内容。

组件中的模板变量只接受普通字符串,没有新行,因此我们需要执行以下操作之一:

  • 内联 HTML 模板非常适合小模板,但在这种情况下会牺牲可读性
  • 添加带有+字符串连接的新行对于一行或两行来说非常好,但会使我们的 JavaScript 膨胀
  • 创建模板块 Vue 为我们提供了使用外部模板的选项,这些模板在视图中使用text/x-template语法和 ID 定义

由于我们的模板非常大,我们将选择在视图末尾声明模板的第三个选项。

在应用外部的 HTML 中,创建一个新的脚本块并添加一个typeID属性:

      <script type="text/x-template" id="team-member-            
       template">
      </script>

然后,我们可以将个人模板移动到此块中,并删除v-for属性,我们仍将在应用本身中使用该属性:

      <script type="text/x-template" id="team-member-
      template">
        <tr v-show="filterRow(person)">
 <td>
 {{ person.name }}
 </td>
 <td>
 <a v-bind:href="'mailto:' + person.email">{{                
             person.email }}</a>
 </td>
 <td v-bind:class="balanceClass(person)">
 {{ format(person, 'balance') }}
 </td>
 <td>
 {{ format(person, 'registered') }}
 </td>
 <td v-bind:class="activeClass(person)">
 {{ format(person, 'isActive') }}
 </td>
 </tr>
      </script>

我们现在需要更新视图以使用team-member组件而不是固定代码。为了使我们的视图更清晰、更容易理解,我们将利用前面提到的<template>HTML 属性。创建一个<template>标记并添加我们之前的v-for循环。为避免混淆,请更新循环以使用individual作为每个人的变量。它们可以相同,但如果变量、组件和道具具有不同的名称,则代码更易于阅读。将v-for更新为v-for="individual in people"

      <table>
       <template v-for="individual in people">
       </template>
      </table>

在视图的template标记中,添加team-member组件的新实例,将individual变量传递给person属性。不要忘记在 person 属性中添加v-bind:,否则组件会将其解释为带有个人值的固定字符串:

      <table>
        <template v-for="individual in people">
          <team-member v-bind:person="individual"></team-           
            member>
        </template>
      </table>

我们现在需要更新组件以使用我们声明的模板,该模板使用template属性和脚本块的 ID 作为值:

      Vue.component('team-member', {
        template: '#team-member-template',
        props: {
          person: Object
        }
      });

在浏览器中查看应用将在 JavaScript 控制台中产生多个错误。这是因为我们引用了几种不再可用的方法,因为它们在父 Vue 实例上,而不是在组件上。如果要验证组件是否正常工作,请将代码更改为仅输出人员姓名,然后按刷新:

      <script type="text/x-template" id="team-member-             
        template">
        <tr v-show="filterRow()">
          <td>
            {{ person.name }}
          </td>
        </tr>
      </script>

创建组件方法和计算函数

现在,我们需要在子组件的 Vue 实例上创建方法,以便可以使用它们。我们可以做的一件事是将父母的方法剪切粘贴到孩子身上,希望它们能起作用;然而,这些方法依赖于父属性(如过滤数据),我们也有机会利用computed属性,这些属性可以缓存数据并加快应用的速度。

现在,从tr元素中删除v-show属性,因为这涉及到过滤,一旦我们的行显示正确,就会涉及到过滤。我们将逐步解决错误,并一次解决一个错误,以帮助您了解使用 Vue 解决问题的方法

CSS 类函数

在浏览器中查看应用时遇到的第一个错误是:

Property or method "balanceClass" is not defined

第一个错误与我们使用的balanceClassactiveClass函数有关。这两个函数都基于人物的数据添加 CSS 类,一旦呈现了组件,这些数据就不会改变。

因此,我们能够使用 Vue 中的缓存。将方法移到组件中,但将它们放在新的computed对象中,而不是methods对象中。

对于组件,每次调用都会创建一个新实例,因此我们可以依赖通过prop传入的person对象,不再需要将person传入函数。从函数中删除参数,视图还将函数中对person的任何引用更新为this.person,以引用存储在组件上的对象:

 computed: {
        /**
         * CSS Classes
         */
        activeClass() {
          return this.person.isActive ? 'active' : 
      'inactive';
        },

        balanceClass() {
          let balanceLevel = 'success';

          if(this.person.balance < 2000) {
            balanceLevel = 'error';
          } else if (this.person.balance < 3000) {
            balanceLevel = 'warning';
          }

          let increasing = false,
              balance = this.person.balance / 1000;

          if(Math.round(balance) == Math.ceil(balance)) {
            increasing = 'increasing';
          }

          return [balanceLevel, increasing];
        }
 },

我们的组件模板中使用此功能的部分现在应该如下所示:

      <td v-bind:class="balanceClass">
    {{ format(person, 'balance') }}
      </td>

格式化值函数

当涉及到将format()函数移动到用于格式化数据的组件时,我们面临两个选项。我们可以一个接一个地移动它并将其放入methods对象,或者我们可以利用 Vue 缓存和约定,为每个值创建一个computed函数。

我们正在构建此应用以实现可伸缩性,因此建议为每个值创建计算函数。它还具有整理模板的优势。在名为balancedateRegisteredstatus的计算对象中创建三个函数。将format功能的相应部分复制到每个部分,再次将person的引用更新为this.person

在我们使用函数参数检索字段的地方,现在可以修复每个函数中的值。您还需要为余额功能添加一个带有货币符号的数据对象在props之后添加:

      data() {
        return {
          currency: '$'
        }
      },

由于team-member组件是我们唯一使用货币符号的地方,我们可以将其从 Vue 应用中删除。我们还可以从父 Vue 实例中删除 format 函数。

总之,我们的 Vueteam-member组件应该如下所示:

      Vue.component('team-member', {
        template: '#team-member-template',
        props: {
          person: Object 
       },
        data() {
          return {
            currency: '$'
          }
        },
        computed: {
          /**
           * CSS Classes
           */
          activeClass() {
            return this.person.isActive ? 'active' : 
            'inactive';
          },
          balanceClass() {
            let balanceLevel = 'success';   
            if(this.person.balance < 2000) {
              balanceLevel = 'error';
            } else if (this.person.balance < 3000) {
              balanceLevel = 'warning';
            }
          let increasing = false,
                balance = this.person.balance / 1000; 
            if(Math.round(balance) == Math.ceil(balance))                           
            {
              increasing = 'increasing';
            }
            return [balanceLevel, increasing];
          }, 
          /**
           * Fields
           */
          balance() {
            return this.currency +       
            this.person.balance.toFixed(2);
          },
          dateRegistered() {
            let registered = new 
            Date(this.person.registered);
            return registered.toLocaleString('en-US');
          },
          status() {
            return (this.person.isActive) ? 'Active' : 
            'Inactive';
          }
        }
      });

而我们的team-member-template与它的实际外观相比应该相当简单:

      <script type="text/x-template" id="team-member-
      template">
        <tr v-show="filterRow()">
          <td>
            {{ person.name }}
          </td>
          <td>
            <a v-bind:href="'mailto:' + person.email">{{ 
            person.email }}</a>
          </td>
          <td v-bind:class="balanceClass">
            {{ balance }}
          </td>
          <td>
            {{ dateRegistered }}
          </td>
          <td v-bind:class="activeClass">
            {{ status }}
          </td>
        </tr>
      </script>

最后,我们的 Vue 实例应该看起来更小:

      const app = new Vue({
        el: '#app',
        data: {
          people: [...],
          filter: {
            field: '',
            query: ''
          }
        },
        methods: {
          isActiveFilterSelected() {
            return (this.filter.field === 'isActive');
          },   
          /**
           * Filtering
           */
          filterRow(person) {
            let visible = true,
                field = this.filter.field,
                query = this.filter.query;  
            if(field) {   
              if(this.isActiveFilterSelected()) {
                visible = (typeof query === 'boolean') ? 
                  (query === person.isActive) : true;
              } else {
                query = String(query),
                field = person[field]; 
          if(typeof field === 'number') {
            query.replace(this.currency, '');
                  try {
                    visible = eval(field + query);
                  } catch(e) {}   
                } else {
                  field = field.toLowerCase();
                  visible = 
                  field.includes(query.toLowerCase())  
                }
              }
            }
            return visible;
          }
          changeFilter(event) {
            this.filter.query = '';
            this.filter.field = event.target.value;
          }
        }
      });

在浏览器中查看应用时,我们应该看到我们的人员列表,这些人员的表单元格中添加了正确的类,字段中添加了格式。

使用道具使过滤再次起作用

v-show="filterRow()"属性重新添加到模板中包含的tr元素中。由于我们的组件在每个实例上都缓存了 person,因此我们不再需要将 person 对象传递给该方法。刷新页面将在 JavaScript 控制台中出现新错误:

Property or method "filterRow" is not defined on the instance but referenced during render

此错误是因为我们的组件具有v-show属性,根据我们的筛选和属性显示和隐藏,但没有相应的filterRow函数。由于我们不将其用于任何其他用途,我们可以将该方法从 Vue 实例移动到组件,并将其添加到methods组件。删除 person 参数,更新方法使用this.person

      filterRow() {
        let visible = true,
            field = this.filter.field,
            query = this.filter.query;
            if(field) {
            if(this.isActiveFilterSelected()) {
            visible = (typeof query === 'boolean') ?                 
           (query === this.person.isActive) : true;
            } else {

            query = String(query),
            field = this.person[field];

            if(typeof field === 'number') {
              query.replace(this.currency, '');
              try {
                visible = eval(field + query);
              } catch(e) {}
              } else {

              field = field.toLowerCase();
              visible = 
            field.includes(query.toLowerCase());
            }
          }
        }
        return visible;
      }

控制台中的下一个错误是:

Cannot read property 'field' of undefined

过滤不起作用的原因是filterRow方法在组件上查找this.filter.fieldthis.filter.query,而不是它所属的父 Vue 实例。

As a quick fix, you can use this.$parent to reference data on the parent element—however, this is not recommended and should only be used in extreme circumstances or to quickly pass the data through.

为了将数据传递到组件,我们将使用另一个道具-类似于我们将人员传递到组件的方式。幸运的是,我们已经对过滤数据进行了分组,因此我们能够传递一个对象,而不是queryfield的单个属性。在组件上创建一个名为filter的新道具,并确保只允许Object通过:

      props: {
        person: Object,
        filter: Object
      },

然后我们可以将道具添加到team-member组件,允许我们传递数据:

      <table>
        <template v-for="individual in people">
          <team-member v-bind:person="individual" v-               
           bind:filter="filter"></team-member>
        </template>
      </table>

为了使我们的过滤工作正常,我们需要传入另一个属性,isActiveFilterSelected()函数。创建另一个名为statusFilter的道具,只允许一个Boolean作为值(因为这是函数的等价物),并将函数传递给其他人。更新filterRow方法以使用此新值。我们的组件现在看起来像:

      Vue.component('team-member', {
        template: '#team-member-template',
        props: {
          person: Object,
          filter: Object,
          statusFilter: Boolean
        },
        data() {
          return {
            currency: '$'
          }
        },
        computed: {
          /**
           * CSS Classes
           */
          activeClass() {
            return this.person.isActive ? 'active' : 
            'inactive';
            },
            balanceClass() {
            let balanceLevel = 'success';

         if(this.person.balance < 2000) {
           balanceLevel = 'error';
          } else if (this.person.balance < 3000) {
            balanceLevel = 'warning';
          }
          let increasing = false,
            balance = this.person.balance / 1000;
           if(Math.round(balance) == Math.ceil(balance)) {
             increasing = 'increasing';
          }
          return [balanceLevel, increasing];
        },
       /**
       * Fields
         */
       balance() {
       return this.currency +    
       this.person.balance.toFixed(2);
       },
      dateRegistered() {
       let registered = new Date(this.registered); 
        return registered.toLocaleString('en-US');
        },
        status() {
           return output = (this.person.isActive) ?    
          'Active' : 'Inactive';
         }
       },
       methods: {
        filterRow() {
         let visible = true,
            field = this.filter.field,
            query = this.filter.query;

         if(field) {  
           if(this.statusFilter) {
             visible = (typeof query === 'boolean') ? 
            (query === this.person.isActive) : true;
           } else {
             query = String(query),
            field = this.person[field];  
              if(typeof field === 'number') {
                query.replace(this.currency, '');  
                 try {
                 visible = eval(field + query);
                } catch(e) {
            } 
           } else {   
            field = field.toLowerCase();
            visible = field.includes(query.toLowerCase());
             }
            }
           }
           return visible;
        }
       }
     });

视图中带有额外道具的组件现在看起来如下所示。请注意,当用作 HTML 属性时,驼色大小写道具变为蛇形大小写(连字符):

      <template v-for="individual in people">
          <team-member v-bind:person="individual" v-               bind:filter="filter" v-bind:status-      
            filter="isActiveFilterSelected()"></team-
            member>
       </template>

使过滤器成为组件

我们现在需要使过滤部分成为自己的组件。在这种情况下,这并不是绝对必要的,但这是一种良好的做法,给我们带来了更多的挑战。

我们在使过滤成为组件时面临的问题是在过滤组件和team-member组件之间传输过滤数据的挑战。Vue 通过自定义事件解决此问题。这些允许您将数据从子组件传递(或“发射”)到父组件或其他组件。

我们将创建一个过滤组件,该组件在过滤更改时将数据传递回父 Vue 实例。该数据已经传递到team-member组件进行过滤。

创建组件

team-member组件一样,在 JavaScript 中声明一个新的Vue.component(),引用模板 ID#filtering-template。在视图中创建一个新的<script>模板块,并赋予它相同的 ID。用<filtering>自定义 HTML 模板替换视图中的筛选表单,并将表单放入filtering-template脚本块中。

您的视图应如下所示:

      <div id="app">
       <filtering></filtering>
       <table>
         <template v-for="individual in people">
           <team-member v-bind:person="individual" v-
            bind:filter="filter" v-
            bind:statusfilter="isActiveFilterSelected()">           </team-member>
         </template>
       </table>
      </div>

 <script type="text/x-template" id="filtering-
      template">
        <form>
          <label for="fiterField">
            Field:
            <select v-on:change="changeFilter($event)"                 id="filterField">
           <option value="">Disable filters</option>
           <option value="isActive">Active user</option>
           <option value="name">Name</option>
           <option value="email">Email</option>
           <option value="balance">Balance</option>
           <option value="registered">Date      
            registered</option>
           </select>
         </label>
        <label for="filterQuery" v-show="this.filter.field 
         && !isActiveFilterSelected()">
            Query:
            <input type="text" id="filterQuery" v-
            model="filter.query">
          </label>
          <span v-show="isActiveFilterSelected()">
            Active:
         <label for="userStateActive">
            Yes:
             <input type="radio" v-bind:value="true"       id="userStateActive" v-model="filter.query">
          </label>
            <label for="userStateInactive">
            No:
        <input type="radio" v-bind:value="false" 
        id="userStateInactive" v-model="filter.query">
         </label>
       </span>
      </form>
 </script>
      <script type="text/x-template" id="team-member-
       template">
       // Team member template
    </script>

您的 JavaScript 中应该包含以下内容:

      Vue.component('filtering', {
        template: '#filtering-template'
      });

解决 JavaScript 错误

team-member组件一样,您将在 JavaScript 控制台中遇到一些错误。可以通过从父实例复制filter数据对象和changeFilterisActiveFilterSelected方法来解决这些问题。我们现在将它们保留在组件和父实例中,但稍后将删除重复项:

      Vue.component('filtering', {
        template: '#filtering-template',

        data() {
 return {
 filter: {
 field: '',
 query: ''
 }
 }
 },

 methods: {
 isActiveFilterSelected() {
 return (this.filter.field === 'isActive');
 },

 changeFilter(event) {
 this.filter.query = '';
 this.filter.field = event.target.value;
 }
 }
      });

运行该应用将显示过滤器和人员列表,但过滤器不会更新人员列表,因为他们尚未通信。

使用自定义事件更改筛选器字段

对于自定义事件,您可以使用$on$emit函数将数据传递回父实例。对于此应用,我们将在父 Vue 实例上存储过滤数据,并从组件中进行更新。然后,team-member组件可以从 Vue 实例读取数据并进行相应过滤。

第一步是在父 Vue 实例上使用 filter 对象。从组件中移除data对象,并通过道具传入父对象-就像我们对team-member组件所做的那样:

      <filtering v-bind:filter="filter"></filtering>

我们现在将修改changeFilter函数以发出事件数据,这样父实例就可以更新filter对象。

filtering组件中删除现有的changeFilter方法,并创建一个名为change-filter-field的新方法。在这个方法中,我们只需要$emit下拉菜单中所选字段的名称。$emit函数有两个参数:一个键和一个值。发出一个change-filter-field键,并将event.target.value作为数据传递。当使用带有多个单词的变量(例如,changeFilterField)时,请确保事件名称($emit函数的第一个参数)和 HTML 属性都使用了连字符:

      changeFilterField(event) {
        this.$emit('change-filter-field', 
      event.target.value);
      }

为了随后将数据传递到父 Vue 实例上的 changeFilter 方法,我们需要向<filtering>元素添加一个新的道具。它使用v-on并绑定到自定义事件名称。然后,它将父方法名作为属性值。将属性添加到元素中:

      <filtering v-bind:filter="filter" v-on:change-filter-field="changeFilter"></filtering>

前面的这个属性告诉 Vue 在发出change-filter-field事件时触发changeFilter方法。然后,我们可以调整方法以接受参数作为值:

      changeFilter(field) {
        this.filter.query = '';
        this.filter.field = field;
      }

然后,这将清除过滤器并更新字段值,然后通过道具将字段值传递到我们的组件。

更新筛选器查询

要发出查询字段,我们将使用一个以前从未使用过的新 Vue 键,称为watchwatch函数跟踪数据属性,可以根据输出运行方法。它能够做的另一件事是发出事件。由于文本字段和单选按钮都设置为更新 field.query 变量,我们将在此基础上创建一个新的watch函数。

在组件上的方法之后创建一个新的watch对象:

      watch: {
        'filter.query': function() {
        }
      }

关键是您希望查看的变量。因为我们的包含一个点,所以需要用引号括起来。在此函数中,创建一个新的change-filter-query$emit事件,输出filter.query的值:

     watch: {
         'filter.query': function() {
         this.$emit('change-filter-query', 
         this.filter.query)
         }
       }

我们现在需要将此方法和自定义事件绑定到视图中的组件,以便它能够将数据传递给父实例。将属性的值设置为changeQuery-我们将创建一个方法来处理此问题:

      <filtering v-bind:filter="filter" v-on:change-      
      filter-field="changeFilter" v-on:change-filter-          
      query="changeQuery"></filtering>

在父 Vue 实例上,创建一个名为changeQuery的新方法,该方法仅根据输入更新filter.query值:

     changeQuery(query) {
       this.filter.query = query;
     }

我们的过滤现在又开始工作了。更新选择框和输入框(或单选按钮)将更新我们的人员列表。我们的 Vue 实例要小得多,我们的模板和方法包含在单独的组件中。

最后一步是避免重复isActiveFilterSelected()方法,因为该方法仅在team-member组件上使用一次,但在filtering组件上使用多次。从父 Vue 实例中删除方法,从team-memberHTML 元素中删除 prop,并用传递的函数内容替换team-member组件中的filterRow方法中的statusFilter变量

最终的 JavaScript 现在看起来像:

      Vue.component('team-member', {
        template: '#team-member-template',
        props: {
          person: Object,
          filter: Object
        },
        data() {
          return {
            currency: '$'
          }
        },
        computed: {
          /**
           * CSS Classes
           */
           activeClass() {
            return this.person.isActive ? 'active' : 'inactive';
          },
          balanceClass() {
            let balanceLevel = 'success';    
            if(this.person.balance < 2000) {
              balanceLevel = 'error';
            } else if (this.person.balance < 3000) {
              balanceLevel = 'warning';
            }
           let increasing = false,
            balance = this.person.balance / 1000;      
            if(Math.round(balance) == Math.ceil(balance))             {
             increasing = 'increasing';
            } 
            return [balanceLevel, increasing];
          },
          /**
           * Fields
           */
          balance() {
            return this.currency +       
          this.person.balance.toFixed(2);
          },
          dateRegistered() {
            let registered = new Date(this.registered);  
            return registered.toLocaleString('en-US');
          },
          status() {
            return output = (this.person.isActive) ? 
           'Active' : 'Inactive';
          }
        },
          methods: {
          filterRow() {
            let visible = true,
            field = this.filter.field,
            query = this.filter.query;         
            if(field) {      
              if(this.filter.field === 'isActive') {
              visible = (typeof query === 'boolean') ? 
             (query === this.person.isActive) : true;
              } else {   
                query = String(query),
                field = this.person[field]; 
                if(typeof field === 'number') {
                  query.replace(this.currency, '');
               try {
              visible = eval(field + query);
            } catch(e) {}

          } else {

            field = field.toLowerCase();
            visible = field.includes(query.toLowerCase());  
              }
           }
          }
            return visible;
          }
          }
         });

     Vue.component('filtering', {
     template: '#filtering-template',
       props: {
       filter: Object
     },
       methods: {
       isActiveFilterSelected() {
        return (this.filter.field === 'isActive');
       },     
        changeFilterField(event) {
        this.filedField = '';
       this.$emit('change-filter-field',                     
        event.target.value);
          },
        },
        watch: {
    'filter.query': function() {
      this.$emit('change-filter-query', this.filter.query)
          }
        }
      });

      const app = new Vue({
        el: '#app',

        data: {
          people: [...],
          filter: {
            field: '',
            query: ''
          }
        },
        methods: { 
          changeFilter(field) {
            this.filter.query = '';
            this.filter.field = field;
          },
          changeQuery(query) {
            this.filter.query = query;
          }
        }
      });

现在的观点是:

     <div id="app">
        <filtering v-bind:filter="filter" v-on:change-
         filter-field="changeFilter" v-on:change-filter-
          query="changeQuery"></filtering>
       <table>
         <template v-for="individual in people">
          <team-member v-bind:person="individual" v-  
          bind:filter="filter"></team-member>
         </template>
        </table>
     </div>
    <script type="text/x-template" id="filtering-
     template">
       <form>
      <label for="fiterField">
       Field:
      <select v-on:change="changeFilterField($event)" 
         id="filterField">
        <option value="">Disable filters</option>
        <option value="isActive">Active user</option>
        <option value="name">Name</option>
        <option value="email">Email</option>
        <option value="balance">Balance</option>
        <option value="registered">Date     
          registered</option>
         </select>
          </label>
         <label for="filterQuery" v-
         show="this.filter.field && 
          !isActiveFilterSelected()">
         Query:
        <input type="text" id="filterQuery" v-    
         model="filter.query">
          </label>

          <span v-show="isActiveFilterSelected()">
           Active:

            <label for="userStateActive">
              Yes:
            <input type="radio" v-bind:value="true"   
          id="userStateActive" v-model="filter.query">
           </label>
          <label for="userStateInactive">
           No:
            <input type="radio" v-bind:value="false"                 id="userStateInactive" v-model="filter.query">
            </label>
          </span>
        </form>
      </script>
      <script type="text/x-template" id="team-member-
      template">
        <tr v-show="filterRow()">
          <td>
            {{ person.name }}
          </td>
          <td>
            <a v-bind:href="'mailto:' + person.email">{{                person.email }}</a>
          </td>
          <td v-bind:class="balanceClass">
            {{ balance }}
          </td>
          <td>
            {{ dateRegistered }}
          </td>
          <td v-bind:class="activeClass">
            {{ status }}
          </td>
        </tr>
      </script>

总结

在过去的三章中,您学习了如何初始化新的 Vue 实例,计算对象、方法和数据对象背后的含义,以及如何列出对象中的数据并对其进行操作以正确显示。您还学习了如何制作组件,以及保持代码干净和优化的好处。

在本书的下一节中,我们将介绍 Vuex,它可以帮助我们更好地存储和操作存储的数据。