refactor(flow): 使用 component 模块重构组件 (#2860)

* refactor(flow): 使用 component 模块重构组件

* test(flow): 优化测试用例

* feat(component): 新增 index 实例成员

* fix(flow): 修复特殊场景下的事件冲突

* fix(flow): update
This commit is contained in:
贤心
2025-10-20 12:16:11 +08:00
committed by GitHub
parent a5678e2ef0
commit 060141c8df
4 changed files with 289 additions and 249 deletions

View File

@@ -2,7 +2,7 @@
title: 流加载 flow
toc: true
---
# 流加载
> 流加载 `flow` 是用于在*信息流*和*图片列表*场景中的滚动按需加载,对前后端的体验和性能优化具有一定帮助。
@@ -26,6 +26,7 @@ toc: true
| API | 描述 |
| --- | --- |
| var flow = layui.flow | 获得 `flow` 模块。 |
| [基础接口](../component/#export) <sup>2.13+</sup> | 该组件由 `component` 构建,因此继承其提供的基础接口。|
| [flow.load(options)](#load) | 信息流加载,核心方法。 |
| [flow.lazyimg(options)](#lazyimg) | 图片懒加载。 |
@@ -74,7 +75,7 @@ toc: true
<img lay-src="https://unpkg.com/outeres@0.0.11/demo/wallpaper.jpg">
</div>
<!-- import layui -->
<!-- import layui -->
<script>
layui.use('flow', function(){
var flow = layui.flow;

View File

@@ -1,92 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>流加载 - layui</title>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>流加载 - layui</title>
<link rel="stylesheet" href="../src/css/layui.css">
<style>
.flow-demo{height: 400px; overflow: auto;}
.flow-default{margin-bottom: 32px; border: 1px solid #e2e2e2; text-align: center;}
.flow-default li{display: inline-block; margin-right: 10px; width: 48%; margin-bottom: 10px; height: 200px; line-height: 200px; text-align: center; background-color: #eee;}
.flow-default img{width: 100%; height: 100%; border: none;}
.flow-default > img{width: 48%; height: 49%; margin-bottom: 5px;}
</style>
</head>
<body class="layui-padding-3">
<div class="flow-default flow-demo" id="ID-flow-demo"></div>
<ul class="flow-default" id="test1"></ul>
<div class="layui-hide">
<ul class="flow-default" id="test2"></ul>
</div>
<link rel="stylesheet" href="../src/css/layui.css">
<div class="flow-default" style="height: 300px; overflow: auto;" id="ID-flow-lazyimg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
<img src="https://unpkg.com/outeres@0.2.0/img/other/loading.gif" lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg">
</div>
<style>
body{padding: 15px;}
.flow-default{ font-size: 0;}
.flow-default li{display: inline-block; margin-right: 10px; font-size: 14px; width: 48%; margin-bottom: 10px; height: 200px; line-height: 200px; text-align: center; background-color: #eee;}
img{width: 500px; height: 300px;}
.flow-default img{width: 100%; height: 100%;}
</style>
</head>
<body>
<script src="../src/layui.js"></script>
<script>
layui.use('flow', function() {
var flow = layui.flow;
// 自动加载
flow.load({
elem: '#ID-flow-demo', // 流加载容器
scrollElem: '#ID-flow-demo', // 滚动条所在元素,一般不用填,此处只是演示需要。
done: function (page, next) {
// 执行下一页的回调
// 模拟数据插入
setTimeout(function () {
var lis = [];
for (var i = 0; i < 8; i++) {
lis.push('<li>' + ((page - 1) * 8 + i + 1) + '</li>');
}
<ul class="flow-default" id="test1"></ul>
// 执行下一页渲染,第二参数为:满足“加载更多”的条件,即后面仍有分页
// pages 为 Ajax返回的总页数只有当前页小于总页数的情况下才会继续出现加载更多
next(lis.join(''), page < 10); // 此处假设总页数为 10
}, 300);
},
});
<div class="layui-hide">
<ul class="flow-default" id="test2"></ul>
</div>
<div class="demo" style="height: 300px; overflow: auto;">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/layer.png">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/layer.png">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/error.png">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/layer.png">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/layer.png">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/layer.png">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/layer.png">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/layer.png">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/layer.png">
<img src="https://sentsin.gitee.io/res/images/demo/loading.gif" lay-src="https://sentsin.gitee.io/res/images/demo/layer.png">
</div>
<script src="../src/layui.js"></script>
<script>
layui.use('flow', function(){
var flow = layui.flow;
flow.load({
elem: '#test1' //流加载容器
//,scrollElem: '.flow-default' //滚动条所在元素默认document
//,isAuto: false
//,end: '没了'
,isLazyimg: true
,done: function(page, next){ //加载下一页
console.log('done:', page)
setTimeout(function(){
var lis = [];
for(var i = 0; i < 6; i++){
lis.push('<li><img lay-src="https://sentsin.gitee.io/res/images/demo/layer.png?v='+ (page+i) +'"></li>')
// 手动加载
var flowInst = flow.load({
elem: '#test1', // 流加载容器
// scrollElem: '.flow-default', // 滚动条所在元素,默认 document
// end: '没有更多数据',
isAuto: false,
isLazyimg: true,
done: function(page, next) { // 加载下一页
console.log('done:', page)
setTimeout(function(){
var lis = [];
for(var i = 0; i < 6; i++){
lis.push('<li><img lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg?v='+ (page+i) +'"></li>')
}
next(lis.join(''), page < 3);
}, 300);
}
next(lis.join(''), page < 3);
}, 500);
}
});
});
flow.load({
elem: '#test2' //流加载容器
//,scrollElem: '.flow-default' //滚动条所在元素默认document
//,isAuto: false
//,end: '没了'
,isLazyimg: true
,done: function(page, next){ //加载下一页
setTimeout(function(){
var lis = [];
for(var i = 0; i < 6; i++){
lis.push('<li><img lay-src="https://sentsin.gitee.io/res/images/demo/layer.png?v='+ (page+i) +'"></li>')
// 重载
flowInst.reload({
moreText: '...'
});
flow.load({
elem: '#test2', // 流加载容器
isLazyimg: true,
done: function(page, next) { // 加载下一页
setTimeout(function(){
var lis = [];
for(var i = 0; i < 6; i++){
lis.push('<li><img lay-src="https://unpkg.com/outeres@0.2.0/demo/wallpaper.jpg?v='+ (page+i) +'"></li>')
}
next(lis.join(''), page < 3);
}, 300);
}
next(lis.join(''), page < 3);
}, 500);
}
});
});
//按屏加载图片
flow.lazyimg({
elem: '.demo img'
,scrollElem: '.demo'
});
});
</script>
</body>
// 按屏加载图片
flow.lazyimg({
elem: '#ID-flow-lazyimg img',
scrollElem: '#ID-flow-lazyimg',
id: 'flow-lazyimg-demo'
});
});
</script>
</body>
</html>

View File

@@ -61,6 +61,7 @@ layui.define(['jquery', 'lay'], function(exports) {
var inst = {
config: options,
id: id,
index: that.index,
// 重置实例
reload: function(options) {

View File

@@ -1,205 +1,227 @@
/**
* flow 流加载组件
* flow
* 流加载组件
*/
layui.define(['i18n', 'jquery'], function(exports) {
layui.define(['i18n', 'component'], function(exports) {
"use strict";
var $ = layui.$;
var i18n = layui.i18n;
var Flow = function(options) {};
var ELEM_MORE = 'layui-flow-more';
var ELEM_LOAD = '<i class="layui-anim layui-anim-rotate layui-anim-loop layui-icon ">&#xe63e;</i>';
// 主方法
Flow.prototype.load = function(options) {
var that = this, page = 0, lock, isOver, lazyimg, timer;
options = options || {};
// 创建组件
var component = layui.component({
name: 'flow',
var elem = $(options.elem); if(!elem[0]) return;
var scrollElem = $(options.scrollElem || document); // 滚动条所在元素
var threshold = 'mb' in options ? options.mb : 50; // 临界距离
var isAuto = 'isAuto' in options ? options.isAuto : true; // 否自动滚动加载
var moreText = options.moreText || i18n.$t('flow.loadMore'); // 手动加载时,加载更多按钮文案
var end = options.end || i18n.$t('flow.noMore'); // “末页”显示文案
var direction = options.direction || 'bottom';
var isTop = direction === 'top';
CONST: {
ELEM_LOAD: '<i class="layui-anim layui-anim-rotate layui-anim-loop layui-icon layui-icon-loading-1"></i>',
ELEM_MORE: 'layui-flow-more',
FLOW_SCROLL_EVENTS: 'scroll.lay_flow_scroll',
LAZYIMG_SCROLL_EVENTS: 'scroll.lay_flow_lazyimg_scroll',
},
// 重复执行时清理旧的事件绑定
that._cleanup(elem, scrollElem);
// 渲染
render: function() {
var that = this;
var options = that.config;
//滚动条所在元素是否为document
var notDocument = options.scrollElem && options.scrollElem !== document;
var page = 0;
var locked;
var finished;
//加载更多
var ELEM_TEXT = '<cite>' + moreText + '</cite>'
,more = $('<div class="layui-flow-more"><a href="javascript:;">'+ ELEM_TEXT +'</a></div>');
var elem = options.elem;
if (!elem[0]) return;
if(!elem.find('.layui-flow-more')[0]){
elem[isTop ? 'prepend' : 'append'](more);
}
var scrollElem = $(options.scrollElem || document); // 滚动条所在元素
var threshold = 'mb' in options ? options.mb : 50; // 临界距离
var isAuto = 'isAuto' in options ? options.isAuto : true; // 否自动滚动加载
var moreText = options.moreText || i18n.$t('flow.loadMore'); // 手动加载时,加载更多按钮文案
var endText = options.end || i18n.$t('flow.noMore'); // “末页”显示文案
var direction = options.direction || 'bottom';
var isTop = direction === 'top';
//加载下一个元素
var next = function(html, over){
var scrollHeightStart = notDocument ? scrollElem.prop('scrollHeight') : document.documentElement.scrollHeight;
var scrollTopStart = scrollElem.scrollTop();
html = $(html);
more[isTop ? 'after' : 'before'](html);
over = over == 0 ? true : null;
over ? more.html(end) : more.find('a').html(ELEM_TEXT);
isOver = over;
lock = null;
lazyimg && lazyimg();
if(isTop){
var scrollHeightEnd = notDocument ? scrollElem.prop('scrollHeight') : document.documentElement.scrollHeight;
if(page === 1){
// 首次渲染后滑动到底部
scrollElem.scrollTop(scrollHeightEnd);
}else if(page > 1){
var nextElementHeight = scrollHeightEnd - scrollHeightStart;
scrollElem.scrollTop(scrollTopStart + nextElementHeight);
}
}
};
// 滚动条所在元素是否为 document
var notDocument = options.scrollElem && options.scrollElem !== document;
//触发请求
var done = function(){
lock = true;
more.find('a').html(ELEM_LOAD);
typeof options.done === 'function' && options.done(++page, next);
};
// 加载更多
var ELEM_TEXT = '<cite>' + moreText + '</cite>';
var $more = $('<div class="'+ CONST.ELEM_MORE +'"><a href="javascript:;">'+ ELEM_TEXT +'</a></div>');
done();
elem.find('.'+ CONST.ELEM_MORE).remove(); // 清除旧的「加载更多」元素
elem[isTop ? 'prepend' : 'append']($more);
//不自动滚动加载
more.find('a').on('click.flow', function(){
var othis = $(this);
if(isOver) return;
lock || done();
});
// 加载下一个元素
var next = function(content, status) {
var scrollHeightStart = notDocument
? scrollElem.prop('scrollHeight')
: document.documentElement.scrollHeight;
var scrollTopStart = scrollElem.scrollTop();
//如果允许图片懒加载
if(options.isLazyimg){
lazyimg = that.lazyimg({
elem: options.elem + ' img'
,scrollElem: options.scrollElem
,direction: options.direction
});
}
$more[isTop ? 'after' : 'before'](content);
status = status == 0 ? true : null;
status ? $more.html(endText) : $moreBtn.html(ELEM_TEXT);
finished = status;
locked = null;
if(!isAuto) return that;
scrollElem.on('scroll.flow', function(){
var othis = $(this), top = othis.scrollTop();
if(timer) clearTimeout(timer);
if(isOver || !elem.width()) return; //如果已经结束,或者元素处于隐藏状态,则不执行滚动加载
timer = setTimeout(function(){
//计算滚动所在容器的可视高度
var height = notDocument ? othis.height() : $(window).height();
//计算滚动所在容器的实际高度
var scrollHeight = notDocument
? othis.prop('scrollHeight')
: document.documentElement.scrollHeight;
//临界点
if(!isTop ? scrollHeight - top - height <= threshold : top <= threshold){
lock || done();
}
}, 100);
});
return that;
};
//图片懒加载
Flow.prototype.lazyimg = function(options){
var that = this, index = 0, haveScroll;
options = options || {};
var scrollElem = $(options.scrollElem || document); //滚动条所在元素
var elem = options.elem || 'img';
var direction = options.direction || 'bottom';
var isTop = direction === 'top';
//滚动条所在元素是否为document
var notDocument = options.scrollElem && options.scrollElem !== document;
//显示图片
var show = function(item, height){
var start = scrollElem.scrollTop(), end = start + height;
var elemTop = notDocument ? function(){
return item.offset().top - scrollElem.offset().top + start;
}() : item.offset().top;
/* 始终只加载在当前屏范围内的图片 */
if((isTop ? elemTop + item.height() : elemTop) >= start && elemTop <= end){
if(item.attr('lay-src')){
var src = item.attr('lay-src');
layui.img(src, function(){
var next = that.lazyimg.elem.eq(index);
item.attr('src', src).removeAttr('lay-src');
/* 当前图片加载就绪后,检测下一个图片是否在当前屏 */
next[0] && render(next);
index++;
}, function(){
var next = that.lazyimg.elem.eq(index);
item.removeAttr('lay-src');
// 如果允许图片懒加载
if (options.isLazyimg) {
component.lazyimg({
elem: options.elem.find('img[lay-src]'),
scrollElem: options.scrollElem,
direction: options.direction,
id: options.id
});
}
}
}, render = function(othis, scroll){
//计算滚动所在容器的可视高度
var height = notDocument ? (scroll||scrollElem).height() : $(window).height();
var start = scrollElem.scrollTop(), end = start + height;
if (isTop) {
var scrollHeightEnd = notDocument ? scrollElem.prop('scrollHeight') : document.documentElement.scrollHeight;
if (page === 1) {
// 首次渲染后滑动到底部
scrollElem.scrollTop(scrollHeightEnd);
} else if(page > 1) {
var nextElementHeight = scrollHeightEnd - scrollHeightStart;
scrollElem.scrollTop(scrollTopStart + nextElementHeight);
}
}
};
var $moreBtn = $more.find('a');
that.lazyimg.elem = $(elem);
// 触发请求
var done = (function fn() {
locked = true;
$moreBtn.html(CONST.ELEM_LOAD);
typeof options.done === 'function' && options.done(++page, next);
return fn;
})();
if(othis){
show(othis, height);
} else {
//计算未加载过的图片
for(var i = 0; i < that.lazyimg.elem.length; i++){
var item = that.lazyimg.elem.eq(i), elemTop = notDocument ? function(){
// 不自动滚动加载
$moreBtn.on('click', function() {
if (finished) return;
locked || done();
});
if (!isAuto) return that;
// 滚动条滚动事件
var timer;
var FLOW_SCROLL_EVENTS = CONST.FLOW_SCROLL_EVENTS + '_' + options.id;
scrollElem.off(FLOW_SCROLL_EVENTS).on(FLOW_SCROLL_EVENTS, function() {
var othis = $(this), top = othis.scrollTop();
if (timer) clearTimeout(timer);
// 如果已经结束,或者元素处于隐藏状态,则不执行滚动加载
if (finished || !elem.width()) return;
timer = setTimeout(function() {
// 计算滚动所在容器的可视高度
var height = notDocument ? othis.height() : $(window).height();
// 计算滚动所在容器的实际高度
var scrollHeight = notDocument
? othis.prop('scrollHeight')
: document.documentElement.scrollHeight;
// 临界点
if(!isTop ? scrollHeight - top - height <= threshold : top <= threshold){
locked || done();
}
}, 100);
});
}
});
var CONST = component.CONST;
/**
* 扩展组件原型方法
*/
// 保留原接口,确保向下兼容
$.extend(component, {
load: function(options) {
return component.render(options);
},
// 图片懒加载
lazyimg: function(options) {
options = options || {};
var scrollElem = $(options.scrollElem || document); // 滚动条所在元素
var elem = options.elem || 'img';
var direction = options.direction || 'bottom';
var isTop = direction === 'top';
var index = 0;
// 滚动条所在元素是否为 document
var notDocument = options.scrollElem && options.scrollElem !== document;
// 显示图片
var render = (function fn(othis) {
var $elem = $(elem);
// 计算滚动所在容器的可视高度
var height = notDocument ? scrollElem.height() : $(window).height();
var start = scrollElem.scrollTop();
var end = start + height;
var show = function(item) {
var elemTop = notDocument ? function(){
return item.offset().top - scrollElem.offset().top + start;
}() : item.offset().top;
show(item, height);
index = i;
/* 始终只加载在当前屏范围内的图片 */
if ((isTop ? elemTop + item.height() : elemTop) >= start && elemTop <= end) {
if(item.attr('lay-src')){
var src = item.attr('lay-src');
layui.img(src, function() {
var next = $elem.eq(index);
item.attr('src', src).removeAttr('lay-src');
//如果图片的top坐标超出了当前屏则终止后续图片的遍历
if(elemTop > end) break;
/* 当前图片加载就绪后,检测下一个图片是否在当前屏 */
next[0] && render(next);
index++;
}, function() {
item.removeAttr('lay-src');
});
}
}
};
if (othis) {
show(othis);
} else {
// 计算未加载过的图片
for (var i = 0; i < $elem.length; i++) {
var item = $elem.eq(i), elemTop = notDocument ? function(){
return item.offset().top - scrollElem.offset().top + start;
}() : item.offset().top;
show(item);
index = i;
// 如果图片的 top 坐标,超出了当前屏,则终止后续图片的遍历
if (elemTop > end) break;
}
}
}
};
render();
return fn;
})();
if(!haveScroll){
// 滚动事件
var timer;
scrollElem.on('scroll.lazyimg' , function(){
var othis = $(this);
if(timer) clearTimeout(timer)
var id = options.id || '';
var LAZYIMG_SCROLL_EVENTS = CONST.LAZYIMG_SCROLL_EVENTS + '_' + id;
scrollElem.off(LAZYIMG_SCROLL_EVENTS).on(LAZYIMG_SCROLL_EVENTS, function() {
if (timer) clearTimeout(timer)
timer = setTimeout(function(){
render(null, othis);
render();
}, 50);
});
haveScroll = true;
return render;
}
return render;
};
});
// 重复执行时清理旧的事件绑定,私有方法
Flow.prototype._cleanup = function(elem, scrollElem){
scrollElem.off('scroll.flow').off('scroll.lazyimg');
elem.find('.layui-flow-more').find('a').off('click.flow');
}
//暴露接口
exports('flow', new Flow());
exports(CONST.MOD_NAME, component);
});