vue 懒加载

懒加载

为什么需要懒加载?

像vue这种单页面应用,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,时间过长,会出啊先长时间的白屏,即使做了loading也是不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时。

简单的说就是:进入首页不用一次加载过多资源,造成用时过长。

懒加载

  • 也叫延迟加载,即在需要的时候进行加载,随用随载。
  • 个人根据功能划分为图片的懒加载和组件的懒加载。

图片懒加载

使用vue-lazyload插件:

  1. 下载

    $ npm install vue-lazyload -D

  2. 注册插件

    // main.js:
    
    import Vue from 'vue'
    import App from './App.vue'
    import VueLazyload from 'vue-lazyload'
    // 使用方法1:
    Vue.use(VueLazyload)
    
    // 使用方法2: 自定义参数选项配置
    Vue.use(VueLazyload, {
      preLoad: 1.3, // 提前加载高度(数字 1 表示 1 屏的高度) 默认值:1.3
      error: 'dist/error.png', // 当加载图片失败的时候
      loading: 'dist/loading.gif', // 图片加载状态下显示的图片
      attempt: 3 //  加载错误后最大尝试次数 默认值:3
    })
    
  1. 在页面中使用

    <!-- mobile.vue -->
    
    <!-- 使用方法1: 可能图片url是直接从后台拿到的,把':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差不多,通过自定义指令去定义的,不过v-lazy-container扫描的是内部的子元素 --> 
    <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里面的变量,不能写真实的图片路径,这样会报错导致没有效果,因为vue的自定义指令必须对应data中的变量 只能是变量;v-lazy-container内部指定元素设置的data-src是图片的真实路径,不能是data变量,这个和v-lazy完全相反。

  2. 给每一个状态添加样式

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

组件懒加载

主要分以下几步:

1.兼容低版本浏览器 => 2.新建懒加载组件 => 3.新建公共骨架屏组件 => 4.异步加载子组件 => 5.页面中使用

1.兼容低版本浏览器

  1. 该项目依赖 IntersectionObserver API,如需在较低版本浏览器运行,需要首先处理兼容低版本浏览器,需要引入插件 IntersectionObserver API polyfill

  2. 使用

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

2.新建一个VueLazyComponent.vue 文件

