🔥 Use vue to write a cat waterfall flow component from scratch (support ssr)

Cat waterfall

As shown in the following dynamic picture, do the irregular pictures of cute cats arouse your girlish heart?

Waterfall flow, also known as waterfall flow layout, is a popular way of website page layout. There are many ways to implement waterfall flow, but the principle is the same. In this paper, let's introduce in detail how to implement cat waterfall flow.

Waterfall flow principle

As shown in the above figure: the first, second, third, fourth and fifth figures are arranged in the first row of the container, that is, near the top.

We will find that figure 6 is not below figure 1, but below figure 3.

In fact, this is the key point of waterfall flow. What is the arrangement of Picture 6?

In fact, it will be placed under the picture with the smallest distance from the bottom to the top in the current arrangement picture, so that the picture difference will not be very large. We can see that 3 is the picture with the smallest height, and then we will put the sixth picture under Figure 3.

Similarly, the seventh figure should be in the position shown in the figure below.

So do you know where the eighth picture should be placed? Let's leave a question for everyone to think about. We'll reveal the answer at the end of the article. If you can't wait, you can slide to the end of the article to see if you guess right.

Preload picture

We probably know the principle of realizing waterfall flow, so how to realize the specific technical implementation?

In fact, it is to set the offset value of the picture according to the width and height of the picture, that is, the top and left values.

This means that we must know the width height ratio of the picture, because the width of a column here needs to be consistent, that is, a fixed value can be set.

If we wait until the rendering is finished to obtain the height, and then set the top value and left value, the interface will flash.

Therefore, we need to preload the pictures and obtain the width and height at the beginning, but do not render. The time is ripe, that is, all the pictures are loaded, that is, the height of all the pictures is calculated and then rendered. It is very simple to say, but how should we operate the specific implementation?

1. Traverse the img array passed in

//imgsArr is a picture array passed in from the outside of the component, in which a src represents the path of the picture
this.imgsArr.forEach((imgItem, imgIndex) => {
    //...
    //...
})
Copy code

2.loadedCount records the loading quantity

//Declare the loadedCount variable to record the number of completed loads. In order to compare with the size of imgsArr, notify the completion of loading (including no graph, loading completed and loading failed)
data(){
    return {
        loadedCount: 0
    }
}
Copy code

3. Without drawing

// When there is no diagram, record the height as 0
if (!imgItem[this.srcKey]) {
    this.imgsArr[imgIndex]._height = "0";
    this.loadedCount++;
    // No graph mode is supported
    if (this.loadedCount == this.imgsArr.length) {
        this.$emit("preloaded");
    }
    return;
}
Copy code

4.Image object

//Use the Image API to execute when the src attribute changes and the load is complete
let oImg = new Image();
oImg.src = imgItem[this.srcKey];
oImg.onload = oImg.onerror = (e) => {
    //...
}
Copy code

5. After loading, calculate the height of the actual picture to be rendered

//Theoretically, the height of the preloaded picture / the width of the preloaded picture = the height of the picture to be rendered / the width of the picture
this.imgsArr[imgIndex]._height =
            e.type == "load"
              ? Math.round(this.imgWidth_c * (oImg.height / oImg.width))
              : this.imgWidth_c;  
Copy code

6. After loading fails, identify the failure flag

if (e.type == "error") {
    this.imgsArr[imgIndex]._error = true;
    this.$emit("imgError", this.imgsArr[imgIndex]);
} 
Copy code

7. After all loads are completed, the event emit preloaded occurs

if (this.loadedCount === this.imgsArr.length) { 
    this.$emit("preloaded");
}
Copy code

Calculate the number of columns

