vue 懶加載

懶加載

為什么需要懶加載?

像vue這種單頁(yè)面應(yīng)用,如果沒(méi)有應(yīng)用懶加載,運(yùn)用webpack打包后的文件將會(huì)異常的大,造成進(jìn)入首頁(yè)時(shí),需要加載的內(nèi)容過(guò)多,時(shí)間過(guò)長(zhǎng),會(huì)出啊先長(zhǎng)時(shí)間的白屏,即使做了loading也是不利于用戶體驗(yàn),而運(yùn)用懶加載則可以將頁(yè)面進(jìn)行劃分,需要的時(shí)候加載頁(yè)面,可以有效的分擔(dān)首頁(yè)所承擔(dān)的加載壓力,減少首頁(yè)加載用時(shí)。

簡(jiǎn)單的說(shuō)就是:進(jìn)入首頁(yè)不用一次加載過(guò)多資源,造成用時(shí)過(guò)長(zhǎng)。

懶加載

  • 也叫延遲加載,即在需要的時(shí)候進(jìn)行加載,隨用隨載。
  • 個(gè)人根據(jù)功能劃分為圖片的懶加載和組件的懶加載。

圖片懶加載

使用vue-lazyload插件:

  1. 下載

    $ npm install vue-lazyload -D

  2. 注冊(cè)插件

    // main.js:
    
    import Vue from 'vue'
    import App from './App.vue'
    import VueLazyload from 'vue-lazyload'
    // 使用方法1:
    Vue.use(VueLazyload)
    
    // 使用方法2: 自定義參數(shù)選項(xiàng)配置
    Vue.use(VueLazyload, {
      preLoad: 1.3, // 提前加載高度(數(shù)字 1 表示 1 屏的高度) 默認(rèn)值:1.3
      error: 'dist/error.png', // 當(dāng)加載圖片失敗的時(shí)候
      loading: 'dist/loading.gif', // 圖片加載狀態(tài)下顯示的圖片
      attempt: 3 //  加載錯(cuò)誤后最大嘗試次數(shù) 默認(rèn)值:3
    })
    
  1. 在頁(yè)面中使用

    <!-- mobile.vue -->
    
    <!-- 使用方法1: 可能圖片url是直接從后臺(tái)拿到的,把':src'替換成'v-lazy'就行 --> 
    <template>
      <ul>
        <li v-for="img in list">
          <img v-lazy="img.src" >
        </li>
      </ul>
    </template>
    
    <!-- 使用方法2: 使用懶加載容器v-lazy-container,和v-lazy差不多,通過(guò)自定義指令去定義的,不過(guò)v-lazy-container掃描的是內(nèi)部的子元素 --> 
    <template>
      <div v-lazy-container="{ selector: 'img'}">
        <img data-src="/static/mobile/bohai/p2/bg.jpg">
        <img data-src="/static/mobile/bohai/p3/bg.jpg">
        ...
        <img data-src="/static/mobile/bohai/p13/bg.jpg">
      </div>
    </template>
    
    
    

    注意:v-lazy='src'中的src一定要使用data里面的變量,不能寫真實(shí)的圖片路徑,這樣會(huì)報(bào)錯(cuò)導(dǎo)致沒(méi)有效果,因?yàn)関ue的自定義指令必須對(duì)應(yīng)data中的變量 只能是變量;v-lazy-container內(nèi)部指定元素設(shè)置的data-src是圖片的真實(shí)路徑,不能是data變量,這個(gè)和v-lazy完全相反。

  2. 給每一個(gè)狀態(tài)添加樣式

    <style>
      img[lazy=loading] { }
      img[lazy=error] { }
      img[lazy=loaded] { }
    </style>
    

組件懶加載

主要分以下幾步:

1.兼容低版本瀏覽器 => 2.新建懶加載組件 => 3.新建公共骨架屏組件 => 4.異步加載子組件 => 5.頁(yè)面中使用

1.兼容低版本瀏覽器

  1. 該項(xiàng)目依賴 IntersectionObserver API,如需在較低版本瀏覽器運(yùn)行,需要首先處理兼容低版本瀏覽器,需要引入插件 IntersectionObserver API polyfill

  2. 使用

    // 1.下載
    $ npm install intersection-observer -D
    
    // 2.在mian.js引入
    import 'intersection-observer';
    

2.新建一個(gè)VueLazyComponent.vue 文件

