1

我正在 vuejs 中创建一个滑块,并使用vue2-dropzone 插件进行文件上传,其中每张幻灯片 ( slide-template.vue) 都有一个vue2-dropzone组件。

当应用程序加载时,图像文件被手动添加到从图像 API(托管在 heroku 上)查询的每个vue2-dropzonemanuallyAddFile插件 API)中

问题是当我删除第一张幻灯片时,从子()组件调用父级(slider.vue)方法removeSlideFn(作为道具传递给子级slide-template.vue)第一张幻灯片被删除但并非完全第一张幻灯片的dropzone图像没有被破坏并且保留在 DOM 中,而不是slide2从 DOM 中删除 ,(下一张幻灯片)的图像(请在代码和框演示中尝试一次以真正了解我的意思)。当我删除时不会发生这种情况,slide2或者slide3仅在slide1.

CodeSandBox 演示

应用程序.vue

<template>
  <div id="app">
    <img width="15%" src="./assets/logo.png">
    <slider />
  </div>
</template>

<script>
import slider from "./components/slider";

export default {
  name: "App",
  components: {
    slider
  }
};
</script>

组件\slider.vue (父)

<template>
    <div>
        <hooper ref="carousel" :style="hooperStyle" :settings="hooperSettings">
            <slide :key="idx" :index="idx" v-for="(slideItem, idx) in slideList">
                <slide-template
                    :slideItem="slideItem" 
                    :slideIDX="idx"
                    :removeSlideFn="removeCurrSlide" />
            </slide>

            <hooper-navigation slot="hooper-addons"></hooper-navigation>
            <hooper-pagination slot="hooper-addons"></hooper-pagination>
        </hooper>

            <div class="buttons has-addons is-centered is-inline-block">
                <button class="button is-info" @click="slidePrev">PREV</button>
                <button class="button is-info" @click="slideNext">NEXT</button>
            </div>
    </div>
</template>

<script>
import {
  Hooper,
  Slide,
  Pagination as HooperPagination,
  Navigation as HooperNavigation
} from "hooper";
import "hooper/dist/hooper.css";

import slideTemplate from "./slide-template.vue";
import { slideShowsRef } from "./utils.js";

export default {
  data() {
    return {
      sliderRef: "SlideShow 1",
      slideList: [],
      hooperSettings: {
        autoPlay: false,
        centerMode: true,
        progress: true
      },
      hooperStyle: {
        height: "265px"
      }
    };
  },
  methods: {
    slidePrev() {
      this.$refs.carousel.slidePrev();
    },
    slideNext() {
      this.$refs.carousel.slideNext();
    },

    //Removes slider identified by IDX
    removeCurrSlide(idx) {
      this.slideList.splice(idx, 1);
    },

    // Fetch data from firebase
    getSliderData() {
      let that = this;
      let mySliderRef = slideShowsRef.child(this.sliderRef);
      mySliderRef.once("value", snap => {
        if (snap.val()) {
          this.slideList = [];
          snap.forEach(childSnapshot => {
            that.slideList.push(childSnapshot.val());
          });
        }
      });
    }
  },
  watch: {
    getSlider: {
      handler: "getSliderData",
      immediate: true
    }
  },
  components: {
    slideTemplate,
    Hooper,
    Slide,
    HooperPagination,
    HooperNavigation
  }
};
</script>

components/slide-template.vue (child, with vue2-dropzone)

<template>
    <div class="slide-wrapper">
        <slideTitle :heading="slideItem.heading" />
        <a class="button delete remove-curr-slide" @click="deleteCurrSlide(slideIDX)" ></a>
    
            <vue2Dropzone
                @vdropzone-file-added="fileWasAdded"
                @vdropzone-thumbnail="thumbnail"
                @vdropzone-mounted="manuallyAddFiles(slideItem.zones)"
                :destroyDropzone="false"
                :include-styling="false"
                :ref="`dropZone${ slideIDX }`"
                :id="`customDropZone${ slideIDX }`"
                :options="dropzoneOptions">
            </vue2Dropzone>
    </div>
</template>

<script>
import slideTitle from "./slide-title.vue";
import vue2Dropzone from "@dkjain/vue2-dropzone";
import { generate_ObjURLfromImageStream, asyncForEach } from "./utils.js";

