Kaynağa Gözat

feat: add lazyContainer comp and demo

vben 4 yıl önce
ebeveyn
işleme
fdeaa00bf2

+ 1 - 0
CHANGELOG.zh_CN.md

@@ -8,6 +8,7 @@
 - 表单新增 submitOnReset 控制是否在重置时重新发起请求
 - 表格新增`sortFn`支持自定义排序
 - 新增动画组件及示例
+- 新增懒加载/延时加载组件及示例
 
 ### ✨ Refactor
 

+ 1 - 1
src/components/Container/index.ts

@@ -1,5 +1,5 @@
 export { default as ScrollContainer } from './src/ScrollContainer.vue';
 export { default as CollapseContainer } from './src/collapse/CollapseContainer.vue';
-export { default as LazyContainer } from './src/LazyContainer';
+export { default as LazyContainer } from './src/LazyContainer.vue';
 
 export * from './src/types.d';

+ 0 - 27
src/components/Container/src/LazyContainer.less

@@ -1,27 +0,0 @@
-.lazy-container-enter {
-  opacity: 0;
-}
-
-.lazy-container-enter-to {
-  opacity: 1;
-}
-
-.lazy-container-enter-from,
-.lazy-container-enter-active {
-  position: absolute;
-  top: 0;
-  width: 100%;
-  transition: opacity 0.3s 0.2s;
-}
-
-.lazy-container-leave {
-  opacity: 1;
-}
-
-.lazy-container-leave-to {
-  opacity: 0;
-}
-
-.lazy-container-leave-active {
-  transition: opacity 0.5s;
-}

+ 0 - 200
src/components/Container/src/LazyContainer.tsx

@@ -1,200 +0,0 @@
-import type { PropType } from 'vue';
-
-import {
-  defineComponent,
-  reactive,
-  onMounted,
-  ref,
-  unref,
-  onUnmounted,
-  TransitionGroup,
-} from 'vue';
-
-import { Skeleton } from 'ant-design-vue';
-import { useRaf } from '/@/hooks/event/useRaf';
-import { useTimeout } from '/@/hooks/core/useTimeout';
-import { getListeners, getSlot } from '/@/utils/helper/tsxHelper';
-
-import './LazyContainer.less';
-
-interface State {
-  isInit: boolean;
-  loading: boolean;
-  intersectionObserverInstance: IntersectionObserver | null;
-}
-export default defineComponent({
-  name: 'LazyContainer',
-  emits: ['before-init', 'init'],
-  props: {
-    // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
-    timeout: {
-      type: Number as PropType<number>,
-      default: 8000,
-      // default: 8000,
-    },
-    // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
-    viewport: {
-      type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,
-      default: () => null,
-    },
-    // 预加载阈值, css单位
-    threshold: {
-      type: String as PropType<string>,
-      default: '0px',
-    },
-
-    // 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
-    direction: {
-      type: String as PropType<'vertical' | 'horizontal'>,
-      default: 'vertical',
-    },
-    // 包裹组件的外层容器的标签名
-    tag: {
-      type: String as PropType<string>,
-      default: 'div',
-    },
-
-    maxWaitingTime: {
-      type: Number as PropType<number>,
-      default: 80,
-    },
-
-    // 是否在不可见的时候销毁
-    autoDestory: {
-      type: Boolean as PropType<boolean>,
-      default: false,
-    },
-
-    // transition name
-    transitionName: {
-      type: String as PropType<string>,
-      default: 'lazy-container',
-    },
-  },
-  setup(props, { attrs, emit, slots }) {
-    const elRef = ref<any>(null);
-    const state = reactive<State>({
-      isInit: false,
-      loading: false,
-      intersectionObserverInstance: null,
-    });
-
-    // If there is a set delay time, it will be executed immediately
-    function immediateInit() {
-      const { timeout } = props;
-      timeout &&
-        useTimeout(() => {
-          init();
-        }, timeout);
-    }
-
-    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(() => {
-        state.isInit = true;
-        emit('init');
-      });
-    }
-    function requestAnimationFrameFn(callback: () => any) {
-      // Prevent waiting too long without executing the callback
-      // Set the maximum waiting time
-      useTimeout(() => {
-        if (state.isInit) {
-          return;
-        }
-        callback();
-      }, props.maxWaitingTime || 80);
-
-      const { requestAnimationFrame } = useRaf();
-
-      return requestAnimationFrame;
-    }
-    function initIntersectionObserver() {
-      const { timeout, direction, threshold, viewport } = props;
-      if (timeout) {
-        return;
-      }
-      // According to the scrolling direction to construct the viewport margin, used to load in advance
-      let rootMargin;
-      switch (direction) {
-        case 'vertical':
-          rootMargin = `${threshold} 0px`;
-          break;
-        case 'horizontal':
-          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);
-      } 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);
-        }
-      }
-      // else {
-      //   const { autoDestory } = props;
-      //   autoDestory && destory();
-      // }
-    }
-    // function destory() {
-    //   emit('beforeDestory');
-    //   state.loading = false;
-    //   nextTick(() => {
-    //     emit('destory');
-    //   });
-    // }
-
-    immediateInit();
-    onMounted(() => {
-      initIntersectionObserver();
-    });
-    onUnmounted(() => {
-      // Cancel the observation before the component is destroyed
-      if (state.intersectionObserverInstance) {
-        const el = unref(elRef);
-        state.intersectionObserverInstance.unobserve(el.$el);
-      }
-    });
-
-    function renderContent() {
-      const { isInit, loading } = state;
-      if (isInit) {
-        return <div key="component">{getSlot(slots, 'default', { loading })}</div>;
-      }
-      if (slots.skeleton) {
-        return <div key="skeleton">{getSlot(slots, 'skeleton') || <Skeleton />}</div>;
-      }
-      return null;
-    }
-    return () => {
-      const { tag, transitionName } = props;
-      return (
-        <TransitionGroup ref={elRef} name={transitionName} tag={tag} {...getListeners(attrs)}>
-          {() => renderContent()}
-        </TransitionGroup>
-      );
-    };
-  },
-});