因?yàn)槭褂玫膽屑虞d組件插件部分不滿足我們官網(wǎng)項(xiàng)目,所以把vue-lazy-component插件的核心代碼取出來(lái),新建一個(gè)VueLazyComponent.vue文件存放,在項(xiàng)目中需要使用到懶加載組件的頁(yè)面引入即可。

  1. 在需要使用懶加載的頁(yè)面引入 VueLazyComponent.vue 文件

    • 引入
    <script>
      // 引入存放在mobile公共文件夾里的VueLazyComponent.vue 來(lái)包裹需要加載的子組件
      import VueLazyComponent from 'components/mobile/common/VueLazyComponent';
      // 引入骨架屏組件 (詳細(xì)見(jiàn)-骨架屏組件) 在子組件未加載時(shí),為待加載的子組件先占據(jù)一塊空間
      import MobileSkeleton from 'components/mobile/common/MobileSkeleton';
    
      export default {
        components: {
          'vue-lazy-component': VueLazyComponent,
          'mobile-skeleton': MobileSkeleton,
        },
      };
    </script>
    
  2. VueLazyComponent.vue 源碼解讀

    • 使用到的參數(shù)和事件

      Props

      參數(shù) 說(shuō)明 類型 可選值 默認(rèn)值
      viewport 組件所在的視口,如果組件是在頁(yè)面容器內(nèi)滾動(dòng),視口就是該容器 HTMLElement true null,代表視窗
      direction 視口的滾動(dòng)方向, vertical代表垂直方向,horizontal代表水平方向 String true vertical
      threshold 預(yù)加載閾值, css單位 String true 0px
      tagName 包裹組件的外層容器的標(biāo)簽名 String true div
      timeout 等待時(shí)間,如果指定了時(shí)間,不論可見(jiàn)與否,在指定時(shí)間之后自動(dòng)加載 Number true -

      Events

      事件名 說(shuō)明 事件參數(shù)
      before-init 模塊可見(jiàn)或延時(shí)截止導(dǎo)致準(zhǔn)備開(kāi)始加載懶加載模塊 -
      init 開(kāi)始加載懶加載模塊,此時(shí)骨架組件開(kāi)始消失 -
      before-enter 懶加載模塊開(kāi)始進(jìn)入 el
      before-leave 骨架組件開(kāi)始離開(kāi) el
      after-leave 骨架組件已經(jīng)離開(kāi) el
      after-enter 懶加載??煲呀?jīng)進(jìn)入 el
      after-init 初始化完成
    <!-- VueLazyComponent.vue -->
    
    <template>
      <transition-group :tag="tagName" name="lazy-component" style="position: relative;"
                        @before-enter="(el) => $emit('before-enter', el)"
                        @before-leave="(el) => $emit('before-leave', el)"
                        @after-enter="(el) => $emit('after-enter', el)"
                        @after-leave="(el) => $emit('after-leave', el)">
        <div v-if="isInit" key="component">
          <slot :loading="loading"></slot>
        </div>
        <div v-else-if="$slots.skeleton" key="skeleton">
          <slot name="skeleton"></slot>
        </div>
        <div v-else key="loading"></div>
      </transition-group>
    </template>
    
    <script>
    export default {
      name: 'VueLazyComponent',
    
      props: {
        timeout: {
          type: Number,
          default: 0
        },
        tagName: {
          type: String,
          default: 'div'
        },
        viewport: {
          type: typeof window !== 'undefined' ? window.HTMLElement : Object,
          default: () => null
        },
        threshold: {
          type: String,
          default: '0px'
        },
        direction: {
          type: String,
          default: 'vertical'
        },
        maxWaitingTime: {
          type: Number,
          default: 50
        }
      },
    
      data() {
        return {
          isInit: false,
          timer: null,
          io: null,
          loading: false
        };
      },
    
      created() {
        // 如果指定timeout則無(wú)論可見(jiàn)與否都是在timeout之后初始化
        if (this.timeout) {
          this.timer = setTimeout(() => {
            this.init();
          }, this.timeout);
        }
      },
    
      mounted() {
        if (!this.timeout) {
          // 根據(jù)滾動(dòng)方向來(lái)構(gòu)造視口外邊距,用于提前加載
          let rootMargin;
          switch (this.direction) {
            case 'vertical':
              rootMargin = `${this.threshold} 0px`;
              break;
            case 'horizontal':
              rootMargin = `0px ${this.threshold}`;
              break;
            default:
            // do nothing
          }
    
          // 觀察視口與組件容器的交叉情況
          this.io = new window.IntersectionObserver(this.intersectionHandler, {
            rootMargin,
            root: this.viewport,
            threshold: [0, Number.MIN_VALUE, 0.01]
          });
          this.io.observe(this.$el);
        }
      },
    
      beforeDestroy() {
        // 在組件銷毀前取消觀察
        if (this.io) {
          this.io.unobserve(this.$el);
        }
      },
    
      methods: {
        // 交叉情況變化處理函數(shù)
        intersectionHandler(entries) {
          if (
            // 正在交叉
            entries[0].isIntersecting ||
            // 交叉率大于0
            entries[0].intersectionRatio
          ) {
            this.init();
            this.io.unobserve(this.$el);
          }
        },
    
        // 處理組件和骨架組件的切換
        init() {
          // 此時(shí)說(shuō)明骨架組件即將被切換
          this.$emit('beforeInit');
          this.$emit('before-init');
    
          // 此時(shí)可以準(zhǔn)備加載懶加載組件的資源
          this.loading = true;
    
          // 由于函數(shù)會(huì)在主線程中執(zhí)行,加載懶加載組件非常耗時(shí),容易卡頓
          // 所以在requestAnimationFrame回調(diào)中延后執(zhí)行
          this.requestAnimationFrame(() => {
            this.isInit = true;
            this.$emit('init');
          });
        },
    
        requestAnimationFrame(callback) {
          // 防止等待太久沒(méi)有執(zhí)行回調(diào)
          // 設(shè)置最大等待時(shí)間
          // setTimeout(() => {
          //   if (this.isInit) return
          //   callback()
          // }, this.maxWaitingTime)
    
          // 兼容不支持requestAnimationFrame 的瀏覽器
          return (callbackto => setTimeout(callbackto, 300))(callback);
        }
      }
    };
    </script>
    

