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 | <script lang="ts"> |
13 | 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 | 26 | import { Skeleton } from 'ant-design-vue'; |
18 | 27 | import { useRaf } from '/@/hooks/event/useRaf'; |
19 | 28 | import { useTimeout } from '/@/hooks/core/useTimeout'; |
20 | - | |
29 | + import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver'; | |
21 | 30 | interface State { |
22 | 31 | isInit: boolean; |
23 | 32 | loading: boolean; |
... | ... | @@ -30,7 +39,7 @@ |
30 | 39 | // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 |
31 | 40 | timeout: { |
32 | 41 | type: Number as PropType<number>, |
33 | - default: 8000, | |
42 | + default: 0, | |
34 | 43 | // default: 8000, |
35 | 44 | }, |
36 | 45 | // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 |
... | ... | @@ -40,6 +49,7 @@ |
40 | 49 | >, |
41 | 50 | default: () => null, |
42 | 51 | }, |
52 | + | |
43 | 53 | // 预加载阈值, css单位 |
44 | 54 | threshold: { |
45 | 55 | type: String as PropType<string>, |
... | ... | @@ -51,6 +61,7 @@ |
51 | 61 | type: String as PropType<'vertical' | 'horizontal'>, |
52 | 62 | default: 'vertical', |
53 | 63 | }, |
64 | + | |
54 | 65 | // 包裹组件的外层容器的标签名 |
55 | 66 | tag: { |
56 | 67 | type: String as PropType<string>, |
... | ... | @@ -62,20 +73,14 @@ |
62 | 73 | default: 80, |
63 | 74 | }, |
64 | 75 | |
65 | - // // 是否在不可见的时候销毁 | |
66 | - // autoDestory: { | |
67 | - // type: Boolean as PropType<boolean>, | |
68 | - // default: false, | |
69 | - // }, | |
70 | - | |
71 | 76 | // transition name |
72 | 77 | transitionName: { |
73 | 78 | type: String as PropType<string>, |
74 | 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 | 84 | const elRef = ref<any>(null); |
80 | 85 | const state = reactive<State>({ |
81 | 86 | isInit: false, |
... | ... | @@ -83,17 +88,10 @@ |
83 | 88 | intersectionObserverInstance: null, |
84 | 89 | }); |
85 | 90 | |
86 | - immediateInit(); | |
87 | 91 | onMounted(() => { |
92 | + immediateInit(); | |
88 | 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 | 96 | // If there is a set delay time, it will be executed immediately |
99 | 97 | function immediateInit() { |
... | ... | @@ -105,9 +103,6 @@ |
105 | 103 | } |
106 | 104 | |
107 | 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 | 106 | state.loading = true; |
112 | 107 | |
113 | 108 | requestAnimationFrameFn(() => { |
... | ... | @@ -120,9 +115,7 @@ |
120 | 115 | // Prevent waiting too long without executing the callback |
121 | 116 | // Set the maximum waiting time |
122 | 117 | useTimeout(() => { |
123 | - if (state.isInit) { | |
124 | - return; | |
125 | - } | |
118 | + if (state.isInit) return; | |
126 | 119 | callback(); |
127 | 120 | }, props.maxWaitingTime || 80); |
128 | 121 | |
... | ... | @@ -132,12 +125,10 @@ |
132 | 125 | } |
133 | 126 | |
134 | 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 | 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 | 132 | switch (direction) { |
142 | 133 | case 'vertical': |
143 | 134 | rootMargin = `${threshold} 0px`; |
... | ... | @@ -146,35 +137,26 @@ |
146 | 137 | rootMargin = `0px ${threshold}`; |
147 | 138 | break; |
148 | 139 | } |
140 | + | |
149 | 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 | 156 | } catch (e) { |
164 | 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 | 160 | return { |
179 | 161 | elRef, |
180 | 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 | 19 | import { |
7 | 20 | // throttle, |
8 | 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 | 19 | import { isFunction } from '/@/utils/is'; |
8 | 20 | export function throttle<T extends unknown[]>( | ... | ... |
src/hooks/core/useTimeout.ts
1 | -import type { TimeoutFnResult, Fn } from './types'; | |
2 | - | |
3 | 1 | import { isFunction } from '/@/utils/is'; |
4 | -import { watch } from 'vue'; | |
2 | +import { Ref, watch } from 'vue'; | |
5 | 3 | |
6 | 4 | import { useTimeoutRef } from '/@/hooks/core/useTimeoutRef'; |
7 | 5 | |
6 | +type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>]; | |
7 | + | |
8 | 8 | export function useTimeout(handle: Fn<any>, wait: number): TimeoutFnResult { |
9 | 9 | if (!isFunction(handle)) { |
10 | 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 | 2 | import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; |
3 | +export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>]; | |
5 | 4 | export function useTimeoutRef(wait: number): TimeoutResult { |
6 | 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
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 | 1 | import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal'; |
3 | 2 | |
4 | 3 | import { Modal, message as Message, notification } from 'ant-design-vue'; |
... | ... | @@ -6,6 +5,11 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-des |
6 | 5 | |
7 | 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 | 13 | interface ConfirmOptions { |
10 | 14 | info: ModalFunc; |
11 | 15 | success: ModalFunc; | ... | ... |