+ 213 - 0
src/components/Container/src/LazyContainer.vue

@@ -0,0 +1,213 @@
+<template>
+  <transition-group v-bind="$attrs" ref="elRef" :name="transitionName" :tag="tag">
+    <div key="component" v-if="isInit">
+      <slot :loading="loading" />
+    </div>
+    <div key="skeleton">
+      <slot name="skeleton" v-if="$slots.skeleton" />
+      <Skeleton v-else />
+    </div>
+  </transition-group>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+
+  import { defineComponent, reactive, onMounted, ref, unref, onUnmounted, toRefs } from 'vue';
+
+  import { Skeleton } from 'ant-design-vue';
+  import { useRaf } from '/@/hooks/event/useRaf';
+  import { useTimeout } from '/@/hooks/core/useTimeout';
+
+  interface State {
+    isInit: boolean;
+    loading: boolean;
+    intersectionObserverInstance: IntersectionObserver | null;
+  }
+  export default defineComponent({
+    name: 'LazyContainer',
+    components: { Skeleton },
+    props: {
+      // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
+      timeout: {
+        type: Number as PropType<number>,
+        default: 8000,
+        // default: 8000,
+      },
+      // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
+      viewport: {
+        type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<
+          HTMLElement
+        >,
+        default: () => null,
+      },
+      // 预加载阈值, css单位
+      threshold: {
+        type: String as PropType<string>,
+        default: '0px',
+      },
+
+      // 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
+      direction: {
+        type: String as PropType<'vertical' | 'horizontal'>,
+        default: 'vertical',
+      },
+      // 包裹组件的外层容器的标签名
+      tag: {
+        type: String as PropType<string>,
+        default: 'div',
+      },
+
+      maxWaitingTime: {
+        type: Number as PropType<number>,
+        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 }) {
+      const elRef = ref<any>(null);
+      const state = reactive<State>({
+        isInit: false,
+        loading: false,
+        intersectionObserverInstance: null,
+      });
+
+      immediateInit();
+      onMounted(() => {
+        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() {
+        const { timeout } = props;
+        timeout &&
+          useTimeout(() => {
+            init();
+          }, timeout);
+      }
+
+      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(() => {
+          state.isInit = true;
+          emit('init');
+        });
+      }
+
+      function requestAnimationFrameFn(callback: () => any) {
+        // Prevent waiting too long without executing the callback
+        // Set the maximum waiting time
+        useTimeout(() => {
+          if (state.isInit) {
+            return;
+          }
+          callback();
+        }, props.maxWaitingTime || 80);
+
+        const { requestAnimationFrame } = useRaf();
+
+        return requestAnimationFrame;
+      }
+
+      function initIntersectionObserver() {
+        const { timeout, direction, threshold, viewport } = props;
+        if (timeout) {
+          return;
+        }
+        // According to the scrolling direction to construct the viewport margin, used to load in advance
+        let rootMargin;
+        switch (direction) {
+          case 'vertical':
+            rootMargin = `${threshold} 0px`;
+            break;
+          case 'horizontal':
+            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);
+        } 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),
+      };
+    },
+  });
+</script>
+<style lang="less">
+  .lazy-container-enter {
+    opacity: 0;
+  }
+
+  .lazy-container-enter-to {
+    opacity: 1;
+  }
+
+  .lazy-container-enter-from,
+  .lazy-container-enter-active {
+    position: absolute;
+    top: 0;
+    width: 100%;
+    transition: opacity 0.3s 0.2s;
+  }
+
+  .lazy-container-leave {
+    opacity: 1;
+  }
+
+  .lazy-container-leave-to {
+    opacity: 0;
+  }
+
+  .lazy-container-leave-active {
+    transition: opacity 0.5s;
+  }
+</style>

