background
Waterfall flow is a common development scenario, and we have some solutions in our in-house component library. However, these schemes are all suitable for a single scenario, and each implementation scheme has more or less some problems. Based on this, we design and develop a waterfall flow component that is compatible with multiple scenarios.
At present, there are three layout methods used to show commodity flow: card flow, fixed waterfall flow, staggered waterfall flow.
The card stream is presented as a drop – down list. This layout allows the user to focus on a single list item, making it easier to read. It is mainly applied to the entry of the secondary list page of the rotation, with the following effect
The fixed waterfall image area size and height remain unchanged. The uniform height makes the whole interface look neat and visually uncluttered. Mainly applied to some channel page scenarios, the effect is as follows
Staggered waterfall flow is visually represented as an uneven multi-column layout composed of elements of equal width and variable height, which is chosen by the home page and the detailed recommendation page for carrying
Problems with existing programs
Of the above three scenarios, the first and second scenarios have fixed image heights and are relatively simple to implement. You can directly use the infinite loading List component. It is the third scenario that often causes problems: staggered waterfall flows. In this scenario, the height of the image should be obtained after the image is loaded, and then added to the lowest column of the waterfall flow. Otherwise, the calculation of the lowest column will be affected, resulting in columns with different lengths.
In zhuan company, the realization of staggered waterfall flow mainly has the following schemes
-
Scheme 1: the left and right column layout is adopted, and the waterfall flow data of the first page is evenly divided and rendered. When data is rendered on the second page, the first data on the second page will be taken out and rendered to the lowest column, and IntersectionObserve will be monitored. After this element appears in the window, the second data will be taken out from the data source and added to the new lowest rendering column, so as to realize the waterfall flow with lazy loading in a loop and back
- Advantages: Adoption
IntersectionObserve
Waterfall flow lazy loading, simple logic - Disadvantages:
- The column layout supports only two columns, not multiple columns.
- The data on the first page do not conform to the specification of waterfall flow, and there is a probability that one column is long and one column is short.
IntersectionObserve
Compatibility problems;- No data loading event is exposed, which makes it easy to pull down the request twice interface when working with infinite loading components
- Advantages: Adoption
-
Scheme 2: Adopt width percentage for style layout, and enable IntersectionObserve monitoring on the first screen rendering. After the element appears in the window, set a setTimeout to load the next waterfall flow element, and add an attribute identifier on the DOM to prevent secondary triggering.
- Advantages: support multi-column layout of parameter configuration, the first screen conforms to the specification of waterfall stream, and exposes the event after the waterfall stream is loaded. With infinite loading, there will be no two interface requests
- Disadvantages:
IntersectionObserve
Compatibility issues remain unresolved; High frequency of internal DOM query and operation; Coupling the logic of infinite loading List, high maintenance cost;setTimeout
There is no guarantee that the image will load in the correct time sequence, resulting in inaccurate retrieval of the lowest column
-
Scheme 3: Use absolute positioning layout scheme. The implementation principle is to create an image object inside each sub-component waterall-item, listen for the onload event and then trigger the parent component Waterfall to rearrange the waterfall stream.
- Advantages: Simple internal logic, easy to maintain, but also complies with waterfall flow specifications, provides several commonly used waterfall flow configuration items, after complete loading will trigger events to notify external components
- Disadvantages: does not support lazy loading of images; Too many redraws (1+2+… +N), not very performance friendly; The time to trigger the redraw is not the most precise time node (via the onload event after the new image, instead of binding the onload event to the current image)
Then I went online to find open source solutions. Here are the top 4 start solutions on Github
- vue-waterfall: indicates the scheme with the largest number of starts
- Disadvantages: You need to know the width and height of the image before the component renders, and we generally don’t return these data in the interface
- vue-waterfall-easy: There is no need to obtain the width and height information of the image in advance, and the image is preloaded before typesetting.
- Disadvantages: Coupling pull-down, infinite loading components; Include PC side logic, package size is large, for the pursuit of performance page is not friendly (as an open source solution, compatible with more scenarios is not wrong, but we have a separate component implementation of these functions); Load all images at once. Lazy loading is not supported
- vue-waterfall2: Supports high adaptability and lazy loading
- Disadvantages: Image objects are created internally multiple times, with a lot of computation and scrolllistening.
- vue2-waterfall: It can be realized by the encapsulation of the two open source schemes of cropage-layout and ImagesLoaded, with simple and clear logic.
- Disadvantages: Lazy loading is not supported
Let me summarize it briefly with a picture
New waterfall flow scheme design
Currently, there is no simple, easy-to-use waterfall component on the mobile side, so the plan is to integrate known solutions and implement a new waterfall component. The new waterfall stream will include some of the following benefits:
- simple
CSS
layout - Simplify the logical level of implementation
- Supports a high degree of adaptation
- Lazy loading support
The layout scheme
Understand that the waterfall flow CSS layout scheme is mainly divided into three kinds
- Absolute positioning: solution 3 above and open source solution
vue-waterfall-easy
This layout is suitable for PC waterfall flow - Width percentage: above option 2 and open source option
vue-waterfall2
Use this layout, but this scheme will have some accuracy problems - Flex layout: Some large e-commerce sites like Mogujie use this layout
Among them, Flex layout compatibility and adaptation is not a problem, should be the best solution for mobile layout. So the new waterfall flow will adopt this layout
Waterfall flow logic implementation
There are three logical implementations of waterfall flow
- new
image
Object,onload
Get the original width and height of the image, then calculate the actual rendered height according to the width allocated by the waterfall flow, and mount it as an inline style toDOM
上 - Directly splice the width and height information of the picture in the image URL returned by the interface, and advance the layout. Mushroom Street adopts this scheme
IntersectionObserver
Listen for the picture element, which appears in the view and starts fetching data from the column head of the waterfall stream data queue and rendering it to the lowest column of the current waterfall stream, and so on for lazy loading of the waterfall stream
Of the three options, the first is more conventional, and is the way most open source solutions are implemented. But the internal need for height conversion, but also does not support lazy loading pictures.
The second scheme should be a better one, pictures can start typesetting before loading, convenient and simple, but also support lazy loading, user experience is good. Mogujie, Tmall, JINGdong and so on are using this scheme. However, this scenario requires some modifications, such as stitching the image information to the URL before the image is stored, or the back-end interface reads the image object and then returns the image information to the front end. Either the transformation cost is large, or the server pressure will increase, which is not suitable for our business.
The third scheme can support lazy loading without the need for other modifications and should be the most suitable one at present. Therefore, IntersectionObserver will be adopted in the new waterfall stream component to realize the typesetting of the waterfall stream
New waterfall flow concrete implementation
IntersectionObserver compatibility
One of the first problems is thatIntersectionObserver
Compatibility issues.IntersectionObserver
While addressing the performance issues associated with traditional scrolllistening, compatibility has not received a mainstream support, as you can seeiOS
The support is not perfect
The official provided a Polyfill to solve the above problems, but this polyfill is large in volume, which is not friendly to some pages that pursue the ultimate performance, so we adopted the method of dynamic introduction of polyfill
// Dynamically introduce Polyfill in the scenario where IntersectionObserver is not supported
const ioPromise = checkIntersectionObserver()
? Promise.resolve()
: import('intersection-observer')
ioPromise.then(() = > {
// do something
})
Copy the code
Only in an unsupported IntersectionObserver environment can the Polyfill be loaded, and the testing method is copied from Vue LazyLoad
const inBrowser = typeof window! = ='undefined' && window! = =null
function checkIntersectionObserver() {
if (
inBrowser &&
'IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype
) {
// Minimal polyfill for Edge 15's lack of `isIntersecting`
// See: https://github.com/w3c/IntersectionObserver/issues/211
if(! ('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
Object.defineProperty(window.IntersectionObserverEntry.prototype, 'isIntersecting', {
get: function() {
return this.intersectionRatio > 0}})}return true
}
return false
}
Copy the code
Waterfall stream image loading sequence
Image loading is an asynchronous process, how to ensure the loading sequence of waterfall images?
After the callback function of IntersectionObserver is triggered, the loading of the next waterfall flow image is likely to have different columns of different length and page jitter, because the image may only be partially loaded when the callback is triggered. This problem exists in both option 1 and Option 2 above
View your document, you can see IntersectionObserver provide IntersectionObserverEntry object callback function will provide the following properties
- Time: indicates the time when visibility changes. It is a high-precision timestamp in milliseconds
- Target: the target element to be observed
DOM
The node object - RootBounds: Information about the rectangular region of the root element,
getBoundingClientRect()
Method, or null if there is no root element (that is, scrolling directly relative to the viewport) - BoundingClientRect: Information about the rectangular region of the target element
- IntersectionRect: information on the intersection area between target element and viewport (or root element)
- IntersectionRatio: intersectionRatio of target elements, i.e
intersectionRect
占boundingClientRect
Is 1 when completely visible and less than or equal to 0 when completely invisible
We can bind the onload event to target and perform the next waterfall data render after onload to ensure the accuracy of the next render when obtaining the lowest column
// Waterfall layout: Take the data at the head of the queue and add it to the column with the smallest waterfall height for rendering. Repeat the loop after the image is fully loaded
observerObj = new IntersectionObserver(
(entries) = > {
for (const entry of entries) {
const { target, isIntersecting } = entry
if (isIntersecting) {
if (target.complete) {
done()
} else {
target.onload = target.onerror = done
}
}
}
}
)
Copy the code
IntersectionObserver triggers twice
It is known that IntersectionObserver is used to monitor target elements and the callback function will be triggered twice when the visibility of target elements changes. Once the target element has just entered the viewport (becoming visible), and once it has completely left the viewport (becoming invisible). To avoid triggering the listening logic a second time, you can stop watching the first time
if (isIntersecting) {
const done = () = > {
// Stop the observation to prevent the listening logic from being triggered twice during the pullback
observerObj.unobserve(target)
}
if (target.complete) {
done()
} else {
target.onload = target.onerror = done
}
}
Copy the code
White screen during first rendering
Due to the serial loading of images, images are rendered one by one, which is very serious when the network is not good, as shown in the following figure
Two solutions are currently available
- Method 1: Parallel rendering is adopted for the pictures in the first screen rendering, and serial rendering is adopted for the subsequent ones. If the interface returns a page of 20 waterfall elements, the first 1-4 images will be rendered in parallel and the next 5-20 will be rendered in serial. You can adjust firstPageCount to suit your needs, and typically the first screen will render 4-6 images.
waterfall() {
// Update the minimum waterfall height column
this.updateMinCol()
// Take the first column from the data source and add it to the column with the smallest waterfall flow height
this.appendColData()
// The first screen uses parallel rendering, and the non-first screen uses serial rendering
if (++count < this.firstPageCount) {
this.$nextTick(() = > this.waterfall())
} else {
this.$nextTick(() = > this.startObserver())
}
}
Copy the code
- Method 2: Add animation to eliminate the impact of the white screen from the visual sense. The component has two built-in animations, which can be transmitted by animation
Blank screen problem during lazy loading
We took the lazy loading approach: loading the next waterfall image after the image is in view is performance-friendly. However, in this case, when the user is scrolling, if the next image is too slow to load, there may be a short blank screen time. How to solve this experience problem
IntersectionObserver has a rootMargin attribute, which can be used to expand the intersecting area so that subsequent data can be loaded in advance. This prevents a white screen when the user scrolls to the bottom and prevents rendering from impacting performance too much. The default is 400px, which is about half of the screen rendered in advance.
// By expanding intersectionRect area, part of data can be loaded in advance to optimize user browsing experience
rootMargin: {
type: String.default: '0px 0px 400px 0px'
}
Copy the code
How to work with infinite loading components
Generally, for the convenience of maintenance, we separate the infinite loading and waterfall flow logic, so when the waterfall flow data rendering needs to notify external components, otherwise it is easy to trigger the infinite loading logic before the waterfall flow rendering is finished, and send two interface requests.
A judgment can be added to the waterfall stream rendering process to notify the external infinite load component of the next request if there is no data in the queue
const done = () = > {
if (this.innerData.length) {
this.waterfall()
} else {
this.$emit('rendered')}}Copy the code
Summary & source code
These are some of the problems and solutions that we encountered when making the new waterfall flow component. Of course, this scheme still needs to be optimized and is currently in use as a component block within the company. Code has not been open source, need source partners can pay attention to the public number around FE, reply waterfall flow can get the source code. For waterfall flow components, if you have better opinions and suggestions, welcome to communicate and discuss.