Commit 87fcd0d21ea78ce916a4f2b9cdcceda5e7866eee
1 parent
35d2bfc5
perf: optimize lazy loading components
Showing
13 changed files
with
180 additions
and
166 deletions
src/components/Container/src/LazyContainer.vue
@@ -12,12 +12,21 @@ | @@ -12,12 +12,21 @@ | ||
12 | <script lang="ts"> | 12 | <script lang="ts"> |
13 | import type { PropType } from 'vue'; | 13 | import type { PropType } from 'vue'; |
14 | 14 | ||
15 | - import { defineComponent, reactive, onMounted, ref, unref, onUnmounted, toRefs } from 'vue'; | 15 | + import { |
16 | + defineComponent, | ||
17 | + reactive, | ||
18 | + onMounted, | ||
19 | + ref, | ||
20 | + unref, | ||
21 | + onUnmounted, | ||
22 | + toRef, | ||
23 | + toRefs, | ||
24 | + } from 'vue'; | ||
16 | 25 | ||
17 | import { Skeleton } from 'ant-design-vue'; | 26 | import { Skeleton } from 'ant-design-vue'; |
18 | import { useRaf } from '/@/hooks/event/useRaf'; | 27 | import { useRaf } from '/@/hooks/event/useRaf'; |
19 | import { useTimeout } from '/@/hooks/core/useTimeout'; | 28 | import { useTimeout } from '/@/hooks/core/useTimeout'; |
20 | - | 29 | + import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver'; |
21 | interface State { | 30 | interface State { |
22 | isInit: boolean; | 31 | isInit: boolean; |
23 | loading: boolean; | 32 | loading: boolean; |
@@ -30,7 +39,7 @@ | @@ -30,7 +39,7 @@ | ||
30 | // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 | 39 | // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 |
31 | timeout: { | 40 | timeout: { |
32 | type: Number as PropType<number>, | 41 | type: Number as PropType<number>, |
33 | - default: 8000, | 42 | + default: 0, |
34 | // default: 8000, | 43 | // default: 8000, |
35 | }, | 44 | }, |
36 | // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 | 45 | // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 |
@@ -40,6 +49,7 @@ | @@ -40,6 +49,7 @@ | ||
40 | >, | 49 | >, |
41 | default: () => null, | 50 | default: () => null, |
42 | }, | 51 | }, |
52 | + | ||
43 | // 预加载阈值, css单位 | 53 | // 预加载阈值, css单位 |
44 | threshold: { | 54 | threshold: { |
45 | type: String as PropType<string>, | 55 | type: String as PropType<string>, |
@@ -51,6 +61,7 @@ | @@ -51,6 +61,7 @@ | ||
51 | type: String as PropType<'vertical' | 'horizontal'>, | 61 | type: String as PropType<'vertical' | 'horizontal'>, |
52 | default: 'vertical', | 62 | default: 'vertical', |
53 | }, | 63 | }, |
64 | + | ||
54 | // 包裹组件的外层容器的标签名 | 65 | // 包裹组件的外层容器的标签名 |
55 | tag: { | 66 | tag: { |
56 | type: String as PropType<string>, | 67 | type: String as PropType<string>, |
@@ -62,20 +73,14 @@ | @@ -62,20 +73,14 @@ | ||
62 | default: 80, | 73 | default: 80, |
63 | }, | 74 | }, |
64 | 75 | ||
65 | - // // 是否在不可见的时候销毁 | ||
66 | - // autoDestory: { | ||
67 | - // type: Boolean as PropType<boolean>, | ||
68 | - // default: false, | ||
69 | - // }, | ||
70 | - | ||
71 | // transition name | 76 | // transition name |
72 | transitionName: { | 77 | transitionName: { |
73 | type: String as PropType<string>, | 78 | type: String as PropType<string>, |
74 | default: 'lazy-container', | 79 | default: 'lazy-container', |
75 | }, | 80 | }, |
76 | }, | 81 | }, |
77 | - emits: ['before-init', 'init'], | ||
78 | - setup(props, { emit, slots }) { | 82 | + emits: ['init'], |
83 | + setup(props, { emit }) { | ||
79 | const elRef = ref<any>(null); | 84 | const elRef = ref<any>(null); |
80 | const state = reactive<State>({ | 85 | const state = reactive<State>({ |
81 | isInit: false, | 86 | isInit: false, |
@@ -83,17 +88,10 @@ | @@ -83,17 +88,10 @@ | ||
83 | intersectionObserverInstance: null, | 88 | intersectionObserverInstance: null, |
84 | }); | 89 | }); |
85 | 90 | ||
86 | - immediateInit(); | ||
87 | onMounted(() => { | 91 | onMounted(() => { |
92 | + immediateInit(); | ||
88 | initIntersectionObserver(); | 93 | initIntersectionObserver(); |
89 | }); | 94 | }); |
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 | - }); | ||
97 | 95 | ||
98 | // If there is a set delay time, it will be executed immediately | 96 | // If there is a set delay time, it will be executed immediately |
99 | function immediateInit() { | 97 | function immediateInit() { |
@@ -105,9 +103,6 @@ | @@ -105,9 +103,6 @@ | ||
105 | } | 103 | } |
106 | 104 | ||
107 | function init() { | 105 | 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; | 106 | state.loading = true; |
112 | 107 | ||
113 | requestAnimationFrameFn(() => { | 108 | requestAnimationFrameFn(() => { |
@@ -120,9 +115,7 @@ | @@ -120,9 +115,7 @@ | ||
120 | // Prevent waiting too long without executing the callback | 115 | // Prevent waiting too long without executing the callback |
121 | // Set the maximum waiting time | 116 | // Set the maximum waiting time |
122 | useTimeout(() => { | 117 | useTimeout(() => { |
123 | - if (state.isInit) { | ||
124 | - return; | ||
125 | - } | 118 | + if (state.isInit) return; |
126 | callback(); | 119 | callback(); |
127 | }, props.maxWaitingTime || 80); | 120 | }, props.maxWaitingTime || 80); |
128 | 121 | ||
@@ -132,12 +125,10 @@ | @@ -132,12 +125,10 @@ | ||
132 | } | 125 | } |
133 | 126 | ||
134 | function initIntersectionObserver() { | 127 | function initIntersectionObserver() { |
135 | - const { timeout, direction, threshold, viewport } = props; | ||
136 | - if (timeout) { | ||
137 | - return; | ||
138 | - } | 128 | + const { timeout, direction, threshold } = props; |
129 | + if (timeout) return; | ||
139 | // According to the scrolling direction to construct the viewport margin, used to load in advance | 130 | // According to the scrolling direction to construct the viewport margin, used to load in advance |
140 | - let rootMargin; | 131 | + let rootMargin: string = '0px'; |
141 | switch (direction) { | 132 | switch (direction) { |
142 | case 'vertical': | 133 | case 'vertical': |
143 | rootMargin = `${threshold} 0px`; | 134 | rootMargin = `${threshold} 0px`; |
@@ -146,35 +137,26 @@ | @@ -146,35 +137,26 @@ | ||
146 | rootMargin = `0px ${threshold}`; | 137 | rootMargin = `0px ${threshold}`; |
147 | break; | 138 | break; |
148 | } | 139 | } |
140 | + | ||
149 | try { | 141 | 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 | - ); | ||
159 | - | ||
160 | - const el = unref(elRef); | ||
161 | - | ||
162 | - state.intersectionObserverInstance.observe(el.$el); | 142 | + const { stop, observer } = useIntersectionObserver({ |
143 | + rootMargin, | ||
144 | + target: toRef(elRef.value, '$el'), | ||
145 | + onIntersect: (entries: any[]) => { | ||
146 | + const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio; | ||
147 | + if (isIntersecting) { | ||
148 | + init(); | ||
149 | + if (observer) { | ||
150 | + stop(); | ||
151 | + } | ||
152 | + } | ||
153 | + }, | ||
154 | + root: toRef(props, 'viewport'), | ||
155 | + }); | ||
163 | } catch (e) { | 156 | } catch (e) { |
164 | init(); | 157 | init(); |
165 | } | 158 | } |
166 | } | 159 | } |
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 | - } | ||
177 | - } | ||
178 | return { | 160 | return { |
179 | elRef, | 161 | elRef, |
180 | ...toRefs(state), | 162 | ...toRefs(state), |
src/hooks/core/types.ts deleted
100644 → 0
1 | -import type { VNode, Ref } from 'vue'; | ||
2 | -import type { ModalFuncProps } from 'ant-design-vue/lib/modal/index'; | ||
3 | - | ||
4 | -export type Fn<T> = () => T; | ||
5 | -export type AnyFn<T> = (...arg: any) => T; | ||
6 | -export type PromiseFn<T> = (...arg: any) => Promise<T>; | ||
7 | -export type CancelFn = () => void; | ||
8 | -export interface DebounceAndThrottleOptions { | ||
9 | - // 立即执行 | ||
10 | - immediate?: boolean; | ||
11 | - | ||
12 | - // 是否为debounce | ||
13 | - debounce?: boolean; | ||
14 | - // 只执行一次 | ||
15 | - once?: boolean; | ||
16 | -} | ||
17 | - | ||
18 | -export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown; | ||
19 | - | ||
20 | -export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [ | ||
21 | - DebounceAndThrottleProcedure<T>, | ||
22 | - CancelFn | ||
23 | -]; | ||
24 | - | ||
25 | -export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>]; | ||
26 | - | ||
27 | -export type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>]; | ||
28 | - | ||
29 | -export interface PromiseState { | ||
30 | - loading: boolean; | ||
31 | - error: Error | null; | ||
32 | - result: any; | ||
33 | - done: boolean; | ||
34 | -} | ||
35 | -export type MessageType = 'success' | 'warning' | 'info' | 'error'; | ||
36 | - | ||
37 | -export interface CloseEventHandler { | ||
38 | - /** | ||
39 | - * Triggers when a message is being closed | ||
40 | - * | ||
41 | - * @param instance The message component that is being closed | ||
42 | - */ | ||
43 | - (instance: MessageComponent): void; | ||
44 | -} | ||
45 | - | ||
46 | -/** Message Component */ | ||
47 | -export declare class MessageComponent { | ||
48 | - /** Close the Loading instance */ | ||
49 | - close(): void; | ||
50 | -} | ||
51 | - | ||
52 | -export type MessageMethods = { | ||
53 | - [key in MessageType]?: (options: MessageOptions | string) => MessageComponent; // Note that "key in". | ||
54 | -}; | ||
55 | - | ||
56 | -/** Options used in Message */ | ||
57 | -export interface MessageOptions { | ||
58 | - title: string; | ||
59 | - /** Message text */ | ||
60 | - message: string | VNode; | ||
61 | - | ||
62 | - /** Message type */ | ||
63 | - type?: MessageType; | ||
64 | - | ||
65 | - /** Custom icon's class, overrides type */ | ||
66 | - iconClass?: string; | ||
67 | - | ||
68 | - /** Custom class name for Message */ | ||
69 | - customClass?: string; | ||
70 | - | ||
71 | - /** Display duration, millisecond. If set to 0, it will not turn off automatically */ | ||
72 | - duration?: number; | ||
73 | - | ||
74 | - /** Whether to show a close button */ | ||
75 | - showClose?: boolean; | ||
76 | - | ||
77 | - /** Whether to center the text */ | ||
78 | - center?: boolean; | ||
79 | - | ||
80 | - /** Whether message is treated as HTML string */ | ||
81 | - dangerouslyUseHTMLString?: boolean; | ||
82 | - | ||
83 | - /** Callback function when closed with the message instance as the parameter */ | ||
84 | - onClose?: CloseEventHandler; | ||
85 | - | ||
86 | - /** Set the distance to the top of viewport. Default is 20 px. */ | ||
87 | - offset?: number; | ||
88 | -} | ||
89 | -export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> { | ||
90 | - iconType: 'warning' | 'success' | 'error' | 'info'; | ||
91 | -} | ||
92 | -export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>; |
src/hooks/core/useCounter.ts
0 → 100644
1 | +import { ref } from 'vue'; | ||
2 | + | ||
3 | +export function useCounter(initialValue = 0) { | ||
4 | + const count = ref(initialValue); | ||
5 | + | ||
6 | + const inc = (delta = 1) => (count.value += delta); | ||
7 | + const dec = (delta = 1) => (count.value -= delta); | ||
8 | + const get = () => count.value; | ||
9 | + const set = (val: number) => (count.value = val); | ||
10 | + const reset = (val = initialValue) => { | ||
11 | + initialValue = val; | ||
12 | + return set(val); | ||
13 | + }; | ||
14 | + | ||
15 | + return { count, inc, dec, get, set, reset }; | ||
16 | +} |
src/hooks/core/useDebounce.ts
1 | -import type { | ||
2 | - DebounceAndThrottleOptions, | ||
3 | - DebounceAndThrottleProcedureResult, | ||
4 | - DebounceAndThrottleProcedure, | ||
5 | -} from './types'; | 1 | +export interface DebounceAndThrottleOptions { |
2 | + // 立即执行 | ||
3 | + immediate?: boolean; | ||
4 | + | ||
5 | + // 是否为debounce | ||
6 | + debounce?: boolean; | ||
7 | + // 只执行一次 | ||
8 | + once?: boolean; | ||
9 | +} | ||
10 | +export type CancelFn = () => void; | ||
11 | + | ||
12 | +export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown; | ||
13 | + | ||
14 | +export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [ | ||
15 | + DebounceAndThrottleProcedure<T>, | ||
16 | + CancelFn | ||
17 | +]; | ||
18 | + | ||
6 | import { | 19 | import { |
7 | // throttle, | 20 | // throttle, |
8 | useThrottle, | 21 | useThrottle, |
src/hooks/core/useThrottle.ts
1 | -import type { | ||
2 | - DebounceAndThrottleOptions, | ||
3 | - DebounceAndThrottleProcedureResult, | ||
4 | - DebounceAndThrottleProcedure, | ||
5 | -} from './types'; | 1 | +export interface DebounceAndThrottleOptions { |
2 | + // 立即执行 | ||
3 | + immediate?: boolean; | ||
4 | + | ||
5 | + // 是否为debounce | ||
6 | + debounce?: boolean; | ||
7 | + // 只执行一次 | ||
8 | + once?: boolean; | ||
9 | +} | ||
10 | +export type CancelFn = () => void; | ||
11 | + | ||
12 | +export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown; | ||
13 | + | ||
14 | +export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [ | ||
15 | + DebounceAndThrottleProcedure<T>, | ||
16 | + CancelFn | ||
17 | +]; | ||
6 | 18 | ||
7 | import { isFunction } from '/@/utils/is'; | 19 | import { isFunction } from '/@/utils/is'; |
8 | export function throttle<T extends unknown[]>( | 20 | export function throttle<T extends unknown[]>( |
src/hooks/core/useTimeout.ts
1 | -import type { TimeoutFnResult, Fn } from './types'; | ||
2 | - | ||
3 | import { isFunction } from '/@/utils/is'; | 1 | import { isFunction } from '/@/utils/is'; |
4 | -import { watch } from 'vue'; | 2 | +import { Ref, watch } from 'vue'; |
5 | 3 | ||
6 | import { useTimeoutRef } from '/@/hooks/core/useTimeoutRef'; | 4 | import { useTimeoutRef } from '/@/hooks/core/useTimeoutRef'; |
7 | 5 | ||
6 | +type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>]; | ||
7 | + | ||
8 | export function useTimeout(handle: Fn<any>, wait: number): TimeoutFnResult { | 8 | export function useTimeout(handle: Fn<any>, wait: number): TimeoutFnResult { |
9 | if (!isFunction(handle)) { | 9 | if (!isFunction(handle)) { |
10 | throw new Error('handle is not Function!'); | 10 | throw new Error('handle is not Function!'); |
src/hooks/core/useTimeoutRef.ts
1 | -import type { TimeoutResult } from './types'; | ||
2 | - | ||
3 | -import { ref } from 'vue'; | 1 | +import { Ref, ref } from 'vue'; |
4 | import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; | 2 | import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; |
3 | +export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>]; | ||
5 | export function useTimeoutRef(wait: number): TimeoutResult { | 4 | export function useTimeoutRef(wait: number): TimeoutResult { |
6 | const readyRef = ref(false); | 5 | const readyRef = ref(false); |
7 | 6 |
src/hooks/event/types.ts deleted
100644 → 0
1 | -export type Fn<T> = () => T; |
src/hooks/event/useEventHub.ts
1 | import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; | 1 | import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; |
2 | -import {} from 'vue'; | ||
3 | import EventHub from '/@/utils/eventHub'; | 2 | import EventHub from '/@/utils/eventHub'; |
4 | const eventHub = new EventHub(); | 3 | const eventHub = new EventHub(); |
5 | export function useEventHub(): EventHub { | 4 | export function useEventHub(): EventHub { |
src/hooks/event/useIntersectionObserver.ts
0 → 100644
1 | +import { Ref, watchEffect, ref } from 'vue'; | ||
2 | + | ||
3 | +interface IntersectionObserverProps { | ||
4 | + target: Ref<Element | null | undefined>; | ||
5 | + root?: Ref<Element | null | undefined>; | ||
6 | + onIntersect: IntersectionObserverCallback; | ||
7 | + rootMargin?: string; | ||
8 | + threshold?: number; | ||
9 | +} | ||
10 | + | ||
11 | +export function useIntersectionObserver({ | ||
12 | + target, | ||
13 | + root, | ||
14 | + onIntersect, | ||
15 | + rootMargin = '0px', | ||
16 | + threshold = 0.1, | ||
17 | +}: IntersectionObserverProps) { | ||
18 | + let cleanup = () => {}; | ||
19 | + const observer: Ref<Nullable<IntersectionObserver>> = ref(null); | ||
20 | + const stopEffect = watchEffect(() => { | ||
21 | + cleanup(); | ||
22 | + | ||
23 | + observer.value = new IntersectionObserver(onIntersect, { | ||
24 | + root: root ? root.value : null, | ||
25 | + rootMargin, | ||
26 | + threshold, | ||
27 | + }); | ||
28 | + | ||
29 | + const current = target.value; | ||
30 | + | ||
31 | + current && observer.value.observe(current); | ||
32 | + | ||
33 | + cleanup = () => { | ||
34 | + if (observer.value) { | ||
35 | + observer.value.disconnect(); | ||
36 | + target.value && observer.value.unobserve(target.value); | ||
37 | + } | ||
38 | + }; | ||
39 | + }); | ||
40 | + | ||
41 | + return { | ||
42 | + observer, | ||
43 | + stop: () => { | ||
44 | + cleanup(); | ||
45 | + stopEffect(); | ||
46 | + }, | ||
47 | + }; | ||
48 | +} |
src/hooks/event/useNow.ts
0 → 100644
1 | +import { ref } from 'vue'; | ||
2 | +import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; | ||
3 | + | ||
4 | +function getTimestamp() { | ||
5 | + return +Date.now(); | ||
6 | +} | ||
7 | + | ||
8 | +export function useNow() { | ||
9 | + const now = ref(getTimestamp()); | ||
10 | + let started = false; | ||
11 | + | ||
12 | + const update = () => { | ||
13 | + requestAnimationFrame(() => { | ||
14 | + now.value = getTimestamp(); | ||
15 | + if (started) update(); | ||
16 | + }); | ||
17 | + }; | ||
18 | + | ||
19 | + const start = () => { | ||
20 | + if (!started) { | ||
21 | + started = true; | ||
22 | + update(); | ||
23 | + } | ||
24 | + }; | ||
25 | + | ||
26 | + const stop = () => { | ||
27 | + started = false; | ||
28 | + }; | ||
29 | + | ||
30 | + start(); | ||
31 | + | ||
32 | + tryOnUnmounted(stop); | ||
33 | + | ||
34 | + return now; | ||
35 | +} |
src/hooks/event/useRaf.ts
src/hooks/web/useMessage.tsx
1 | -import type { ModalOptionsEx, ModalOptionsPartial } from '/@/hooks/core/types'; | ||
2 | import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal'; | 1 | import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal'; |
3 | 2 | ||
4 | import { Modal, message as Message, notification } from 'ant-design-vue'; | 3 | import { Modal, message as Message, notification } from 'ant-design-vue'; |
@@ -6,6 +5,11 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-des | @@ -6,6 +5,11 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-des | ||
6 | 5 | ||
7 | import { useSetting } from '/@/hooks/core/useSetting'; | 6 | import { useSetting } from '/@/hooks/core/useSetting'; |
8 | 7 | ||
8 | +export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> { | ||
9 | + iconType: 'warning' | 'success' | 'error' | 'info'; | ||
10 | +} | ||
11 | +export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>; | ||
12 | + | ||
9 | interface ConfirmOptions { | 13 | interface ConfirmOptions { |
10 | info: ModalFunc; | 14 | info: ModalFunc; |
11 | success: ModalFunc; | 15 | success: ModalFunc; |