懒加载
为什么需要懒加载?
像vue这种单页面应用,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,时间过长,会出啊先长时间的白屏,即使做了loading也是不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时。
简单的说就是:进入首页不用一次加载过多资源,造成用时过长。
懒加载
- 也叫延迟加载,即在需要的时候进行加载,随用随载。
- 个人根据功能划分为图片的懒加载和组件的懒加载。
图片懒加载
使用vue-lazyload插件:
-
下载
$ npm install vue-lazyload -D -
注册插件
// 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 })
-
在页面中使用
<!-- 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完全相反。
-
给每一个状态添加样式
<style> img[lazy=loading] { } img[lazy=error] { } img[lazy=loaded] { } </style>
组件懒加载
主要分以下几步:
1.兼容低版本浏览器 => 2.新建懒加载组件 => 3.新建公共骨架屏组件 => 4.异步加载子组件 => 5.页面中使用
1.兼容低版本浏览器
该项目依赖 IntersectionObserver API,如需在较低版本浏览器运行,需要首先处理兼容低版本浏览器,需要引入插件 IntersectionObserver API polyfill
-
使用
// 1.下载 $ npm install intersection-observer -D // 2.在mian.js引入 import 'intersection-observer';
2.新建一个VueLazyComponent.vue 文件
因为使用的懒加载组件插件部分不满足我们官网项目,所以把vue-lazy-component插件的核心代码取出来,新建一个VueLazyComponent.vue文件存放,在项目中需要使用到懒加载组件的页面引入即可。
-
在需要使用懒加载的页面引入 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> -
VueLazyComponent.vue 源码解读
-
使用到的参数和事件
Props
参数 说明 类型 可选值 默认值 viewport 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 HTMLElement true null,代表视窗direction 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向String true verticalthreshold 预加载阈值, css单位 String true 0pxtagName 包裹组件的外层容器的标签名 String true divtimeout 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 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 component: () => import('/component_url/'); 2 component: (resolve) => { require(['/component_url/'],resolve) } -
页面中路由配置
// 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.页面中使用
-
项目中使用
<!-- 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/>组件渲染成功。
- 通过
-
使用这个懒加载组件遇到的问题
-
用懒加载组件包裹的子组件
<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()方法,能解决动画未执行的问题.
-