因为使用的懒加载组件插件部分不满足我们官网项目,所以把vue-lazy-component插件的核心代码取出来,新建一个VueLazyComponent.vue文件存放,在项目中需要使用到懒加载组件的页面引入即可。

  1. 在需要使用懒加载的页面引入 VueLazyComponent.vue 文件

    • 引入
    <script>
      // 引入存放在mobile公共文件夹里的VueLazyComponent.vue 来包裹需要加载的子组件
      import VueLazyComponent from 'components/mobile/common/VueLazyComponent';
      // 引入骨架屏组件 (详细见-骨架屏组件) 在子组件未加载时,为待加载的子组件先占据一块空间
      import MobileSkeleton from 'components/mobile/common/MobileSkeleton';
    
      export default {
        components: {
          'vue-lazy-component': VueLazyComponent,
          'mobile-skeleton': MobileSkeleton,
        },
      };
    </script>
    
  2. VueLazyComponent.vue 源码解读

    • 使用到的参数和事件

      Props

      参数 说明 类型 可选值 默认值
      viewport 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 HTMLElement true null,代表视窗
      direction 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向 String true vertical
      threshold 预加载阈值, css单位 String true 0px
      tagName 包裹组件的外层容器的标签名 String true div
      timeout 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 Number true -

      Events

      事件名 说明 事件参数
      before-init 模块可见或延时截止导致准备开始加载懒加载模块 -
      init 开始加载懒加载模块,此时骨架组件开始消失 -
      before-enter 懒加载模块开始进入 el
      before-leave 骨架组件开始离开 el
      after-leave 骨架组件已经离开 el
      after-enter 懒加载模快已经进入 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则无论可见与否都是在timeout之后初始化
        if (this.timeout) {
          this.timer = setTimeout(() => {
            this.init();
          }, this.timeout);
        }
      },
    
      mounted() {
        if (!this.timeout) {
          // 根据滚动方向来构造视口外边距,用于提前加载
          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: {
        // 交叉情况变化处理函数
        intersectionHandler(entries) {
          if (
            // 正在交叉
            entries[0].isIntersecting ||
            // 交叉率大于0
            entries[0].intersectionRatio
          ) {
            this.init();
            this.io.unobserve(this.$el);
          }
        },
    
        // 处理组件和骨架组件的切换
        init() {
          // 此时说明骨架组件即将被切换
          this.$emit('beforeInit');
          this.$emit('before-init');
    
          // 此时可以准备加载懒加载组件的资源
          this.loading = true;
    
          // 由于函数会在主线程中执行,加载懒加载组件非常耗时,容易卡顿
          // 所以在requestAnimationFrame回调中延后执行
          this.requestAnimationFrame(() => {
            this.isInit = true;
            this.$emit('init');
          });
        },
    
        requestAnimationFrame(callback) {
          // 防止等待太久没有执行回调
          // 设置最大等待时间
          // setTimeout(() => {
          //   if (this.isInit) return
          //   callback()
          // }, this.maxWaitingTime)
    
          // 兼容不支持requestAnimationFrame 的浏览器
          return (callbackto => setTimeout(callbackto, 300))(callback);
        }
      }
    };
    </script>
    

3.新建骨架屏组件

​ 骨架屏组件并没有去获取获取不同子组件的dom节点生成,只是为了和参考的懒加载插件的逻辑保持一致,子组件是异步加载,所以在子组件加载前,父组件上有slot="skeleton"的组件会先执行,为子组件在加载前在页面上占据位置。

4.异步引入组件

  1. 两种语法形式

    // 两种语法形式:
        1 component: () => import('/component_url/');
        2 component: (resolve) => {
            require(['/component_url/'],resolve)
          }
    
  2. 页面中路由配置

    // 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.页面中使用

  1. 项目中使用

    <!-- xxx.vue -->
    
    <vue-lazy-component>
       <template slot-scope="scope">
             <!-- 真实组件-->
          <mobile-xxx v-if="scope.loading"/>
       </template>
         <!-- 骨架组件,在真实组件渲染完毕后消失 -->
       <mobile-skeleton slot="skeleton"/>
    </vue-lazy-component>
    
    • 通过 vue-lazy-component标签的包裹,先预加载mobile-skeleton页面预留空间,待滑到当前页面时,显示当前子组件。
    • 通过 slot-scope 特性从子组件获取数据,scope.loading=true时,<mobile-paceOs-p13/>组件渲染成功。
  2. 使用这个懒加载组件遇到的问题

    • 用懒加载组件包裹的子组件<mobile-xxx/>在页面上滑出、进入时,动画未执行.

      <!--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>
      
    • 通过打印,发现是xxx.vue文件里,on-scroll-view包裹的元素获取的父节点不是最外层父节点,所以在OnScrollView.vue修改:

      // OnScrollView.vue
      
      // 当前元素头部距离整个页面顶部的距离
      // this.offsetTop = component.offsetTop + component.offsetParent.offsetTop;
      this.offsetTop = component.offsetTop + this.getParentsNode().offsetTop;
      
      // 获取元素包裹的父节点
      getParentsNode() {
        let node = this.$el.offsetParent;
        if (this.targetClass !== '') { 
          // 如果当前元素的父节点的class不存在'lazyload',则一直向上找
          while (node.getAttribute('class').indexOf(this.targetClass) === -1) {
            node = node.offsetParent;
          }
        }
        return node;
      }
      
    • on-scroll-view标签里添加targetClass="lazyLoad"

      <!--xxx.vue-->
      
          <on-scroll-view :config="onScrollViewConfig" targetClass="lazyLoad">
            ...
          </on-scroll-view>
      </template>
      
    • transition-group标签添加class="lazyLoad"

      <!-- VueLazyComponent.vue -->
      
      <template>
        <transition-group :tag="tagName" name="lazy-component" style="position: relative;" class="lazyLoad"...>
        </transition-group>
      </template>
      
    • 通过添加的getParentsNode()方法,能解决动画未执行的问题.

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容