Commit fdeaa00bf24b0710ca341fafba8327c786ab9879
1 parent
a0c31974
feat: add lazyContainer comp and demo
Showing
8 changed files
with
277 additions
and
214 deletions
CHANGELOG.zh_CN.md
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> |