+ 4 - 0
src/router/menus/modules/demo/comp.ts

@@ -49,6 +49,10 @@ const menu: MenuModule = {
         name: '详情组件',
       },
       {
+        path: 'lazy',
+        name: '懒加载组件',
+      },
+      {
         path: 'verify',
         name: '验证组件',
         children: [

+ 8 - 1
src/router/routes/modules/demo/comp.ts

@@ -99,7 +99,14 @@ export default {
         title: '详情组件',
       },
     },
-
+    {
+      path: '/lazy',
+      name: 'lazyDemo',
+      component: () => import('/@/views/demo/comp/lazy/index.vue'),
+      meta: {
+        title: '懒加载组件',
+      },
+    },
     {
       path: '/verify',
       name: 'VerifyDemo',

+ 19 - 0
src/views/demo/comp/lazy/TargetContent.vue

@@ -0,0 +1,19 @@
+<template>
+  <Card hoverable :style="{ width: '240px', background: '#fff' }">
+    <template #cover>
+      <img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />
+    </template>
+    <CardMeta title="懒加载组件" />
+  </Card>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { Card } from 'ant-design-vue';
+
+  export default defineComponent({
+    components: { CardMeta: Card.Meta, Card },
+    setup() {
+      return {};
+    },
+  });
+</script>

+ 46 - 0
src/views/demo/comp/lazy/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="p-4 lazy-base-demo">
+    <Alert message="基础示例" description="向下滚动到可见区域才会加载组件" type="info" show-icon />
+    <div class="lazy-base-demo-wrap">
+      <h1>向下滚动</h1>
+      <LazyContainer @init="() => {}">
+        <TargetContent />
+        <template #skeleton>
+          <Skeleton :rows="10" />
+        </template>
+      </LazyContainer>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { Skeleton, Alert } from 'ant-design-vue';
+  import TargetContent from './TargetContent.vue';
+  import { LazyContainer } from '/@/components/Container/index';
+  export default defineComponent({
+    components: { LazyContainer, TargetContent, Skeleton, Alert },
+    setup() {
+      return {};
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .lazy-base-demo {
+    &-wrap {
+      display: flex;
+      width: 50%;
+      height: 2000px;
+      margin: 20px auto;
+      text-align: center;
+      background: #fff;
+      justify-content: center;
+      flex-direction: column;
+      align-items: center;
+    }
+
+    h1 {
+      height: 1300px;
+      margin: 20px 0;
+    }
+  }
+</style>