Vue: дочерний компонент не изменится после получения реквизита от родителей - PullRequest
0 голосов
/ 18 октября 2019

Итак, у меня проблема с взаимодействием родительских и дочерних компонентов с помощью vue. Дело в том, что после перехода к компоненту он должен вызвать ajax для получения данных с сервера. После получения данных родительский компонент должен отправить их всем дочерним компонентам через реквизиты, но данные реквизита не отображаются. Дочерний компонент только начинает показывать данные реквизита, только после того, как я изменю свой код в редакторе. Итак, вот код для моего родительского компонента

<template>
  <div id="single-product-container">
    <product-header :name="singleProductName" :details="singleProductDetail" />
    <product-spec :spec="singleProductSpec" />
  </div>
</template>

<script>
import SingleProductHeader from '@/pages/SingleProductPage/single-product-header'
import SingleProductSpec from '@/pages/SingleProductPage/single-product-spec'
import singleProductApi from '@/api/product.api'

export default {
  data () {
    return {
      singleProductData: null,
      singleProductDetail: [],
      singleProductName: '',
      singleProductSpec: null
    }
  },
  methods: {
    getAllSingleProductDetail () {
      const productName = this.$route.params.product
      const location = this.location || 'jakarta'
      let vehicleType = null
      const path = this.$route.fullPath
      let self = this
      if (path.includes('motorcycle')) {
        vehicleType = 'motorcycle'
      } else if (path.includes('car')) {
        vehicleType = 'car'
      }
      singleProductApi.getSingleProductRequest(location, productName, vehicleType)
        .then(singleProductResponse => {
          console.log(singleProductResponse)
          let specObj = singleProductResponse.specification
          self.singleProductDetail = singleProductResponse.detail
          self.singleProductName = singleProductResponse.product_name
          self.singleProductSpec = specObj
          self.singleProductData = singleProductResponse
        })
        .catch(error => {
          throw error
        })
    }
  },
  mounted () {
    document.title = this.$route.params.product
  },
  created () {
     this.getAllSingleProductDetail()
  },
  components: {
    'product-header': SingleProductHeader,
    'product-spec': SingleProductSpec
  }
}
</script>

, и это мой компонент для одного продукта, который не будет загружать данные реквизита:

<template>
  <div id="product-spec">
    <div class="product-spec-title">
      Spesifikasi
    </div>
    <div class="produk-laris-wrapper">
      <div class="tab-navigation-wrapper tab-navigation-default">
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': mesinActive}" v-on:click="openSpaceTab(event, 'mesin')">
          <p class="tab-text tab-text-default">Mesin</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': rangkaActive}" v-on:click="openSpaceTab(event, 'rangka')">
          <p class="tab-text tab-text-default">Rangka & Kaki</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': dimensiActive}" v-on:click="openSpaceTab(event, 'dimensi')">
          <p class="tab-text tab-text-default">Dimensi & Berat</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': kapasitasActive}" v-on:click="openSpaceTab(event, 'kapasitas')">
          <p class="tab-text tab-text-default">Kapasitas</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class="{ 'active-default': kelistrikanActive}" v-on:click="openSpaceTab(event, 'kelistrikan')">
          <p class="tab-text tab-text-default">Kelistrikan</p>
        </div>
      </div>
      <div id="tab-1" class="spec-tab-panel" v-bind:style="{ display: mesinTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in mesinData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-2" class="spec-tab-panel" v-bind:style="{ display: rangkaTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in rangkaData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-3" class="spec-tab-panel" v-bind:style="{ display: dimensiTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in dimensiData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-4" class="spec-tab-panel" v-bind:style="{ display: kapasitasTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in kapasitasData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-5" class="spec-tab-panel" v-bind:style="{ display: kelistrikanTab }">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in kelistrikanData" :key="name">
              <td> {{ name }} </td>
              <td> {{ value }} </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    location: String,
    spec: Object
  },
  data () {
    return {
      mesinActive: true,
      rangkaActive: false,
      dimensiActive: false,
      kapasitasActive: false,
      kelistrikanActive: false,
      mesinTab: 'block',
      rangkaTab: 'none',
      dimensiTab: 'none',
      kapasitasTab: 'none',
      kelistrikanTab: 'none',
      mesinData: {},
      rangkaData: {},
      dimensiData: {},
      kapasitasData: {},
      kelistrikanData: {}
    }
  },
  methods: {
    openSpaceTab (evt, tab) {
      if (tab === 'mesin') {
        this.mesinActive = true
        this.rangkaActive = false
        this.dimensiActive = false
        this.kapasitasActive = false
        this.kelistrikanActive = false
        this.mesinTab = 'block'
        this.rangkaTab = 'none'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'none'
      } else if (tab === 'rangka') {
        this.mesinActive = false
        this.rangkaActive = true
        this.dimensiActive = false
        this.kapasitasActive = false
        this.kelistrikanActive = false
        this.mesinTab = 'none'
        this.rangkaTab = 'block'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'none'
      } else if (tab === 'dimensi') {
        this.mesinActive = false
        this.rangkaActive = false
        this.dimensiActive = true
        this.kapasitasActive = false
        this.kelistrikanActive = false
        this.mesinTab = 'none'
        this.rangkaTab = 'none'
        this.dimensiTab = 'block'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'none'
      } else if (tab === 'kapasitas') {
        this.mesinActive = false
        this.rangkaActive = false
        this.dimensiActive = false
        this.kapasitasActive = true
        this.kelistrikanActive = false
        this.mesinTab = 'none'
        this.rangkaTab = 'none'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'block'
        this.kelistrikanTab = 'none'
      } else if (tab === 'kelistrikan') {
        this.mesinActive = false
        this.rangkaActive = false
        this.dimensiActive = false
        this.kapasitasActive = false
        this.kelistrikanActive = true
        this.mesinTab = 'none'
        this.rangkaTab = 'none'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'block'
      }
    }
  },
  created () {
    this.mesinData = this.spec.mesin
    this.rangkaData = this.spec.rangka
    this.dimensiData = this.spec.dimensi
    this.kapasitasData = this.spec.kapasitas
    this.kelistrikanData = this.spec.kelistrikan
  }
}
</script>

