Vue.js:3种反模式

Vue.js可能是最好的JavaScript框架之一。 它具有直观的API,它快速,灵活且易于使用。 但是,Vue.js的灵活性会带来某些危险。 一些使用此框架的开发人员容易受到小疏忽。 这可能会对应用程序性能或从长远来看对它们提供支持的能力产生不利影响。



该材料的作者(我们今天将其翻译发表)提供了解析在Vue.js上开发应用程序的人所犯的一些常见错误的信息。

计算属性内部的副作用


计算属性是一种非常方便的Vue.js机制,可让您使用依赖于其他状态片段的状态片段来组织工作。 计算属性仅应用于显示存储在状态中的数据,该状态取决于状态中的其他数据。 如果事实证明您在计算所得的属性内调用某些方法或将某些值写入其他状态变量,则可能意味着您做错了什么。 考虑一个例子。

export default {   data() {     return {       array: [1, 2, 3]     };   },   computed: {     reversedArray() {       return this.array.reverse(); //   -         }   } }; 

如果我们尝试打印arrayreversedArray ,我们会注意到两个数组包含相同的值。

  : [ 3, 2, 1 ]  : [ 3, 2, 1 ] 

这是因为计算出的reversedArray属性通过调用其.reverse()方法来修改原始array属性。 这是一个非常简单的示例,演示了意外的系统行为。 再看一个例子。

假设我们有一个组件,该组件显示有关特定订单中包含的商品或服务价格的详细信息。

 export default {  props: {    order: {      type: Object,      default: () => ({})    }  },  computed:{    grandTotal() {      let total = (this.order.total + this.order.tax) * (1 - this.order.discount);      this.$emit('total-change', total)      return total.toFixed(2);    }  } } 

在这里,我们创建了一个计算属性,该属性显示订单的总成本,包括税金和折扣。 由于我们知道总订单价值在这里发生变化,因此我们可以尝试引发一个事件,该事件通知grandTotal更改的父组件。

 <price-details :order="order"               @total-change="totalChange"> </price-details> export default {  //        methods: {    totalChange(grandTotal) {      if (this.isSpecialCustomer) {        this.order = {          ...this.order,          discount: this.order.discount + 0.1        };      }    }  } }; 

现在想象一下,有时(尽管很少)会出现我们与特殊客户合作的情况。 我们为这些客户额外提供10%的折扣。 我们可以尝试通过将其discount属性添加0.1来更改order对象并增加折扣大小。

但是,这将导致严重错误。


错误讯息


特殊客户的订单值计算不正确

在类似的情况下,会发生以下情况:计算出的属性在无限循环中不断“重新计数”。 我们更改折扣,计算出的属性对此作出反应,重新计算订单的总成本并生成一个事件。 处理此事件时,折扣再次增加,这将导致对计算所得属性的重新计算,依此类推-达到无穷大。

在您看来,这样的错误不可能在实际的应用程序中发生。 但是真的是这样吗? 我们的脚本(如果在此应用程序中发生类似的事情)将非常难以调试。 这样的错误将非常难以跟踪。 事实是,要发生此错误,必须由特殊买家下订单,并且一个这样的订单可能有1000个常规订单。

更改嵌套属性


有时,开发人员可能会想从props的属性(对象或数组)中编辑某些内容。 这样的愿望可以由这样的事实来决定:做到这一点非常“简单”。 但是值得吗? 考虑一个例子。

 <template>  <div class="hello">    <div>Name: {{product.name}}</div>    <div>Price: {{product.price}}</div>    <div>Stock: {{product.stock}}</div>    <button @click="addToCart" :disabled="product.stock <= 0">Add to card</button>  </div> </template> export default {  name: "HelloWorld",  props: {    product: {      type: Object,      default: () => ({})    }  },  methods: {    addToCart() {      if (this.product.stock > 0) {        this.$emit("add-to-cart");        this.product.stock--;      }    }  } }; 

在这里,我们有Product.vue组件,该组件显示产品的名称,其价值和所拥有的商品数量。 该组件还显示一个按钮,允许购买者将货物放入购物篮。 单击该按钮后,减小product.stock属性的值似乎非常容易且方便。 要做到这一点真的很简单。 但是,如果您这样做,可能会遇到几个问题:

  • 我们执行属性的更改(变异),并且不向父实体报告任何内容。
  • 这可能导致意外的系统行为,甚至更糟的是,出现奇怪的错误。
  • 我们在product组件中引入了一些逻辑,这些逻辑可能不应该出现在其中。

想象一个假设的情况,其中另一个开发人员首先遇到我们的代码并看到了父组件。

 <template>   <Product :product="product" @add-to-cart="addProductToCart(product)"></Product> </template> import Product from "./components/Product"; export default {  name: "App",  components: {    Product  },  data() {    return {      product: {        name: "Laptop",        price: 1250,        stock: 2      }    };  },  methods: {    addProductToCart(product) {      if (product.stock > 0) {        product.stock--;      }    }  } }; 

开发人员的想法可能如下:“显然,我需要在addProductToCart方法中减少product.stock addProductToCart ” 但是,如果这样做,我们将遇到一个小错误。 如果现在按下按钮,则货物数量将不会减少1,而是会减少2。

想象一下,这是一种特殊情况,当这种支票仅针对稀有商品进行检查或与特殊折扣有关时。 如果此代码投入生产,那么一切都会以这样的事实结束:我们的客户将购买2份副本,而不是产品的1份副本。

如果此示例对您来说不令人信服,请想象另一种情况。 使其成为用户填写的表格。 我们将user的本质作为属性传递给表单,并将编辑用户的名称和电子邮件地址。 下面显示的代码似乎是“正确的”。

 //   <template>  <div>    <span> Email {{user.email}}</span>    <span> Name {{user.name}}</span>    <user-form :user="user" @submit="updateUser"/>  </div> </template> import UserForm from "./UserForm" export default {  components: {UserForm},  data() {   return {     user: {      email: 'loreipsum@email.com',      name: 'Lorem Ipsum'     }   }  },  methods: {    updateUser() {     //            }  } } //   UserForm.vue <template>  <div>   <input placeholder="Email" type="email" v-model="user.email"/>   <input placeholder="Name" v-model="user.name"/>   <button @click="$emit('submit')">Save</button>  </div> </template> export default {  props: {    user: {     type: Object,     default: () => ({})    }  } } 

使用v-model指令可以很容易地开始user 。 Vue.js允许这样做。 为什么不那样做呢? 考虑一下:

  • 如果要求您需要向表单添加“取消”按钮,然后单击该按钮来取消所做的更改,该怎么办?
  • 如果服务器调用失败怎么办? 如何撤消user对象更改?
  • 在保存相应的更改之前,我们是否真的要在父组件中显示更改的名称和电子邮件地址?

解决问题的一种简单方法是在将user对象作为属性发送之前克隆user对象:

 <user-form :user="{...user}"> 

尽管这可能可行,但我们只是规避了问题,但没有解决。 我们的UserForm组件必须具有自己的本地状态。 这是我们可以做的。

 <template>  <div>   <input placeholder="Email" type="email" v-model="form.email"/>   <input placeholder="Name" v-model="form.name"/>   <button @click="onSave">Save</button>   <button @click="onCancel">Save</button>  </div> </template> export default {  props: {    user: {     type: Object,     default: () => ({})    }  },  data() {   return {    form: {}   }  },  methods: {   onSave() {    this.$emit('submit', this.form)   },   onCancel() {    this.form = {...this.user}    this.$emit('cancel')   }  }  watch: {    user: {     immediate: true,     handler: function(userFromProps){      if(userFromProps){        this.form = {          ...this.form,          ...userFromProps        }      }     }    }  } } 

尽管这段代码看起来确实很复杂,但是比以前的版本要好。 它使您摆脱了上述问题。 我们希望对user属性进行更改( watch )并将其复制到内部form数据中。 结果,表单现在具有其自己的状态,并且我们具有以下功能:

  • 您可以通过重新分配表单来撤消更改: this.form = {...this.user}
  • 表单有一个孤立的状态。
  • 如果我们不需要父组件,那么我们的操作不会对其产生影响。
  • 当我们尝试保存更改时,我们控制发生的情况。

直接访问父组件


如果一个组件引用另一个组件并对其执行某些操作,则可能导致矛盾和错误,这可能导致应用程序的异常行为以及其中相关组件的外观。

考虑一个非常简单的示例-一个实现下拉菜单的组件。 假设我们有一个dropdown组件(父级)和一个dropdown-menu组件(子级)。 当用户单击某个菜单项时,我们需要关闭dropdown-menu 。 隐藏和显示此组件是由dropdown的父组件完成的。 看一个例子。

 // Dropdown.vue ( ) <template>  <div>    <button @click="showMenu = !showMenu">Click me</button>    <dropdown-menu v-if="showMenu" :items="items"></dropdown-menu>  </div> <template> export default {  props: {   items: Array  },  data() {   return {     selectedOption: null,     showMenu: false   }  } } // DropdownMenu.vue ( ) <template>  <ul>    <li v-for="item in items" @click="selectOption(item)">{{item.name}}</li>  </ul> <template> export default {  props: {   items: Array  },  methods: {    selectOption(item) {     this.$parent.selectedOption = item     this.$parent.showMenu = false    }  } } 

注意selectOption方法。 尽管这种情况很少发生,但有人可能想直接联系$parent 。 这种愿望可以通过做起来非常简单的事实来解释。

乍一看,这样的代码似乎正常工作。 但是在这里您可以看到一些问题:

  • 如果我们更改showMenuselectedOption属性怎么办? 下拉菜单将无法关闭,并且不会选择任何项目。
  • 如果您需要使用某种过渡为dropdown-menu设置动画,该怎么办?

 // Dropdown.vue ( ) <template>  <div>    <button @click="showMenu = !showMenu">Click me</button>    <transition name="fade">      <dropdown-menu v-if="showMenu" :items="items"></dropdown-menu>    </dropdown-menu>  </div> <template> 

同样,由于$parent的更改,此代码将不起作用。 dropdown组件不再是dropdown-menu的父dropdown-menu 。 现在, dropdown-menu父级是transition组件。

属性向下传递到组件层次结构,事件向上传递。 这些词包含解决我们问题的正确方法的含义。 这是为事件修改的示例。

 // Dropdown.vue ( ) <template>  <div>    <button @click="showMenu = !showMenu">Click me</button>    <dropdown-menu v-if="showMenu" :items="items" @select-option="onOptionSelected"></dropdown-menu>  </div> <template> export default {  props: {   items: Array  },  data() {   return {     selectedOption: null,     showMenu: false   }  },  methods: {    onOptionSelected(option) {      this.selectedOption = option      this.showMenu = true    }  } } // DropdownMenu.vue ( ) <template>  <ul>    <li v-for="item in items" @click="selectOption(item)">{{item.name}}</li>  </ul> </template> export default {  props: {   items: Array  },  methods: {    selectOption(item) {     this.$emit('select-option', item)    }  } } 

现在,由于使用了事件,子组件不再绑定到父组件。 我们可以使用父组件中的数据自由更改属性,并使用动画过渡。 但是,我们可能不会考虑我们的代码如何影响父组件。 我们只是将发生的情况通知该组件。 在这种情况下, dropdown组件本身将决定如何处理用户对菜单项的选择以及关闭菜单的操作。

总结


最短的代码并不总是最成功的。 涉及“简单快速”结果的开发技术通常存在缺陷。 为了正确使用任何编程语言,库或框架,您需要耐心和时间。 对于Vue.js来说确实如此。

亲爱的读者们! 您在实践中是否遇到过任何麻烦,例如本文中讨论的麻烦?

Source: https://habr.com/ru/post/zh-CN463327/


All Articles