3.新建骨架屏組件

? 骨架屏組件并沒(méi)有去獲取獲取不同子組件的dom節(jié)點(diǎn)生成,只是為了和參考的懶加載插件的邏輯保持一致,子組件是異步加載,所以在子組件加載前,父組件上有slot="skeleton"的組件會(huì)先執(zhí)行,為子組件在加載前在頁(yè)面上占據(jù)位置。

4.異步引入組件

  1. 兩種語(yǔ)法形式

    // 兩種語(yǔ)法形式:
        1 component: () => import('/component_url/');
        2 component: (resolve) => {
            require(['/component_url/'],resolve)
          }
    
  2. 頁(yè)面中路由配置

    // xxx.vue
    
    export default {
        name: 'xxx',
        metaInfo: {
          title: 'xxx',
        },
        components: {
          'mobile-header-container': MobileHeaderContainer,
          'vue-lazy-component': VueLazyComponent,
          'mobile-skeleton': MobileSkeleton,
         xxx: () => import('components/xxx'),
          xxxP2: () => import('components/mobile/xxx'),
          ...
          MobileFooterContainer: () => import('components/mobile/common/FooterContainer'),
        },
      };
    </script>
    

5.頁(yè)面中使用

  1. 項(xiàng)目中使用

    <!-- xxx.vue -->
    
    <vue-lazy-component>
       <template slot-scope="scope">
             <!-- 真實(shí)組件-->
          <mobile-xxx v-if="scope.loading"/>
       </template>
         <!-- 骨架組件,在真實(shí)組件渲染完畢后消失 -->
       <mobile-skeleton slot="skeleton"/>
    </vue-lazy-component>
    
    • 通過(guò) vue-lazy-component標(biāo)簽的包裹,先預(yù)加載mobile-skeleton頁(yè)面預(yù)留空間,待滑到當(dāng)前頁(yè)面時(shí),顯示當(dāng)前子組件。
    • 通過(guò) slot-scope 特性從子組件獲取數(shù)據(jù),scope.loading=true時(shí),<mobile-paceOs-p13/>組件渲染成功。
  2. 使用這個(gè)懶加載組件遇到的問(wèn)題

    • 用懶加載組件包裹的子組件<mobile-xxx/>在頁(yè)面上滑出、進(jìn)入時(shí),動(dòng)畫未執(zhí)行.

      <!--PaceOs.vue-->
      
      <vue-lazy-component>
        <template slot-scope="scope">
         <mobile-xxx v-if="scope.loading"/>
        </template>
        <mobile-skeleton slot="skeleton"/>
      </vue-lazy-component>
      
      <!--xxx.vue-->
      
      <template>
        <div class="xxx">
             ...
          <on-scroll-view :config="onScrollViewConfig">
            <div class="p3-dial zIndex1" ref="img1">
              <img src="xxx.png">
            </div>
            ...
          </on-scroll-view>
        </div>
      </template>
      
    • 通過(guò)打印,發(fā)現(xiàn)是xxx.vue文件里,on-scroll-view包裹的元素獲取的父節(jié)點(diǎn)不是最外層父節(jié)點(diǎn),所以在OnScrollView.vue修改:

      // OnScrollView.vue
      
      // 當(dāng)前元素頭部距離整個(gè)頁(yè)面頂部的距離
      // this.offsetTop = component.offsetTop + component.offsetParent.offsetTop;
      this.offsetTop = component.offsetTop + this.getParentsNode().offsetTop;
      
      // 獲取元素包裹的父節(jié)點(diǎn)
      getParentsNode() {
        let node = this.$el.offsetParent;
        if (this.targetClass !== '') { 
          // 如果當(dāng)前元素的父節(jié)點(diǎn)的class不存在'lazyload',則一直向上找
          while (node.getAttribute('class').indexOf(this.targetClass) === -1) {
            node = node.offsetParent;
          }
        }
        return node;
      }
      
    • on-scroll-view標(biāo)簽里添加targetClass="lazyLoad"

      <!--xxx.vue-->
      
          <on-scroll-view :config="onScrollViewConfig" targetClass="lazyLoad">
            ...
          </on-scroll-view>
      </template>
      
    • transition-group標(biāo)簽添加class="lazyLoad"

      <!-- VueLazyComponent.vue -->
      
      <template>
        <transition-group :tag="tagName" name="lazy-component" style="position: relative;" class="lazyLoad"...>
        </transition-group>
      </template>
      
    • 通過(guò)添加的getParentsNode()方法,能解決動(dòng)畫未執(zhí)行的問(wèn)題.

參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容