Author: Huang Yi, Didi Public Front End
Dealing with scrolling lists is a common requirement in our daily mobile project development. For example, didi could have a list scrolling vertically like this, as shown below:
It can also be a horizontal scrolling navigation bar, as shown in the figure:
You can open “wechat – > Wallet – > Didi Chuxing” to experience the effect.
So when we do this kind of scrolling, we’re going to use a third-party library that I wrote, better Scroll.
What is the better – scroll
Better Scroll is a mobile scrolling solution, which is based on iscroll rewrite, and the main difference between it and IScroll is here. Better Scroll is also very powerful, not only for normal scroll lists, but also for rotations, pickers and so on.
The principle of better Scroll
Many students may have used Better-Scroll, and the question I received the most feedback is:
My better Scroll is initialized, but I can’t scroll.
Failure to roll is a phenomenon, and we need to get to the bottom of it. Before this, we first look at the browser scroll principle: browser scroll bar we will encounter, when the height of the page content exceeds the height of the viewport, there will be vertical scroll bar; A horizontal scroll bar appears when the width of the page content exceeds the viewport width. That is, when our viewport can not display the content, the user will scroll through the screen to see the rest of the content.
So the same is true for Better Scroll. Let’s first look at the HTML structure for better Scroll:
<div class="wrapper">
<ul class="content">
<li>.</li>
<li>.</li>.</ul>
</div>Copy the code
To make it more intuitive, let’s look at another picture:
The green part is the Wrapper, the parent container, which will have a fixed height. The yellow part is content, which is the first child of the parent container, and its height increases with the size of the content. So, content can’t be scrolled if it’s not higher than the height of the parent container, but if it’s higher than the height of the parent container, we can scroll the content area, and that’s how the Better Scroll works.
So, how do we initialize the better Scroll, if the above HTML structure, then the initialization code is as follows:
import BScroll from 'better-scroll'
let wrapper = document.querySelector('.wrapper')
let scroll = new BScroll(wrapper, {})Copy the code
Better Scroll exposes a BScroll class, so we only need to initialize a new instance of the class. The first parameter is our WRAPPER DOM object, and the second parameter is some configuration parameters. Please refer to the documentation for Better Scroll.
The timing of the initialization of the Better Scroll is important because when it initializes, it calculates the height and width of the parent and child elements to determine whether it can scroll vertically and horizontally. Therefore, when we initialize it, we must ensure that the contents of the parent and child elements have been rendered correctly. If the DOM structure of a child or parent element changes, the scroll.refresh() method must be called to recalculate to ensure proper scrolling. Therefore, the reason why the better Scroll could not scroll according to the feedback of students is probably that the timing of initializing the Better Scroll is not right, or the BETTER Scroll is not recalculated when the DOM structure sends changes.
Better meet Vue – scroll
I believe that many students are familiar with Vue. Js, when Better Scroll meets Vue, what kind of spark will be rubbed out?
How to use better Scroll in Vue
Many students started to use Better-Scroll because of my teaching course “Vue.js High Imitation Ele. me Takeout App”. In that course, we did a combination of Better Scroll and Vue to do a lot of list scrolling. Use in Vue as follows:
<template> <div class="wrapper" ref="wrapper"> <ul class="content"> <li>... </li> <li>... </li> ... </ul> </div> </template> <script> import BScroll from 'better-scroll' export default { mounted() { this.$nextTick(() => { this.scroll = new Bscroll(this.$refs.wrapper, {}) }) } } </script>Copy the code
Vue.js provides us with an interface to get DOM objects — vm.$refs. In this case, we access the DOM object via this.$refs.wrapper, and in the mounted hook function, we initialize the Better Scroll in this. Since the WRAPPER’S DOM is rendered at this point, we can properly calculate the height of it and its inner content to ensure proper scrolling.
This.$nextTick is an asynchronous function. In order to ensure that the DOM is rendered, you can see the internal implementation details of this function. $nextTick setTimeout(fn, 20); 20 ms is an experience value, and each Tick is about 17 ms.
Processing of asynchronous data
In our actual work, the list data is usually obtained asynchronously, so the time when we initialize better-Scroll should be after the data is obtained, and the code is as follows:
<template> <div class="wrapper" ref="wrapper"> <ul class="content"> <li v-for="item in data">{{item}}</li> </ul> </div> </template> <script> import BScroll from 'better-scroll' export default { data() { return { data: []}}, created() { requestData().then((res) => { this.data = res.data this.$nextTick(() => { this.scroll = new Bscroll(this.$refs.wrapper, {}) }) }) } } </script>Copy the code
RequestData is pseudocode that makes an HTTP request to get data from the server and returns a promise (in real projects we might use axios or Vue-Resource). After we obtain the data, we need to initialize the Better-Scroll in an asynchronous way, because Vue is data-driven, and it is an asynchronous process to re-render the page when the Vue data changes (this.data = res.data). Our initialization time is after the DOM is re-rendered, so we use this.$nextTick instead of setTimeout(fn, 20).
Why is this method created in the mounted hook function and not in the mounted hook function? Since requestData sends a network request, this is an asynchronous process. By the time we get the response data, the Vue DOM has already been rendered, but data changes — > DOM re-rendering is still an asynchronous process, so even after we get the data, We also need to initialize the better Scroll asynchronously.
Dynamic update of data
In our actual development, in addition to asynchronous data retrieval, there are also some scenarios that can dynamically update the data in the list, such as the common pull-down load, pull-up refresh, etc. For example, we use Better Scroll with Vue to achieve the drop-down loading function, the code is as follows:
<template> <div class="wrapper" ref="wrapper"> <ul class="content"> <li v-for="item in data">{{item}}</li> </ul> <div class="loading-wrapper"></div> </div> </template> <script> import BScroll from 'better-scroll' export default { data() { return { data: [] } }, created() { this.loadData() }, methods: { loadData() { requestData().then((res) => { this.data = res.data.concat(this.data) this.$nextTick(() => { if (! this.scroll) { this.scroll = new Bscroll(this.$refs.wrapper, {}) this.scroll.on('touchend', (pos) = > {/ / iso under the if (pos) y > 50) {enclosing the loadData ()}})} else {this. Scroll. Refresh ()}})})}}} < / script >Copy the code
This code is a little more complicated than before, when we release our finger in the slider, Better Scroll will send out a Touchend event, we listen for this event, and determine pos. Y > 50 (we define this behavior as a drop down action). If it is a drop down, we rerequest the data and concat the new data with the previous data, updating the list data, and the data changes are mapped to the DOM changes. One thing to note here is that we are checking this.scroll. If it has not been initialized we will initialize it with new BScroll and bind some events, otherwise we will call this.scroll.
Here, we realize the drop-down refresh function of the list through Better Scroll and Vue, and the drop-down load is similar, everything looks OK. However, we find that there is a lot of imperative code here (which is not recommended by Vue. Js). If we have a lot of components like scroll, we need to write a lot of similar imperative and repetitive code. These are not ok for a person who pursues programming compulsion.
Abstraction and encapsulation of scroll components
Therefore, we have a strong need to abstract out a Scroll component, similar to the small program’s Scroll view component, convenient for developers to use.
First of all, we need to consider that the Scroll component is essentially a list component that can be scrolled. As for the DOM structure of the list, it only needs to meet the DOM structure specification of Better-Scroll. What label should be used specifically? The secondary nodes (such as the loading layer for pull-down refresh and pull-up loading) are not the scroll component’s concern. Therefore, the DOM structure of the Scroll component is very simple, as follows:
<template>
<div ref="wrapper">
<slot></slot>
</div>
</template>Copy the code
Here we use a special element of Vue, the slot slot, which allows us to flexibly customize the DOM structure of the list. Let’s look at the JS section:
<script type="text/ecmascript-6">
import BScroll from 'better-scroll'
export default {
props: {
/** * 1 Will send the scroll event to intercept the stream. * 2 The scroll event will be sent in real time during scrolling without stream interception. * 3 In addition to real-time scroll events, swipe can still issue scroll events in real-time */
probeType: {
type: Number.default: 1
},
/** * Whether to send the click event */
click: {
type: Boolean.default: true
},
/** * Whether horizontal scrolling is enabled */
scrollX: {
type: Boolean.default: false
},
/** * Whether to send rolling events */
listenScroll: {
type: Boolean.default: false
},
/** * list of data */
data: {
type: Array.default: null
},
/** * whether to send events that scroll to the bottom for pull-up loading */
pullup: {
type: Boolean.default: false
},
/** * Whether to send the top drop-down event for the drop-down refresh */
pulldown: {
type: Boolean.default: false
},
/** * Whether to send the event that the list starts scrolling */
beforeScroll: {
type: Boolean.default: false
},
/** * Refresh scroll delay after data is updated. * /
refreshDelay: {
type: Number.default: 20
}
},
mounted() {
// Ensure that the better Scroll is initialized after DOM rendering is complete
setTimeout((a)= > {
this._initScroll()
}, 20)},methods: {
_initScroll() {
if (!this.$refs.wrapper) {
return
}
// Better Scroll initialization
this.scroll = new BScroll(this.$refs.wrapper, {
probeType: this.probeType,
click: this.click,
scrollX: this.scrollX
})
// Whether to send rolling events
if (this.listenScroll) {
let me = this
this.scroll.on('scroll', (pos) => {
me.$emit('scroll', pos)
})
}
// Whether to send the scroll to the bottom event for pull-up loading
if (this.pullup) {
this.scroll.on('scrollEnd', () = > {// Scroll to the bottom
if (this.scroll.y <= (this.scroll.maxScrollY + 50)) {
this.$emit('scrollToEnd')}}}// Whether to send a top drop-down event for drop-down refresh
if (this.pulldown) {
this.scroll.on('touchend', (pos) => {
// Pull down
if (pos.y > 50) {
this.$emit('pulldown')}}}// Whether to send the event that the list starts scrolling
if (this.beforeScroll) {
this.scroll.on('beforeScrollStart', () = > {this.$emit('beforeScroll')
})
}
},
disable() {
// Proxy the better scroll disable method
this.scroll && this.scroll.disable()
},
enable() {
// Proxy the better scroll enable method
this.scroll && this.scroll.enable()
},
refresh() {
// The refresh method of the better Scroll proxy
this.scroll && this.scroll.refresh()
},
scrollTo() {
// Delegate the better scroll scrollTo method
this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
},
scrollToElement() {
// Delegate the better Scroll scrollToElement method
this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)}},watch: {
// Monitor data changes and call refresh after the refreshDelay time to recalcalate to ensure normal rolling effect
data() {
setTimeout((a)= > {
this.refresh()
}, this.refreshDelay)
}
}
}
</script>Copy the code
The JS part is actually a layer of Vue encapsulation for Better-Scroll. In the form of props, some control of better-Scroll customization is handed over to the parent component. Some methods exposed by methods make a layer proxy for the better scroll method; When the data passed in by watch is changed, the refresh method should be called at an appropriate time to recalculate the Better Scroll to ensure the normal scrolling effect. The reason for the setting of the refreshDelay is that if we animate the transition-group for the list operation, the DOM will be rendered after the animation is complete.
With this layer wrapped around the Scroll component, let’s modify the most complex code (assuming we’ve registered the Scroll component globally).
<template>
<scroll class="wrapper"
:data="data"
:pulldown="pulldown"
@pulldown="loadData">
<ul class="content">
<li v-for="item in data">{{item}}</li>
</ul>
<div class="loading-wrapper"></div>
</scroll>
</template>
<script>
import BScroll from 'better-scroll'
export default {
data() {
return {
data: [],
pulldown: true
}
},
created() {
this.loadData()
},
methods: {
loadData() {
requestData().then((res) => {
this.data = res.data.concat(this.data)
})
}
}
}
</script>Copy the code
It can be obviously seen that our JS part has simplified a lot of code, and there is no more imperative operation for Better-Scroll, and the data request and Better-Scroll are also stripped. The parent component only needs to pass data to Scroll component through prop. The scroll component can be guaranteed to scroll. At the same time, if you want to implement the pull-down refresh function, you just need to set pullDown to true through prop and listen for pulldown events to do some data fetching and updating. The whole logic is very clear.
Some thoughts triggered by Vue of plug-in
In this article I want to not only teach you how to encapsulate a Scroll component, but also convey some thought processes for Vue third-party plug-ins (native JS implementations). Many students who learn vue. js may still stay in the degree of “how to achieve the XX effect with vue. js”. In fact, there are two key points to Vue plug-in, one is to understand the implementation principle of the plug-in itself, the other is to understand the features of vue. js. Understanding the implementation principle of the plug-in itself requires a process of thinking and studying, which may be difficult, but the harvest is also huge; To understand the features of vue. js, we need to use vue. js more, learn to accumulate and summarize from the usual project, but also good at referring to the official documents of vue. js, pay attention to some vue. js upgrade.
Therefore, we refuse to be a party, but we do not encourage you to build wheels at any time. While we are using some ready-made plug-ins, we also hope that you can think more, explore the nature behind the phenomenon, and change the sentence “how to achieve XX effect with vue.js” from a question mark to a full stop.
AD time
Recently, we launched an advanced course in vue. js. Students who want to advance in vue. js and mobile terminal development can pay attention to a wave of ~