Vue组件懒加载(异步组件、按需加载)

5,122 阅读3分钟

Vue组件懒加载(异步组件、按需加载)

也称按需加载,只在组件需要渲染(组件第一次显示)的时候进行加载渲染并缓存,缓存是以备下次访问。

实现组件的按需加载

  • 一种是使用component的is属性+computed实现

    • component属性is和import

    • component属性is和require

    • component属性is和require.ensure()

      webpack编译时,会静态的解析代码中的require.ensure(),同时将模块添加到一个分开的chunk中,新的chunk会被webpack通过jsonp来按需加载。此方法内部依赖于Promise。

      参数1: 是字符串数组,模块的依赖,会提前加载。一般都是空的。

      参数2: 依赖项加载完成之后的回调函数

      所有的依赖加载完成之后,webpack会执行这个回调函数,require对象的一个实现会作为一个参数传递给回调函数,因此,可以require依赖和其他模块提供下一步的执行。

      参数3: chunk名称

      相同chunk名称的文件 所有依赖都会被放进相同文件束。webpack把这个模块导出一个js文件,然后用到这个模块的时候,就动态构造script标签插入DOM,再由浏览器去请求。回调函数是在依赖加载完成之后执行。

举个栗子

 // First or Second or Third子组件
 <template>
   <div>
     <!--    First or Second or Third-->
     First
   </div>
 </template>
 ​
 <script>
 export default {
   name: "First",// First or Second or Third
   created() {
     console.log("First==>created",)// First or Second or Third
   },
   destroyed() {
     console.log("First==>destroyed",)// First or Second or Third
   }
 }
 </script>

控制台输出:

 // First==>created
 // First==>destroyed
 // Second==>created
 // Second==>destroyed
 // Third==>created
 <template>
   <div style="border: 1px solid red">
     <div class="fir_nav">
       <button v-on:click='switchComponent("First")'>First-1</button>
       <button v-on:click='switchComponent("Second")'>Second-2</button>
       <button v-on:click='switchComponent("Third")'>Third-3</button>
     </div>
     <div class="fir_con">
       <component v-bind:is='getComponent'></component>
     </div>
   </div>
 </template>
 ​
 <script>
 ​
 // import的方式
 // const ConfigComponent = {
 //   First: () => import("./First"),
 //   Second: () => import("./Second"),
 //   Third: () => import("./Third"),
 // }
 ​
 // require的方式
 // const ConfigComponent = {
 //   First: resolve => (require(["./First"], resolve)),
 //   Second: resolve => (require(["./Second"], resolve)),
 //   Third: resolve => (require(["./Third"], resolve)),
 // }
 ​
 // require.ensure()的方式
 // 该方式打包后发现dist目录下发现有FirstChunk,ThirdChunk的js文件
 const ConfigComponent = {
   First: r => require.ensure([], () => r(require("./First")), 'FirstChunk'),
   Second: r => require.ensure([], () => r(require("./Second")), 'FirstChunk'),
   Third: r => require.ensure([], () => r(require("./Third")), 'ThirdChunk'),
 }
 ​
 export default {
   data() {
     return {
       activeComponent: ConfigComponent.First
     }
   },
   computed: {
     getComponent() {
       return this.activeComponent;
     }
   },
   methods: {
     switchComponent(active) {
       this.activeComponent = ConfigComponent[active]
     }
   },
 }
 </script>
  • 另一种是使用v-if实现。

    缺点:不管用那种方式引入,都需要将组件的所有可能都写出来

 <template>
   <div style="border: 1px solid red">
     <div class="fir_nav">
       <button v-on:click='switchComponent("First")'>First-1</button>
       <button v-on:click='switchComponent("Second")'>Second-2</button>
       <button v-on:click='switchComponent("Third")'>Third-3</button>
     </div>
     <div class="fir_con">
       <FirstComponent v-if="ComponentShow.First"></FirstComponent>
       <component v-if="ComponentShow.Second" v-bind:is="SecondComponent"></component>
       <component v-if="ComponentShow.Third" v-bind:is="ThirdComponent"></component>
     </div>
   </div>
 </template>
 ​
 <script>
 ​
 // 传统方式
 import FirstComponent from "./First"
 ​
 export default {
   name: 'Login',
   components: {
     FirstComponent,
   },
   data() {
     return {
       ComponentShow: {
         First: false,
         Second: false,
         Third: false,
       },
       SecondComponent: () => import("./Second"),
       ThirdComponent: () => import("./Third")
     }
   },
   methods: {
     switchComponent(active) {
       for (const key in this.ComponentShow) {
         this.ComponentShow[key] = false
       }
       this.ComponentShow[active] = true
     }
   },
 }
 </script>

初始加载的资源过多导致页面花费了大量时间加载子资源,导致页面的 load 时长被严重拖长。

页面的问题总结结论:

  • 页面由大量模块组成
  • 所有模块是同时进行加载
  • 模块中图片内容较多
  • 每个模块的依赖资源较多(包括js文件、接口文件、css文件等)

解决思路:异步组件

  1. 组件化分治

    • 将各模块拆分为组件模块,每个模块之间降低耦合。
    • 组件依赖的资源全部封装在组件内部进行调用
  2. 组件懒加载

    • 优先加载可见模块
    • 其余不可见模块懒加载,待可见或即将可见时加载

问题解决

  • IntersectionObserver API

    传统通过监听滚动事件、resize 事件来判断模块是否可见,代码不仅繁琐,而且一不小心没有函数去抖就又可能导致严重的性能问题。

    IntersectionObserver 允许你配置一个回调函数,每当 target ,元素和设备视口或者其他指定元素发生交集的时候该回调函数将会被执行。

  • v-if

    优先加载可见模块时,我们希望加载条件为假不去渲染、加载条件为真的时候才渲染。

    使用 Vue.js 提供的 v-if 指令,就可以做到真正的惰性渲染。

  • 骨架屏设置过渡

    真实组件开始渲染的时候,需要一定的时间和空间,时间指的是真实组件从创建到渲染的时间,包括请求接口、请求资源和渲染的时间,空间指的是页面布局中需要给真实组件留出刚好的位置。

    通过设置过渡效果给真实组件留出一定的加载时间。

 <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
     },
     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
       }
 ​
       try {
         // 观察视口与组件容器的交叉情况
         this.io = new window.IntersectionObserver(this.intersectionHandler, {
           rootMargin,
           root: this.viewport,
           threshold: [0, Number.MIN_VALUE, 0.01]
         });
         this.io.observe(this.$el);
       } catch (e) {
         this.init()
       }
     }
   },
 ​
   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 (window.requestAnimationFrame || ((callback) => setTimeout(callback, 1000 / 60)))(callback)
     }
   }
 }
 </script>
 <style>
 .lazy-component-enter {
   opacity: 0;
 }
 ​
 .lazy-component-enter-to {
    opacity: 1;
 }
 ​
 .lazy-component-enter-active {
   transition: opacity 0.3s 0.2s;
   position: absolute;
   top: 0;
   width: 100%;
 }
 ​
 .lazy-component-leave {
   opacity: 1;
 }
 ​
 .lazy-component-leave-to {
   opacity: 0;
 }
 ​
 .lazy-component-leave-active {
   transition: opacity 0.5s;
 }
 </style>

性能优化之组件懒加载
最后一句

学习心得!若有不正,还望斧正。希望掘友们不要吝啬对我的建议。