我是一棵树 2019-06-27
由于我在网上看了很多关于vue插件的实例,发现几乎都没有什么详细的教程,自己琢磨了半天也没有什么进步。都是写的比较精简。终于狠下心来,我们来自己憋一个插件出来吧w(゚Д゚)w!这次我们通过一个常用插件——懒加载,来体验一下vue的插件开发。
萌新小白,前端开发入门一年不到,欢迎交流,给我提出批评意见谢谢!!
(原创来源我的博客 欢迎交流,GitHub项目地址:vue-simple-lazyload上面是所有源码,觉得对写插件有帮助就点个star吧)
合适的打包工具可以达到事半功倍的效果。一开始我的首选有两个,一个是webpack,一个是rollup。下面简单介绍一下我为什么选择了rollup。
众所周知,webpack是一个几乎囊括了所有静态资源,可以动态按需加载的一个包工具。而rollup也是一个模块打包器,可以把一个大块复杂的代码拆分成各个小模块。
深思熟虑后,我觉得webpack也可以打包,但是首先,有点“杀鸡焉用牛刀”的感觉。而我的这个懒加载插件则需要提供给别人使用,同时又要保证整个插件的“轻量性”(打包完大概6KB,而webpack则比较大),不喜欢像webpack那样在这插件上臃肿的表现。
对于非应用级的程序,我比较倾向于使用rollup.js。
|——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为工具类,主要是一些可以通用的模块方法。大概的结构就是这样。
好的设计思路是一个插件的灵魂,我以自己不在道上的设计能力,借鉴了许多大神的思想!很不自信地设计了懒加载的插件项目结构,见下:
上述都是关于vue插件的一些文件,下面我们来说我们自己定义的一些:
原理如下:
通过监听滚动条滚动,来不停地遍历上述imagebox里面的item数组(这个数组存放着需要懒加载的图片预加载地址),如果item里面有值,那么就进行图片的请求。进行请求的同时,我们把这个元素加入到itemPending里面去,如果加载完了就放到itemAlready里面,失败的放到failed里面,这就是基本的实现思路。
懒加载的实现过程,我们这里先精简化。具体思路如下:
=》把所有用指令绑定的元素添加数组初始化
=》监听滚动条滚动
=》判断元素是否进入可视范围
=》如果进入可视范围,进行src预加载(存入缓存数组)
=》对于pending的图片,进行正在加载赋值,对于finsh完的图片,加载预加载src里面的值,对于error的图片,进行错误图片src赋值
Vue插件里面介绍是这样的
MyPlugin.install = function (Vue, options) { // 1. 添加全局方法或属性 Vue.myGlobalMethod = function () { // 逻辑... } // 2. 添加全局资源 Vue.directive('my-directive', { bind (el, binding, vnode, oldVnode) { // 逻辑... } ... }) // 3. 注入组件 Vue.mixin({ created: function () { // 逻辑... } ... }) // 4. 添加实例方法 Vue.prototype.$myMethod = function (methodOptions) { // 逻辑... } }
在外面暴露的方法就是install,使用的时候直接Vue.use("插件名称")直接可以使用。我们在install方法里面填写关于指令(directive)和混合(mixin),然后对外公开这个方法,option没填写的话就是默认空对象。
混合主要是为了混入vue内部属性,是除了以上全局方法后又可以在全局使用的一种方式。
工欲善其事必先利其器,我们先编写rollup的配置代码(这里做了精简,复杂的可以参照我的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({//关于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/...是路径,这里注意一下,我们只需要运行
npm run main
就可以进行打包了。
关于rollup不懂的地方或者使用,我有一篇博文也简单介绍了从零开始学习Rollup.js的前端打包利器
首先我们要先画好雏形,如何将eventlistener文件和directive联系在一起?
第一步首先肯定是引用eventlistener,同时因为它是一个类,我们这里只要指令每次inserted的时候,我们都新new一个对象进行初始化,把能传的值都传过去。可以看到下面的操作
import eventlistener from './cores/eventlistener' var listener = null; export default { inserted: function (el,binding, vnode, oldVnode) { var EventListener = new eventlistener(el,binding, vnode);//这里我们new一个新对象,把el(元素),binding(绑定的值),vnode(虚拟node)都传过去 listener = EventListener; EventListener.init();//假设有一个init初始化函数 EventListener.startListen();//这里初始化完进行监听 }, update: function(el,{name,value,oldValue,expression}, vnode, oldVnode){ }, unbind: function(){ } }
其次我们要考虑在update钩子中,我们需要干什么?
有这样一种业务:当你一次性绑定完所有数据的时候,如果这个图片已经预加载完了,那么你再怎么改变这个指令绑定的值,都不能够实现刷新图片了,所以我们在update更新新的图片地址是有必要的。同时解绑的时候,取消绑定也是有必要的,那么继续往下写:
import eventlistener from './cores/eventlistener' var listener = null; export default { inserted: function (el,binding, vnode, oldVnode) { var EventListener = new eventlistener(el,binding, vnode);//这里我们new一个新对象,把el(元素),binding(绑定的值),vnode(虚拟node)都传过去 listener = EventListener; EventListener.init();//假设有一个init初始化函数 EventListener.startListen();//这里初始化完进行监听 }, update: function(el,{name,value,oldValue,expression}, vnode, oldVnode){ if(value === oldValue){//没有变化就返回 return; } listener.update(el,value);//有变化就进行更新(假设有update这个方法) }, unbind: function(){ listener.removeListen();//解绑移除监听 } }
先写生命周期中inserted的时候绑定监听。加入的时候new一个监听对象保存所有包括所有dom的。我们继续往下看。
首先先把之前说到的几个数组都初始化了。那么作为图像盒子(imagebox)的对象实例,我们需要哪些方法或属性呢?
首先初始化的时候的add方法肯定要,需要判断一下是否有这个元素,没有的话就加入到item里面去。同时类似的还有addFailed,addPending等方法。
如果item中的元素加载完了,那么随之而来的就需要删除item中的元素,那么对应的remove方法也是必须要的,同时类似的还有removePending等方法。
export default class ImageBox { constructor() { this.eleAll = []; this.item = []; this.itemAlready = []; this.itemPending = []; this.itemFailed = []; } add(ele,src) {//insert插入的时候把所有的dom加入到数组中去初始化 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); } }
上述是一个图片的box,用于存取页面加载时候,image图片对象的box存取。主要思路是分了三个数组,一个存储所有的图片,一个存储正在加载的图片,一个存储加载失败的图片,然后最重要的是!!!
把这个imagebox要混入到全局,使其可以当做全局变量在全局使用。
import imagebox from './imagebox' const mixin = { data () { return { imagebox: new imagebox()//这里声明一个new对象,存在全局的变量中,混入vue内部,可以全局使用 } } } export default mixin;
下所示代码中:
根据上述思路,完成下列代码:
(补充:这里有个update方法,思路是更新了后,进行所有的数组遍历,找到相对应的元素,然后进行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); } } }
const isSeen = function(item,imagebox){ var ele = item.ele; var src = item.src; //图片距离页面顶部的距离 var top = ele.getBoundingClientRect().top; //页面可视区域的高度 var windowHeight = document.documentElement.clientHeight || document.body.clientHeight; //top + 10 已经进入了可视区域10像素 if(top + 10 < windowHeight){ return true; }else{ return false; } } export { isSeen };
这个文件主要是监听的一些逻辑,那么肯定需要一些对象实例的属性。首先el元素肯定需要,binding,vnode,$vm肯定都先写进来。
其次imagebox肯定也需要,是图片的对象实例。
init的方法这里和imagebox中的add方法联系起来,init一个就加入imagebox中的item一个新的元素。
startListen方法是用于在监听后进行逻辑操作。
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//混合mixin进去的选项 } init(){ if(!typeof this.binding.value === 'string'){ throw new Error("您的图片源不是String类型,请重试"); return; } this.imagebox = this.vnode.context.imagebox; this.imagebox.add(this.el,this.binding.value);//每有一个item,就往box中增加一个新的元素 this.listenProcess(); } startListen(){ const _self = this; document.addEventListener('scroll',(e)=>{ _self.listenProcess(e);//这里开始操作 }) } }
上面主要初始化了很多属性,包括vue的虚拟dom和各种包括el元素dom,binding指令传过来的值等等初始化。
此文件主要为了处理监听页面滚动的,监听是否图片进入到可视范围内,然后进行一系列下方的各种操作。
根据image.onload,image.onerror
方法进行图片预加载的逻辑操作,如果看得见这个图片,那么就进行图片的加载(同时加入到pending里面去),加载完进行判断。
下列是process的函数listenProcess
:
const _self = this; if(this.imagebox.item.length == 0){ return; }; this.imagebox.item.forEach((item)=>{ if(isSeen(item)){//这里判断元素是否看得见 var image = new Image();//这里在赋值src前new一个image对象进行缓存,缓冲一下,可以做后续的加载或失败的函数处理 image.src = item.src; _self._imageStyle(item);//改变item的样式 _self.imagebox.addPending(item.ele,item.src);//在对象imagebox中加入了正在pending请求的item(后续会介绍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);//添加到已经加载完的item数组里面 this._imageSet(item.ele,item.src) } imageOnerror(item){//出现错误的时候 this._removeImageStyle(item.ele); this.imagebox.addFailed(item.ele,item.src);//添加到出现错误item数组里面 this._imageSet(item.ele,this.$lazyload.options.errorUrl)//把配置中的错误图片url填入 } _imageStyle(item){ item.ele.style.background = `url(${this.$lazyload.options.loadUrl}) no-repeat center`; } _removeImageStyle(ele){ ele.style.background = ''; } _imageSet(ele,value){//关于图片赋值src的操作 ele.src = value; }
补充一个update方法:
update(ele,src){ console.log("更新了"); console.log(this.imagebox); this.imagebox.update(ele,src);//调用imagebox中的update方法 this.listenProcess();//再进行是否看得见的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类型,请重试"); 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); } //监听的操作函数,包括判断image的box进行请求等 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; } }
所有的解释都已经写在上面的代码块里面了。
最后把一些加载的图片默认配置或者失败图片地址完成下列代码:
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); } }
此类暂时用来存储各种配置和lazy的预定默认值,options里面存加载的时候的图片地址和错误加载的时候的图片地址。
默认值是最上面两个值,是不传数据默认的配置。
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 };
把上述所有的进行一个综合,放在这个入口文件进行向外暴露。
index就是整个项目的入口文件,至此我们完成了懒加载插件的基本代码编写。
打包后的代码:
(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; //图片距离页面顶部的距离 var top = ele.getBoundingClientRect().top; //页面可视区域的高度 var windowHeight = document.documentElement.clientHeight || document.body.clientHeight; //top + 10 已经进入了可视区域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',//这里写你的加载时候的图片配置 errorUrl:'./404.svg'//错误加载的图片配置 });
<img v-simple-lazy="item" v-for="(item,$key) in imageArr">
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' ]
测试地址:戳我戳我
其实这些代码的编写还是比较简单的,写完过后进行总结,你会发现,其中最难的是:
整个项目的结构,和代码模块之间的逻辑关系。
这个才是最难掌握的,如果涉及到大一点的项目,好一点的项目结构能让整个项目进度等等因素发生巨大的变化,提升巨大的效率。而写插件最难的就是在这。
如何有效地拆分代码?如何有效地进行项目结构的构造? 这才是整个插件编写的核心。
之前写过一个vue关于表单验证的插件,也是被项目结构搞得焦头烂额,这里把简单的懒加载基本代码做一个总结。写这个纯粹是个人兴趣。希望可以给入门的插件开发新人给予一点点帮助。
所以我深知写插件的时候,它结构和模块化的重要性。而结构和模块化的优秀,会让你事半功倍。另外欢迎大家来我的博客参观唐益达的博客,只写原创。
萌新小白,前端开发入门一年不到,欢迎交流!
background-color: blue;background-color: yellow;<input type="button" value="变蓝" @click="changeColorT