Как я уже сказал,единственная проблема с одним компонентом спецификации продукта состоит в том, что он не загружает данные реквизита. Проблема в том, что он загружает только данные реквизита, когда я изменяю код в моем текстовом редакторе (это странно, я знаю). Я начал осознавать это, когда приступил к отладке, и когда я изменил свой код в компоненте с одним продуктом-спецификацией, затем начали загружаться данные реквизита. И если я не изменю свой код компонента одного продукта, данные реквизита не будут загружаться независимо от того, как долго я буду ждать.

1 Ответ

2 голосов
/ 18 октября 2019

ОК, так что давайте рассмотрим, что происходит по порядку:

  1. Создается родительский компонент, запускающий хук created и инициирующий загрузку данных с сервера.
  2. Родительский компонент выполняет рендеринг, создавая дочерние компоненты. Значение опоры для spec будет null, поскольку данные еще не загружены, а singleProductSpec по-прежнему null.
  3. Хук created для single-product-spec работает. Поскольку this.spec равно null Я бы предположил, что это приводит к ошибке, хотя в вопросе об ошибке не упоминалось.
  4. В какой-то момент в будущем загрузка данных завершается, обновляя значение singleProductSpec,Это зависимость рендеринга родительского компонента, поэтому этот компонент будет добавлен в очередь рендеринга.
  5. Родительский компонент выполнит повторный рендеринг. Новое значение singleProductSpec будет передано как spec prop в single-product-spec. Новый экземпляр single-product-spec будет не создан, он будет просто повторно использовать тот, который он создал, он сначала отобразил.

В этот момент больше ничего не произойдет. Хук created single-product-spec не будет перезапущен, так как он только что был создан.

Когда вы редактируете исходный код дочернего компонента, он вызывает горячую перезагрузку этого компонента. ,Точный эффект такого изменения будет отличаться, но часто это приведет к воссозданию этого потомка без воссоздания родителя. Поскольку родительский объект уже загрузил данные с сервера, вновь созданному дочернему элементу будет передано полностью заполненное значение spec. Это позволяет читать его в хуке created.

Есть несколько способов решить эту проблему.

Во-первых, мы можем избежать создания single-product-spec до тех пор, пока данные не будут готовы:

<product-spec v-if="singleProductSpec" :spec="singleProductSpec" />

Это просто позволит избежать создания компонента во время первоначального рендеринга, так что при запуске хука дочернего элемента created он получит доступ к нужным данным. Это, вероятно, тот подход, который вам следует использовать.

Второй способ сделать это - использовать key. Ключи используются для сопряжения компонентов при повторном рендеринге, чтобы Vue знал, какой старый компонент соответствует какому новому компоненту. Если key изменится, то Vue отбросит старый дочерний компонент и вместо этого создаст новый. При создании нового компонента он запускает хук created. Это, вероятно, не лучший подход для вашего сценария, так как неясно, что должен делать дочерний компонент, когда передан spec из null.

Третий подход будет состоять в использовании watch в дочернем компоненте. Это будет отслеживать, когда значение spec изменится, и скопировать соответствующие значения в локальные свойства данных компонента. Несмотря на то, что в некоторых случаях использование watch, как это целесообразно, обычно указывает на слабое место в дизайне компонента.

