懒加载
为什么需要懒加载?
像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 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 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()
方法,能解决动画未执行的问题.
-