Commit fdeaa00bf24b0710ca341fafba8327c786ab9879

Authored by vben
1 parent a0c31974

feat: add lazyContainer comp and demo

CHANGELOG.zh_CN.md
@@ -8,6 +8,7 @@ @@ -8,6 +8,7 @@
8 - 表单新增 submitOnReset 控制是否在重置时重新发起请求 8 - 表单新增 submitOnReset 控制是否在重置时重新发起请求
9 - 表格新增`sortFn`支持自定义排序 9 - 表格新增`sortFn`支持自定义排序
10 - 新增动画组件及示例 10 - 新增动画组件及示例
  11 +- 新增懒加载/延时加载组件及示例
11 12
12 ### ✨ Refactor 13 ### ✨ Refactor
13 14
src/components/Container/index.ts
1 export { default as ScrollContainer } from './src/ScrollContainer.vue'; 1 export { default as ScrollContainer } from './src/ScrollContainer.vue';
2 export { default as CollapseContainer } from './src/collapse/CollapseContainer.vue'; 2 export { default as CollapseContainer } from './src/collapse/CollapseContainer.vue';
3 -export { default as LazyContainer } from './src/LazyContainer'; 3 +export { default as LazyContainer } from './src/LazyContainer.vue';
4 4
5 export * from './src/types.d'; 5 export * from './src/types.d';
src/components/Container/src/LazyContainer.less deleted 100644 → 0
1 -.lazy-container-enter {  
2 - opacity: 0;  
3 -}  
4 -  
5 -.lazy-container-enter-to {  
6 - opacity: 1;  
7 -}  
8 -  
9 -.lazy-container-enter-from,  
10 -.lazy-container-enter-active {  
11 - position: absolute;  
12 - top: 0;  
13 - width: 100%;  
14 - transition: opacity 0.3s 0.2s;  
15 -}  
16 -  
17 -.lazy-container-leave {  
18 - opacity: 1;  
19 -}  
20 -  
21 -.lazy-container-leave-to {  
22 - opacity: 0;  
23 -}  
24 -  
25 -.lazy-container-leave-active {  
26 - transition: opacity 0.5s;  
27 -}  
src/components/Container/src/LazyContainer.tsx renamed to src/components/Container/src/LazyContainer.vue
1 -import type { PropType } from 'vue';  
2 -  
3 -import {  
4 - defineComponent,  
5 - reactive,  
6 - onMounted,  
7 - ref,  
8 - unref,  
9 - onUnmounted,  
10 - TransitionGroup,  
11 -} from 'vue';  
12 -  
13 -import { Skeleton } from 'ant-design-vue';  
14 -import { useRaf } from '/@/hooks/event/useRaf';  
15 -import { useTimeout } from '/@/hooks/core/useTimeout';  
16 -import { getListeners, getSlot } from '/@/utils/helper/tsxHelper';  
17 -  
18 -import './LazyContainer.less';  
19 -  
20 -interface State {  
21 - isInit: boolean;  
22 - loading: boolean;  
23 - intersectionObserverInstance: IntersectionObserver | null;  
24 -}  
25 -export default defineComponent({  
26 - name: 'LazyContainer',  
27 - emits: ['before-init', 'init'],  
28 - props: {  
29 - // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载  
30 - timeout: {  
31 - type: Number as PropType<number>,  
32 - default: 8000,  
33 - // default: 8000,  
34 - },  
35 - // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器  
36 - viewport: {  
37 - type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,  
38 - default: () => null,  
39 - },  
40 - // 预加载阈值, css单位  
41 - threshold: {  
42 - type: String as PropType<string>,  
43 - default: '0px', 1 +<template>
  2 + <transition-group v-bind="$attrs" ref="elRef" :name="transitionName" :tag="tag">
  3 + <div key="component" v-if="isInit">
  4 + <slot :loading="loading" />
  5 + </div>
  6 + <div key="skeleton">
  7 + <slot name="skeleton" v-if="$slots.skeleton" />
  8 + <Skeleton v-else />
  9 + </div>
  10 + </transition-group>
  11 +</template>
  12 +<script lang="ts">
  13 + import type { PropType } from 'vue';
  14 +
  15 + import { defineComponent, reactive, onMounted, ref, unref, onUnmounted, toRefs } from 'vue';
  16 +
  17 + import { Skeleton } from 'ant-design-vue';
  18 + import { useRaf } from '/@/hooks/event/useRaf';
  19 + import { useTimeout } from '/@/hooks/core/useTimeout';
  20 +
  21 + interface State {
  22 + isInit: boolean;
  23 + loading: boolean;
  24 + intersectionObserverInstance: IntersectionObserver | null;
  25 + }
  26 + export default defineComponent({
  27 + name: 'LazyContainer',
  28 + components: { Skeleton },
  29 + props: {
  30 + // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
  31 + timeout: {
  32 + type: Number as PropType<number>,
  33 + default: 8000,
  34 + // default: 8000,
  35 + },
  36 + // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
  37 + viewport: {
  38 + type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<
  39 + HTMLElement
  40 + >,
  41 + default: () => null,
  42 + },
  43 + // 预加载阈值, css单位
  44 + threshold: {
  45 + type: String as PropType<string>,
  46 + default: '0px',
  47 + },
  48 +
  49 + // 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
  50 + direction: {
  51 + type: String as PropType<'vertical' | 'horizontal'>,
  52 + default: 'vertical',
  53 + },
  54 + // 包裹组件的外层容器的标签名
  55 + tag: {
  56 + type: String as PropType<string>,
  57 + default: 'div',
  58 + },
  59 +
  60 + maxWaitingTime: {
  61 + type: Number as PropType<number>,
  62 + default: 80,
  63 + },
  64 +
  65 + // // 是否在不可见的时候销毁
  66 + // autoDestory: {
  67 + // type: Boolean as PropType<boolean>,
  68 + // default: false,
  69 + // },
  70 +
  71 + // transition name
  72 + transitionName: {
  73 + type: String as PropType<string>,
  74 + default: 'lazy-container',
  75 + },
44 }, 76 },
  77 + emits: ['before-init', 'init'],
  78 + setup(props, { emit, slots }) {
  79 + const elRef = ref<any>(null);
  80 + const state = reactive<State>({
  81 + isInit: false,
  82 + loading: false,
  83 + intersectionObserverInstance: null,
  84 + });
45 85
46 - // 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向  
47 - direction: {  
48 - type: String as PropType<'vertical' | 'horizontal'>,  
49 - default: 'vertical',  
50 - },  
51 - // 包裹组件的外层容器的标签名  
52 - tag: {  
53 - type: String as PropType<string>,  
54 - default: 'div',  
55 - }, 86 + immediateInit();
  87 + onMounted(() => {
  88 + initIntersectionObserver();
  89 + });
  90 + onUnmounted(() => {
  91 + // Cancel the observation before the component is destroyed
  92 + if (state.intersectionObserverInstance) {
  93 + const el = unref(elRef);
  94 + state.intersectionObserverInstance.unobserve(el.$el);
  95 + }
  96 + });
56 97
57 - maxWaitingTime: {  
58 - type: Number as PropType<number>,  
59 - default: 80,  
60 - }, 98 + // If there is a set delay time, it will be executed immediately
  99 + function immediateInit() {
  100 + const { timeout } = props;
  101 + timeout &&
  102 + useTimeout(() => {
  103 + init();
  104 + }, timeout);
  105 + }
61 106
62 - // 是否在不可见的时候销毁  
63 - autoDestory: {  
64 - type: Boolean as PropType<boolean>,  
65 - default: false,  
66 - }, 107 + function init() {
  108 + // At this point, the skeleton component is about to be switched
  109 + emit('before-init');
  110 + // At this point you can prepare to load the resources of the lazy-loaded component
  111 + state.loading = true;
67 112
68 - // transition name  
69 - transitionName: {  
70 - type: String as PropType<string>,  
71 - default: 'lazy-container',  
72 - },  
73 - },  
74 - setup(props, { attrs, emit, slots }) {  
75 - const elRef = ref<any>(null);  
76 - const state = reactive<State>({  
77 - isInit: false,  
78 - loading: false,  
79 - intersectionObserverInstance: null,  
80 - });  
81 -  
82 - // If there is a set delay time, it will be executed immediately  
83 - function immediateInit() {  
84 - const { timeout } = props;  
85 - timeout && 113 + requestAnimationFrameFn(() => {
  114 + state.isInit = true;
  115 + emit('init');
  116 + });
  117 + }
  118 +
  119 + function requestAnimationFrameFn(callback: () => any) {
  120 + // Prevent waiting too long without executing the callback
  121 + // Set the maximum waiting time
86 useTimeout(() => { 122 useTimeout(() => {
87 - init();  
88 - }, timeout);  
89 - }  
90 -  
91 - function init() {  
92 - // At this point, the skeleton component is about to be switched  
93 - emit('before-init');  
94 - // At this point you can prepare to load the resources of the lazy-loaded component  
95 - state.loading = true;  
96 -  
97 - requestAnimationFrameFn(() => {  
98 - state.isInit = true;  
99 - emit('init');  
100 - });  
101 - }  
102 - function requestAnimationFrameFn(callback: () => any) {  
103 - // Prevent waiting too long without executing the callback  
104 - // Set the maximum waiting time  
105 - useTimeout(() => {  
106 - if (state.isInit) {  
107 - return;  
108 - }  
109 - callback();  
110 - }, props.maxWaitingTime || 80); 123 + if (state.isInit) {
  124 + return;
  125 + }
  126 + callback();
  127 + }, props.maxWaitingTime || 80);
111 128
112 - const { requestAnimationFrame } = useRaf(); 129 + const { requestAnimationFrame } = useRaf();
113 130
114 - return requestAnimationFrame;  
115 - }  
116 - function initIntersectionObserver() {  
117 - const { timeout, direction, threshold, viewport } = props;  
118 - if (timeout) {  
119 - return; 131 + return requestAnimationFrame;
120 } 132 }
121 - // According to the scrolling direction to construct the viewport margin, used to load in advance  
122 - let rootMargin;  
123 - switch (direction) {  
124 - case 'vertical':  
125 - rootMargin = `${threshold} 0px`;  
126 - break;  
127 - case 'horizontal':  
128 - rootMargin = `0px ${threshold}`;  
129 - break;  
130 - }  
131 - try {  
132 - // Observe the intersection of the viewport and the component container  
133 - state.intersectionObserverInstance = new window.IntersectionObserver(intersectionHandler, {  
134 - rootMargin,  
135 - root: viewport,  
136 - threshold: [0, Number.MIN_VALUE, 0.01],  
137 - });  
138 133
139 - const el = unref(elRef); 134 + function initIntersectionObserver() {
  135 + const { timeout, direction, threshold, viewport } = props;
  136 + if (timeout) {
  137 + return;
  138 + }
  139 + // According to the scrolling direction to construct the viewport margin, used to load in advance
  140 + let rootMargin;
  141 + switch (direction) {
  142 + case 'vertical':
  143 + rootMargin = `${threshold} 0px`;
  144 + break;
  145 + case 'horizontal':
  146 + rootMargin = `0px ${threshold}`;
  147 + break;
  148 + }
  149 + try {
  150 + // Observe the intersection of the viewport and the component container
  151 + state.intersectionObserverInstance = new window.IntersectionObserver(
  152 + intersectionHandler,
  153 + {
  154 + rootMargin,
  155 + root: viewport,
  156 + threshold: [0, Number.MIN_VALUE, 0.01],
  157 + }
  158 + );
140 159
141 - state.intersectionObserverInstance.observe(el.$el);  
142 - } catch (e) {  
143 - init();  
144 - }  
145 - }  
146 - // Cross-condition change handling function  
147 - function intersectionHandler(entries: any[]) {  
148 - const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;  
149 - if (isIntersecting) {  
150 - init();  
151 - if (state.intersectionObserverInstance) {  
152 const el = unref(elRef); 160 const el = unref(elRef);
153 - state.intersectionObserverInstance.unobserve(el.$el);  
154 - }  
155 - }  
156 - // else {  
157 - // const { autoDestory } = props;  
158 - // autoDestory && destory();  
159 - // }  
160 - }  
161 - // function destory() {  
162 - // emit('beforeDestory');  
163 - // state.loading = false;  
164 - // nextTick(() => {  
165 - // emit('destory');  
166 - // });  
167 - // }  
168 -  
169 - immediateInit();  
170 - onMounted(() => {  
171 - initIntersectionObserver();  
172 - });  
173 - onUnmounted(() => {  
174 - // Cancel the observation before the component is destroyed  
175 - if (state.intersectionObserverInstance) {  
176 - const el = unref(elRef);  
177 - state.intersectionObserverInstance.unobserve(el.$el);  
178 - }  
179 - });  
180 161
181 - function renderContent() {  
182 - const { isInit, loading } = state;  
183 - if (isInit) {  
184 - return <div key="component">{getSlot(slots, 'default', { loading })}</div>; 162 + state.intersectionObserverInstance.observe(el.$el);
  163 + } catch (e) {
  164 + init();
  165 + }
185 } 166 }
186 - if (slots.skeleton) {  
187 - return <div key="skeleton">{getSlot(slots, 'skeleton') || <Skeleton />}</div>; 167 + // Cross-condition change handling function
  168 + function intersectionHandler(entries: any[]) {
  169 + const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
  170 + if (isIntersecting) {
  171 + init();
  172 + if (state.intersectionObserverInstance) {
  173 + const el = unref(elRef);
  174 + state.intersectionObserverInstance.unobserve(el.$el);
  175 + }
  176 + }
188 } 177 }
189 - return null;  
190 - }  
191 - return () => {  
192 - const { tag, transitionName } = props;  
193 - return (  
194 - <TransitionGroup ref={elRef} name={transitionName} tag={tag} {...getListeners(attrs)}>  
195 - {() => renderContent()}  
196 - </TransitionGroup>  
197 - );  
198 - };  
199 - },  
200 -}); 178 + return {
  179 + elRef,
  180 + ...toRefs(state),
  181 + };
  182 + },
  183 + });
  184 +</script>
  185 +<style lang="less">
  186 + .lazy-container-enter {
  187 + opacity: 0;
  188 + }
  189 +
  190 + .lazy-container-enter-to {
  191 + opacity: 1;
  192 + }
  193 +
  194 + .lazy-container-enter-from,
  195 + .lazy-container-enter-active {
  196 + position: absolute;
  197 + top: 0;
  198 + width: 100%;
  199 + transition: opacity 0.3s 0.2s;
  200 + }
  201 +
  202 + .lazy-container-leave {
  203 + opacity: 1;
  204 + }
  205 +
  206 + .lazy-container-leave-to {
  207 + opacity: 0;
  208 + }
  209 +
  210 + .lazy-container-leave-active {
  211 + transition: opacity 0.5s;
  212 + }
  213 +</style>
