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 | 1 | export { default as ScrollContainer } from './src/ScrollContainer.vue'; |
2 | 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 | 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 | 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 | 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
src/router/routes/modules/demo/comp.ts
... | ... | @@ -99,7 +99,14 @@ export default { |
99 | 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 | 111 | path: '/verify', |
105 | 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> | ... | ... |