export default {
  props: ["slideIDX", "slideItem", "removeSlideFn"],
  data() {
    return {
      dropzoneOptions: {
        url: "https://vuejs-slider-node-lokijs-api.herokuapp.com/imageUpload",
        thumbnailWidth: 150,
        autoProcessQueue: false,
        maxFiles: 1,
        maxFilesize: 2,
        addRemoveLinks: true,
        previewTemplate: this.template()
      }
    };
  },
  components: {
    slideTitle,
    vue2Dropzone
  },
  methods: {
    template: function() {
      return `<div class="dz-preview dz-file-preview">
                        <div class="dz-image">
                            <img data-dz-thumbnail/>
                        </div>
                        <div class="dz-details">
                            <!-- <div class="dz-size"><span data-dz-size></span></div> -->
                            
                            <!-- <div class="dz-filename"><span data-dz-name></span></div>  -->
                        </div>
                        <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
                        <div class="dz-error-message"><span data-dz-errormessage></span></div>
                        <div class="dz-success-mark"><i class="fa fa-check"></i></div>
                        <div class="dz-error-mark"><i class="fa fa-close"></i></div>
                    </div>`;
    },
    thumbnail: function(file, dataUrl) {
      var j, len, ref, thumbnailElement;
      if (file.previewElement) {
        file.previewElement.classList.remove("dz-file-preview");
        ref = file.previewElement.querySelectorAll("[data-dz-thumbnail]");
        for (j = 0, len = ref.length; j < len; j++) {
          thumbnailElement = ref[j];
          thumbnailElement.alt = file.name;
        }
        thumbnailElement.src = dataUrl;
        return setTimeout(
          (function(_this) {
            return function() {
              return file.previewElement.classList.add("dz-image-preview");
            };
          })(this),
          1
        );
      }
    },
    // Drag & Drop Events
    async manuallyAddFiles(zoneData) {
      if (zoneData) {
        let dropZone = `dropZone${this.slideIDX}`;

        asyncForEach(zoneData, async fileInfo => {
          var mockFile = {
            size: fileInfo.size,
            name: fileInfo.originalName || fileInfo.name,
            type: fileInfo.type,
            id: fileInfo.id,
            childZoneId: fileInfo.childZoneId
          };
          let url = `https://vuejs-slider-node-lokijs-api.herokuapp.com/images/${
            fileInfo.id
          }`;

          let objURL = await generate_ObjURLfromImageStream(url);
          this.$refs[dropZone].manuallyAddFile(mockFile, objURL);
        });
      }
    },

    fileWasAdded(file) {
      console.log("Successfully Loaded Files from Server");
    },

    deleteCurrSlide(idx) {
      this.removeSlideFn(idx);
    }
  }
};
</script>

<style lang="scss">
.slide-wrapper {
  position: relative;
}
[id^="customDropZone"] {
  background-color: orange;
  font-family: "Arial", sans-serif;
  letter-spacing: 0.2px;
  /* color: #777; */
  transition: background-color 0.2s linear;
  //   height: 200px;
  padding: 40px;
}

[id^="customDropZone"] .dz-preview {
  width: 160px;
  display: inline-block;
}
[id^="customDropZone"] .dz-preview .dz-image {
  width: 80px;
  height: 80px;
  margin-left: 40px;
  margin-bottom: 10px;
}
[id^="customDropZone"] .dz-preview .dz-image > div {
  width: inherit;
  height: inherit;
  //   border-radius: 50%;
  background-size: contain;
}
[id^="customDropZone"] .dz-preview .dz-image > img {
  width: 100%;
}

[id^="customDropZone"] .dz-preview .dz-details {
  color: white;
  transition: opacity 0.2s linear;
  text-align: center;
}
[id^="customDropZone"] .dz-success-mark,
.dz-error-mark {
  display: none;
}
.dz-size {
  border: 2px solid blue;
}

#previews {
  border: 2px solid red;
  min-height: 50px;
  z-index: 9999;
}

.button.delete.remove-curr-slide {
  padding: 12px;
  margin-top: 5px;
  margin-left: 5px;
  position: absolute;
  right: 150px;
  background-color: red;
}
</style>

slide-title.vue(没那么重要)

<template>
    <h2 contenteditable @blur="save"> {{ heading }} </h2>
</template>

<script>
export default {
  props: ["heading"],
  methods: {
    save() {
      this.$emit("onTitleUpdate", event.target.innerText.trim());
    }
  }
};
</script>

utils.js(实用程序)

