mirror of
https://github.com/vbenjs/gf-vben-admin.git
synced 2025-01-24 04:10:20 +08:00
perf: optimize lazy loading components
This commit is contained in:
parent
35d2bfc562
commit
87fcd0d21e
@ -12,12 +12,21 @@
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { defineComponent, reactive, onMounted, ref, unref, onUnmounted, toRefs } from 'vue';
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
onMounted,
|
||||
ref,
|
||||
unref,
|
||||
onUnmounted,
|
||||
toRef,
|
||||
toRefs,
|
||||
} from 'vue';
|
||||
|
||||
import { Skeleton } from 'ant-design-vue';
|
||||
import { useRaf } from '/@/hooks/event/useRaf';
|
||||
import { useTimeout } from '/@/hooks/core/useTimeout';
|
||||
|
||||
import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver';
|
||||
interface State {
|
||||
isInit: boolean;
|
||||
loading: boolean;
|
||||
@ -30,7 +39,7 @@
|
||||
// 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
|
||||
timeout: {
|
||||
type: Number as PropType<number>,
|
||||
default: 8000,
|
||||
default: 0,
|
||||
// default: 8000,
|
||||
},
|
||||
// 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
|
||||
@ -40,6 +49,7 @@
|
||||
>,
|
||||
default: () => null,
|
||||
},
|
||||
|
||||
// 预加载阈值, css单位
|
||||
threshold: {
|
||||
type: String as PropType<string>,
|
||||
@ -51,6 +61,7 @@
|
||||
type: String as PropType<'vertical' | 'horizontal'>,
|
||||
default: 'vertical',
|
||||
},
|
||||
|
||||
// 包裹组件的外层容器的标签名
|
||||
tag: {
|
||||
type: String as PropType<string>,
|
||||
@ -62,20 +73,14 @@
|
||||
default: 80,
|
||||
},
|
||||
|
||||
// // 是否在不可见的时候销毁
|
||||
// autoDestory: {
|
||||
// type: Boolean as PropType<boolean>,
|
||||
// default: false,
|
||||
// },
|
||||
|
||||
// transition name
|
||||
transitionName: {
|
||||
type: String as PropType<string>,
|
||||
default: 'lazy-container',
|
||||
},
|
||||
},
|
||||
emits: ['before-init', 'init'],
|
||||
setup(props, { emit, slots }) {
|
||||
emits: ['init'],
|
||||
setup(props, { emit }) {
|
||||
const elRef = ref<any>(null);
|
||||
const state = reactive<State>({
|
||||
isInit: false,
|
||||
@ -83,17 +88,10 @@
|
||||
intersectionObserverInstance: null,
|
||||
});
|
||||
|
||||
immediateInit();
|
||||
onMounted(() => {
|
||||
immediateInit();
|
||||
initIntersectionObserver();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
// Cancel the observation before the component is destroyed
|
||||
if (state.intersectionObserverInstance) {
|
||||
const el = unref(elRef);
|
||||
state.intersectionObserverInstance.unobserve(el.$el);
|
||||
}
|
||||
});
|
||||
|
||||
// If there is a set delay time, it will be executed immediately
|
||||
function immediateInit() {
|
||||
@ -105,9 +103,6 @@
|
||||
}
|
||||
|
||||
function init() {
|
||||
// At this point, the skeleton component is about to be switched
|
||||
emit('before-init');
|
||||
// At this point you can prepare to load the resources of the lazy-loaded component
|
||||
state.loading = true;
|
||||
|
||||
requestAnimationFrameFn(() => {
|
||||
@ -120,9 +115,7 @@
|
||||
// Prevent waiting too long without executing the callback
|
||||
// Set the maximum waiting time
|
||||
useTimeout(() => {
|
||||
if (state.isInit) {
|
||||
return;
|
||||
}
|
||||
if (state.isInit) return;
|
||||
callback();
|
||||
}, props.maxWaitingTime || 80);
|
||||
|
||||
@ -132,12 +125,10 @@
|
||||
}
|
||||
|
||||
function initIntersectionObserver() {
|
||||
const { timeout, direction, threshold, viewport } = props;
|
||||
if (timeout) {
|
||||
return;
|
||||
}
|
||||
const { timeout, direction, threshold } = props;
|
||||
if (timeout) return;
|
||||
// According to the scrolling direction to construct the viewport margin, used to load in advance
|
||||
let rootMargin;
|
||||
let rootMargin: string = '0px';
|
||||
switch (direction) {
|
||||
case 'vertical':
|
||||
rootMargin = `${threshold} 0px`;
|
||||
@ -146,35 +137,26 @@
|
||||
rootMargin = `0px ${threshold}`;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
// Observe the intersection of the viewport and the component container
|
||||
state.intersectionObserverInstance = new window.IntersectionObserver(
|
||||
intersectionHandler,
|
||||
{
|
||||
rootMargin,
|
||||
root: viewport,
|
||||
threshold: [0, Number.MIN_VALUE, 0.01],
|
||||
}
|
||||
);
|
||||
|
||||
const el = unref(elRef);
|
||||
|
||||
state.intersectionObserverInstance.observe(el.$el);
|
||||
const { stop, observer } = useIntersectionObserver({
|
||||
rootMargin,
|
||||
target: toRef(elRef.value, '$el'),
|
||||
onIntersect: (entries: any[]) => {
|
||||
const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
|
||||
if (isIntersecting) {
|
||||
init();
|
||||
if (observer) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
},
|
||||
root: toRef(props, 'viewport'),
|
||||
});
|
||||
} catch (e) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
// Cross-condition change handling function
|
||||
function intersectionHandler(entries: any[]) {
|
||||
const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
|
||||
if (isIntersecting) {
|
||||
init();
|
||||
if (state.intersectionObserverInstance) {
|
||||
const el = unref(elRef);
|
||||
state.intersectionObserverInstance.unobserve(el.$el);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
elRef,
|
||||
...toRefs(state),
|
||||
|
@ -1,92 +0,0 @@
|
||||
import type { VNode, Ref } from 'vue';
|
||||
import type { ModalFuncProps } from 'ant-design-vue/lib/modal/index';
|
||||
|
||||
export type Fn<T> = () => T;
|
||||
export type AnyFn<T> = (...arg: any) => T;
|
||||
export type PromiseFn<T> = (...arg: any) => Promise<T>;
|
||||
export type CancelFn = () => void;
|
||||
export interface DebounceAndThrottleOptions {
|
||||
// 立即执行
|
||||
immediate?: boolean;
|
||||
|
||||
// 是否为debounce
|
||||
debounce?: boolean;
|
||||
// 只执行一次
|
||||
once?: boolean;
|
||||
}
|
||||
|
||||
export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown;
|
||||
|
||||
export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
|
||||
DebounceAndThrottleProcedure<T>,
|
||||
CancelFn
|
||||
];
|
||||
|
||||
export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>];
|
||||
|
||||
export type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>];
|
||||
|
||||
export interface PromiseState {
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
result: any;
|
||||
done: boolean;
|
||||
}
|
||||
export type MessageType = 'success' | 'warning' | 'info' | 'error';
|
||||
|
||||
export interface CloseEventHandler {
|
||||
/**
|
||||
* Triggers when a message is being closed
|
||||
*
|
||||
* @param instance The message component that is being closed
|
||||
*/
|
||||
(instance: MessageComponent): void;
|
||||
}
|
||||
|
||||
/** Message Component */
|
||||
export declare class MessageComponent {
|
||||
/** Close the Loading instance */
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export type MessageMethods = {
|
||||
[key in MessageType]?: (options: MessageOptions | string) => MessageComponent; // Note that "key in".
|
||||
};
|
||||
|
||||
/** Options used in Message */
|
||||
export interface MessageOptions {
|
||||
title: string;
|
||||
/** Message text */
|
||||
message: string | VNode;
|
||||
|
||||
/** Message type */
|
||||
type?: MessageType;
|
||||
|
||||
/** Custom icon's class, overrides type */
|
||||
iconClass?: string;
|
||||
|
||||
/** Custom class name for Message */
|
||||
customClass?: string;
|
||||
|
||||
/** Display duration, millisecond. If set to 0, it will not turn off automatically */
|
||||
duration?: number;
|
||||
|
||||
/** Whether to show a close button */
|
||||
showClose?: boolean;
|
||||
|
||||
/** Whether to center the text */
|
||||
center?: boolean;
|
||||
|
||||
/** Whether message is treated as HTML string */
|
||||
dangerouslyUseHTMLString?: boolean;
|
||||
|
||||
/** Callback function when closed with the message instance as the parameter */
|
||||
onClose?: CloseEventHandler;
|
||||
|
||||
/** Set the distance to the top of viewport. Default is 20 px. */
|
||||
offset?: number;
|
||||
}
|
||||
export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
|
||||
iconType: 'warning' | 'success' | 'error' | 'info';
|
||||
}
|
||||
export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;
|
16
src/hooks/core/useCounter.ts
Normal file
16
src/hooks/core/useCounter.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export function useCounter(initialValue = 0) {
|
||||
const count = ref(initialValue);
|
||||
|
||||
const inc = (delta = 1) => (count.value += delta);
|
||||
const dec = (delta = 1) => (count.value -= delta);
|
||||
const get = () => count.value;
|
||||
const set = (val: number) => (count.value = val);
|
||||
const reset = (val = initialValue) => {
|
||||
initialValue = val;
|
||||
return set(val);
|
||||
};
|
||||
|
||||
return { count, inc, dec, get, set, reset };
|
||||
}
|
@ -1,8 +1,21 @@
|
||||
import type {
|
||||
DebounceAndThrottleOptions,
|
||||
DebounceAndThrottleProcedureResult,
|
||||
DebounceAndThrottleProcedure,
|
||||
} from './types';
|
||||
export interface DebounceAndThrottleOptions {
|
||||
// 立即执行
|
||||
immediate?: boolean;
|
||||
|
||||
// 是否为debounce
|
||||
debounce?: boolean;
|
||||
// 只执行一次
|
||||
once?: boolean;
|
||||
}
|
||||
export type CancelFn = () => void;
|
||||
|
||||
export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown;
|
||||
|
||||
export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
|
||||
DebounceAndThrottleProcedure<T>,
|
||||
CancelFn
|
||||
];
|
||||
|
||||
import {
|
||||
// throttle,
|
||||
useThrottle,
|
||||
|
@ -1,8 +1,20 @@
|
||||
import type {
|
||||
DebounceAndThrottleOptions,
|
||||
DebounceAndThrottleProcedureResult,
|
||||
DebounceAndThrottleProcedure,
|
||||
} from './types';
|
||||
export interface DebounceAndThrottleOptions {
|
||||
// 立即执行
|
||||
immediate?: boolean;
|
||||
|
||||
// 是否为debounce
|
||||
debounce?: boolean;
|
||||
// 只执行一次
|
||||
once?: boolean;
|
||||
}
|
||||
export type CancelFn = () => void;
|
||||
|
||||
export type DebounceAndThrottleProcedure<T extends unknown[]> = (...args: T) => unknown;
|
||||
|
||||
export type DebounceAndThrottleProcedureResult<T extends unknown[]> = [
|
||||
DebounceAndThrottleProcedure<T>,
|
||||
CancelFn
|
||||
];
|
||||
|
||||
import { isFunction } from '/@/utils/is';
|
||||
export function throttle<T extends unknown[]>(
|
||||
|
@ -1,10 +1,10 @@
|
||||
import type { TimeoutFnResult, Fn } from './types';
|
||||
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { watch } from 'vue';
|
||||
import { Ref, watch } from 'vue';
|
||||
|
||||
import { useTimeoutRef } from '/@/hooks/core/useTimeoutRef';
|
||||
|
||||
type TimeoutFnResult = [Fn<void>, Fn<void>, Ref<boolean>];
|
||||
|
||||
export function useTimeout(handle: Fn<any>, wait: number): TimeoutFnResult {
|
||||
if (!isFunction(handle)) {
|
||||
throw new Error('handle is not Function!');
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { TimeoutResult } from './types';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { Ref, ref } from 'vue';
|
||||
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
|
||||
export type TimeoutResult = [Ref<boolean>, Fn<void>, Fn<void>];
|
||||
export function useTimeoutRef(wait: number): TimeoutResult {
|
||||
const readyRef = ref(false);
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
export type Fn<T> = () => T;
|
@ -1,5 +1,4 @@
|
||||
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
|
||||
import {} from 'vue';
|
||||
import EventHub from '/@/utils/eventHub';
|
||||
const eventHub = new EventHub();
|
||||
export function useEventHub(): EventHub {
|
||||
|
48
src/hooks/event/useIntersectionObserver.ts
Normal file
48
src/hooks/event/useIntersectionObserver.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Ref, watchEffect, ref } from 'vue';
|
||||
|
||||
interface IntersectionObserverProps {
|
||||
target: Ref<Element | null | undefined>;
|
||||
root?: Ref<Element | null | undefined>;
|
||||
onIntersect: IntersectionObserverCallback;
|
||||
rootMargin?: string;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
export function useIntersectionObserver({
|
||||
target,
|
||||
root,
|
||||
onIntersect,
|
||||
rootMargin = '0px',
|
||||
threshold = 0.1,
|
||||
}: IntersectionObserverProps) {
|
||||
let cleanup = () => {};
|
||||
const observer: Ref<Nullable<IntersectionObserver>> = ref(null);
|
||||
const stopEffect = watchEffect(() => {
|
||||
cleanup();
|
||||
|
||||
observer.value = new IntersectionObserver(onIntersect, {
|
||||
root: root ? root.value : null,
|
||||
rootMargin,
|
||||
threshold,
|
||||
});
|
||||
|
||||
const current = target.value;
|
||||
|
||||
current && observer.value.observe(current);
|
||||
|
||||
cleanup = () => {
|
||||
if (observer.value) {
|
||||
observer.value.disconnect();
|
||||
target.value && observer.value.unobserve(target.value);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
observer,
|
||||
stop: () => {
|
||||
cleanup();
|
||||
stopEffect();
|
||||
},
|
||||
};
|
||||
}
|
35
src/hooks/event/useNow.ts
Normal file
35
src/hooks/event/useNow.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { ref } from 'vue';
|
||||
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
|
||||
|
||||
function getTimestamp() {
|
||||
return +Date.now();
|
||||
}
|
||||
|
||||
export function useNow() {
|
||||
const now = ref(getTimestamp());
|
||||
let started = false;
|
||||
|
||||
const update = () => {
|
||||
requestAnimationFrame(() => {
|
||||
now.value = getTimestamp();
|
||||
if (started) update();
|
||||
});
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (!started) {
|
||||
started = true;
|
||||
update();
|
||||
}
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
started = false;
|
||||
};
|
||||
|
||||
start();
|
||||
|
||||
tryOnUnmounted(stop);
|
||||
|
||||
return now;
|
||||
}
|
@ -49,7 +49,6 @@ if (isServer) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function useRaf() {
|
||||
// if (getCurrentInstance()) {
|
||||
// onUnmounted(() => {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { ModalOptionsEx, ModalOptionsPartial } from '/@/hooks/core/types';
|
||||
import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal';
|
||||
|
||||
import { Modal, message as Message, notification } from 'ant-design-vue';
|
||||
@ -6,6 +5,11 @@ import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-des
|
||||
|
||||
import { useSetting } from '/@/hooks/core/useSetting';
|
||||
|
||||
export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
|
||||
iconType: 'warning' | 'success' | 'error' | 'info';
|
||||
}
|
||||
export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;
|
||||
|
||||
interface ConfirmOptions {
|
||||
info: ModalFunc;
|
||||
success: ModalFunc;
|
||||
|
Loading…
Reference in New Issue
Block a user