Vue插件開(kāi)發(fā)初體驗(yàn)——(懶加載)
前言
閑來(lái)無(wú)事,想自己開(kāi)發(fā)一個(gè)簡(jiǎn)單的Vue懶加載插件威酒,能力的提升我覺(jué)得是可以通過(guò)編寫(xiě)插件實(shí)現(xiàn)窑睁,研究了一下官網(wǎng)的Vue插件編寫(xiě)。馬上自己獨(dú)立開(kāi)始編寫(xiě)懶加載插件葵孤。
一担钮、寫(xiě)在前面
由于我在網(wǎng)上看了很多關(guān)于vue插件的實(shí)例,發(fā)現(xiàn)幾乎都沒(méi)有什么詳細(xì)的教程尤仍,自己琢磨了半天也沒(méi)有什么進(jìn)步箫津。都是寫(xiě)的比較精簡(jiǎn)。終于狠下心來(lái)宰啦,我們來(lái)自己憋一個(gè)插件出來(lái)吧w(?Д?)w苏遥!這次我們通過(guò)一個(gè)常用插件——懶加載,來(lái)體驗(yàn)一下vue的插件開(kāi)發(fā)赡模。
萌新小白暖眼,前端開(kāi)發(fā)入門(mén)一年不到,歡迎交流纺裁,給我提出批評(píng)意見(jiàn)謝謝=氤Α!
(原創(chuàng)來(lái)源我的博客 歡迎交流欺缘,GitHub項(xiàng)目地址:vue-simple-lazyload上面是所有源碼栋豫,喜歡就點(diǎn)個(gè)star吧)
二、前期準(zhǔn)備
2.1 選擇合適的打包工具
合適的打包工具可以達(dá)到事半功倍的效果谚殊。一開(kāi)始我的首選有兩個(gè)丧鸯,一個(gè)是webpack,一個(gè)是rollup嫩絮。下面簡(jiǎn)單介紹一下我為什么選擇了rollup丛肢。
眾所周知,webpack是一個(gè)幾乎囊括了所有靜態(tài)資源剿干,可以動(dòng)態(tài)按需加載的一個(gè)包工具蜂怎。而rollup也是一個(gè)模塊打包器,可以把一個(gè)大塊復(fù)雜的代碼拆分成各個(gè)小模塊置尔。
深思熟慮后杠步,我覺(jué)得webpack也可以打包,但是首先,有點(diǎn)“殺雞焉用牛刀”的感覺(jué)幽歼。而我的這個(gè)懶加載插件則需要提供給別人使用朵锣,同時(shí)又要保證整個(gè)插件的“輕量性”(打包完大概6KB,而webpack則比較大)甸私,不喜歡像webpack那樣在這插件上臃腫的表現(xiàn)诚些。
對(duì)于非應(yīng)用級(jí)的程序,我比較傾向于使用rollup.js皇型。
2.2 確認(rèn)項(xiàng)目結(jié)構(gòu)
|——package.json
|——config
| |——rollup.config.js
|——dist
| |——bundle.js
|——src
| |——index.js
| |——directive.js
| |——mixin.js
| |——imagebox.js
| |——lazyload.js
| |——utils
| | |——utils.js
| |——cores
| |——eventlistener.js
config文件夾下放置rollup的配置文件泣刹。src為源文件夾,cores下面的文件夾為主要的模塊犀被,utils為工具類椅您,主要是一些可以通用的模塊方法。大概的結(jié)構(gòu)就是這樣寡键。
2.3 設(shè)計(jì)思路
好的設(shè)計(jì)思路是一個(gè)插件的靈魂掀泳,我以自己不在道上的設(shè)計(jì)能力,借鑒了許多大神的思想西轩!很不自信地設(shè)計(jì)了懶加載的插件項(xiàng)目結(jié)構(gòu)员舵,見(jiàn)下:
- index.js:這個(gè)文件是入口總文件,主要為了暴露vue插件的install方法藕畔,供外部調(diào)用马僻。
- directive.js:這個(gè)文件是vue指令的文件,懶加載主要使用到的就是指令注服,我們這里定義v-simple-lazy指令邏輯寫(xiě)到這個(gè)文件內(nèi)韭邓。
- mixin.js:我們的核心骨來(lái)了,這個(gè)混合(mixin)定義在vue中解釋得有點(diǎn)不清楚溶弟,可能是我理解能力有問(wèn)題吧(?_?)女淑。我們這里簡(jiǎn)單點(diǎn)說(shuō),混合就是vue的一些性質(zhì)(比如雙綁)按照它給的規(guī)則辜御,跟你自己的定義鸭你,可以把你定義的一些變量,混入到vue實(shí)例中擒权,也就是說(shuō)袱巨,當(dāng)做vue的$vm實(shí)例的變量來(lái)用,同時(shí)擁有了實(shí)例的一些特性碳抄。
上述都是關(guān)于vue插件的一些文件愉老,下面我們來(lái)說(shuō)我們自己定義的一些:
- imagebox.js(類):顧名思義,這個(gè)就是一個(gè)圖像的盒子(box)纳鼎,用來(lái)存儲(chǔ)你定義懶加載的圖片的一些預(yù)加載的地址和一些方法俺夕。我們這里設(shè)計(jì)5個(gè)數(shù)組用來(lái)存放裳凸,分別是:item贱鄙,itemAlready劝贸,itemPending,itemFailed逗宁,itemAll映九,分別作用是:用來(lái)存放當(dāng)前需要加載的圖片地址和元素,已經(jīng)加載完的圖片地址和元素瞎颗,正在加載中的圖片地址和元素件甥,加載失敗的圖片地址和元素,所有綁定了指令的圖片地址和元素哼拔。原理我們下面會(huì)說(shuō)引有。
- core的eventlistener.js(類):這個(gè)文件很重要,主要存放當(dāng)我們監(jiān)聽(tīng)滾動(dòng)條的時(shí)候倦逐,需要處理的一些邏輯譬正。
- lazyload.js:這個(gè)主要用來(lái)存放一些不變的量,比如加載圖片的默認(rèn)地址等等檬姥。
2.4 編寫(xiě)思路和原理
原理如下:
通過(guò)監(jiān)聽(tīng)滾動(dòng)條滾動(dòng)曾我,來(lái)不停地遍歷上述imagebox里面的item數(shù)組(這個(gè)數(shù)組存放著需要懶加載的圖片預(yù)加載地址),如果item里面有值健民,那么就進(jìn)行圖片的請(qǐng)求抒巢。進(jìn)行請(qǐng)求的同時(shí),我們把這個(gè)元素加入到itemPending里面去秉犹,如果加載完了就放到itemAlready里面蛉谜,失敗的放到failed里面,這就是基本的實(shí)現(xiàn)思路崇堵。
懶加載的實(shí)現(xiàn)過(guò)程悦陋,我們這里先精簡(jiǎn)化。具體思路如下:
=》把所有用指令綁定的元素添加數(shù)組初始化
=》監(jiān)聽(tīng)滾動(dòng)條滾動(dòng)
=》判斷元素是否進(jìn)入可視范圍
=》如果進(jìn)入可視范圍筑辨,進(jìn)行src預(yù)加載(存入緩存數(shù)組)
=》對(duì)于pending的圖片俺驶,進(jìn)行正在加載賦值,對(duì)于finsh完的圖片棍辕,加載預(yù)加載src里面的值暮现,對(duì)于error的圖片,進(jìn)行錯(cuò)誤圖片src賦值
三楚昭、主要代碼的編寫(xiě)
3.1 確認(rèn)入口文件
Vue插件里面介紹是這樣的
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或?qū)傩? Vue.myGlobalMethod = function () {
// 邏輯...
}
// 2. 添加全局資源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 邏輯...
}
...
})
// 3. 注入組件
Vue.mixin({
created: function () {
// 邏輯...
}
...
})
// 4. 添加實(shí)例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 邏輯...
}
}
在外面暴露的方法就是install,使用的時(shí)候直接Vue.use("插件名稱")直接可以使用栖袋。我們?cè)趇nstall方法里面填寫(xiě)關(guān)于指令(directive)和混合(mixin),然后對(duì)外公開(kāi)這個(gè)方法抚太,option沒(méi)填寫(xiě)的話就是默認(rèn)空對(duì)象塘幅。
混合主要是為了混入vue內(nèi)部屬性昔案,是除了以上全局方法后又可以在全局使用的一種方式。
3.2 先編寫(xiě)配置文件
工欲善其事必先利其器电媳,我們先編寫(xiě)rollup的配置代碼(這里做了精簡(jiǎn)踏揣,復(fù)雜的可以參照我的github程序)。
rollup.config.js
import buble from 'rollup-plugin-buble';
import babel from 'rollup-plugin-babel';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
export default {
input: 'src/index.js',//入口
output: {
file: 'dist/bundle.js',//輸出的出口
format: 'umd',//格式:相似的還有cjs匾乓,amd捞稿,iife等
},
moduleName: 'LazyLoad',//打包的模塊名稱,可以再Vue.use()方法使用
plugins:[
resolve(),
commonjs(),//支持commonJS
buble(),
babel({//關(guān)于ES6
exclude: 'node_modules/**' // 只編譯我們的源代碼
})
]
};
package.json
{
"name": "lazyload",
"version": "1.0.0",
"description": "vue懶加載插件",
"main": "index.js",
"scripts": {
"main": "rollup -c config/rollup.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "TangHy",
"license": "MIT",
"dependencies": {
"path": "^0.12.7",
"rollup": "^0.57.1"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-buble": "^0.19.2",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-node-resolve": "^3.3.0"
}
}
注意其中的命令:
rollup -c config/rollup.config.js
后面的config/...是路徑拼缝,這里注意一下娱局,我們只需要運(yùn)行
npm run main
就可以進(jìn)行打包了。
3.3 編寫(xiě)主程序代碼
3.3.1 directive.js
首先我們要先畫(huà)好雛形咧七,如何將eventlistener文件和directive聯(lián)系在一起衰齐?
第一步首先肯定是引用eventlistener,同時(shí)因?yàn)樗且粋€(gè)類继阻,我們這里只要指令每次inserted的時(shí)候耻涛,我們都新new一個(gè)對(duì)象進(jìn)行初始化,把能傳的值都傳過(guò)去穴翩∪冢可以看到下面的操作
import eventlistener from './cores/eventlistener'
var listener = null;
export default {
inserted: function (el,binding, vnode, oldVnode) {
var EventListener = new eventlistener(el,binding, vnode);//這里我們new一個(gè)新對(duì)象,把el(元素)芒帕,binding(綁定的值)歉嗓,vnode(虛擬node)都傳過(guò)去
listener = EventListener;
EventListener.init();//假設(shè)有一個(gè)init初始化函數(shù)
EventListener.startListen();//這里初始化完進(jìn)行監(jiān)聽(tīng)
},
update: function(el,{name,value,oldValue,expression}, vnode, oldVnode){
},
unbind: function(){
}
}
其次我們要考慮在update鉤子中,我們需要干什么背蟆?
有這樣一種業(yè)務(wù):當(dāng)你一次性綁定完所有數(shù)據(jù)的時(shí)候鉴分,如果這個(gè)圖片已經(jīng)預(yù)加載完了,那么你再怎么改變這個(gè)指令綁定的值带膀,都不能夠?qū)崿F(xiàn)刷新圖片了志珍,所以我們?cè)趗pdate更新新的圖片地址是有必要的。同時(shí)解綁的時(shí)候垛叨,取消綁定也是有必要的伦糯,那么繼續(xù)往下寫(xiě):
import eventlistener from './cores/eventlistener'
var listener = null;
export default {
inserted: function (el,binding, vnode, oldVnode) {
var EventListener = new eventlistener(el,binding, vnode);//這里我們new一個(gè)新對(duì)象,把el(元素)嗽元,binding(綁定的值)敛纲,vnode(虛擬node)都傳過(guò)去
listener = EventListener;
EventListener.init();//假設(shè)有一個(gè)init初始化函數(shù)
EventListener.startListen();//這里初始化完進(jìn)行監(jiān)聽(tīng)
},
update: function(el,{name,value,oldValue,expression}, vnode, oldVnode){
if(value === oldValue){//沒(méi)有變化就返回
return;
}
listener.update(el,value);//有變化就進(jìn)行更新(假設(shè)有update這個(gè)方法)
},
unbind: function(){
listener.removeListen();//解綁移除監(jiān)聽(tīng)
}
}
先寫(xiě)生命周期中inserted的時(shí)候綁定監(jiān)聽(tīng)。加入的時(shí)候new一個(gè)監(jiān)聽(tīng)對(duì)象保存所有包括所有dom的剂癌。我們繼續(xù)往下看淤翔。
3.4 核心代碼
3.4.1 imagebox.js類
首先先把之前說(shuō)到的幾個(gè)數(shù)組都初始化了。那么作為圖像盒子(imagebox)的對(duì)象實(shí)例佩谷,我們需要哪些方法或?qū)傩阅兀?/p>
首先初始化的時(shí)候的add方法肯定要旁壮,需要判斷一下是否有這個(gè)元素监嗜,沒(méi)有的話就加入到item里面去。同時(shí)類似的還有addFailed抡谐,addPending等方法裁奇。
如果item中的元素加載完了,那么隨之而來(lái)的就需要?jiǎng)h除item中的元素,那么對(duì)應(yīng)的remove方法也是必須要的,同時(shí)類似的還有removePending等方法。
export default class ImageBox {
constructor() {
this.eleAll = [];
this.item = [];
this.itemAlready = [];
this.itemPending = [];
this.itemFailed = [];
}
add(ele,src) {//insert插入的時(shí)候把所有的dom加入到數(shù)組中去初始化
const index = this.itemAlready.findIndex((_item)=>{
return _item.ele === ele;
})
if(index === -1){
this.item.push({
ele:ele,
src:src
})
}
}
addPending(ele,src){
this._addPending(ele,src);
this._remove(ele);
}
}
上述是一個(gè)圖片的box童叠,用于存取頁(yè)面加載時(shí)候框喳,image圖片對(duì)象的box存取课幕。主要思路是分了三個(gè)數(shù)組厦坛,一個(gè)存儲(chǔ)所有的圖片,一個(gè)存儲(chǔ)正在加載的圖片乍惊,一個(gè)存儲(chǔ)加載失敗的圖片杜秸,然后最重要的是!H笠铩撬碟!
把這個(gè)imagebox要混入到全局,使其可以當(dāng)做全局變量在全局使用莉撇。
mixin.js
import imagebox from './imagebox'
const mixin = {
data () {
return {
imagebox: new imagebox()//這里聲明一個(gè)new對(duì)象呢蛤,存在全局的變量中,混入vue內(nèi)部棍郎,可以全局使用
}
}
}
export default mixin;
根據(jù)上述思路其障,完成下列代碼:
(補(bǔ)充:這里有個(gè)update方法,思路是更新了后涂佃,進(jìn)行所有的數(shù)組遍歷励翼,找到相對(duì)應(yīng)的元素,然后進(jìn)行src就是其值的更新)
export default class ImageBox {
constructor() {
this.eleAll = [];
this.item = [];
this.itemAlready = [];
this.itemPending = [];
this.itemFailed = [];
}
add(ele,src) {
const index = this.itemAlready.findIndex((_item)=>{
return _item.ele === ele;
})
if(index === -1){
this.item.push({
ele:ele,
src:src
})
}
}
update(ele,src){
let index = this.itemAlready.findIndex(item=>{
return item.ele === ele;
});
if(index != -1){
this.itemAlready.splice(index,1);
this.add(ele,src);
return;
};
let _index = this.itemFailed.findIndex(item=>{
return item.ele === ele;
});
if(_index !=-1){
this.itemFailed.splice(_index,1);
this.add(ele,src);
return;
};
}
addFailed(ele,src){
this._addFailed(ele,src);
this._removeFromPending(ele);
}
addPending(ele,src){
this._addPending(ele,src);
this._remove(ele);
}
addAlready(ele,src){
this._addAlready(ele,src);
this._removeFromPending(ele);
}
_addAlready(ele,src) {
const index = this.itemAlready.findIndex((_item)=>{
return _item.ele === ele;
})
if(index === -1){
this.itemAlready.push({
ele:ele,
src:src
})
}
}
_addPending(ele,src) {
const index = this.itemPending.findIndex((_item)=>{
return _item.ele === ele;
})
if(index === -1){
this.itemPending.push({
ele:ele,
src:src
})
}
}
_addFailed(ele,src) {
const index = this.itemFailed.findIndex((_item)=>{
return _item.ele === ele;
})
if(index === -1){
this.itemFailed.push({
ele:ele,
src:src
})
}
}
_remove(ele) {
const index = this.item.findIndex((_item)=>{
return _item.ele === ele;
});
if(index!=-1){
this.item.splice(index,1);
}
}
_removeFromPending(ele) {
const index = this.itemPending.findIndex((_item)=>{
return _item.ele === ele;
});
if(index!=-1){
this.itemPending.splice(index,1);
}
}
}
3.4.2 utils.js
const isSeen = function(item,imagebox){
var ele = item.ele;
var src = item.src;
//圖片距離頁(yè)面頂部的距離
var top = ele.getBoundingClientRect().top;
//頁(yè)面可視區(qū)域的高度
var windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
//top + 10 已經(jīng)進(jìn)入了可視區(qū)域10像素
if(top + 10 < windowHeight){
return true;
}else{
return false;
}
}
export {
isSeen
};
3.4.3 eventlistener.js
這個(gè)文件主要是監(jiān)聽(tīng)的一些邏輯辜荠,那么肯定需要一些對(duì)象實(shí)例的屬性汽抚。首先el元素肯定需要,binding伯病,vnode造烁,$vm肯定都先寫(xiě)進(jìn)來(lái)。
其次imagebox肯定也需要午笛,是圖片的對(duì)象實(shí)例惭蟋。
init的方法這里和imagebox中的add方法聯(lián)系起來(lái),init一個(gè)就加入imagebox中的item一個(gè)新的元素季研。
startListen方法是用于在監(jiān)聽(tīng)后進(jìn)行邏輯操作敞葛。
import {isSeen} from '../utils/utils'//引入工具類的里面的是否看得見(jiàn)元素這個(gè)方法判斷
export default class EventListener {
constructor(el,binding,vnode) {
this.el = el;//初始化各種需要的屬性
this.binding = binding;
this.vnode = vnode;
this.imagebox = null;
this.$vm = vnode.context;
this.$lazyload = vnode.context.$lazyload//混合mixin進(jìn)去的選項(xiàng)
}
init(){
if(!typeof this.binding.value === 'string'){
throw new Error("您的圖片源不是String類型,請(qǐng)重試");
return;
}
this.imagebox = this.vnode.context.imagebox;
this.imagebox.add(this.el,this.binding.value);//每有一個(gè)item与涡,就往box中增加一個(gè)新的元素
this.listenProcess();
}
startListen(){
const _self = this;
document.addEventListener('scroll',(e)=>{
_self.listenProcess(e);//這里開(kāi)始操作
})
}
}
上面主要初始化了很多屬性惹谐,包括vue的虛擬dom和各種包括el元素dom持偏,binding指令傳過(guò)來(lái)的值等等初始化。
此文件主要為了處理監(jiān)聽(tīng)頁(yè)面滾動(dòng)的氨肌,監(jiān)聽(tīng)是否圖片進(jìn)入到可視范圍內(nèi)鸿秆,然后進(jìn)行一系列下方的各種操作。
根據(jù)image.onload怎囚,image.onerror
方法進(jìn)行圖片預(yù)加載的邏輯操作,如果看得見(jiàn)這個(gè)圖片,那么就進(jìn)行圖片的加載(同時(shí)加入到pending里面去)卿叽,加載完進(jìn)行判斷。
下列是process的函數(shù)listenProcess
:
const _self = this;
if(this.imagebox.item.length == 0){
return;
};
this.imagebox.item.forEach((item)=>{
if(isSeen(item)){//這里判斷元素是否看得見(jiàn)
var image = new Image();//這里在賦值src前new一個(gè)image對(duì)象進(jìn)行緩存恳守,緩沖一下考婴,可以做后續(xù)的加載或失敗的函數(shù)處理
image.src = item.src;
_self._imageStyle(item);//改變item的樣式
_self.imagebox.addPending(item.ele,item.src);//在對(duì)象imagebox中加入了正在pending請(qǐng)求的item(后續(xù)會(huì)介紹imagebox類)
image.onload = function(){//加載成功的處理
if(image.complete){
_self.imageOnload(item);
}
}
image.onerror = function(){//加載失敗的處理
_self.imageOnerror(item);
}
}
})
還有其余的一些方法:
imageOnload(item){//圖片加載完的操作
this._removeImageStyle(item.ele);
this.imagebox.addAlready(item.ele,item.src);//添加到已經(jīng)加載完的item數(shù)組里面
this._imageSet(item.ele,item.src)
}
imageOnerror(item){//出現(xiàn)錯(cuò)誤的時(shí)候
this._removeImageStyle(item.ele);
this.imagebox.addFailed(item.ele,item.src);//添加到出現(xiàn)錯(cuò)誤item數(shù)組里面
this._imageSet(item.ele,this.$lazyload.options.errorUrl)//把配置中的錯(cuò)誤圖片url填入
}
_imageStyle(item){
item.ele.style.background = `url(${this.$lazyload.options.loadUrl}) no-repeat center`;
}
_removeImageStyle(ele){
ele.style.background = '';
}
_imageSet(ele,value){//關(guān)于圖片賦值src的操作
ele.src = value;
}
補(bǔ)充一個(gè)update方法:
update(ele,src){
console.log("更新了");
console.log(this.imagebox);
this.imagebox.update(ele,src);//調(diào)用imagebox中的update方法
this.listenProcess();//再進(jìn)行是否看得見(jiàn)的process操作
}
下面是所有的eventlistener.js代碼:
import {isSeen} from '../utils/utils'
export default class EventListener {
constructor(el,binding,vnode) {
this.el = el;
this.binding = binding;
this.vnode = vnode;
this.imagebox = null;
this.$vm = vnode.context;
this.$lazyload = vnode.context.$lazyload
}
init(){
if(!typeof this.binding.value === 'string'){
throw new Error("您的圖片源不是String類型,請(qǐng)重試");
return;
}
this.imagebox = this.vnode.context.imagebox;
this.imagebox.add(this.el,this.binding.value);
this.listenProcess();
}
startListen(){
var listenProcess = this.listenProcess;
document.addEventListener('scroll',listenProcess.bind(this),false);
}
removeListen(){
var listenProcess = this.listenProcess;
document.removeEventListener('scroll',listenProcess.bind(this),false);
}
listenProcess(){
const _self = this;
if(this.imagebox.item.length == 0){
return;
};
this.imagebox.item.forEach((item)=>{
if(isSeen(item)){
var image = new Image();
image.src = item.src;
_self._imageStyle(item);
_self.imagebox.addPending(item.ele,item.src);
image.onload = function(){
if(image.complete){
_self.imageOnload(item);
}
}
image.onerror = function(){
_self.imageOnerror(item);
}
}
})
}
update(ele,src){
console.log("更新了");
console.log(this.imagebox);
this.imagebox.update(ele,src);
this.listenProcess();
}
imageOnload(item){
this._removeImageStyle(item.ele);
this.imagebox.addAlready(item.ele,item.src);
this._imageSet(item.ele,item.src)
}
imageOnerror(item){
this._removeImageStyle(item.ele);
this.imagebox.addFailed(item.ele,item.src);
this._imageSet(item.ele,this.$lazyload.options.errorUrl)
}
_imageStyle(item){
item.ele.style.background = `url(${this.$lazyload.options.loadUrl}) no-repeat center`;
}
_removeImageStyle(ele){
ele.style.background = '';
}
_imageSet(ele,value){
ele.src = value;
}
}
所有的解釋都已經(jīng)寫(xiě)在上面的代碼塊里面了催烘。
3.4.4 lazyload.js
最后把一些加載的圖片默認(rèn)配置或者失敗圖片地址完成下列代碼:
const DEFAULT_ERROR_URL = './404.svg';
const DEFAULT_LOAD_URL = './loading-spin.svg';
export default class LazyLoad {
constructor() {
this.options = {
loadUrl: DEFAULT_LOAD_URL,
errorUrl: DEFAULT_ERROR_URL
};
}
register(options){
Object.assign(this.options, options);
}
}
此類暫時(shí)用來(lái)存儲(chǔ)各種配置和lazy的預(yù)定默認(rèn)值沥阱,options里面存加載的時(shí)候的圖片地址和錯(cuò)誤加載的時(shí)候的圖片地址。
默認(rèn)值是最上面兩個(gè)值伊群,是不傳數(shù)據(jù)默認(rèn)的配置考杉。
3.4.5 index.js
import directive from './directive';
import mixin from './mixin';
import lazyload from './lazyload';
const install = ( Vue,options = {} )=>{
const lazy = new lazyload();
lazy.register(options);
Vue.prototype.$lazyload = lazy
Vue.mixin(mixin);
Vue.directive('simple-lazy',directive);
}
export default {
install
};
把上述所有的進(jìn)行一個(gè)綜合,放在這個(gè)入口文件進(jìn)行向外暴露舰始。
index就是整個(gè)項(xiàng)目的入口文件崇棠,至此我們完成了懶加載插件的基本代碼編寫(xiě)。
四丸卷、打包成.js可以外部直接引用
打包后的代碼:
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.LazyLoad = factory());
}(this, (function () { 'use strict';
var isSeen = function isSeen(item, imagebox) {
var ele = item.ele;
var src = item.src;
//圖片距離頁(yè)面頂部的距離
var top = ele.getBoundingClientRect().top;
//頁(yè)面可視區(qū)域的高度
var windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
//top + 10 已經(jīng)進(jìn)入了可視區(qū)域10像素
if (top + 10 < windowHeight) {
return true;
} else {
return false;
}
};
var EventListener = function EventListener(el, binding, vnode) {
this.el = el;
this.binding = binding;
this.vnode = vnode;
this.imagebox = null;
this.$vm = vnode.context;
this.$lazyload = vnode.context.$lazyload;
};
EventListener.prototype.init = function init() {
this.imagebox = this.vnode.context.imagebox;
this.imagebox.add(this.el, this.binding.value);
this.listenProcess();
};
EventListener.prototype.startListen = function startListen() {
var listenProcess = this.listenProcess;
window.addEventListener('scroll', listenProcess.bind(this), false);
};
EventListener.prototype.removeListen = function removeListen() {
var listenProcess = this.listenProcess;
window.removeEventListener('scroll', listenProcess.bind(this), false);
};
EventListener.prototype.listenProcess = function listenProcess() {
var _self = this;
if (this.imagebox.item.length == 0) {
return;
}
this.imagebox.item.forEach(function (item) {
if (isSeen(item)) {
var image = new Image();
image.src = item.src;
_self._imageStyle(item);
_self.imagebox.addPending(item.ele, item.src);
image.onload = function () {
if (image.complete) {
_self.imageOnload(item);
}
};
image.onerror = function () {
_self.imageOnerror(item);
};
}
});
};
EventListener.prototype.update = function update(ele, src) {
console.log("更新了");
console.log(this.imagebox);
this.imagebox.update(ele, src);
this.listenProcess();
};
EventListener.prototype.imageOnload = function imageOnload(item) {
this._removeImageStyle(item.ele);
this.imagebox.addAlready(item.ele, item.src);
this._imageSet(item.ele, item.src);
};
EventListener.prototype.imageOnerror = function imageOnerror(item) {
this._removeImageStyle(item.ele);
this.imagebox.addFailed(item.ele, item.src);
this._imageSet(item.ele, this.$lazyload.options.errorUrl);
};
EventListener.prototype._imageStyle = function _imageStyle(item) {
item.ele.style.background = "url(" + this.$lazyload.options.loadUrl + ") no-repeat center";
};
EventListener.prototype._removeImageStyle = function _removeImageStyle(ele) {
ele.style.background = '';
};
EventListener.prototype._imageSet = function _imageSet(ele, value) {
ele.src = value;
};
var listener = null;
var directive = {
inserted: function inserted(el, binding, vnode, oldVnode) {
var EventListener$$1 = new EventListener(el, binding, vnode);
listener = EventListener$$1;
EventListener$$1.init();
EventListener$$1.startListen();
},
update: function update(el, ref, vnode, oldVnode) {
var name = ref.name;
var value = ref.value;
var oldValue = ref.oldValue;
var expression = ref.expression;
if (value === oldValue) {
return;
}
listener.update(el, value);
},
unbind: function unbind() {
listener.removeListen();
}
};
var ImageBox = function ImageBox() {
this.eleAll = [];
this.item = [];
this.itemAlready = [];
this.itemPending = [];
this.itemFailed = [];
};
ImageBox.prototype.add = function add(ele, src) {
var index = this.itemAlready.findIndex(function (_item) {
return _item.ele === ele;
});
if (index === -1) {
this.item.push({
ele: ele,
src: src
});
}
};
ImageBox.prototype.update = function update(ele, src) {
var index = this.itemAlready.findIndex(function (item) {
return item.ele === ele;
});
if (index != -1) {
this.itemAlready.splice(index, 1);
this.add(ele, src);
return;
}
var _index = this.itemFailed.findIndex(function (item) {
return item.ele === ele;
});
if (_index != -1) {
this.itemFailed.splice(_index, 1);
this.add(ele, src);
return;
}};
ImageBox.prototype.addFailed = function addFailed(ele, src) {
this._addFailed(ele, src);
this._removeFromPending(ele);
};
ImageBox.prototype.addPending = function addPending(ele, src) {
this._addPending(ele, src);
this._remove(ele);
};
ImageBox.prototype.addAlready = function addAlready(ele, src) {
this._addAlready(ele, src);
this._removeFromPending(ele);
};
ImageBox.prototype._addAlready = function _addAlready(ele, src) {
var index = this.itemAlready.findIndex(function (_item) {
return _item.ele === ele;
});
if (index === -1) {
this.itemAlready.push({
ele: ele,
src: src
});
}
};
ImageBox.prototype._addPending = function _addPending(ele, src) {
var index = this.itemPending.findIndex(function (_item) {
return _item.ele === ele;
});
if (index === -1) {
this.itemPending.push({
ele: ele,
src: src
});
}
};
ImageBox.prototype._addFailed = function _addFailed(ele, src) {
var index = this.itemFailed.findIndex(function (_item) {
return _item.ele === ele;
});
if (index === -1) {
this.itemFailed.push({
ele: ele,
src: src
});
}
};
ImageBox.prototype._remove = function _remove(ele) {
var index = this.item.findIndex(function (_item) {
return _item.ele === ele;
});
if (index != -1) {
this.item.splice(index, 1);
}
};
ImageBox.prototype._removeFromPending = function _removeFromPending(ele) {
var index = this.itemPending.findIndex(function (_item) {
return _item.ele === ele;
});
if (index != -1) {
this.itemPending.splice(index, 1);
}
};
var mixin = {
data: function data() {
return {
imagebox: new ImageBox()
};
}
};
var DEFAULT_ERROR_URL = './404.svg';
var DEFAULT_LOAD_URL = './loading-spin.svg';
var LazyLoad = function LazyLoad() {
this.options = {
loadUrl: DEFAULT_LOAD_URL,
errorUrl: DEFAULT_ERROR_URL
};
};
LazyLoad.prototype.register = function register(options) {
Object.assign(this.options, options);
};
var install = function install(Vue, options) {
if (options === void 0) options = {};
var lazy = new LazyLoad();
lazy.register(options);
Vue.prototype.$lazyload = lazy;
Vue.mixin(mixin);
Vue.directive('simple-lazy', directive);
};
var index = {
install: install
};
return index;
})));
使用方法
Vue.use(LazyLoad,{
loadUrl:'./loading-spin.svg',//這里寫(xiě)你的加載時(shí)候的圖片配置
errorUrl:'./404.svg'//錯(cuò)誤加載的圖片配置
});
元素中使用指令
<img v-simple-lazy="item" v-for="(item,$key) in imageArr">
imageArr測(cè)試數(shù)據(jù)
imageArr:[
'http://covteam.u.qiniudn.com/test16.jpg?imageView2/2/format/webp',
'http://covteam.u.qiniudn.com/test14.jpg?imageView2/2/format/webp',
'http://covteam.u.qiniudn.com/test15.jpg?imageView2/2/format/webp',
'http://covteam.u.qiniudn.com/test17.jpg?imageView2/2/format/webp',
'http://hilongjw.github.io/vue-lazyload/dist/test9.jpg',
'http://hilongjw.github.io/vue-lazyload/dist/test10.jpg',
'http://hilongjw.github.io/vue-lazyload/dist/test14.jpg'
]
測(cè)試地址:戳我戳我
五枕稀、后記
其實(shí)這些代碼的編寫(xiě)還是比較簡(jiǎn)單的,寫(xiě)完過(guò)后進(jìn)行總結(jié)及老,你會(huì)發(fā)現(xiàn)抽莱,其中最難的是:
整個(gè)項(xiàng)目的結(jié)構(gòu),和代碼模塊之間的邏輯關(guān)系骄恶。
這個(gè)才是最難掌握的食铐,如果涉及到大一點(diǎn)的項(xiàng)目,好一點(diǎn)的項(xiàng)目結(jié)構(gòu)能讓整個(gè)項(xiàng)目進(jìn)度等等因素發(fā)生巨大的變化僧鲁,提升巨大的效率虐呻。而寫(xiě)插件最難的就是在這。
如何有效地拆分代碼寞秃?如何有效地進(jìn)行項(xiàng)目結(jié)構(gòu)的構(gòu)造? 這才是整個(gè)插件編寫(xiě)的核心斟叼。
之前寫(xiě)過(guò)一個(gè)vue關(guān)于表單驗(yàn)證的插件,也是被項(xiàng)目結(jié)構(gòu)搞得焦頭爛額春寿,這里把簡(jiǎn)單的懶加載基本代碼做一個(gè)總結(jié)朗涩。寫(xiě)這個(gè)純粹是個(gè)人興趣。
所以我深知寫(xiě)插件的時(shí)候绑改,它結(jié)構(gòu)和模塊化的重要性谢床。而結(jié)構(gòu)和模塊化的優(yōu)秀兄一,會(huì)讓你事半功倍。另外歡迎大家來(lái)我的博客參觀唐益達(dá)的博客识腿,只寫(xiě)原創(chuàng)出革。
原創(chuàng)來(lái)源我的博客 http://www.tangyida.top/detail/149 歡迎交流。
GitHub項(xiàng)目地址:https://github.com/xdnloveme/vue-simple-lazyload
萌新小白渡讼,前端開(kāi)發(fā)入門(mén)一年不到骂束,歡迎交流!