export async function generate_ObjURLfromImageStream(url) {
  return await fetch(url)
    .then(response => {
      return response.body;
    })
    .then(rs => {
      const reader = rs.getReader();

      return new ReadableStream({
        async start(controller) {
          while (true) {
            const { done, value } = await reader.read();

            // When no more data needs to be consumed, break the reading
            if (done) {
              break;
            }

            // Enqueue the next data chunk into our target stream
            controller.enqueue(value);
          }

          // Close the stream
          controller.close();
          reader.releaseLock();
        }
      });
    })
    // Create a new response out of the stream
    .then(rs => new Response(rs))
    // Create an object URL for the response
    .then(response => {
      return response.blob();
    })
    .then(blob => {
      // generate a objectURL (blob:url/<uuid> list)
      return URL.createObjectURL(blob);
    })
    .catch(console.error);
}

从技术上讲,这就是应用程序的工作方式,slider.vue 从数据库(firebase)加载和获取数据并存储在数据数组slideList中,循环slideList并将每个 slideData(prop slideItem)传递给 vue-dropzone 组件(在 slide-template.vue 中) ,当 dropzone 挂载时,它会触发manuallyAddFiles(slideItem.zones)@vdropzone-mounted定义事件。

异步manuallyAddFiles()从 API(托管在 heroku 上)获取图像,为图像创建 (generate_ObjURLfromImageStream(url)) 唯一的 blob URL (blob:/),然后调用插件 APIdropZone.manuallyAddFile()将图像加载到相应的 dropzone 中。

要删除当前幻灯片,孩子deleteCurrSlide()调用父母的(slider.vue)removeSlideFn(作为道具传递)方法与idx当前幻灯片。removeSlideFn用于splice移除对应数组 idx 处的项目this.slideList.splice(idx, 1)

问题是当我删除第一张幻灯片时,第一张幻灯片被删除但不是完全删除,第一张幻灯片的 dropzone 图像没有被破坏并且仍然保留在 DOM 中,而是slide2从 DOM 中删除(下一张幻灯片)的图像.

CodeSandBox 演示

我不确定是什么导致了这个问题,可能是由于 vue 的反应系统或Vue 的 Array 反应性警告导致了这个问题。

任何人都可以帮助我理解和解决这个问题,如果可能的话,指出问题根源的原因。

非常感谢您的帮助。

谢谢,

4

2 回答 2

3

我想你可能误解了发生了什么:

在 VueJS 中,有一个缓存方法允许重用生成的现有组件: - 每个对象在渲染时都被认为是相等的(在 DOM 级别)。

所以 VueJS 删除了最后一行,因为它可能是要求最少的计算,然后重新计算预期的状态。这有很多侧面案例(有时,本地状态不会重新计算)。要避免这种情况:按照文档中的建议,使用 :key 来跟踪对象的 id。从文档中:

当 Vue 更新使用 v-for 渲染的元素列表时,默认情况下它使用“就地补丁”策略。如果数据项的顺序发生了变化,而不是移动 DOM 元素以匹配项的顺序,Vue 将就地修补每个元素并确保它反映应该在该特定索引处呈现的内容。这类似于 Vue 1.x 中 track-by="$index" 的行为。

这种默认模式是高效的,但仅适用于您的列表渲染输出不依赖于子组件状态或临时 DOM 状态(例如表单输入值)的情况。

为了给 Vue 一个提示以便它可以跟踪每个节点的身份,从而重用和重新排序现有元素,您需要为每个项目提供一个唯一的 key 属性。key 的理想值是每个项目的唯一 ID。这个特殊的属性大致相当于 1.x 中的 track-by,但它的工作原理就像一个属性,所以你需要使用 v-bind 将它绑定到动态值...

new Vue({
    el: "#app",
    data: {
        counterrow: 1,
        rows: [],
    },
    methods: {
        addrow: function() {
            this.counterrow += 1;
            this.rows.push({
                id: this.counterrow,
                model: ""
            });
        },
        removerows: function(index) {
            this.rows.splice(index, 1);
        },
    },
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
    <table>
        <tr>
            <td><input type="text" name="test1" /></td>
            <td><button class="btn" @click="addrow">add row</button></td>
        </tr>
        <tr v-for="(row,index) in rows" :key="row.id">
            <td><input type="text" name="test2" v-model="row.model" /></td>
            <td><button class="btn" @click="removerows(index)">remove </button></td>
        </tr>
    </table>
</div>

在这段代码中:

我更正了 counterrow 从未增加的事实我添加了一个 :key

:key 的文档

于 2019-12-30T06:59:09.737 回答
0

你是什​​么意思

问题是当我删除第一张幻灯片时,第一张幻灯片被删除但不是完全删除,第一张幻灯片的 dropzone 图像没有被破坏并且仍然保留在 DOM 中,而是从 slide2 的图像(下一张幻灯片)中删除DOM。

据我所知,元素不再在 DOM 中

于 2019-12-27T15:14:32.480 回答