Commit 87fcd0d21ea78ce916a4f2b9cdcceda5e7866eee

Authored by vben
1 parent 35d2bfc5

perf: optimize lazy loading components

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
@@ -49,7 +49,6 @@ if (isServer) { @@ -49,7 +49,6 @@ if (isServer) {
49 }; 49 };
50 } 50 }
51 } 51 }
52 -  
53 export function useRaf() { 52 export function useRaf() {
54 // if (getCurrentInstance()) { 53 // if (getCurrentInstance()) {
55 // onUnmounted(() => { 54 // onUnmounted(() => {
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 &#39;@ant-des @@ -6,6 +5,11 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from &#39;@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;