懶加載
為什么需要懶加載?
像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插件:
-
下載
$ npm install vue-lazyload -D -
注冊(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 })
-
在頁(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完全相反。
-
給每一個(gè)狀態(tài)添加樣式
<style> img[lazy=loading] { } img[lazy=error] { } img[lazy=loaded] { } </style>
組件懶加載
主要分以下幾步:
1.兼容低版本瀏覽器 => 2.新建懶加載組件 => 3.新建公共骨架屏組件 => 4.異步加載子組件 => 5.頁(yè)面中使用
1.兼容低版本瀏覽器
該項(xiàng)目依賴 IntersectionObserver API,如需在較低版本瀏覽器運(yùn)行,需要首先處理兼容低版本瀏覽器,需要引入插件 IntersectionObserver API polyfill
-
使用
// 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è)面引入即可。
-
在需要使用懶加載的頁(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> -
VueLazyComponent.vue 源碼解讀
-
使用到的參數(shù)和事件
Props
參數(shù) 說(shuō)明 類型 可選值 默認(rèn)值 viewport 組件所在的視口,如果組件是在頁(yè)面容器內(nèi)滾動(dòng),視口就是該容器 HTMLElement true null,代表視窗direction 視口的滾動(dòng)方向, vertical代表垂直方向,horizontal代表水平方向String true verticalthreshold 預(yù)加載閾值, css單位 String true 0pxtagName 包裹組件的外層容器的標(biāo)簽名 String true divtimeout 等待時(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.異步引入組件
-
兩種語(yǔ)法形式
// 兩種語(yǔ)法形式: 1 component: () => import('/component_url/'); 2 component: (resolve) => { require(['/component_url/'],resolve) } -
頁(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è)面中使用
-
項(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/>組件渲染成功。
- 通過(guò)
-
使用這個(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)題.
-