calcuCols() {
    // You need to calculate how many columns of data to render
    let waterfallWidth = this.width ? this.width : window.innerWidth;
    //Render at least one column
    let cols = Math.max(parseInt(waterfallWidth / this.colWidth),1); 
    //Max cannot exceed maxCols column
    return this.isMobile ? 2 : Math.min(cols,this.maxCols;
}
Copy code

Use on/on/on/emit to listen and load

//After loading, the page starts rendering imgsArr_c is the real rendering array
this.$emit("preloaded");

this.$on("preloaded", () => { 
    this.imgsArr_c = this.imgsArr.concat([]); // The preload is complete and the rendering starts
    // ...
});
Copy code

Use $nextTick to find update opportunities

When a property in the data changes, this value is not immediately rendered to the page, but first placed on the watcher queue (asynchronous). Tasks on the watcher queue will be executed only when the current task is idle. As a result, there will be a certain delay in mounting the changed data to the dom, which leads to the fact that when we change the attribute value and immediately get the changed value through the dom, we find that the obtained value is not the changed value, but the previous value.

The data above corresponds to our imgsArr_c.

this.$nextTick is used to execute delayed callback after the end of the next DOM update cycle. Use this method immediately after modifying the data to get the updated dom.

this.$nextTick(() => {
    //Indicates the end of loading
    this.isPreloading = false;
    this.waterfall();
});
Copy code

Arrange (core) using the waterfall method

waterfall() {
    //Select all pictures
    this.imgBoxEls = this.$el.getElementsByClassName("img-box");
    //If there is none, nothing can be arranged, so it returns directly
    if (!this.imgBoxEls) return;
    //Declare top, left, height and colwidth, that is, the width of the column
    let top,
        left,
        height,
        colWidth = this.isMobile
          ? this.imgBoxEls[0].offsetWidth
          : this.colWidth;
    //The coordinate size of the starting arrangement. If the arrangement starts from 0, set colsHeightArr to null. colsHeightArr is used to compare which of the currently arranged pictures is the smallest
    if (this.beginIndex == 0) this.colsHeightArr = [];
    //Arrange from 0
    for (let i = this.beginIndex; i < this.imgsArr.length; i++) {
        if (!this.imgBoxEls[i]) return;
        //Gets the height of the rendered element
        height = this.imgBoxEls[i].offsetHeight;
        if (i < this.cols) {
            //If it is less than the number of columns, push all the elements in the first row into the array, set top to 0, and left to multiply the column coordinates by the column width
            this.colsHeightArr.push(height);
            top = 0;
            left = i * colWidth;
        } else {
            //When the first row is arranged, calculate the current minimum height
            let minHeight = Math.min.apply(null, this.colsHeightArr); // Minimum height
            //When the first row is arranged, calculate the current minimum index
            let minIndex = this.colsHeightArr.indexOf(minHeight); // Minimum height cable
            //The top value of the new element is the smallest value in the array
            top = minHeight;
            //The value on the left is the minimum index multiplied by the column width
            left = minIndex * colWidth;
            // Sets the location where the element is positioned
            // Update colsHeightArr
            this.colsHeightArr[minIndex] = minHeight + height;
        }
        //Set the left and top values of a single element
        this.imgBoxEls[i].style.left = left + "px";
        this.imgBoxEls[i].style.top = top + "px";
      }
      this.beginIndex = this.imgsArr.length; // After the arrangement, the new pictures are preloaded and arranged from this index
}
Copy code

Add responsive

window.addEventListener("resize", this.response);

response: function () {
      let old = this.cols;
      //Recalculate the number of columns
      this.cols = this.calcuCols();
      //If the number of columns does not change, you do not need to rearrange
      if (old === this.cols) return; // Exit directly when the number of columns remains unchanged
      this.beginIndex = 0; // The index of the element to begin the arrangement
      this.waterfall();
}
Copy code

Add scroll bottom

this.scroll();

scroll() {
      this.$refs.scrollEl.addEventListener("scroll", this.scrollFn);
}

scrollFn() {
      let scrollEl = this.$refs.scrollEl;
      //If preloading
      if (this.isPreloading) return;
      let minHeight = Math.min.apply(null, this.colsHeightArr);
      if (
        scrollEl.scrollTop + scrollEl.offsetHeight >
        minHeight - this.reachBottomDistance
      ) {
        this.isPreloading = true;
        this.$emit("scrollReachBottom");
      }
}
Copy code

More details

For more details, the source code is here github Welcome to star!

Publish to npm for everyone to use

npm install @parrotjs/vue-waterfall -S
 Copy code

You can go to my github README.md for details

Added by zzlong on Thu, 04 Nov 2021 09:45:08 +0200