Однако в вашем коде есть другие проблемы ...

  1. Непонятно, зачем вы копируете значения из реквизита в локальные данные. Вы можете просто использовать опору напрямую. Если вы делаете это просто для того, чтобы дать им более короткие имена, просто используйте вместо этого вычисляемое свойство. Единственная законная причина для их копирования, как это, заключается в том, что значения свойств могут быть изменены внутри дочернего элемента, а свойство используется только для передачи начального значения. Даже в этом случае вы бы не использовали хук created, вы просто делали бы это внутри функции data. См. https://vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow.
  2. Вы дублируете все 5 раз для 5 вкладок. Это должно быть реализовано с использованием массива объектов, каждый из которых содержит все необходимые данные для вкладки.
  3. Свойства mesinActive и mesinTab оба представляют одни и те же базовые данные. Вы не должны иметь оба в data. По крайней мере, один должен быть вычисляемым свойством, хотя лично я, вероятно, просто избавлюсь от mesinTab в целом. Вместо этого используйте CSS-классы, чтобы применить соответствующий стиль, и просто используйте mesinActive, чтобы решить, какие классы применять (как у вас в другом месте). Очевидно, то же самое относится и к другим xActive / xTab свойствам.
  4. Ваши вкладки представляют собой одну форму выбора. Использование 5 логических значений для представления одного выбора не является подходящей структурой данных. Правильный способ сделать это - иметь одно свойство, которое идентифицирует текущую вкладку. Специфика может варьироваться, это может содержать индекс вкладки, или объект, представляющий данные вкладки, или идентификатор, представляющий вкладку.
  5. Вам не нужно использовать let self = this с функциями стрелок. Значение this сохраняется из окружающей области.

Правильно реализованный код для single-product-spec должен сворачиваться практически до нуля. Вы должны быть в состоянии избавиться от около 80% кода. Я ожидаю, что метод openSpaceTab будет однострочным, если вы просто используете соответствующие структуры данных для хранения всех ваших данных.

Обновление:

Какзапрошена перезапись вашего компонента с учетом пунктов 1-4 из раздела «Другие проблемы» моего ответа.

const ProductSpecTitle = {
  template: `
    <div>
      <div class="product-spec-title">
        Spesifikasi
      </div>
      <div class="produk-laris-wrapper">    
        <div class="tab-navigation-wrapper tab-navigation-default">
          <div
            v-for="tab of tabs"
            :key="tab.id"
            class="tab-navigation tab-default"
            :class="{ 'active-default': tab.active }"
            @click="openSpaceTab(tab.id)"
          >
            <p class="tab-text tab-text-default">{{ tab.text }}</p>
          </div>
        </div>
        <div
          v-for="tab in tabs"
          class="spec-tab-panel"
          :class="{ 'spec-tab-panel-active': tab.active }"
        >
          <table class="spec-table">
            <tbody>
              <tr
                v-for="(value, name) in tab.data"
                :key="name"
                class="spec-row"
              >
                <td> {{ name }} </td>
                <td> {{ value }} </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  `,
  
  props: {
    spec: Object
  },
  
  data () {
    return {
      selectedTab: 'mesin'
    }
  },
  
  computed: {
    tabs () {
      const tabs = [
        { id: 'mesin', text: 'Mesin' },
        { id: 'rangka', text: 'Rangka & Kaki' },
        { id: 'dimensi', text: 'Dimensi & Berat' },
        { id: 'kapasitas', text: 'Kapasitas' },
        { id: 'kelistrikan', text: 'Kelistrikan' }
      ]
      
      for (const tab of tabs) {
        tab.active = tab.id === this.selectedTab
        tab.data = this.spec[tab.id]
      }
      
      return tabs
    }
  },

  methods: {
    openSpaceTab (tab) {
      this.selectedTab = tab
    }
  }
}

new Vue({
  el: '#app',
  
  components: {
    ProductSpecTitle
  },
  
  data () {
    return {
      spec: {
        mesin: { a: 1, b: 2 },
        rangka: { c: 3, d: 4 },
        dimensi: { e: 5, f: 6 },
        kapasitas: { g: 7, h: 8 },
        kelistrikan: { i: 9, j: 10 }
      }
    }
  }
})
.tab-navigation-wrapper {
  display: flex;
  margin-top: 10px;
}

.tab-navigation {
  border: 1px solid #000;
  cursor: pointer;
}

.tab-text {
  margin: 10px;
}

.active-default {
  background: #ccf;
}

.spec-tab-panel {
  display: none;
}

.spec-tab-panel-active {
  display: block;
  margin-top: 10px;
}

.spec-table {
  border-collapse: collapse;
}

.spec-table td {
  border: 1px solid #000;
  padding: 5px;
}
<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>

<div id="app">
  <product-spec-title :spec="spec"></product-spec-title>
</div>
...