src/router/menus/modules/demo/comp.ts
@@ -49,6 +49,10 @@ const menu: MenuModule = { @@ -49,6 +49,10 @@ const menu: MenuModule = {
49 name: '详情组件', 49 name: '详情组件',
50 }, 50 },
51 { 51 {
  52 + path: 'lazy',
  53 + name: '懒加载组件',
  54 + },
  55 + {
52 path: 'verify', 56 path: 'verify',
53 name: '验证组件', 57 name: '验证组件',
54 children: [ 58 children: [
src/router/routes/modules/demo/comp.ts
@@ -99,7 +99,14 @@ export default { @@ -99,7 +99,14 @@ export default {
99 title: '详情组件', 99 title: '详情组件',
100 }, 100 },
101 }, 101 },
102 - 102 + {
  103 + path: '/lazy',
  104 + name: 'lazyDemo',
  105 + component: () => import('/@/views/demo/comp/lazy/index.vue'),
  106 + meta: {
  107 + title: '懒加载组件',
  108 + },
  109 + },
103 { 110 {
104 path: '/verify', 111 path: '/verify',
105 name: 'VerifyDemo', 112 name: 'VerifyDemo',
src/views/demo/comp/lazy/TargetContent.vue 0 → 100644
  1 +<template>
  2 + <Card hoverable :style="{ width: '240px', background: '#fff' }">
  3 + <template #cover>
  4 + <img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />
  5 + </template>
  6 + <CardMeta title="懒加载组件" />
  7 + </Card>
  8 +</template>
  9 +<script lang="ts">
  10 + import { defineComponent } from 'vue';
  11 + import { Card } from 'ant-design-vue';
  12 +
  13 + export default defineComponent({
  14 + components: { CardMeta: Card.Meta, Card },
  15 + setup() {
  16 + return {};
  17 + },
  18 + });
  19 +</script>
src/views/demo/comp/lazy/index.vue 0 → 100644
  1 +<template>
  2 + <div class="p-4 lazy-base-demo">
  3 + <Alert message="基础示例" description="向下滚动到可见区域才会加载组件" type="info" show-icon />
  4 + <div class="lazy-base-demo-wrap">
  5 + <h1>向下滚动</h1>
  6 + <LazyContainer @init="() => {}">
  7 + <TargetContent />
  8 + <template #skeleton>
  9 + <Skeleton :rows="10" />
  10 + </template>
  11 + </LazyContainer>
  12 + </div>
  13 + </div>
  14 +</template>
  15 +<script lang="ts">
  16 + import { defineComponent } from 'vue';
  17 + import { Skeleton, Alert } from 'ant-design-vue';
  18 + import TargetContent from './TargetContent.vue';
  19 + import { LazyContainer } from '/@/components/Container/index';
  20 + export default defineComponent({
  21 + components: { LazyContainer, TargetContent, Skeleton, Alert },
  22 + setup() {
  23 + return {};
  24 + },
  25 + });
  26 +</script>
  27 +<style lang="less" scoped>
  28 + .lazy-base-demo {
  29 + &-wrap {
  30 + display: flex;
  31 + width: 50%;
  32 + height: 2000px;
  33 + margin: 20px auto;
  34 + text-align: center;
  35 + background: #fff;
  36 + justify-content: center;
  37 + flex-direction: column;
  38 + align-items: center;
  39 + }
  40 +
  41 + h1 {
  42 + height: 1300px;
  43 + margin: 20px 0;
  44 + }
  45 + }
  46 +</style>